isle/entrypoint/src/nebula/nebula.go
Brian Picciano ffd276bd3e Refactor how nebula certs are signed and propagated
I had previously made the mistake of thinking that the Curve25519 key
which is generated for each host to use in nebula communication could
also be used for signing. This is not the case, Ed25519 is used for
signing and is different thant Curve25519.

Rather than figuring out how to convert the Curve25519 key into an
Ed25519 key, which there is no apparent support for in the standard
library, I opted to instead ship a separate key just for signing with
each host. Doing this required a bit of refactoring in order to keep all
the different keys straight and ensure all data which needs a signature
still has it.
2022-11-05 15:23:29 +01:00

281 lines
8.2 KiB
Go

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