Refactor how signing/encryption keys are typed and (un)marshaled

This commit is contained in:
Brian Picciano 2024-06-15 23:02:24 +02:00
parent 65fa208a34
commit c645a8c767
13 changed files with 298 additions and 175 deletions

View File

@ -69,7 +69,7 @@ in rec {
'';
};
vendorHash = "sha256-P1TXG0fG8/6n37LmM5ApYctqoZzJFlvFAO2Zl85SVvk=";
vendorHash = "sha256-33gwBj+6x9I/yz0Qf4G8YXRgC/HfwHCedqzrCE4FHHk=";
subPackages = [
"./cmd/entrypoint"

View File

@ -78,10 +78,5 @@ type Host struct {
// This assumes that the Host and its data has already been verified against the
// CA signing key.
func (h Host) IP() net.IP {
ip, err := nebula.IPFromHostCertPEM(h.PublicCredentials.CertPEM)
if err != nil {
panic(fmt.Errorf("could not parse IP out of cert for host %q: %w", h.Name, err))
}
return ip
return h.PublicCredentials.Cert.Details.Ips[0].IP
}

View File

@ -381,13 +381,23 @@ var subCmdAdminCreateNebulaCert = subCmd{
return fmt.Errorf("reading public key from %q: %w", *pubKeyPath, err)
}
nebulaHostCertPEM, err := nebula.NewHostCertPEM(
adm.Nebula.CACredentials, string(hostPubPEM), *hostName, ip,
var hostPub nebula.EncryptingPublicKey
if err := hostPub.UnmarshalNebulaPEM(hostPubPEM); err != nil {
return fmt.Errorf("unmarshaling public key as PEM: %w", err)
}
nebulaHostCert, err := nebula.NewHostCert(
adm.Nebula.CACredentials, hostPub, *hostName, ip,
)
if err != nil {
return fmt.Errorf("creating cert: %w", err)
}
nebulaHostCertPEM, err := nebulaHostCert.MarshalToPEM()
if err != nil {
return fmt.Errorf("marshaling cert to PEM: %w", err)
}
if _, err := os.Stdout.Write([]byte(nebulaHostCertPEM)); err != nil {
return fmt.Errorf("writing to stdout: %w", err)
}

View File

@ -4,8 +4,6 @@ import (
"fmt"
"isle/jsonutil"
"os"
"github.com/slackhq/nebula/cert"
)
var subCmdNebulaShow = subCmd{
@ -23,10 +21,10 @@ var subCmdNebulaShow = subCmd{
return fmt.Errorf("loading host bootstrap: %w", err)
}
caPublicCreds := hostBootstrap.CAPublicCredentials
caCert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(caPublicCreds.CertPEM))
caCert := hostBootstrap.CAPublicCredentials.Cert
caCertPEM, err := caCert.MarshalToPEM()
if err != nil {
return fmt.Errorf("unmarshaling ca.crt: %w", err)
return fmt.Errorf("marshaling CA cert to PEM: %w", err)
}
if len(caCert.Details.Subnets) != 1 {
@ -48,7 +46,7 @@ var subCmdNebulaShow = subCmd{
SubnetCIDR string
Lighthouses []outLighthouse
}{
CACert: caPublicCreds.CertPEM,
CACert: string(caCertPEM),
SubnetCIDR: subnet.String(),
}

View File

@ -10,6 +10,7 @@ import (
"path/filepath"
"code.betamike.com/micropelago/pmux/pmuxlib"
"github.com/slackhq/nebula/cert"
)
// waitForNebula waits for the nebula interface to have been started up. It does
@ -56,11 +57,29 @@ func nebulaPmuxProcConfig(
staticHostMap[ip] = []string{host.Nebula.PublicAddr}
}
caCertPEM, err := hostBootstrap.CAPublicCredentials.Cert.MarshalToPEM()
if err != nil {
return pmuxlib.ProcessConfig{}, fmt.Errorf(
"marshaling CA cert to PEM: :%w", err,
)
}
hostCertPEM, err := hostBootstrap.PublicCredentials.Cert.MarshalToPEM()
if err != nil {
return pmuxlib.ProcessConfig{}, fmt.Errorf(
"marshaling host cert to PEM: :%w", err,
)
}
hostKeyPEM := cert.MarshalX25519PrivateKey(
hostBootstrap.PrivateCredentials.EncryptingPrivateKey.Bytes(),
)
config := map[string]interface{}{
"pki": map[string]string{
"ca": hostBootstrap.CAPublicCredentials.CertPEM,
"cert": hostBootstrap.PublicCredentials.CertPEM,
"key": hostBootstrap.PrivateCredentials.PrivateKeyPEM,
"ca": string(caCertPEM),
"cert": string(hostCertPEM),
"key": string(hostKeyPEM),
},
"static_host_map": staticHostMap,
"punchy": map[string]bool{

View File

@ -23,6 +23,7 @@ require (
github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/jtolds/gls v4.20.0+incompatible // indirect
github.com/jxskiss/base62 v1.1.0 // indirect
github.com/klauspost/compress v1.13.5 // indirect
github.com/klauspost/cpuid v1.3.1 // indirect
github.com/minio/md5-simd v1.1.0 // indirect

View File

@ -24,6 +24,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw=
github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc=
github.com/klauspost/compress v1.13.5 h1:9O69jUPDcsT9fEm74W92rZL9FQY7rCdaXVneq+yyzl4=
github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=

117
go/nebula/encrypting_key.go Normal file
View File

@ -0,0 +1,117 @@
package nebula
import (
"crypto/ecdh"
"crypto/rand"
"fmt"
"github.com/slackhq/nebula/cert"
)
var (
encPrivKeyPrefix = []byte("x0")
encPubKeyPrefix = []byte("X0")
x25519 = ecdh.X25519()
)
// EncryptingPublicKey wraps an X25519-based ECDH public key to provide
// convenient text (un)marshaling methods.
type EncryptingPublicKey struct{ inner *ecdh.PublicKey }
// MarshalText implements the encoding.TextMarshaler interface.
func (pk EncryptingPublicKey) MarshalText() ([]byte, error) {
return encodeWithPrefix(encPubKeyPrefix, pk.inner.Bytes()), nil
}
// Bytes returns the raw bytes of the EncryptingPublicKey.
func (k EncryptingPublicKey) Bytes() []byte {
return k.inner.Bytes()
}
// UnmarshalText implements the encoding.TextUnmarshaler interface.
func (pk *EncryptingPublicKey) UnmarshalText(b []byte) error {
b, err := decodeWithPrefix(encPubKeyPrefix, b)
if err != nil {
return fmt.Errorf("unmarshaling: %w", err)
}
if pk.inner, err = x25519.NewPublicKey(b); err != nil {
return fmt.Errorf("converting bytes to public key: %w", err)
}
return nil
}
// UnmarshalNebulaPEM unmarshals the EncryptingPublicKey as a nebula host public
// key PEM.
func (pk *EncryptingPublicKey) UnmarshalNebulaPEM(b []byte) error {
b, _, err := cert.UnmarshalEd25519PublicKey(b)
if err != nil {
return fmt.Errorf("unmarshaling: %w", err)
}
if pk.inner, err = x25519.NewPublicKey(b); err != nil {
return fmt.Errorf("converting bytes to public key: %w", err)
}
return nil
}
func (pk EncryptingPublicKey) String() string {
b, err := pk.MarshalText()
if err != nil {
panic(err)
}
return string(b)
}
// EncryptingPrivateKey wraps an X25519-based ECDH private key to provide
// convenient text (un)marshaling methods.
type EncryptingPrivateKey struct{ inner *ecdh.PrivateKey }
// NewEncryptingPrivateKey generates and returns a fresh EncryptingPrivateKey.
func NewEncryptingPrivateKey() EncryptingPrivateKey {
k, err := x25519.GenerateKey(rand.Reader)
if err != nil {
panic(err)
}
return EncryptingPrivateKey{k}
}
// PublicKey returns the public key which corresponds with this private key.
func (k EncryptingPrivateKey) PublicKey() EncryptingPublicKey {
return EncryptingPublicKey{k.inner.PublicKey()}
}
// MarshalText implements the encoding.TextMarshaler interface.
func (k EncryptingPrivateKey) MarshalText() ([]byte, error) {
return encodeWithPrefix(encPrivKeyPrefix, k.inner.Bytes()), nil
}
// Bytes returns the raw bytes of the EncryptingPrivateKey.
func (k EncryptingPrivateKey) Bytes() []byte {
return k.inner.Bytes()
}
// UnmarshalText implements the encoding.TextUnmarshaler interface.
func (k *EncryptingPrivateKey) UnmarshalText(b []byte) error {
b, err := decodeWithPrefix(encPrivKeyPrefix, b)
if err != nil {
return fmt.Errorf("unmarshaling: %w", err)
}
if k.inner, err = x25519.NewPrivateKey(b); err != nil {
return fmt.Errorf("converting bytes to private key: %w", err)
}
return nil
}
func (k EncryptingPrivateKey) String() string {
b, err := k.MarshalText()
if err != nil {
panic(err)
}
return string(b)
}

View File

@ -1,72 +0,0 @@
package nebula
import (
"crypto/ed25519"
"crypto/rand"
"encoding/json"
"fmt"
"github.com/slackhq/nebula/cert"
)
// SigningPrivateKey wraps an ed25519.PrivateKey to provide convenient JSON
// (un)marshaling methods.
type SigningPrivateKey ed25519.PrivateKey
// MarshalJSON implements the json.Marshaler interface.
func (k SigningPrivateKey) MarshalJSON() ([]byte, error) {
pemStr := cert.MarshalEd25519PrivateKey(ed25519.PrivateKey(k))
return json.Marshal(string(pemStr))
}
// UnmarshalJSON implements the json.Unmarshaler interface.
func (k *SigningPrivateKey) UnmarshalJSON(b []byte) error {
var pemStr string
if err := json.Unmarshal(b, &pemStr); err != nil {
return fmt.Errorf("unmarshaling into string: %w", err)
}
key, _, err := cert.UnmarshalEd25519PrivateKey([]byte(pemStr))
if err != nil {
return fmt.Errorf("unmarshaling from PEM: %w", err)
}
*k = SigningPrivateKey(key)
return nil
}
// SigningPublicKey wraps an ed25519.PublicKey to provide convenient JSON
// (un)marshaling methods.
type SigningPublicKey ed25519.PublicKey
// MarshalJSON implements the json.Marshaler interface.
func (k SigningPublicKey) MarshalJSON() ([]byte, error) {
pemStr := cert.MarshalEd25519PublicKey(ed25519.PublicKey(k))
return json.Marshal(string(pemStr))
}
// MarshalJSON implements the json.Unmarshaler interface.
func (k *SigningPublicKey) UnmarshalJSON(b []byte) error {
var pemStr string
if err := json.Unmarshal(b, &pemStr); err != nil {
return fmt.Errorf("unmarshaling into string: %w", err)
}
key, _, err := cert.UnmarshalEd25519PublicKey([]byte(pemStr))
if err != nil {
return fmt.Errorf("unmarshaling from PEM: %w", err)
}
*k = SigningPublicKey(key)
return nil
}
// GenerateSigningPair generates and returns a new key pair which can be used
// for signing arbitrary blobs of bytes.
func GenerateSigningPair() (SigningPublicKey, SigningPrivateKey) {
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
panic(fmt.Errorf("generating ed25519 key: %w", err))
}
return SigningPublicKey(pub), SigningPrivateKey(priv)
}

View File

@ -3,35 +3,32 @@
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
Cert cert.NebulaCertificate
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
EncryptingPrivateKey EncryptingPrivateKey
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
Cert cert.NebulaCertificate
SigningKey SigningPublicKey
}
@ -42,33 +39,28 @@ type CACredentials struct {
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,
// NewHostCert generates and signs a new host certificate containing the given
// public key.
func NewHostCert(
caCreds CACredentials,
hostPub EncryptingPublicKey,
hostName string,
ip net.IP,
) (
string, error,
cert.NebulaCertificate, 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)
}
caCert := caCreds.Public.Cert
issuer, err := caCert.Sha256Sum()
if err != nil {
return "", fmt.Errorf("getting ca.crt issuer: %w", err)
return cert.NebulaCertificate{}, 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)
return cert.NebulaCertificate{}, fmt.Errorf("invalid ip %q, not contained by network subnet %q", ip, subnet)
}
hostCert := cert.NebulaCertificate{
@ -80,26 +72,21 @@ func NewHostCertPEM(
}},
NotBefore: time.Now(),
NotAfter: expireAt,
PublicKey: hostPub,
PublicKey: hostPub.Bytes(),
IsCA: false,
Issuer: issuer,
},
}
if err := hostCert.CheckRootConstrains(caCert); err != nil {
return "", fmt.Errorf("validating certificate constraints: %w", err)
if err := hostCert.CheckRootConstrains(&caCert); err != nil {
return cert.NebulaCertificate{}, 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)
return cert.NebulaCertificate{}, 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
return hostCert, nil
}
// NewHostCredentials generates a new key/cert for a nebula host using the CA
@ -110,39 +97,27 @@ func NewHostCredentials(
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 (
encPrivKey = NewEncryptingPrivateKey()
encPubKey = encPrivKey.PublicKey()
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()
)
signingPubKey, signingPrivKey := GenerateSigningPair()
hostPubPEM := cert.MarshalX25519PublicKey(hostPub)
hostKeyPEM := cert.MarshalX25519PrivateKey(hostKey)
hostCertPEM, err := NewHostCertPEM(caCreds, string(hostPubPEM), hostName, ip)
hostCert, err := NewHostCert(caCreds, encPubKey, hostName, ip)
if err != nil {
err = fmt.Errorf("creating host certificate: %w", err)
return
}
pub = HostPublicCredentials{
CertPEM: hostCertPEM,
Cert: hostCert,
SigningKey: signingPubKey,
}
priv = HostPrivateCredentials{
PrivateKeyPEM: string(hostKeyPEM),
SigningPrivateKey: signingPrivKey,
EncryptingPrivateKey: encPrivKey,
SigningPrivateKey: signingPrivKey,
}
return
@ -151,14 +126,11 @@ func NewHostCredentials(
// 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)
var (
signingPubKey, signingPrivKey = GenerateSigningPair()
now = time.Now()
expireAt = now.Add(2 * 365 * 24 * time.Hour)
)
caCert := cert.NebulaCertificate{
Details: cert.NebulaCertificateDetails{
@ -175,33 +147,11 @@ func NewCACredentials(domain string, subnet *net.IPNet) (CACredentials, error) {
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),
Cert: caCert,
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
}

78
go/nebula/signing_key.go Normal file
View File

@ -0,0 +1,78 @@
package nebula
import (
"crypto/ed25519"
"crypto/rand"
"fmt"
)
var (
sigPrivKeyPrefix = []byte("s0")
sigPubKeyPrefix = []byte("S0")
)
// SigningPrivateKey wraps an ed25519.PrivateKey to provide convenient text
// (un)marshaling methods.
type SigningPrivateKey ed25519.PrivateKey
// MarshalText implements the encoding.TextMarshaler interface.
func (k SigningPrivateKey) MarshalText() ([]byte, error) {
return encodeWithPrefix(sigPrivKeyPrefix, k), nil
}
// UnmarshalText implements the encoding.TextUnmarshaler interface.
func (k *SigningPrivateKey) UnmarshalText(b []byte) error {
b, err := decodeWithPrefix(sigPrivKeyPrefix, b)
if err != nil {
return fmt.Errorf("unmarshaling: %w", err)
}
*k = SigningPrivateKey(b)
return nil
}
func (k SigningPrivateKey) String() string {
b, err := k.MarshalText()
if err != nil {
panic(err)
}
return string(b)
}
// SigningPublicKey wraps an ed25519.PublicKey to provide convenient text
// (un)marshaling methods.
type SigningPublicKey ed25519.PublicKey
// MarshalText implements the encoding.TextMarshaler interface.
func (pk SigningPublicKey) MarshalText() ([]byte, error) {
return encodeWithPrefix(sigPubKeyPrefix, pk), nil
}
// UnmarshalText implements the encoding.TextUnmarshaler interface.
func (pk *SigningPublicKey) UnmarshalText(b []byte) error {
b, err := decodeWithPrefix(sigPubKeyPrefix, b)
if err != nil {
return fmt.Errorf("unmarshaling: %w", err)
}
*pk = SigningPublicKey(b)
return nil
}
func (pk SigningPublicKey) String() string {
b, err := pk.MarshalText()
if err != nil {
panic(err)
}
return string(b)
}
// GenerateSigningPair generates and returns a new key pair which can be used
// for signing arbitrary blobs of bytes.
func GenerateSigningPair() (SigningPublicKey, SigningPrivateKey) {
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
panic(fmt.Errorf("generating ed25519 key: %w", err))
}
return SigningPublicKey(pub), SigningPrivateKey(priv)
}

25
go/nebula/util.go Normal file
View File

@ -0,0 +1,25 @@
package nebula
import (
"bytes"
"errors"
"fmt"
"github.com/jxskiss/base62"
)
func encodeWithPrefix(prefix []byte, b []byte) []byte {
res := make([]byte, 0, len(prefix)+len(b)*4)
res = append(res, prefix...)
res = base62.EncodeToBuf(res, b)
return res
}
func decodeWithPrefix(prefix []byte, b []byte) ([]byte, error) {
if len(b) < len(prefix) {
return nil, errors.New("input is too short")
} else if !bytes.HasPrefix(b, prefix) {
return nil, fmt.Errorf("missing expected prefix %q", prefix)
}
return base62.Decode(b[len(prefix):])
}