@ -3,8 +3,11 @@
package nebula
import (
"crypto"
"crypto/ed25519"
"crypto/rand"
"encoding/pem"
"errors"
"fmt"
"io"
"net"
@ -14,65 +17,69 @@ import (
"golang.org/x/crypto/curve25519"
)
// HostCert contains the certificate and private key files which will need to
// be present on a particular host. Each file is PEM encoded.
type HostCert struct {
CACert string
HostKey string
HostCert string
// 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
HostKeyPEM string
HostCertPEM string
}
// CACert contains the certificate and private files which can be used to create
// HostCerts. Each file is PEM encoded.
type CACert struct {
CACert string
CAKey string
// CACredentials contains the certificate and private files which can be used to
// create and validate HostCreden tial s. Each file is PEM encoded.
type CACredentials struct {
CACertPEM string
CAKeyPEM string
}
// NewHostCert generates a new key/cert for a nebula host using the CA key
// which will be found in the adminFS.
func NewHostCert (
caCert CACert , hostName string , ip net . IP ,
// 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 ,
) (
HostCert , error ,
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 ( caCert . CAKey ) )
caKey , _ , err := cert . UnmarshalEd25519PrivateKey ( [ ] byte ( caCreds . CAKeyPEM ) )
if err != nil {
return HostCert { } , fmt . Errorf ( "unmarshaling ca.key: %w" , err )
return HostCredentials { } , fmt . Errorf ( "unmarshaling ca.key: %w" , err )
}
caCrt , _ , err := cert . UnmarshalNebulaCertificateFromPEM ( [ ] byte ( caCert . CACert ) )
caCe rt , _ , err := cert . UnmarshalNebulaCertificateFromPEM ( [ ] byte ( caCreds . CACertPEM ) )
if err != nil {
return HostCert { } , fmt . Errorf ( "unmarshaling ca.crt: %w" , err )
return HostCredentials { } , fmt . Errorf ( "unmarshaling ca.crt: %w" , err )
}
issuer , err := caCrt . Sha256Sum ( )
issuer , err := caCe rt . Sha256Sum ( )
if err != nil {
return HostCert { } , fmt . Errorf ( "getting ca.crt issuer: %w" , err )
return HostCredentials { } , fmt . Errorf ( "getting ca.crt issuer: %w" , err )
}
expireAt := caCrt . Details . NotAfter . Add ( - 1 * time . Second )
expireAt := caCe rt . Details . NotAfter . Add ( - 1 * time . Second )
subnet := caCrt . Details . Subnets [ 0 ]
subnet := caCe rt . Details . Subnets [ 0 ]
if ! subnet . Contains ( ip ) {
return HostCert { } , fmt . Errorf ( "invalid ip %q, not contained by network subnet %q" , ip , subnet )
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 HostCert { } , fmt . Errorf ( "reading random bytes to form private key: %w" , err )
return HostCredentials { } , fmt . Errorf ( "reading random bytes to form private key: %w" , err )
}
curve25519 . ScalarBaseMult ( & pubkey , & privkey )
hostPub , hostKey = pubkey [ : ] , privkey [ : ]
}
hostCrt := cert . NebulaCertificate {
hostCe rt := cert . NebulaCertificate {
Details : cert . NebulaCertificateDetails {
Name : hostName ,
Ips : [ ] * net . IPNet { {
@ -87,31 +94,31 @@ func NewHostCert(
} ,
}
if err := hostCrt . CheckRootConstrains ( caCrt ) ; err != nil {
return HostCert { } , fmt . Errorf ( "validating certificate constraints: %w" , err )
if err := hostCe rt . CheckRootConstrains ( caCe rt ) ; err != nil {
return HostCredentials { } , fmt . Errorf ( "validating certificate constraints: %w" , err )
}
if err := hostCrt . Sign ( caKey ) ; err != nil {
return HostCert { } , fmt . Errorf ( "signing host cert with ca.key: %w" , err )
if err := hostCe rt . Sign ( caKey ) ; err != nil {
return HostCredentials { } , fmt . Errorf ( "signing host cert with ca.key: %w" , err )
}
hostKeyPEM := cert . MarshalX25519PrivateKey ( hostKey )
hostCrtPEM , err := hostCrt . MarshalToPEM ( )
hostCe rtPEM , err := hostCe rt . MarshalToPEM ( )
if err != nil {
return HostCert { } , fmt . Errorf ( "marshalling host.crt: %w" , err )
return HostCredentials { } , fmt . Errorf ( "marshalling host.crt: %w" , err )
}
return HostCert {
CACert : caCert . CACert ,
HostKey : string ( hostKeyPEM ) ,
HostCert : string ( hostCrtPEM ) ,
return HostCredentials {
CACertPEM : caCreds . CACertPEM ,
HostKeyPEM : string ( hostKeyPEM ) ,
HostCertPEM : string ( hostCe rtPEM ) ,
} , nil
}
// NewCACert generates a CACert . The domain should be the network's root domain,
// NewCACredentials generates a CACredentials . The domain should be the network's root domain,
// and is included in the signing certificate's Name field.
func NewCACert ( domain string , subnet * net . IPNet ) ( CACert , error ) {
func NewCACredentials ( domain string , subnet * net . IPNet ) ( CACredentials , error ) {
pubKey , privKey , err := ed25519 . GenerateKey ( rand . Reader )
if err != nil {
@ -121,7 +128,7 @@ func NewCACert(domain string, subnet *net.IPNet) (CACert, error) {
now := time . Now ( )
expireAt := now . Add ( 2 * 365 * 24 * time . Hour )
caCrt := cert . NebulaCertificate {
caCe rt := cert . NebulaCertificate {
Details : cert . NebulaCertificateDetails {
Name : fmt . Sprintf ( "%s cryptic-net root cert" , domain ) ,
Subnets : [ ] * net . IPNet { subnet } ,
@ -132,19 +139,134 @@ func NewCACert(domain string, subnet *net.IPNet) (CACert, error) {
} ,
}
if err := caCrt . Sign ( privKey ) ; err != nil {
return CACert { } , fmt . Errorf ( "signing caCrt: %w" , err )
if err := caCe rt . Sign ( privKey ) ; err != nil {
return CACredentials { } , fmt . Errorf ( "signing caCe rt: %w" , err )
}
caKeyPEM := cert . MarshalEd25519PrivateKey ( privKey )
caCrtPem , err := caCrt . MarshalToPEM ( )
caCertPEM , err := caCe rt . MarshalToPEM ( )
if err != nil {
return CACert { } , fmt . Errorf ( "marshaling caCrt: %w" , err )
return CACredentials { } , fmt . Errorf ( "marshaling caCe rt: %w" , err )
}
return CACert {
CACert : string ( caCrtPem ) ,
CAKey : string ( caKeyPEM ) ,
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
}