Skip to content

Commit 2227d9c

Browse files
Merge pull request #7 from disentangle-network/feature/pq-handshake
Merge PQ handshake, CLI, and e2e test to master
2 parents d2beb29 + ade6a4b commit 2227d9c

File tree

19 files changed

+570
-53
lines changed

19 files changed

+570
-53
lines changed

cert/cert.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,20 @@ func Recombine(v Version, rawCertBytes, publicKey []byte, curve Curve) (Certific
158158
return c, nil
159159
}
160160

161+
// UnmarshalCertificateFromBytes unmarshals a certificate from raw bytes when the full certificate
162+
// (including public key) is already present. This is used for PQ certificates where the public key
163+
// is not stripped during handshake marshaling.
164+
func UnmarshalCertificateFromBytes(b []byte, v Version, curve Curve) (Certificate, error) {
165+
switch v {
166+
case VersionPre1, Version1:
167+
return unmarshalCertificateV1(b, nil)
168+
case Version2:
169+
return unmarshalCertificateV2(b, nil, curve)
170+
default:
171+
return nil, ErrUnknownVersion
172+
}
173+
}
174+
161175
// CalculateAlternateFingerprint calculates a 2nd fingerprint representation for P256 certificates
162176
// CAPool blocklist testing through `VerifyCertificate` and `VerifyCachedCertificate` automatically performs this step.
163177
func CalculateAlternateFingerprint(c Certificate) (string, error) {

cert/cert_v2.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -239,13 +239,14 @@ func (c *certificateV2) VerifyPrivateKey(curve Curve, key []byte) error {
239239
pub = privkey.PublicKey().Bytes()
240240
case Curve_PQ:
241241
// Host certs use ML-KEM-1024 key agreement keys
242-
if len(key) != mlkem1024.PrivateKeySize {
242+
var sk mlkem1024.PrivateKey
243+
if err := sk.Unpack(key); err != nil {
244+
return ErrInvalidPrivateKey
245+
}
246+
derivedPub, err := sk.Public().(*mlkem1024.PublicKey).MarshalBinary()
247+
if err != nil {
243248
return ErrInvalidPrivateKey
244249
}
245-
_, sk := mlkem1024.NewKeyFromSeed(key[:mlkem1024.KeySeedSize])
246-
pub, _ = sk.MarshalBinary()
247-
// Compare only the public key portion
248-
derivedPub := pub[:mlkem1024.PublicKeySize]
249250
if !bytes.Equal(derivedPub, c.publicKey) {
250251
return ErrPublicPrivateKeyMismatch
251252
}

cert/pem.go

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,25 @@ const ( //cert banners
1515
)
1616

1717
const ( //key-agreement-key banners
18-
X25519PrivateKeyBanner = "NEBULA X25519 PRIVATE KEY"
19-
X25519PublicKeyBanner = "NEBULA X25519 PUBLIC KEY"
20-
P256PrivateKeyBanner = "NEBULA P256 PRIVATE KEY"
21-
P256PublicKeyBanner = "NEBULA P256 PUBLIC KEY"
18+
X25519PrivateKeyBanner = "NEBULA X25519 PRIVATE KEY"
19+
X25519PublicKeyBanner = "NEBULA X25519 PUBLIC KEY"
20+
P256PrivateKeyBanner = "NEBULA P256 PRIVATE KEY"
21+
P256PublicKeyBanner = "NEBULA P256 PUBLIC KEY"
2222
MLKEM1024PrivateKeyBanner = "NEBULA MLKEM1024 PRIVATE KEY"
2323
MLKEM1024PublicKeyBanner = "NEBULA MLKEM1024 PUBLIC KEY"
2424
)
2525

2626
/* including "ECDSA" in the P256 banners is a clue that these keys should be used only for signing */
2727
const ( //signing key banners
28-
EncryptedECDSAP256PrivateKeyBanner = "NEBULA ECDSA P256 ENCRYPTED PRIVATE KEY"
29-
ECDSAP256PrivateKeyBanner = "NEBULA ECDSA P256 PRIVATE KEY"
30-
ECDSAP256PublicKeyBanner = "NEBULA ECDSA P256 PUBLIC KEY"
31-
EncryptedEd25519PrivateKeyBanner = "NEBULA ED25519 ENCRYPTED PRIVATE KEY"
32-
Ed25519PrivateKeyBanner = "NEBULA ED25519 PRIVATE KEY"
33-
Ed25519PublicKeyBanner = "NEBULA ED25519 PUBLIC KEY"
34-
EncryptedMLDSA87PrivateKeyBanner = "NEBULA MLDSA87 ENCRYPTED PRIVATE KEY"
35-
MLDSA87PrivateKeyBanner = "NEBULA MLDSA87 PRIVATE KEY"
36-
MLDSA87PublicKeyBanner = "NEBULA MLDSA87 PUBLIC KEY"
28+
EncryptedECDSAP256PrivateKeyBanner = "NEBULA ECDSA P256 ENCRYPTED PRIVATE KEY"
29+
ECDSAP256PrivateKeyBanner = "NEBULA ECDSA P256 PRIVATE KEY"
30+
ECDSAP256PublicKeyBanner = "NEBULA ECDSA P256 PUBLIC KEY"
31+
EncryptedEd25519PrivateKeyBanner = "NEBULA ED25519 ENCRYPTED PRIVATE KEY"
32+
Ed25519PrivateKeyBanner = "NEBULA ED25519 PRIVATE KEY"
33+
Ed25519PublicKeyBanner = "NEBULA ED25519 PUBLIC KEY"
34+
EncryptedMLDSA87PrivateKeyBanner = "NEBULA MLDSA87 ENCRYPTED PRIVATE KEY"
35+
MLDSA87PrivateKeyBanner = "NEBULA MLDSA87 PRIVATE KEY"
36+
MLDSA87PublicKeyBanner = "NEBULA MLDSA87 PUBLIC KEY"
3737
)
3838

3939
// UnmarshalCertificateFromPEM will try to unmarshal the first pem block in a byte array, returning any non consumed

cert/sign_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ func TestCertificateV2_SignPQ(t *testing.T) {
173173
assert.Equal(t, pubBytes, c.PublicKey())
174174

175175
// Verify signature length matches ML-DSA-87
176-
assert.Equal(t, mldsa87.SignatureSize, len(c.Signature()))
176+
assert.Len(t, c.Signature(), mldsa87.SignatureSize)
177177

178178
// Marshal and unmarshal roundtrip
179179
b, err := c.Marshal()

cert_test/cert.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"net/netip"
1010
"time"
1111

12+
"github.com/cloudflare/circl/kem/mlkem/mlkem1024"
13+
"github.com/cloudflare/circl/sign/mldsa/mldsa87"
1214
"github.com/slackhq/nebula/cert"
1315
"golang.org/x/crypto/curve25519"
1416
"golang.org/x/crypto/ed25519"
@@ -30,6 +32,13 @@ func NewTestCaCert(version cert.Version, curve cert.Curve, before, after time.Ti
3032

3133
pub = elliptic.Marshal(elliptic.P256(), privk.PublicKey.X, privk.PublicKey.Y)
3234
priv = privk.D.FillBytes(make([]byte, 32))
35+
case cert.Curve_PQ:
36+
pk, sk, err := mldsa87.GenerateKey(rand.Reader)
37+
if err != nil {
38+
panic(err)
39+
}
40+
pub = pk.Bytes()
41+
priv = sk.Bytes()
3342
default:
3443
// There is no default to allow the underlying lib to respond with an error
3544
}
@@ -84,6 +93,8 @@ func NewTestCert(v cert.Version, curve cert.Curve, ca cert.Certificate, key []by
8493
pub, priv = X25519Keypair()
8594
case cert.Curve_P256:
8695
pub, priv = P256Keypair()
96+
case cert.Curve_PQ:
97+
pub, priv = PQKeypair()
8798
default:
8899
panic("unknown curve")
89100
}
@@ -163,3 +174,13 @@ func P256Keypair() ([]byte, []byte) {
163174
pubkey := privkey.PublicKey()
164175
return pubkey.Bytes(), privkey.Bytes()
165176
}
177+
178+
func PQKeypair() ([]byte, []byte) {
179+
pk, sk, err := mlkem1024.GenerateKeyPair(rand.Reader)
180+
if err != nil {
181+
panic(err)
182+
}
183+
pubBytes, _ := pk.MarshalBinary()
184+
privBytes, _ := sk.MarshalBinary()
185+
return pubBytes, privBytes
186+
}

cmd/nebula-cert/ca.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"strings"
1414
"time"
1515

16+
"github.com/cloudflare/circl/sign/mldsa/mldsa87"
1617
"github.com/skip2/go-qrcode"
1718
"github.com/slackhq/nebula/cert"
1819
"github.com/slackhq/nebula/pkclient"
@@ -59,7 +60,7 @@ func newCaFlags() *caFlags {
5960
cf.argonParallelism = cf.set.Uint("argon-parallelism", 4, "Optional: Argon2 parallelism parameter used for encrypted private key passphrase")
6061
cf.argonIterations = cf.set.Uint("argon-iterations", 1, "Optional: Argon2 iterations parameter used for encrypted private key passphrase")
6162
cf.encryption = cf.set.Bool("encrypt", false, "Optional: prompt for passphrase and write out-key in an encrypted format")
62-
cf.curve = cf.set.String("curve", "25519", "EdDSA/ECDSA Curve (25519, P256)")
63+
cf.curve = cf.set.String("curve", "25519", "EdDSA/ECDSA/PQ Curve (25519, P256, PQ)")
6364
cf.p11url = p11Flag(cf.set)
6465

6566
cf.ips = cf.set.String("ips", "", "Deprecated, see -networks")
@@ -243,11 +244,23 @@ func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error
243244
}
244245
rawPriv = eKey.Bytes()
245246
pub = eKey.PublicKey().Bytes()
247+
case "PQ":
248+
curve = cert.Curve_PQ
249+
pk, sk, err := mldsa87.GenerateKey(rand.Reader)
250+
if err != nil {
251+
return fmt.Errorf("error while generating ML-DSA-87 keys: %s", err)
252+
}
253+
pub = pk.Bytes()
254+
rawPriv = sk.Bytes()
246255
default:
247256
return fmt.Errorf("invalid curve: %s", *cf.curve)
248257
}
249258
}
250259

260+
if curve == cert.Curve_PQ && version != cert.Version2 {
261+
return fmt.Errorf("PQ curve requires certificate version 2")
262+
}
263+
251264
t := &cert.TBSCertificate{
252265
Version: version,
253266
Name: *cf.name,

cmd/nebula-cert/ca_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func Test_caHelp(t *testing.T) {
3434
" -argon-parallelism uint\n"+
3535
" \tOptional: Argon2 parallelism parameter used for encrypted private key passphrase (default 4)\n"+
3636
" -curve string\n"+
37-
" \tEdDSA/ECDSA Curve (25519, P256) (default \"25519\")\n"+
37+
" \tEdDSA/ECDSA/PQ Curve (25519, P256, PQ) (default \"25519\")\n"+
3838
" -duration duration\n"+
3939
" \tOptional: amount of time the certificate should be valid for. Valid time units are seconds: \"s\", minutes: \"m\", hours: \"h\" (default 8760h0m0s)\n"+
4040
" -encrypt\n"+

cmd/nebula-cert/keygen.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func newKeygenFlags() *keygenFlags {
2424
cf.set.Usage = func() {}
2525
cf.outPubPath = cf.set.String("out-pub", "", "Required: path to write the public key to")
2626
cf.outKeyPath = cf.set.String("out-key", "", "Required: path to write the private key to")
27-
cf.curve = cf.set.String("curve", "25519", "ECDH Curve (25519, P256)")
27+
cf.curve = cf.set.String("curve", "25519", "ECDH/KEM Curve (25519, P256, PQ)")
2828
cf.p11url = p11Flag(cf.set)
2929
return &cf
3030
}
@@ -64,6 +64,9 @@ func keygen(args []string, out io.Writer, errOut io.Writer) error {
6464
case "P256":
6565
pub, rawPriv = p256Keypair()
6666
curve = cert.Curve_P256
67+
case "PQ":
68+
pub, rawPriv = pqKeypair()
69+
curve = cert.Curve_PQ
6770
default:
6871
return fmt.Errorf("invalid curve: %s", *cf.curve)
6972
}

cmd/nebula-cert/keygen_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func Test_keygenHelp(t *testing.T) {
2121
t,
2222
"Usage of "+os.Args[0]+" keygen <flags>: create a public/private key pair. the public key can be passed to `nebula-cert sign`\n"+
2323
" -curve string\n"+
24-
" \tECDH Curve (25519, P256) (default \"25519\")\n"+
24+
" \tECDH/KEM Curve (25519, P256, PQ) (default \"25519\")\n"+
2525
" -out-key string\n"+
2626
" \tRequired: path to write the private key to\n"+
2727
" -out-pub string\n"+

cmd/nebula-cert/sign.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414

1515
"github.com/skip2/go-qrcode"
1616
"github.com/slackhq/nebula/cert"
17+
"github.com/slackhq/nebula/noiseutil"
1718
"github.com/slackhq/nebula/pkclient"
1819
"golang.org/x/crypto/curve25519"
1920
)
@@ -405,6 +406,8 @@ func newKeypair(curve cert.Curve) ([]byte, []byte) {
405406
return x25519Keypair()
406407
case cert.Curve_P256:
407408
return p256Keypair()
409+
case cert.Curve_PQ:
410+
return pqKeypair()
408411
default:
409412
return nil, nil
410413
}
@@ -433,6 +436,14 @@ func p256Keypair() ([]byte, []byte) {
433436
return pubkey.Bytes(), privkey.Bytes()
434437
}
435438

439+
func pqKeypair() ([]byte, []byte) {
440+
pub, priv, err := noiseutil.PQKEMKeypair()
441+
if err != nil {
442+
panic(err)
443+
}
444+
return pub, priv
445+
}
446+
436447
func signSummary() string {
437448
return "sign <flags>: create and sign a certificate"
438449
}

0 commit comments

Comments
 (0)