2022-10-11 19:24:53 +00:00
|
|
|
// Package nebula contains helper functions and types which are useful for
|
|
|
|
// setting up nebula configs, processes, and deployments.
|
|
|
|
package nebula
|
|
|
|
|
|
|
|
import (
|
2022-10-29 19:11:40 +00:00
|
|
|
"crypto"
|
2022-10-15 11:14:38 +00:00
|
|
|
"crypto/ed25519"
|
2022-10-11 19:24:53 +00:00
|
|
|
"crypto/rand"
|
2022-10-29 19:11:40 +00:00
|
|
|
"encoding/pem"
|
|
|
|
"errors"
|
2022-10-11 19:24:53 +00:00
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"net"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/slackhq/nebula/cert"
|
|
|
|
"golang.org/x/crypto/curve25519"
|
|
|
|
)
|
|
|
|
|
2022-10-29 19:11:40 +00:00
|
|
|
// ErrInvalidSignature is returned from functions when a signature validation
|
|
|
|
// fails.
|
|
|
|
var ErrInvalidSignature = errors.New("invalid signature")
|
|
|
|
|
2022-11-05 14:23:29 +00:00
|
|
|
// HostPublicCredentials contains certificate and signing public keys which are
|
|
|
|
// able to be broadcast publicly.
|
|
|
|
type HostPublicCredentials struct {
|
|
|
|
CertPEM string `yaml:"cert_pem"`
|
|
|
|
SigningKeyPEM string `yaml:"signing_key_pem"`
|
|
|
|
}
|
|
|
|
|
2022-10-29 19:11:40 +00:00
|
|
|
// HostCredentials contains the certificate and private key files which will
|
|
|
|
// need to be present on a particular host. Each file is PEM encoded.
|
|
|
|
type HostCredentials struct {
|
2022-11-05 14:23:29 +00:00
|
|
|
Public HostPublicCredentials `yaml:"public"`
|
2023-08-27 14:09:03 +00:00
|
|
|
PrivateKeyPEM string `yaml:"key_pem"` // TODO should be private_key_pem
|
2022-11-05 14:23:29 +00:00
|
|
|
SigningPrivateKeyPEM string `yaml:"signing_private_key_pem"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// CAPublicCredentials contains certificate and signing public keys which are
|
|
|
|
// able to be broadcast publicly. The signing public key is the same one which
|
|
|
|
// is embedded into the certificate.
|
|
|
|
type CAPublicCredentials struct {
|
|
|
|
CertPEM string `yaml:"cert_pem"`
|
|
|
|
SigningKeyPEM string `yaml:"signing_key_pem"`
|
2022-10-11 19:24:53 +00:00
|
|
|
}
|
|
|
|
|
2022-10-29 19:11:40 +00:00
|
|
|
// CACredentials contains the certificate and private files which can be used to
|
|
|
|
// create and validate HostCredentials. Each file is PEM encoded.
|
|
|
|
type CACredentials struct {
|
2022-11-05 14:23:29 +00:00
|
|
|
Public CAPublicCredentials `yaml:"public"`
|
|
|
|
SigningPrivateKeyPEM string `yaml:"signing_private_key_pem"`
|
2022-10-15 11:14:38 +00:00
|
|
|
}
|
|
|
|
|
2023-08-27 14:09:03 +00:00
|
|
|
// NewHostCertPEM generates and signs a new host certificate containing the
|
|
|
|
// given public key.
|
|
|
|
func NewHostCertPEM(
|
|
|
|
caCreds CACredentials, hostPubPEM string, hostName string, ip net.IP,
|
2022-10-11 19:24:53 +00:00
|
|
|
) (
|
2023-08-27 14:09:03 +00:00
|
|
|
string, error,
|
2022-10-11 19:24:53 +00:00
|
|
|
) {
|
2023-08-27 14:09:03 +00:00
|
|
|
hostPub, _, err := cert.UnmarshalX25519PublicKey([]byte(hostPubPEM))
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("unmarshaling public key PEM: %w", err)
|
|
|
|
}
|
2022-10-11 19:24:53 +00:00
|
|
|
|
2022-11-05 14:23:29 +00:00
|
|
|
caSigningKey, _, err := cert.UnmarshalEd25519PrivateKey([]byte(caCreds.SigningPrivateKeyPEM))
|
2022-10-11 19:24:53 +00:00
|
|
|
if err != nil {
|
2023-08-27 14:09:03 +00:00
|
|
|
return "", fmt.Errorf("unmarshaling ca.key: %w", err)
|
2022-10-11 19:24:53 +00:00
|
|
|
}
|
|
|
|
|
2022-11-05 14:23:29 +00:00
|
|
|
caCert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(caCreds.Public.CertPEM))
|
2022-10-11 19:24:53 +00:00
|
|
|
if err != nil {
|
2023-08-27 14:09:03 +00:00
|
|
|
return "", fmt.Errorf("unmarshaling ca.crt: %w", err)
|
2022-10-11 19:24:53 +00:00
|
|
|
}
|
|
|
|
|
2022-10-29 19:11:40 +00:00
|
|
|
issuer, err := caCert.Sha256Sum()
|
2022-10-11 19:24:53 +00:00
|
|
|
if err != nil {
|
2023-08-27 14:09:03 +00:00
|
|
|
return "", fmt.Errorf("getting ca.crt issuer: %w", err)
|
2022-10-11 19:24:53 +00:00
|
|
|
}
|
|
|
|
|
2022-10-29 19:11:40 +00:00
|
|
|
expireAt := caCert.Details.NotAfter.Add(-1 * time.Second)
|
2022-10-11 19:24:53 +00:00
|
|
|
|
2022-10-29 19:11:40 +00:00
|
|
|
subnet := caCert.Details.Subnets[0]
|
2022-10-16 20:17:26 +00:00
|
|
|
if !subnet.Contains(ip) {
|
2023-08-27 14:09:03 +00:00
|
|
|
return "", fmt.Errorf("invalid ip %q, not contained by network subnet %q", ip, subnet)
|
2022-10-11 19:24:53 +00:00
|
|
|
}
|
|
|
|
|
2022-10-29 19:11:40 +00:00
|
|
|
hostCert := cert.NebulaCertificate{
|
2022-10-11 19:24:53 +00:00
|
|
|
Details: cert.NebulaCertificateDetails{
|
2022-10-16 20:17:26 +00:00
|
|
|
Name: hostName,
|
|
|
|
Ips: []*net.IPNet{{
|
|
|
|
IP: ip,
|
|
|
|
Mask: subnet.Mask,
|
|
|
|
}},
|
2022-10-11 19:24:53 +00:00
|
|
|
NotBefore: time.Now(),
|
|
|
|
NotAfter: expireAt,
|
|
|
|
PublicKey: hostPub,
|
|
|
|
IsCA: false,
|
|
|
|
Issuer: issuer,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2022-10-29 19:11:40 +00:00
|
|
|
if err := hostCert.CheckRootConstrains(caCert); err != nil {
|
2023-08-27 14:09:03 +00:00
|
|
|
return "", fmt.Errorf("validating certificate constraints: %w", err)
|
2022-10-11 19:24:53 +00:00
|
|
|
}
|
|
|
|
|
2022-11-05 14:23:29 +00:00
|
|
|
if err := hostCert.Sign(caSigningKey); err != nil {
|
2023-08-27 14:09:03 +00:00
|
|
|
return "", fmt.Errorf("signing host cert with ca.key: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
hostCertPEM, err := hostCert.MarshalToPEM()
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("marshalling host.crt: %w", err)
|
2022-10-11 19:24:53 +00:00
|
|
|
}
|
|
|
|
|
2023-08-27 14:09:03 +00:00
|
|
|
return string(hostCertPEM), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewHostCredentials generates a new key/cert for a nebula host using the CA
|
|
|
|
// key which will be found in the adminFS.
|
|
|
|
func NewHostCredentials(
|
|
|
|
caCreds CACredentials, hostName string, ip net.IP,
|
|
|
|
) (
|
|
|
|
HostCredentials, error,
|
|
|
|
) {
|
|
|
|
|
|
|
|
// The logic here is largely based on
|
|
|
|
// https://github.com/slackhq/nebula/blob/v1.4.0/cmd/nebula-cert/sign.go
|
|
|
|
|
|
|
|
signingPubKey, signingPrivKey, err := ed25519.GenerateKey(rand.Reader)
|
|
|
|
if err != nil {
|
|
|
|
panic(fmt.Errorf("generating ed25519 key: %w", err))
|
|
|
|
}
|
|
|
|
|
|
|
|
signingPrivKeyPEM := cert.MarshalEd25519PrivateKey(signingPrivKey)
|
|
|
|
signingPubKeyPEM := cert.MarshalEd25519PublicKey(signingPubKey)
|
|
|
|
|
|
|
|
var hostPub, hostKey []byte
|
|
|
|
{
|
|
|
|
var pubkey, privkey [32]byte
|
|
|
|
if _, err := io.ReadFull(rand.Reader, privkey[:]); err != nil {
|
|
|
|
return HostCredentials{}, fmt.Errorf("reading random bytes to form private key: %w", err)
|
|
|
|
}
|
|
|
|
curve25519.ScalarBaseMult(&pubkey, &privkey)
|
|
|
|
hostPub, hostKey = pubkey[:], privkey[:]
|
|
|
|
}
|
|
|
|
|
|
|
|
hostPubPEM := cert.MarshalX25519PublicKey(hostPub)
|
2022-10-11 19:24:53 +00:00
|
|
|
hostKeyPEM := cert.MarshalX25519PrivateKey(hostKey)
|
|
|
|
|
2023-08-27 14:09:03 +00:00
|
|
|
hostCertPEM, err := NewHostCertPEM(caCreds, string(hostPubPEM), hostName, ip)
|
2022-10-11 19:24:53 +00:00
|
|
|
if err != nil {
|
2023-08-27 14:09:03 +00:00
|
|
|
return HostCredentials{}, fmt.Errorf("creating host certificate: %w", err)
|
2022-10-11 19:24:53 +00:00
|
|
|
}
|
|
|
|
|
2022-10-29 19:11:40 +00:00
|
|
|
return HostCredentials{
|
2022-11-05 14:23:29 +00:00
|
|
|
Public: HostPublicCredentials{
|
2023-08-27 14:09:03 +00:00
|
|
|
CertPEM: hostCertPEM,
|
2022-11-05 14:23:29 +00:00
|
|
|
SigningKeyPEM: string(signingPubKeyPEM),
|
|
|
|
},
|
2023-08-27 14:09:03 +00:00
|
|
|
PrivateKeyPEM: string(hostKeyPEM),
|
2022-11-05 14:23:29 +00:00
|
|
|
SigningPrivateKeyPEM: string(signingPrivKeyPEM),
|
2022-10-11 19:24:53 +00:00
|
|
|
}, nil
|
|
|
|
}
|
2022-10-15 11:14:38 +00:00
|
|
|
|
2022-10-29 19:11:40 +00:00
|
|
|
// NewCACredentials generates a CACredentials. The domain should be the network's root domain,
|
2022-10-15 11:14:38 +00:00
|
|
|
// and is included in the signing certificate's Name field.
|
2022-10-29 19:11:40 +00:00
|
|
|
func NewCACredentials(domain string, subnet *net.IPNet) (CACredentials, error) {
|
2022-10-15 11:14:38 +00:00
|
|
|
|
2022-11-05 14:23:29 +00:00
|
|
|
// The logic here is largely based on
|
|
|
|
// https://github.com/slackhq/nebula/blob/v1.4.0/cmd/nebula-cert/ca.go
|
|
|
|
|
|
|
|
signingPubKey, signingPrivKey, err := ed25519.GenerateKey(rand.Reader)
|
2022-10-15 11:14:38 +00:00
|
|
|
if err != nil {
|
|
|
|
panic(fmt.Errorf("generating ed25519 key: %w", err))
|
|
|
|
}
|
|
|
|
|
|
|
|
now := time.Now()
|
|
|
|
expireAt := now.Add(2 * 365 * 24 * time.Hour)
|
|
|
|
|
2022-10-29 19:11:40 +00:00
|
|
|
caCert := cert.NebulaCertificate{
|
2022-10-15 11:14:38 +00:00
|
|
|
Details: cert.NebulaCertificateDetails{
|
2023-08-05 21:53:17 +00:00
|
|
|
Name: fmt.Sprintf("%s isle root cert", domain),
|
2022-10-16 20:17:26 +00:00
|
|
|
Subnets: []*net.IPNet{subnet},
|
2022-10-15 11:14:38 +00:00
|
|
|
NotBefore: now,
|
|
|
|
NotAfter: expireAt,
|
2022-11-05 14:23:29 +00:00
|
|
|
PublicKey: signingPubKey,
|
2022-10-15 11:14:38 +00:00
|
|
|
IsCA: true,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2022-11-05 14:23:29 +00:00
|
|
|
if err := caCert.Sign(signingPrivKey); err != nil {
|
2022-10-29 19:11:40 +00:00
|
|
|
return CACredentials{}, fmt.Errorf("signing caCert: %w", err)
|
2022-10-15 11:14:38 +00:00
|
|
|
}
|
|
|
|
|
2022-11-05 14:23:29 +00:00
|
|
|
signingPrivKeyPEM := cert.MarshalEd25519PrivateKey(signingPrivKey)
|
|
|
|
signingPubKeyPEM := cert.MarshalEd25519PublicKey(signingPubKey)
|
2022-10-15 11:14:38 +00:00
|
|
|
|
2022-11-05 14:23:29 +00:00
|
|
|
certPEM, err := caCert.MarshalToPEM()
|
2022-10-15 11:14:38 +00:00
|
|
|
if err != nil {
|
2022-10-29 19:11:40 +00:00
|
|
|
return CACredentials{}, fmt.Errorf("marshaling caCert: %w", err)
|
2022-10-15 11:14:38 +00:00
|
|
|
}
|
|
|
|
|
2022-10-29 19:11:40 +00:00
|
|
|
return CACredentials{
|
2022-11-05 14:23:29 +00:00
|
|
|
Public: CAPublicCredentials{
|
|
|
|
CertPEM: string(certPEM),
|
|
|
|
SigningKeyPEM: string(signingPubKeyPEM),
|
|
|
|
},
|
|
|
|
SigningPrivateKeyPEM: string(signingPrivKeyPEM),
|
2022-10-15 11:14:38 +00:00
|
|
|
}, nil
|
|
|
|
}
|
2022-10-29 19:11:40 +00:00
|
|
|
|
|
|
|
// IPFromHostCertPEM is a convenience function for parsing the IP of a host out
|
|
|
|
// of its nebula cert.
|
|
|
|
func IPFromHostCertPEM(hostCertPEM string) (net.IP, error) {
|
|
|
|
|
|
|
|
hostCert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(hostCertPEM))
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("unmarshaling host certificate as PEM: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
ips := hostCert.Details.Ips
|
|
|
|
if len(ips) == 0 {
|
|
|
|
return nil, fmt.Errorf("malformed nebula host cert: no IPs")
|
|
|
|
}
|
|
|
|
|
|
|
|
return ips[0].IP, nil
|
|
|
|
}
|
|
|
|
|
2022-11-05 14:23:29 +00:00
|
|
|
// SignAndWrap signs the given bytes using the host key, and writes an
|
2022-10-29 19:11:40 +00:00
|
|
|
// encoded, versioned structure containing the signature and the given bytes.
|
2022-11-05 14:23:29 +00:00
|
|
|
func SignAndWrap(into io.Writer, signingKeyPEM string, b []byte) error {
|
2022-10-29 19:11:40 +00:00
|
|
|
|
2022-11-05 14:23:29 +00:00
|
|
|
key, _, err := cert.UnmarshalEd25519PrivateKey([]byte(signingKeyPEM))
|
2022-10-29 19:11:40 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("unmarshaling private key: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
sig, err := key.Sign(rand.Reader, b, crypto.Hash(0))
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("generating signature: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, err := into.Write([]byte("0")); err != nil {
|
|
|
|
return fmt.Errorf("writing version byte: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = pem.Encode(into, &pem.Block{
|
2022-11-05 14:23:29 +00:00
|
|
|
Type: "NEBULA ED25519 SIGNATURE",
|
2022-10-29 19:11:40 +00:00
|
|
|
Bytes: sig,
|
|
|
|
})
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("writing PEM encoding of signature: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, err := into.Write(b); err != nil {
|
|
|
|
return fmt.Errorf("writing input bytes: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Unwrap reads a stream of bytes which was produced by SignAndWrap, and returns
|
2022-11-05 14:23:29 +00:00
|
|
|
// the original input to SignAndWrap as well as the signature which was
|
2022-10-29 19:11:40 +00:00
|
|
|
// created. ValidateSignature can be used to validate the signature.
|
|
|
|
func Unwrap(from io.Reader) (b, sig []byte, err error) {
|
|
|
|
|
|
|
|
full, err := io.ReadAll(from)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, fmt.Errorf("reading full input: %w", err)
|
|
|
|
} else if len(full) < 3 {
|
|
|
|
return nil, nil, fmt.Errorf("input too small")
|
|
|
|
} else if full[0] != '0' {
|
|
|
|
return nil, nil, fmt.Errorf("unexpected version byte: %d", full[0])
|
|
|
|
}
|
|
|
|
|
|
|
|
full = full[1:]
|
|
|
|
|
|
|
|
pemBlock, rest := pem.Decode(full)
|
|
|
|
if pemBlock == nil {
|
|
|
|
return nil, nil, fmt.Errorf("PEM-encoded signature could not be decoded")
|
|
|
|
}
|
|
|
|
|
|
|
|
return rest, pemBlock.Bytes, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// ValidateSignature can be used to validate a signature produced by Unwrap.
|
2022-11-05 14:23:29 +00:00
|
|
|
func ValidateSignature(signingPubKeyPEM string, b, sig []byte) error {
|
2022-10-29 19:11:40 +00:00
|
|
|
|
2022-11-05 14:23:29 +00:00
|
|
|
pubKey, _, err := cert.UnmarshalEd25519PublicKey([]byte(signingPubKeyPEM))
|
2022-10-29 19:11:40 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("unmarshaling certificate as PEM: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if !ed25519.Verify(pubKey, b, sig) {
|
|
|
|
return ErrInvalidSignature
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|