// Package nebula contains helper functions and types which are useful for // setting up nebula configs, processes, and deployments. package nebula import ( "crypto/rand" "fmt" "io" "net" "time" "github.com/slackhq/nebula/cert" "golang.org/x/crypto/curve25519" ) // HostPublicCredentials contains certificate and signing public keys which are // able to be broadcast publicly. type HostPublicCredentials struct { CertPEM string SigningKey SigningPublicKey } // HostPrivateCredentials contains the private key files which will // need to be present on a particular host. type HostPrivateCredentials struct { PrivateKeyPEM string SigningPrivateKey SigningPrivateKey } // 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 SigningKey SigningPublicKey } // 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 SigningPrivateKey SigningPrivateKey } // NewHostCertPEM generates and signs a new host certificate containing the // given public key. func NewHostCertPEM( caCreds CACredentials, hostPubPEM string, hostName string, ip net.IP, ) ( string, error, ) { hostPub, _, err := cert.UnmarshalX25519PublicKey([]byte(hostPubPEM)) if err != nil { return "", fmt.Errorf("unmarshaling public key PEM: %w", err) } caCert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(caCreds.Public.CertPEM)) if err != nil { return "", fmt.Errorf("unmarshaling ca.crt: %w", err) } issuer, err := caCert.Sha256Sum() if err != nil { return "", 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 "", fmt.Errorf("invalid ip %q, not contained by network subnet %q", ip, subnet) } 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 "", fmt.Errorf("validating certificate constraints: %w", err) } if err := signCert(&hostCert, caCreds.SigningPrivateKey); err != nil { 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) } 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, ) ( pub HostPublicCredentials, priv HostPrivateCredentials, err error, ) { // The logic here is largely based on // https://github.com/slackhq/nebula/blob/v1.4.0/cmd/nebula-cert/sign.go var hostPub, hostKey []byte { var pubkey, privkey [32]byte if _, err = io.ReadFull(rand.Reader, privkey[:]); err != nil { err = fmt.Errorf("reading random bytes to form private key: %w", err) return } curve25519.ScalarBaseMult(&pubkey, &privkey) hostPub, hostKey = pubkey[:], privkey[:] } signingPubKey, signingPrivKey := GenerateSigningPair() hostPubPEM := cert.MarshalX25519PublicKey(hostPub) hostKeyPEM := cert.MarshalX25519PrivateKey(hostKey) hostCertPEM, err := NewHostCertPEM(caCreds, string(hostPubPEM), hostName, ip) if err != nil { err = fmt.Errorf("creating host certificate: %w", err) return } pub = HostPublicCredentials{ CertPEM: hostCertPEM, SigningKey: signingPubKey, } priv = HostPrivateCredentials{ PrivateKeyPEM: string(hostKeyPEM), SigningPrivateKey: signingPrivKey, } return } // 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 := GenerateSigningPair() 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 := signCert(&caCert, signingPrivKey); err != nil { return CACredentials{}, fmt.Errorf("signing caCert: %w", err) } certPEM, err := caCert.MarshalToPEM() if err != nil { return CACredentials{}, fmt.Errorf("marshaling caCert: %w", err) } return CACredentials{ Public: CAPublicCredentials{ CertPEM: string(certPEM), SigningKey: signingPubKey, }, SigningPrivateKey: signingPrivKey, }, 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 }