Skip to content

Commit 8824eea

Browse files
authored
helper functions to more correctly marshal curve 25519 public keys (#1481)
1 parent 071589f commit 8824eea

8 files changed

Lines changed: 181 additions & 14 deletions

File tree

cert/cert.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ type Certificate interface {
5858
// PublicKey is the raw bytes to be used in asymmetric cryptographic operations.
5959
PublicKey() []byte
6060

61+
// MarshalPublicKeyPEM is the value of PublicKey marshalled to PEM
62+
MarshalPublicKeyPEM() []byte
63+
6164
// Curve identifies which curve was used for the PublicKey and Signature.
6265
Curve() Curve
6366

cert/cert_v1.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ func (c *certificateV1) PublicKey() []byte {
8383
return c.details.publicKey
8484
}
8585

86+
func (c *certificateV1) MarshalPublicKeyPEM() []byte {
87+
return marshalCertPublicKeyToPEM(c)
88+
}
89+
8690
func (c *certificateV1) Signature() []byte {
8791
return c.signature
8892
}

cert/cert_v1_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cert
22

33
import (
4+
"crypto/ed25519"
45
"fmt"
56
"net/netip"
67
"testing"
@@ -13,6 +14,7 @@ import (
1314
)
1415

1516
func TestCertificateV1_Marshal(t *testing.T) {
17+
t.Parallel()
1618
before := time.Now().Add(time.Second * -60).Round(time.Second)
1719
after := time.Now().Add(time.Second * 60).Round(time.Second)
1820
pubKey := []byte("1234567890abcedfghij1234567890ab")
@@ -60,6 +62,58 @@ func TestCertificateV1_Marshal(t *testing.T) {
6062
assert.Equal(t, nc.Groups(), nc2.Groups())
6163
}
6264

65+
func TestCertificateV1_PublicKeyPem(t *testing.T) {
66+
t.Parallel()
67+
before := time.Now().Add(time.Second * -60).Round(time.Second)
68+
after := time.Now().Add(time.Second * 60).Round(time.Second)
69+
pubKey := ed25519.PublicKey("1234567890abcedfghij1234567890ab")
70+
71+
nc := certificateV1{
72+
details: detailsV1{
73+
name: "testing",
74+
networks: []netip.Prefix{},
75+
unsafeNetworks: []netip.Prefix{},
76+
groups: []string{"test-group1", "test-group2", "test-group3"},
77+
notBefore: before,
78+
notAfter: after,
79+
publicKey: pubKey,
80+
isCA: false,
81+
issuer: "1234567890abcedfghij1234567890ab",
82+
},
83+
signature: []byte("1234567890abcedfghij1234567890ab"),
84+
}
85+
86+
assert.Equal(t, Version1, nc.Version())
87+
assert.Equal(t, Curve_CURVE25519, nc.Curve())
88+
pubPem := "-----BEGIN NEBULA X25519 PUBLIC KEY-----\nMTIzNDU2Nzg5MGFiY2VkZmdoaWoxMjM0NTY3ODkwYWI=\n-----END NEBULA X25519 PUBLIC KEY-----\n"
89+
assert.Equal(t, string(nc.MarshalPublicKeyPEM()), pubPem)
90+
assert.False(t, nc.IsCA())
91+
92+
nc.details.isCA = true
93+
assert.Equal(t, Curve_CURVE25519, nc.Curve())
94+
pubPem = "-----BEGIN NEBULA ED25519 PUBLIC KEY-----\nMTIzNDU2Nzg5MGFiY2VkZmdoaWoxMjM0NTY3ODkwYWI=\n-----END NEBULA ED25519 PUBLIC KEY-----\n"
95+
assert.Equal(t, string(nc.MarshalPublicKeyPEM()), pubPem)
96+
assert.True(t, nc.IsCA())
97+
98+
pubP256KeyPem := []byte(`-----BEGIN NEBULA P256 PUBLIC KEY-----
99+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
100+
AAAAAAAAAAAAAAAAAAAAAAA=
101+
-----END NEBULA P256 PUBLIC KEY-----
102+
`)
103+
pubP256Key, _, _, err := UnmarshalPublicKeyFromPEM(pubP256KeyPem)
104+
require.NoError(t, err)
105+
nc.details.curve = Curve_P256
106+
nc.details.publicKey = pubP256Key
107+
assert.Equal(t, Curve_P256, nc.Curve())
108+
assert.Equal(t, string(nc.MarshalPublicKeyPEM()), string(pubP256KeyPem))
109+
assert.True(t, nc.IsCA())
110+
111+
nc.details.isCA = false
112+
assert.Equal(t, Curve_P256, nc.Curve())
113+
assert.Equal(t, string(nc.MarshalPublicKeyPEM()), string(pubP256KeyPem))
114+
assert.False(t, nc.IsCA())
115+
}
116+
63117
func TestCertificateV1_Expired(t *testing.T) {
64118
nc := certificateV1{
65119
details: detailsV1{

cert/cert_v2.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@ func (c *certificateV2) PublicKey() []byte {
114114
return c.publicKey
115115
}
116116

117+
func (c *certificateV2) MarshalPublicKeyPEM() []byte {
118+
return marshalCertPublicKeyToPEM(c)
119+
}
120+
117121
func (c *certificateV2) Signature() []byte {
118122
return c.signature
119123
}

cert/cert_v2_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
)
1616

1717
func TestCertificateV2_Marshal(t *testing.T) {
18+
t.Parallel()
1819
before := time.Now().Add(time.Second * -60).Round(time.Second)
1920
after := time.Now().Add(time.Second * 60).Round(time.Second)
2021
pubKey := []byte("1234567890abcedfghij1234567890ab")
@@ -75,6 +76,58 @@ func TestCertificateV2_Marshal(t *testing.T) {
7576
assert.Equal(t, nc.Groups(), nc2.Groups())
7677
}
7778

79+
func TestCertificateV2_PublicKeyPem(t *testing.T) {
80+
t.Parallel()
81+
before := time.Now().Add(time.Second * -60).Round(time.Second)
82+
after := time.Now().Add(time.Second * 60).Round(time.Second)
83+
pubKey := ed25519.PublicKey("1234567890abcedfghij1234567890ab")
84+
85+
nc := certificateV2{
86+
details: detailsV2{
87+
name: "testing",
88+
networks: []netip.Prefix{},
89+
unsafeNetworks: []netip.Prefix{},
90+
groups: []string{"test-group1", "test-group2", "test-group3"},
91+
notBefore: before,
92+
notAfter: after,
93+
isCA: false,
94+
issuer: "1234567890abcedfghij1234567890ab",
95+
},
96+
publicKey: pubKey,
97+
signature: []byte("1234567890abcedfghij1234567890ab"),
98+
}
99+
100+
assert.Equal(t, Version2, nc.Version())
101+
assert.Equal(t, Curve_CURVE25519, nc.Curve())
102+
pubPem := "-----BEGIN NEBULA X25519 PUBLIC KEY-----\nMTIzNDU2Nzg5MGFiY2VkZmdoaWoxMjM0NTY3ODkwYWI=\n-----END NEBULA X25519 PUBLIC KEY-----\n"
103+
assert.Equal(t, string(nc.MarshalPublicKeyPEM()), pubPem)
104+
assert.False(t, nc.IsCA())
105+
106+
nc.details.isCA = true
107+
assert.Equal(t, Curve_CURVE25519, nc.Curve())
108+
pubPem = "-----BEGIN NEBULA ED25519 PUBLIC KEY-----\nMTIzNDU2Nzg5MGFiY2VkZmdoaWoxMjM0NTY3ODkwYWI=\n-----END NEBULA ED25519 PUBLIC KEY-----\n"
109+
assert.Equal(t, string(nc.MarshalPublicKeyPEM()), pubPem)
110+
assert.True(t, nc.IsCA())
111+
112+
pubP256KeyPem := []byte(`-----BEGIN NEBULA P256 PUBLIC KEY-----
113+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
114+
AAAAAAAAAAAAAAAAAAAAAAA=
115+
-----END NEBULA P256 PUBLIC KEY-----
116+
`)
117+
pubP256Key, _, _, err := UnmarshalPublicKeyFromPEM(pubP256KeyPem)
118+
require.NoError(t, err)
119+
nc.curve = Curve_P256
120+
nc.publicKey = pubP256Key
121+
assert.Equal(t, Curve_P256, nc.Curve())
122+
assert.Equal(t, string(nc.MarshalPublicKeyPEM()), string(pubP256KeyPem))
123+
assert.True(t, nc.IsCA())
124+
125+
nc.details.isCA = false
126+
assert.Equal(t, Curve_P256, nc.Curve())
127+
assert.Equal(t, string(nc.MarshalPublicKeyPEM()), string(pubP256KeyPem))
128+
assert.False(t, nc.IsCA())
129+
}
130+
78131
func TestCertificateV2_Expired(t *testing.T) {
79132
nc := certificateV2{
80133
details: detailsV2{

cert/pem.go

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,26 @@ import (
77
"golang.org/x/crypto/ed25519"
88
)
99

10-
const (
11-
CertificateBanner = "NEBULA CERTIFICATE"
12-
CertificateV2Banner = "NEBULA CERTIFICATE V2"
13-
X25519PrivateKeyBanner = "NEBULA X25519 PRIVATE KEY"
14-
X25519PublicKeyBanner = "NEBULA X25519 PUBLIC KEY"
15-
EncryptedEd25519PrivateKeyBanner = "NEBULA ED25519 ENCRYPTED PRIVATE KEY"
16-
Ed25519PrivateKeyBanner = "NEBULA ED25519 PRIVATE KEY"
17-
Ed25519PublicKeyBanner = "NEBULA ED25519 PUBLIC KEY"
18-
19-
P256PrivateKeyBanner = "NEBULA P256 PRIVATE KEY"
20-
P256PublicKeyBanner = "NEBULA P256 PUBLIC KEY"
10+
const ( //cert banners
11+
CertificateBanner = "NEBULA CERTIFICATE"
12+
CertificateV2Banner = "NEBULA CERTIFICATE V2"
13+
)
14+
15+
const ( //key-agreement-key banners
16+
X25519PrivateKeyBanner = "NEBULA X25519 PRIVATE KEY"
17+
X25519PublicKeyBanner = "NEBULA X25519 PUBLIC KEY"
18+
P256PrivateKeyBanner = "NEBULA P256 PRIVATE KEY"
19+
P256PublicKeyBanner = "NEBULA P256 PUBLIC KEY"
20+
)
21+
22+
/* including "ECDSA" in the P256 banners is a clue that these keys should be used only for signing */
23+
const ( //signing key banners
2124
EncryptedECDSAP256PrivateKeyBanner = "NEBULA ECDSA P256 ENCRYPTED PRIVATE KEY"
2225
ECDSAP256PrivateKeyBanner = "NEBULA ECDSA P256 PRIVATE KEY"
26+
ECDSAP256PublicKeyBanner = "NEBULA ECDSA P256 PUBLIC KEY"
27+
EncryptedEd25519PrivateKeyBanner = "NEBULA ED25519 ENCRYPTED PRIVATE KEY"
28+
Ed25519PrivateKeyBanner = "NEBULA ED25519 PRIVATE KEY"
29+
Ed25519PublicKeyBanner = "NEBULA ED25519 PUBLIC KEY"
2330
)
2431

2532
// 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) {
5158

5259
}
5360

61+
func marshalCertPublicKeyToPEM(c Certificate) []byte {
62+
if c.IsCA() {
63+
return MarshalSigningPublicKeyToPEM(c.Curve(), c.PublicKey())
64+
} else {
65+
return MarshalPublicKeyToPEM(c.Curve(), c.PublicKey())
66+
}
67+
}
68+
69+
// MarshalPublicKeyToPEM returns a PEM representation of a public key used for ECDH.
70+
// if your public key came from a certificate, prefer Certificate.PublicKeyPEM() if possible, to avoid mistakes!
5471
func MarshalPublicKeyToPEM(curve Curve, b []byte) []byte {
5572
switch curve {
5673
case Curve_CURVE25519:
@@ -62,6 +79,19 @@ func MarshalPublicKeyToPEM(curve Curve, b []byte) []byte {
6279
}
6380
}
6481

82+
// MarshalSigningPublicKeyToPEM returns a PEM representation of a public key used for signing.
83+
// if your public key came from a certificate, prefer Certificate.PublicKeyPEM() if possible, to avoid mistakes!
84+
func MarshalSigningPublicKeyToPEM(curve Curve, b []byte) []byte {
85+
switch curve {
86+
case Curve_CURVE25519:
87+
return pem.EncodeToMemory(&pem.Block{Type: Ed25519PublicKeyBanner, Bytes: b})
88+
case Curve_P256:
89+
return pem.EncodeToMemory(&pem.Block{Type: P256PublicKeyBanner, Bytes: b})
90+
default:
91+
return nil
92+
}
93+
}
94+
6595
func UnmarshalPublicKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) {
6696
k, r := pem.Decode(b)
6797
if k == nil {
@@ -73,7 +103,7 @@ func UnmarshalPublicKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) {
73103
case X25519PublicKeyBanner, Ed25519PublicKeyBanner:
74104
expectedLen = 32
75105
curve = Curve_CURVE25519
76-
case P256PublicKeyBanner:
106+
case P256PublicKeyBanner, ECDSAP256PublicKeyBanner:
77107
// Uncompressed
78108
expectedLen = 65
79109
curve = Curve_P256

cert/pem_test.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
177177
}
178178

179179
func TestUnmarshalPublicKeyFromPEM(t *testing.T) {
180+
t.Parallel()
180181
pubKey := []byte(`# A good key
181182
-----BEGIN NEBULA ED25519 PUBLIC KEY-----
182183
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
@@ -230,6 +231,7 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
230231
}
231232

232233
func TestUnmarshalX25519PublicKey(t *testing.T) {
234+
t.Parallel()
233235
pubKey := []byte(`# A good key
234236
-----BEGIN NEBULA X25519 PUBLIC KEY-----
235237
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
@@ -240,6 +242,12 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
240242
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
241243
AAAAAAAAAAAAAAAAAAAAAAA=
242244
-----END NEBULA P256 PUBLIC KEY-----
245+
`)
246+
oldPubP256Key := []byte(`# A good key
247+
-----BEGIN NEBULA ECDSA P256 PUBLIC KEY-----
248+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
249+
AAAAAAAAAAAAAAAAAAAAAAA=
250+
-----END NEBULA ECDSA P256 PUBLIC KEY-----
243251
`)
244252
shortKey := []byte(`# A short key
245253
-----BEGIN NEBULA X25519 PUBLIC KEY-----
@@ -256,15 +264,22 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
256264
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
257265
-END NEBULA X25519 PUBLIC KEY-----`)
258266

259-
keyBundle := appendByteSlices(pubKey, pubP256Key, shortKey, invalidBanner, invalidPem)
267+
keyBundle := appendByteSlices(pubKey, pubP256Key, oldPubP256Key, shortKey, invalidBanner, invalidPem)
260268

261269
// Success test case
262270
k, rest, curve, err := UnmarshalPublicKeyFromPEM(keyBundle)
263271
assert.Len(t, k, 32)
264272
require.NoError(t, err)
265-
assert.Equal(t, rest, appendByteSlices(pubP256Key, shortKey, invalidBanner, invalidPem))
273+
assert.Equal(t, rest, appendByteSlices(pubP256Key, oldPubP256Key, shortKey, invalidBanner, invalidPem))
266274
assert.Equal(t, Curve_CURVE25519, curve)
267275

276+
// Success test case
277+
k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest)
278+
assert.Len(t, k, 65)
279+
require.NoError(t, err)
280+
assert.Equal(t, rest, appendByteSlices(oldPubP256Key, shortKey, invalidBanner, invalidPem))
281+
assert.Equal(t, Curve_P256, curve)
282+
268283
// Success test case
269284
k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest)
270285
assert.Len(t, k, 65)

connection_manager_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,10 @@ func (d *dummyCert) PublicKey() []byte {
446446
return d.publicKey
447447
}
448448

449+
func (d *dummyCert) MarshalPublicKeyPEM() []byte {
450+
return cert.MarshalPublicKeyToPEM(d.curve, d.publicKey)
451+
}
452+
449453
func (d *dummyCert) Signature() []byte {
450454
return d.signature
451455
}

0 commit comments

Comments
 (0)