From 8824eeaea25d4d0d9b67b308d7d5dac2032bc79e Mon Sep 17 00:00:00 2001 From: Jack Doan Date: Thu, 2 Oct 2025 13:56:41 -0500 Subject: [PATCH] helper functions to more correctly marshal curve 25519 public keys (#1481) --- cert/cert.go | 3 +++ cert/cert_v1.go | 4 +++ cert/cert_v1_test.go | 54 ++++++++++++++++++++++++++++++++++++++ cert/cert_v2.go | 4 +++ cert/cert_v2_test.go | 53 +++++++++++++++++++++++++++++++++++++ cert/pem.go | 52 ++++++++++++++++++++++++++++-------- cert/pem_test.go | 19 ++++++++++++-- connection_manager_test.go | 4 +++ 8 files changed, 180 insertions(+), 13 deletions(-) diff --git a/cert/cert.go b/cert/cert.go index 47283f5..855815a 100644 --- a/cert/cert.go +++ b/cert/cert.go @@ -58,6 +58,9 @@ type Certificate interface { // PublicKey is the raw bytes to be used in asymmetric cryptographic operations. PublicKey() []byte + // MarshalPublicKeyPEM is the value of PublicKey marshalled to PEM + MarshalPublicKeyPEM() []byte + // Curve identifies which curve was used for the PublicKey and Signature. Curve() Curve diff --git a/cert/cert_v1.go b/cert/cert_v1.go index f6689a3..09a181d 100644 --- a/cert/cert_v1.go +++ b/cert/cert_v1.go @@ -83,6 +83,10 @@ func (c *certificateV1) PublicKey() []byte { return c.details.publicKey } +func (c *certificateV1) MarshalPublicKeyPEM() []byte { + return marshalCertPublicKeyToPEM(c) +} + func (c *certificateV1) Signature() []byte { return c.signature } diff --git a/cert/cert_v1_test.go b/cert/cert_v1_test.go index c687172..3b7d585 100644 --- a/cert/cert_v1_test.go +++ b/cert/cert_v1_test.go @@ -1,6 +1,7 @@ package cert import ( + "crypto/ed25519" "fmt" "net/netip" "testing" @@ -13,6 +14,7 @@ import ( ) func TestCertificateV1_Marshal(t *testing.T) { + t.Parallel() before := time.Now().Add(time.Second * -60).Round(time.Second) after := time.Now().Add(time.Second * 60).Round(time.Second) pubKey := []byte("1234567890abcedfghij1234567890ab") @@ -60,6 +62,58 @@ func TestCertificateV1_Marshal(t *testing.T) { assert.Equal(t, nc.Groups(), nc2.Groups()) } +func TestCertificateV1_PublicKeyPem(t *testing.T) { + t.Parallel() + before := time.Now().Add(time.Second * -60).Round(time.Second) + after := time.Now().Add(time.Second * 60).Round(time.Second) + pubKey := ed25519.PublicKey("1234567890abcedfghij1234567890ab") + + nc := certificateV1{ + details: detailsV1{ + name: "testing", + networks: []netip.Prefix{}, + unsafeNetworks: []netip.Prefix{}, + groups: []string{"test-group1", "test-group2", "test-group3"}, + notBefore: before, + notAfter: after, + publicKey: pubKey, + isCA: false, + issuer: "1234567890abcedfghij1234567890ab", + }, + signature: []byte("1234567890abcedfghij1234567890ab"), + } + + assert.Equal(t, Version1, nc.Version()) + assert.Equal(t, Curve_CURVE25519, nc.Curve()) + pubPem := "-----BEGIN NEBULA X25519 PUBLIC KEY-----\nMTIzNDU2Nzg5MGFiY2VkZmdoaWoxMjM0NTY3ODkwYWI=\n-----END NEBULA X25519 PUBLIC KEY-----\n" + assert.Equal(t, string(nc.MarshalPublicKeyPEM()), pubPem) + assert.False(t, nc.IsCA()) + + nc.details.isCA = true + assert.Equal(t, Curve_CURVE25519, nc.Curve()) + pubPem = "-----BEGIN NEBULA ED25519 PUBLIC KEY-----\nMTIzNDU2Nzg5MGFiY2VkZmdoaWoxMjM0NTY3ODkwYWI=\n-----END NEBULA ED25519 PUBLIC KEY-----\n" + assert.Equal(t, string(nc.MarshalPublicKeyPEM()), pubPem) + assert.True(t, nc.IsCA()) + + pubP256KeyPem := []byte(`-----BEGIN NEBULA P256 PUBLIC KEY----- +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAA= +-----END NEBULA P256 PUBLIC KEY----- +`) + pubP256Key, _, _, err := UnmarshalPublicKeyFromPEM(pubP256KeyPem) + require.NoError(t, err) + nc.details.curve = Curve_P256 + nc.details.publicKey = pubP256Key + assert.Equal(t, Curve_P256, nc.Curve()) + assert.Equal(t, string(nc.MarshalPublicKeyPEM()), string(pubP256KeyPem)) + assert.True(t, nc.IsCA()) + + nc.details.isCA = false + assert.Equal(t, Curve_P256, nc.Curve()) + assert.Equal(t, string(nc.MarshalPublicKeyPEM()), string(pubP256KeyPem)) + assert.False(t, nc.IsCA()) +} + func TestCertificateV1_Expired(t *testing.T) { nc := certificateV1{ details: detailsV1{ diff --git a/cert/cert_v2.go b/cert/cert_v2.go index ac7a9b2..ac21cb1 100644 --- a/cert/cert_v2.go +++ b/cert/cert_v2.go @@ -114,6 +114,10 @@ func (c *certificateV2) PublicKey() []byte { return c.publicKey } +func (c *certificateV2) MarshalPublicKeyPEM() []byte { + return marshalCertPublicKeyToPEM(c) +} + func (c *certificateV2) Signature() []byte { return c.signature } diff --git a/cert/cert_v2_test.go b/cert/cert_v2_test.go index c84f8c9..84362ef 100644 --- a/cert/cert_v2_test.go +++ b/cert/cert_v2_test.go @@ -15,6 +15,7 @@ import ( ) func TestCertificateV2_Marshal(t *testing.T) { + t.Parallel() before := time.Now().Add(time.Second * -60).Round(time.Second) after := time.Now().Add(time.Second * 60).Round(time.Second) pubKey := []byte("1234567890abcedfghij1234567890ab") @@ -75,6 +76,58 @@ func TestCertificateV2_Marshal(t *testing.T) { assert.Equal(t, nc.Groups(), nc2.Groups()) } +func TestCertificateV2_PublicKeyPem(t *testing.T) { + t.Parallel() + before := time.Now().Add(time.Second * -60).Round(time.Second) + after := time.Now().Add(time.Second * 60).Round(time.Second) + pubKey := ed25519.PublicKey("1234567890abcedfghij1234567890ab") + + nc := certificateV2{ + details: detailsV2{ + name: "testing", + networks: []netip.Prefix{}, + unsafeNetworks: []netip.Prefix{}, + groups: []string{"test-group1", "test-group2", "test-group3"}, + notBefore: before, + notAfter: after, + isCA: false, + issuer: "1234567890abcedfghij1234567890ab", + }, + publicKey: pubKey, + signature: []byte("1234567890abcedfghij1234567890ab"), + } + + assert.Equal(t, Version2, nc.Version()) + assert.Equal(t, Curve_CURVE25519, nc.Curve()) + pubPem := "-----BEGIN NEBULA X25519 PUBLIC KEY-----\nMTIzNDU2Nzg5MGFiY2VkZmdoaWoxMjM0NTY3ODkwYWI=\n-----END NEBULA X25519 PUBLIC KEY-----\n" + assert.Equal(t, string(nc.MarshalPublicKeyPEM()), pubPem) + assert.False(t, nc.IsCA()) + + nc.details.isCA = true + assert.Equal(t, Curve_CURVE25519, nc.Curve()) + pubPem = "-----BEGIN NEBULA ED25519 PUBLIC KEY-----\nMTIzNDU2Nzg5MGFiY2VkZmdoaWoxMjM0NTY3ODkwYWI=\n-----END NEBULA ED25519 PUBLIC KEY-----\n" + assert.Equal(t, string(nc.MarshalPublicKeyPEM()), pubPem) + assert.True(t, nc.IsCA()) + + pubP256KeyPem := []byte(`-----BEGIN NEBULA P256 PUBLIC KEY----- +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAA= +-----END NEBULA P256 PUBLIC KEY----- +`) + pubP256Key, _, _, err := UnmarshalPublicKeyFromPEM(pubP256KeyPem) + require.NoError(t, err) + nc.curve = Curve_P256 + nc.publicKey = pubP256Key + assert.Equal(t, Curve_P256, nc.Curve()) + assert.Equal(t, string(nc.MarshalPublicKeyPEM()), string(pubP256KeyPem)) + assert.True(t, nc.IsCA()) + + nc.details.isCA = false + assert.Equal(t, Curve_P256, nc.Curve()) + assert.Equal(t, string(nc.MarshalPublicKeyPEM()), string(pubP256KeyPem)) + assert.False(t, nc.IsCA()) +} + func TestCertificateV2_Expired(t *testing.T) { nc := certificateV2{ details: detailsV2{ diff --git a/cert/pem.go b/cert/pem.go index 7ad28d1..a5aabdc 100644 --- a/cert/pem.go +++ b/cert/pem.go @@ -7,19 +7,26 @@ import ( "golang.org/x/crypto/ed25519" ) -const ( - CertificateBanner = "NEBULA CERTIFICATE" - CertificateV2Banner = "NEBULA CERTIFICATE V2" - X25519PrivateKeyBanner = "NEBULA X25519 PRIVATE KEY" - X25519PublicKeyBanner = "NEBULA X25519 PUBLIC KEY" - EncryptedEd25519PrivateKeyBanner = "NEBULA ED25519 ENCRYPTED PRIVATE KEY" - Ed25519PrivateKeyBanner = "NEBULA ED25519 PRIVATE KEY" - Ed25519PublicKeyBanner = "NEBULA ED25519 PUBLIC KEY" +const ( //cert banners + CertificateBanner = "NEBULA CERTIFICATE" + CertificateV2Banner = "NEBULA CERTIFICATE V2" +) - P256PrivateKeyBanner = "NEBULA P256 PRIVATE KEY" - P256PublicKeyBanner = "NEBULA P256 PUBLIC KEY" +const ( //key-agreement-key banners + X25519PrivateKeyBanner = "NEBULA X25519 PRIVATE KEY" + X25519PublicKeyBanner = "NEBULA X25519 PUBLIC KEY" + P256PrivateKeyBanner = "NEBULA P256 PRIVATE KEY" + P256PublicKeyBanner = "NEBULA P256 PUBLIC KEY" +) + +/* including "ECDSA" in the P256 banners is a clue that these keys should be used only for signing */ +const ( //signing key banners EncryptedECDSAP256PrivateKeyBanner = "NEBULA ECDSA P256 ENCRYPTED PRIVATE KEY" ECDSAP256PrivateKeyBanner = "NEBULA ECDSA P256 PRIVATE KEY" + ECDSAP256PublicKeyBanner = "NEBULA ECDSA P256 PUBLIC KEY" + EncryptedEd25519PrivateKeyBanner = "NEBULA ED25519 ENCRYPTED PRIVATE KEY" + Ed25519PrivateKeyBanner = "NEBULA ED25519 PRIVATE KEY" + Ed25519PublicKeyBanner = "NEBULA ED25519 PUBLIC KEY" ) // UnmarshalCertificateFromPEM will try to unmarshal the first pem block in a byte array, returning any non consumed @@ -51,6 +58,16 @@ func UnmarshalCertificateFromPEM(b []byte) (Certificate, []byte, error) { } +func marshalCertPublicKeyToPEM(c Certificate) []byte { + if c.IsCA() { + return MarshalSigningPublicKeyToPEM(c.Curve(), c.PublicKey()) + } else { + return MarshalPublicKeyToPEM(c.Curve(), c.PublicKey()) + } +} + +// MarshalPublicKeyToPEM returns a PEM representation of a public key used for ECDH. +// if your public key came from a certificate, prefer Certificate.PublicKeyPEM() if possible, to avoid mistakes! func MarshalPublicKeyToPEM(curve Curve, b []byte) []byte { switch curve { case Curve_CURVE25519: @@ -62,6 +79,19 @@ func MarshalPublicKeyToPEM(curve Curve, b []byte) []byte { } } +// MarshalSigningPublicKeyToPEM returns a PEM representation of a public key used for signing. +// if your public key came from a certificate, prefer Certificate.PublicKeyPEM() if possible, to avoid mistakes! +func MarshalSigningPublicKeyToPEM(curve Curve, b []byte) []byte { + switch curve { + case Curve_CURVE25519: + return pem.EncodeToMemory(&pem.Block{Type: Ed25519PublicKeyBanner, Bytes: b}) + case Curve_P256: + return pem.EncodeToMemory(&pem.Block{Type: P256PublicKeyBanner, Bytes: b}) + default: + return nil + } +} + func UnmarshalPublicKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) { k, r := pem.Decode(b) if k == nil { @@ -73,7 +103,7 @@ func UnmarshalPublicKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) { case X25519PublicKeyBanner, Ed25519PublicKeyBanner: expectedLen = 32 curve = Curve_CURVE25519 - case P256PublicKeyBanner: + case P256PublicKeyBanner, ECDSAP256PublicKeyBanner: // Uncompressed expectedLen = 65 curve = Curve_P256 diff --git a/cert/pem_test.go b/cert/pem_test.go index 6e49249..ff4410c 100644 --- a/cert/pem_test.go +++ b/cert/pem_test.go @@ -177,6 +177,7 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= } func TestUnmarshalPublicKeyFromPEM(t *testing.T) { + t.Parallel() pubKey := []byte(`# A good key -----BEGIN NEBULA ED25519 PUBLIC KEY----- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= @@ -230,6 +231,7 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= } func TestUnmarshalX25519PublicKey(t *testing.T) { + t.Parallel() pubKey := []byte(`# A good key -----BEGIN NEBULA X25519 PUBLIC KEY----- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= @@ -240,6 +242,12 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAA= -----END NEBULA P256 PUBLIC KEY----- +`) + oldPubP256Key := []byte(`# A good key +-----BEGIN NEBULA ECDSA P256 PUBLIC KEY----- +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAA= +-----END NEBULA ECDSA P256 PUBLIC KEY----- `) shortKey := []byte(`# A short key -----BEGIN NEBULA X25519 PUBLIC KEY----- @@ -256,15 +264,22 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= -END NEBULA X25519 PUBLIC KEY-----`) - keyBundle := appendByteSlices(pubKey, pubP256Key, shortKey, invalidBanner, invalidPem) + keyBundle := appendByteSlices(pubKey, pubP256Key, oldPubP256Key, shortKey, invalidBanner, invalidPem) // Success test case k, rest, curve, err := UnmarshalPublicKeyFromPEM(keyBundle) assert.Len(t, k, 32) require.NoError(t, err) - assert.Equal(t, rest, appendByteSlices(pubP256Key, shortKey, invalidBanner, invalidPem)) + assert.Equal(t, rest, appendByteSlices(pubP256Key, oldPubP256Key, shortKey, invalidBanner, invalidPem)) assert.Equal(t, Curve_CURVE25519, curve) + // Success test case + k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest) + assert.Len(t, k, 65) + require.NoError(t, err) + assert.Equal(t, rest, appendByteSlices(oldPubP256Key, shortKey, invalidBanner, invalidPem)) + assert.Equal(t, Curve_P256, curve) + // Success test case k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest) assert.Len(t, k, 65) diff --git a/connection_manager_test.go b/connection_manager_test.go index 3f0dc40..647dd72 100644 --- a/connection_manager_test.go +++ b/connection_manager_test.go @@ -446,6 +446,10 @@ func (d *dummyCert) PublicKey() []byte { return d.publicKey } +func (d *dummyCert) MarshalPublicKeyPEM() []byte { + return cert.MarshalPublicKeyToPEM(d.curve, d.publicKey) +} + func (d *dummyCert) Signature() []byte { return d.signature }