// Package nebula contains helper functions and types which are useful for // setting up nebula configs, processes, and deployments. package nebula import ( "crypto" "crypto/ed25519" "crypto/rand" "encoding/pem" "errors" "fmt" "io" "net" "time" "github.com/slackhq/nebula/cert" "golang.org/x/crypto/curve25519" ) // ErrInvalidSignature is returned from functions when a signature validation // fails. var ErrInvalidSignature = errors.New("invalid signature") // 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"` } // 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 { Public HostPublicCredentials `yaml:"public"` KeyPEM string `yaml:"key_pem"` 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"` } // CACredentials contains the certificate and private files which can be used to // create and validate HostCredentials. Each file is PEM encoded. type CACredentials struct { Public CAPublicCredentials `yaml:"public"` SigningPrivateKeyPEM string `yaml:"signing_private_key_pem"` } // 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 caSigningKey, _, err := cert.UnmarshalEd25519PrivateKey([]byte(caCreds.SigningPrivateKeyPEM)) if err != nil { return HostCredentials{}, fmt.Errorf("unmarshaling ca.key: %w", err) } caCert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(caCreds.Public.CertPEM)) if err != nil { return HostCredentials{}, fmt.Errorf("unmarshaling ca.crt: %w", err) } issuer, err := caCert.Sha256Sum() if err != nil { return HostCredentials{}, fmt.Errorf("getting ca.crt issuer: %w", err) } expireAt := caCert.Details.NotAfter.Add(-1 * time.Second) subnet := caCert.Details.Subnets[0] if !subnet.Contains(ip) { return HostCredentials{}, fmt.Errorf("invalid ip %q, not contained by network subnet %q", ip, subnet) } 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[:] } hostCert := cert.NebulaCertificate{ Details: cert.NebulaCertificateDetails{ Name: hostName, Ips: []*net.IPNet{{ IP: ip, Mask: subnet.Mask, }}, NotBefore: time.Now(), NotAfter: expireAt, PublicKey: hostPub, IsCA: false, Issuer: issuer, }, } if err := hostCert.CheckRootConstrains(caCert); err != nil { return HostCredentials{}, fmt.Errorf("validating certificate constraints: %w", err) } if err := hostCert.Sign(caSigningKey); err != nil { return HostCredentials{}, fmt.Errorf("signing host cert with ca.key: %w", err) } hostKeyPEM := cert.MarshalX25519PrivateKey(hostKey) hostCertPEM, err := hostCert.MarshalToPEM() if err != nil { return HostCredentials{}, fmt.Errorf("marshalling host.crt: %w", err) } return HostCredentials{ Public: HostPublicCredentials{ CertPEM: string(hostCertPEM), SigningKeyPEM: string(signingPubKeyPEM), }, KeyPEM: string(hostKeyPEM), SigningPrivateKeyPEM: string(signingPrivKeyPEM), }, nil } // NewCACredentials generates a CACredentials. The domain should be the network's root domain, // and is included in the signing certificate's Name field. func NewCACredentials(domain string, subnet *net.IPNet) (CACredentials, error) { // 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) if err != nil { panic(fmt.Errorf("generating ed25519 key: %w", err)) } now := time.Now() expireAt := now.Add(2 * 365 * 24 * time.Hour) caCert := cert.NebulaCertificate{ Details: cert.NebulaCertificateDetails{ Name: fmt.Sprintf("%s isle root cert", domain), Subnets: []*net.IPNet{subnet}, NotBefore: now, NotAfter: expireAt, PublicKey: signingPubKey, IsCA: true, }, } if err := caCert.Sign(signingPrivKey); err != nil { return CACredentials{}, fmt.Errorf("signing caCert: %w", err) } signingPrivKeyPEM := cert.MarshalEd25519PrivateKey(signingPrivKey) signingPubKeyPEM := cert.MarshalEd25519PublicKey(signingPubKey) certPEM, err := caCert.MarshalToPEM() if err != nil { return CACredentials{}, fmt.Errorf("marshaling caCert: %w", err) } return CACredentials{ Public: CAPublicCredentials{ CertPEM: string(certPEM), SigningKeyPEM: string(signingPubKeyPEM), }, SigningPrivateKeyPEM: string(signingPrivKeyPEM), }, nil } // 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 } // SignAndWrap signs the given bytes using the host key, and writes an // encoded, versioned structure containing the signature and the given bytes. func SignAndWrap(into io.Writer, signingKeyPEM string, b []byte) error { key, _, err := cert.UnmarshalEd25519PrivateKey([]byte(signingKeyPEM)) 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{ Type: "NEBULA ED25519 SIGNATURE", 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 // the original input to SignAndWrap as well as the signature which was // 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. func ValidateSignature(signingPubKeyPEM string, b, sig []byte) error { pubKey, _, err := cert.UnmarshalEd25519PublicKey([]byte(signingPubKeyPEM)) if err != nil { return fmt.Errorf("unmarshaling certificate as PEM: %w", err) } if !ed25519.Verify(pubKey, b, sig) { return ErrInvalidSignature } return nil }