isle/go/nebula/nebula.go

208 lines
5.6 KiB
Go

// 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
}