// 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") // 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 { CACertPEM string `yaml:"ca_cert_pem"` HostKeyPEM string `yaml:"host_key_pem"` HostCertPEM string `yaml:"host_cert_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 { CACertPEM string `yaml:"ca_cert_pem"` CAKeyPEM string `yaml:"ca_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 caKey, _, err := cert.UnmarshalEd25519PrivateKey([]byte(caCreds.CAKeyPEM)) if err != nil { return HostCredentials{}, fmt.Errorf("unmarshaling ca.key: %w", err) } caCert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(caCreds.CACertPEM)) 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) } 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(caKey); 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{ CACertPEM: caCreds.CACertPEM, HostKeyPEM: string(hostKeyPEM), HostCertPEM: string(hostCertPEM), }, 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) { pubKey, privKey, 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 cryptic-net root cert", domain), Subnets: []*net.IPNet{subnet}, NotBefore: now, NotAfter: expireAt, PublicKey: pubKey, IsCA: true, }, } if err := caCert.Sign(privKey); err != nil { return CACredentials{}, fmt.Errorf("signing caCert: %w", err) } caKeyPEM := cert.MarshalEd25519PrivateKey(privKey) caCertPEM, err := caCert.MarshalToPEM() if err != nil { return CACredentials{}, fmt.Errorf("marshaling caCert: %w", err) } return CACredentials{ CACertPEM: string(caCertPEM), CAKeyPEM: string(caKeyPEM), }, nil } // ValidateHostCertPEM checks if the given host certificate was signed by the // given CA certificate, and returns ErrInvalidSignature if validation fails. func ValidateHostCertPEM(caCertPEM, hostCertPEM string) error { caCert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(caCertPEM)) if err != nil { return fmt.Errorf("unmarshaling CA certificate as PEM: %w", err) } hostCert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(hostCertPEM)) if err != nil { return fmt.Errorf("unmarshaling host certificate as PEM: %w", err) } caPubKey := ed25519.PublicKey(caCert.Details.PublicKey) if !hostCert.CheckSignature(caPubKey) { return ErrInvalidSignature } return 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 keyPEM, and writes an // encoded, versioned structure containing the signature and the given bytes. func SignAndWrap(into io.Writer, keyPEM string, b []byte) error { key, _, err := cert.UnmarshalEd25519PrivateKey([]byte(keyPEM)) 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: "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 inpute 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(certPEM string, b, sig []byte) error { cert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(certPEM)) if err != nil { return fmt.Errorf("unmarshaling certificate as PEM: %w", err) } pubKey := ed25519.PublicKey(cert.Details.PublicKey) if !ed25519.Verify(pubKey, b, sig) { return ErrInvalidSignature } return nil }