mirror of
https://github.com/slackhq/nebula.git
synced 2026-11-15 12:36:52 +01:00
cert: Name the post-quantum algorithms of a version 3 certificate with a PQSuite, like its curve, and pick the hybrid subtype by the suite's KEM
This commit is contained in:
1 parent
84e28c62ea
commit
656164b259
29 files changed
+1124
-365
No files matched your search
+6
-4
@@ -10,10 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Added
|
||||
|
||||
- Version 3 certificates and post-quantum hybrid handshakes (`ix_hybrid_mlkem1024`). A version 3 certificate is a
|
||||
version 2 certificate extended with an ML-KEM-1024 key (hosts) or an ML-DSA-87 key (CAs) and a deterministic
|
||||
ML-DSA-87 CA signature, and a node holding one still presents the version 2 certificate it extends to peers that
|
||||
need it. `nebula-cert pq-extend` upgrades existing CAs and hosts in place, and extending a certificate twice gives
|
||||
the same certificate. See `docs/pq-handshake.md`.
|
||||
version 2 certificate extended with a post-quantum key and a deterministic post-quantum CA signature, and a node
|
||||
holding one still presents the version 2 certificate it extends to peers that need it. Its post-quantum suite names
|
||||
the algorithms, as its curve does the classical ones; the one suite today is `MLKEM1024_MLDSA87`, an ML-KEM-1024
|
||||
key for hosts and ML-DSA-87 for CAs and their signatures, chosen with `nebula-cert -pq-suite`. `nebula-cert
|
||||
pq-extend` upgrades existing CAs and hosts in place, and extending a certificate twice gives the same certificate.
|
||||
See `docs/pq-handshake.md`.
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -266,6 +266,10 @@ func (ncp *CAPool) verify(c Certificate, now time.Time, certFp string, signerFp
|
||||
return nil, ErrCurveMismatch
|
||||
}
|
||||
|
||||
if PQSuiteOf(signer.Certificate) != PQSuiteOf(c) {
|
||||
return nil, ErrPQSuiteMismatch
|
||||
}
|
||||
|
||||
if signer.Certificate.Expired(now) {
|
||||
return nil, ErrRootExpired
|
||||
}
|
||||
|
||||
+86
-94
@@ -20,28 +20,32 @@ import (
|
||||
// the fields of the v2 certificate it extends, so a v3 holder can always marshal that v2 form, byte for byte, and use
|
||||
// it with peers that only speak v2. The v2 form keeps its own classical CA signature and fingerprint.
|
||||
//
|
||||
// Hosts carry an ML-KEM-1024 encapsulation key and CAs an ML-DSA-87 public key. The issuing CA signs every field
|
||||
// with ML-DSA-87, including the classical signature, so the classical signature can not be swapped out either. The
|
||||
// ML-DSA-87 signature is deterministic, so a certificate extended twice is the same certificate.
|
||||
// The certificate's PQSuite says which post-quantum algorithms it uses, the way its curve does for the classical
|
||||
// ones. Hosts carry a public key for the suite's KEM and CAs one for its signature algorithm. The issuing CA, which
|
||||
// has the same suite, signs every field with that signature algorithm, including the suite and the classical
|
||||
// signature, so neither can be swapped out. Signatures are deterministic, so a certificate extended twice is the same
|
||||
// certificate.
|
||||
//
|
||||
// Wherever a v3 certificate deals in keys they are composite: the classical key followed by the post-quantum one.
|
||||
// PublicKey returns classical || post-quantum, and VerifyPrivateKey and signing take the private keys the same way.
|
||||
|
||||
const (
|
||||
TagCertPQPublicKey = 4 | classContextSpecific
|
||||
TagCertPQSignature = 5 | classContextSpecific
|
||||
TagCertPQSuite = 4 | classContextSpecific
|
||||
TagCertPQPublicKey = 5 | classContextSpecific
|
||||
TagCertPQSignature = 6 | classContextSpecific
|
||||
)
|
||||
|
||||
// Sizes of the keys and signatures of PQSuite_MLKEM1024_MLDSA87.
|
||||
const (
|
||||
// MLKEM1024PublicKeySize is the size of the ML-KEM-1024 encapsulation key in a v3 host certificate.
|
||||
// MLKEM1024PublicKeySize is the size of an ML-KEM-1024 encapsulation key.
|
||||
MLKEM1024PublicKeySize = mlkem.EncapsulationKeySize1024
|
||||
// MLKEM1024PrivateKeySize is the size of the ML-KEM-1024 decapsulation key seed in a v3 host private key.
|
||||
// MLKEM1024PrivateKeySize is the size of an ML-KEM-1024 decapsulation key seed.
|
||||
MLKEM1024PrivateKeySize = mlkem.SeedSize
|
||||
// MLDSA87PublicKeySize is the size of the ML-DSA-87 public key in a v3 CA certificate.
|
||||
// MLDSA87PublicKeySize is the size of an ML-DSA-87 public key.
|
||||
MLDSA87PublicKeySize = mldsa.MLDSA87PublicKeySize
|
||||
// MLDSA87PrivateKeySize is the size of the ML-DSA-87 private key seed in a v3 CA private key.
|
||||
// MLDSA87PrivateKeySize is the size of an ML-DSA-87 private key seed.
|
||||
MLDSA87PrivateKeySize = mldsa.PrivateKeySize
|
||||
// MLDSA87SignatureSize is the size of the ML-DSA-87 signature in every v3 certificate.
|
||||
// MLDSA87SignatureSize is the size of an ML-DSA-87 signature.
|
||||
MLDSA87SignatureSize = mldsa.MLDSA87SignatureSize
|
||||
|
||||
// pqSignatureContext is the ML-DSA context string for certificate signatures.
|
||||
@@ -52,38 +56,16 @@ type certificateV3 struct {
|
||||
// v2 is the version 2 certificate this one extends. Its public key is the classical key.
|
||||
v2 certificateV2
|
||||
|
||||
// pqPublicKey is the ML-KEM-1024 encapsulation key of a host, or the ML-DSA-87 public key of a CA.
|
||||
// pqSuite names the post-quantum algorithms, and so the kind of pqPublicKey and pqSignature.
|
||||
pqSuite PQSuite
|
||||
|
||||
// pqPublicKey is the suite's KEM public key of a host, or its signature algorithm's public key of a CA.
|
||||
pqPublicKey []byte
|
||||
|
||||
// pqSignature is the issuing CA's ML-DSA-87 signature over every other field.
|
||||
// pqSignature is the issuing CA's post-quantum signature over every other field.
|
||||
pqSignature []byte
|
||||
}
|
||||
|
||||
// pqPublicKeySize returns the size of the post-quantum public key a v3 certificate carries.
|
||||
func pqPublicKeySize(isCA bool) int {
|
||||
if isCA {
|
||||
return MLDSA87PublicKeySize
|
||||
}
|
||||
return MLKEM1024PublicKeySize
|
||||
}
|
||||
|
||||
// pqPrivateKeySize returns the size of the post-quantum half of a v3 private key.
|
||||
func pqPrivateKeySize(isCA bool) int {
|
||||
if isCA {
|
||||
return MLDSA87PrivateKeySize
|
||||
}
|
||||
return MLKEM1024PrivateKeySize
|
||||
}
|
||||
|
||||
// splitPQPublicKey splits a composite public key into its classical and post-quantum halves.
|
||||
func splitPQPublicKey(isCA bool, key []byte) ([]byte, []byte, error) {
|
||||
n := pqPublicKeySize(isCA)
|
||||
if len(key) <= n {
|
||||
return nil, nil, ErrInvalidPublicKey
|
||||
}
|
||||
return key[:len(key)-n], key[len(key)-n:], nil
|
||||
}
|
||||
|
||||
// V2Form returns the version 2 certificate a version 3 certificate extends, or nil for any other version. It is
|
||||
// what a v3 holder presents to peers that do not speak v3.
|
||||
func V2Form(c Certificate) Certificate {
|
||||
@@ -159,24 +141,26 @@ func (c *certificateV3) Fingerprint() (string, error) {
|
||||
return hex.EncodeToString(sum[:]), nil
|
||||
}
|
||||
|
||||
// CheckSignature checks both signatures against the composite public key of the signing CA.
|
||||
// CheckSignature checks both signatures against the composite public key of the signing CA. Like the classical
|
||||
// signature with the curve, the post-quantum signature is checked with the algorithm of this certificate's suite,
|
||||
// which the signing CA has to share.
|
||||
func (c *certificateV3) CheckSignature(key []byte) bool {
|
||||
classical, pq, err := splitPQPublicKey(true, key)
|
||||
if err != nil || len(pq) != MLDSA87PublicKeySize {
|
||||
p := pqSuites[c.pqSuite]
|
||||
if p == nil {
|
||||
return false
|
||||
}
|
||||
classical, pq, err := c.pqSuite.SplitPublicKey(true, key)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if !c.v2.CheckSignature(classical) {
|
||||
return false
|
||||
}
|
||||
pk, err := mldsa.NewPublicKey(mldsa.MLDSA87(), pq)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
b, err := c.marshalForPQSigning()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return mldsa.Verify(pk, b, c.pqSignature, &mldsa.Options{Context: pqSignatureContext}) == nil
|
||||
return p.dsa.verify(pq, b, c.pqSignature)
|
||||
}
|
||||
|
||||
func (c *certificateV3) Expired(t time.Time) bool {
|
||||
@@ -185,28 +169,17 @@ func (c *certificateV3) Expired(t time.Time) bool {
|
||||
|
||||
// VerifyPrivateKey checks a composite private key, classical || post-quantum seed, against the composite public key.
|
||||
func (c *certificateV3) VerifyPrivateKey(curve Curve, key []byte) error {
|
||||
n := pqPrivateKeySize(c.IsCA())
|
||||
if len(key) <= n {
|
||||
return ErrInvalidPrivateKey
|
||||
classical, seed, err := c.pqSuite.SplitPrivateKey(c.IsCA(), key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.v2.VerifyPrivateKey(curve, key[:len(key)-n]); err != nil {
|
||||
if err := c.v2.VerifyPrivateKey(curve, classical); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var pub []byte
|
||||
seed := key[len(key)-n:]
|
||||
if c.IsCA() {
|
||||
sk, err := mldsa.NewPrivateKey(mldsa.MLDSA87(), seed)
|
||||
if err != nil {
|
||||
return ErrInvalidPrivateKey
|
||||
}
|
||||
pub = sk.PublicKey().Bytes()
|
||||
} else {
|
||||
dk, err := mlkem.NewDecapsulationKey1024(seed)
|
||||
if err != nil {
|
||||
return ErrInvalidPrivateKey
|
||||
}
|
||||
pub = dk.EncapsulationKey().Bytes()
|
||||
pub, err := c.pqSuite.publicKey(c.IsCA(), seed)
|
||||
if err != nil {
|
||||
return ErrInvalidPrivateKey
|
||||
}
|
||||
if !bytes.Equal(pub, c.pqPublicKey) {
|
||||
return ErrPublicPrivateKeyMismatch
|
||||
@@ -227,7 +200,8 @@ func (c *certificateV3) String() string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// MarshalForHandshakes leaves out both public keys, the handshake carries them as the composite static key.
|
||||
// MarshalForHandshakes leaves out both public keys, the handshake carries them as the composite static key. It keeps
|
||||
// the suite, which says how to split that key.
|
||||
func (c *certificateV3) MarshalForHandshakes() ([]byte, error) {
|
||||
return c.marshal(false, false)
|
||||
}
|
||||
@@ -237,7 +211,7 @@ func (c *certificateV3) Marshal() ([]byte, error) {
|
||||
}
|
||||
|
||||
// marshalForPQSigning returns the bytes the post-quantum signature covers: every field but that signature, with the
|
||||
// curve always present.
|
||||
// curve always present. The suite is always present anyway.
|
||||
func (c *certificateV3) marshalForPQSigning() ([]byte, error) {
|
||||
return c.marshal(true, true)
|
||||
}
|
||||
@@ -265,6 +239,10 @@ func (c *certificateV3) marshal(withPublicKeys, forSigning bool) ([]byte, error)
|
||||
b.AddBytes(c.v2.signature)
|
||||
})
|
||||
|
||||
b.AddASN1(TagCertPQSuite, func(b *cryptobyte.Builder) {
|
||||
b.AddUint8(uint8(c.pqSuite))
|
||||
})
|
||||
|
||||
if withPublicKeys {
|
||||
b.AddASN1(TagCertPQPublicKey, func(b *cryptobyte.Builder) {
|
||||
b.AddBytes(c.pqPublicKey)
|
||||
@@ -310,6 +288,7 @@ func (c *certificateV3) marshalJSON() (m, error) {
|
||||
mb["v2Fingerprint"] = mb["fingerprint"]
|
||||
mb["fingerprint"] = fp
|
||||
mb["version"] = Version3
|
||||
mb["pqSuite"] = c.pqSuite.String()
|
||||
mb["pqPublicKey"] = fmt.Sprintf("%x", c.pqPublicKey)
|
||||
mb["pqSignature"] = fmt.Sprintf("%x", c.pqSignature)
|
||||
return mb, nil
|
||||
@@ -318,6 +297,7 @@ func (c *certificateV3) marshalJSON() (m, error) {
|
||||
func (c *certificateV3) Copy() Certificate {
|
||||
nc := &certificateV3{
|
||||
v2: *c.v2.Copy().(*certificateV2),
|
||||
pqSuite: c.pqSuite,
|
||||
pqPublicKey: make([]byte, len(c.pqPublicKey)),
|
||||
pqSignature: make([]byte, len(c.pqSignature)),
|
||||
}
|
||||
@@ -331,20 +311,14 @@ func (c *certificateV3) validate() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(c.pqPublicKey) != pqPublicKeySize(c.IsCA()) {
|
||||
if !c.pqSuite.Valid() {
|
||||
return fmt.Errorf("%w: %v", ErrUnknownPQSuite, c.pqSuite)
|
||||
}
|
||||
if !c.pqSuite.checkPublicKey(c.IsCA(), c.pqPublicKey) {
|
||||
return ErrInvalidPublicKey
|
||||
}
|
||||
if c.IsCA() {
|
||||
if _, err := mldsa.NewPublicKey(mldsa.MLDSA87(), c.pqPublicKey); err != nil {
|
||||
return ErrInvalidPublicKey
|
||||
}
|
||||
} else {
|
||||
if _, err := mlkem.NewEncapsulationKey1024(c.pqPublicKey); err != nil {
|
||||
return ErrInvalidPublicKey
|
||||
}
|
||||
}
|
||||
|
||||
if len(c.pqSignature) != MLDSA87SignatureSize {
|
||||
if len(c.pqSignature) != c.pqSuite.signatureSize() {
|
||||
return ErrBadFormat
|
||||
}
|
||||
return nil
|
||||
@@ -390,6 +364,15 @@ func unmarshalCertificateV3(b []byte, publicKey []byte, curve Curve) (*certifica
|
||||
return nil, ErrBadFormat
|
||||
}
|
||||
|
||||
var rawSuite cryptobyte.String
|
||||
if !body.ReadASN1(&rawSuite, TagCertPQSuite) || len(rawSuite) != 1 {
|
||||
return nil, ErrBadFormat
|
||||
}
|
||||
suite := PQSuite(rawSuite[0])
|
||||
if !suite.Valid() {
|
||||
return nil, fmt.Errorf("%w: %v", ErrUnknownPQSuite, suite)
|
||||
}
|
||||
|
||||
if fromHandshake {
|
||||
if body.PeekASN1Tag(TagCertPQPublicKey) {
|
||||
return nil, ErrCertPubkeyPresent
|
||||
@@ -413,7 +396,7 @@ func unmarshalCertificateV3(b []byte, publicKey []byte, curve Curve) (*certifica
|
||||
}
|
||||
|
||||
if fromHandshake {
|
||||
k, p, err := splitPQPublicKey(details.isCA, publicKey)
|
||||
k, p, err := suite.SplitPublicKey(details.isCA, publicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -429,6 +412,7 @@ func unmarshalCertificateV3(b []byte, publicKey []byte, curve Curve) (*certifica
|
||||
publicKey: classical,
|
||||
signature: signature,
|
||||
},
|
||||
pqSuite: suite,
|
||||
pqPublicKey: pq,
|
||||
pqSignature: pqSignature,
|
||||
}
|
||||
@@ -440,17 +424,21 @@ func unmarshalCertificateV3(b []byte, publicKey []byte, curve Curve) (*certifica
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// ExtendToV3 builds the version 3 certificate that extends the version 2 certificate c with pqPublicKey, the composite
|
||||
// public key's post-quantum half. pqSign produces the ML-DSA-87 signature, see SignPQ.
|
||||
// ExtendToV3 builds the version 3 certificate in suite that extends the version 2 certificate c with pqPublicKey, the
|
||||
// composite public key's post-quantum half. pqSign produces the suite's post-quantum signature, see SignPQ.
|
||||
//
|
||||
// A CA certificate signs its own extension: signer must be nil and pqSign must use the private key for pqPublicKey.
|
||||
// A host certificate is extended by its issuing CA: signer is the v3 form of that CA. In both cases c's classical
|
||||
// signature, and so its fingerprint, are left unchanged, so certificates in use can be extended in place.
|
||||
func ExtendToV3(c Certificate, pqPublicKey []byte, signer Certificate, pqSign SignerLambda) (Certificate, error) {
|
||||
// A host certificate is extended by its issuing CA: signer is the v3 form of that CA, which must be in the same suite.
|
||||
// In both cases c's classical signature, and so its fingerprint, are left unchanged, so certificates in use can be
|
||||
// extended in place.
|
||||
func ExtendToV3(c Certificate, suite PQSuite, pqPublicKey []byte, signer Certificate, pqSign SignerLambda) (Certificate, error) {
|
||||
c2, ok := c.(*certificateV2)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("only version 2 certificates can be extended to version 3, got version %d", c.Version())
|
||||
}
|
||||
if !suite.Valid() {
|
||||
return nil, fmt.Errorf("%w: %v", ErrUnknownPQSuite, suite)
|
||||
}
|
||||
|
||||
var signingKey []byte
|
||||
if c2.IsCA() {
|
||||
@@ -465,6 +453,9 @@ func ExtendToV3(c Certificate, pqPublicKey []byte, signer Certificate, pqSign Si
|
||||
if signer == nil || signer.Version() != Version3 || !signer.IsCA() {
|
||||
return nil, fmt.Errorf("a host certificate must be extended by a version 3 CA")
|
||||
}
|
||||
if PQSuiteOf(signer) != suite {
|
||||
return nil, ErrPQSuiteMismatch
|
||||
}
|
||||
signerV2 := V2Form(signer)
|
||||
issuer, err := signerV2.Fingerprint()
|
||||
if err != nil {
|
||||
@@ -481,9 +472,10 @@ func ExtendToV3(c Certificate, pqPublicKey []byte, signer Certificate, pqSign Si
|
||||
|
||||
c3 := &certificateV3{
|
||||
v2: *c2.Copy().(*certificateV2),
|
||||
pqSuite: suite,
|
||||
pqPublicKey: bytes.Clone(pqPublicKey),
|
||||
}
|
||||
if len(c3.pqPublicKey) != pqPublicKeySize(c3.IsCA()) {
|
||||
if len(c3.pqPublicKey) != suite.PublicKeySize(c3.IsCA()) {
|
||||
return nil, ErrInvalidPublicKey
|
||||
}
|
||||
|
||||
@@ -509,16 +501,16 @@ func ExtendToV3(c Certificate, pqPublicKey []byte, signer Certificate, pqSign Si
|
||||
return c3, nil
|
||||
}
|
||||
|
||||
// SignPQ returns a SignerLambda that signs certificates with the ML-DSA-87 private key seed. Signatures are
|
||||
// deterministic (FIPS 204 section 3.4), so signing the same certificate twice gives the same bytes and the same
|
||||
// fingerprint, and extending a certificate is idempotent. The hedged variant guards signers an attacker can fault
|
||||
// physically, which a CA key in a file is not; the Ed25519 signature beside it is deterministic too.
|
||||
func SignPQ(seed []byte) (SignerLambda, error) {
|
||||
sk, err := mldsa.NewPrivateKey(mldsa.MLDSA87(), seed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// SignPQ returns a SignerLambda that signs certificates with the private key seed of suite's signature algorithm.
|
||||
// Signatures are deterministic, so signing the same certificate twice gives the same bytes and the same fingerprint,
|
||||
// and extending a certificate is idempotent.
|
||||
func SignPQ(suite PQSuite, seed []byte) (SignerLambda, error) {
|
||||
p := pqSuites[suite]
|
||||
if p == nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrUnknownPQSuite, suite)
|
||||
}
|
||||
return func(b []byte) ([]byte, error) {
|
||||
return sk.SignDeterministic(b, &mldsa.Options{Context: pqSignatureContext})
|
||||
}, nil
|
||||
if len(seed) != p.dsa.privateKeySize {
|
||||
return nil, ErrInvalidPrivateKey
|
||||
}
|
||||
return p.dsa.signer(seed)
|
||||
}
|
||||
+44
-21
@@ -46,7 +46,7 @@ func TestCertificateV3_Sign(t *testing.T) {
|
||||
assert.Equal(t, caV2Fp, host.Issuer())
|
||||
|
||||
// A v2 CA can not sign a v3 certificate, nor a v3 CA a v2 one.
|
||||
_, err = (&TBSCertificate{Version: Version3, Curve: curve, Name: "x", Networks: host.Networks(), NotBefore: host.NotBefore(), NotAfter: host.NotAfter(), PublicKey: host.PublicKey()}).Sign(V2Form(ca), curve, caKey)
|
||||
_, err = (&TBSCertificate{Version: Version3, Curve: curve, PQSuite: DefaultPQSuite, Name: "x", Networks: host.Networks(), NotBefore: host.NotBefore(), NotAfter: host.NotAfter(), PublicKey: host.PublicKey()}).Sign(V2Form(ca), curve, caKey)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
@@ -174,13 +174,13 @@ func TestCertificateV3_StrictParse(t *testing.T) {
|
||||
require.ErrorIs(t, err, ErrBadFormat, "trailing bytes after the envelope")
|
||||
|
||||
trailing := rebuildV3(t, host, func(fields map[asn1.Tag][]byte, order *[]asn1.Tag) {
|
||||
fields[6|classContextSpecific] = []byte{6 | classContextSpecific, 1, 0}
|
||||
*order = append(*order, 6|classContextSpecific)
|
||||
fields[7|classContextSpecific] = []byte{7 | classContextSpecific, 1, 0}
|
||||
*order = append(*order, 7|classContextSpecific)
|
||||
})
|
||||
_, err = unmarshalCertificateV3(trailing, nil, Curve_CURVE25519)
|
||||
require.ErrorIs(t, err, ErrBadFormat, "unknown field after the pq signature")
|
||||
|
||||
for _, tag := range []asn1.Tag{TagCertPQPublicKey, TagCertPQSignature} {
|
||||
for _, tag := range []asn1.Tag{TagCertPQSuite, TagCertPQPublicKey, TagCertPQSignature} {
|
||||
missing := rebuildV3(t, host, func(fields map[asn1.Tag][]byte, order *[]asn1.Tag) {
|
||||
*order = removeTag(*order, tag)
|
||||
})
|
||||
@@ -202,6 +202,17 @@ func TestCertificateV3_StrictParse(t *testing.T) {
|
||||
})
|
||||
_, err = unmarshalCertificateV3(short, nil, Curve_CURVE25519)
|
||||
require.ErrorIs(t, err, ErrInvalidPublicKey)
|
||||
|
||||
// The suite is one known value, PQSuiteUnknown included in what is not.
|
||||
for _, suite := range [][]byte{{byte(PQSuiteUnknown)}, {200}, {}, {1, 1}} {
|
||||
bad := rebuildV3(t, host, func(fields map[asn1.Tag][]byte, order *[]asn1.Tag) {
|
||||
var b cryptobyte.Builder
|
||||
b.AddASN1(TagCertPQSuite, func(b *cryptobyte.Builder) { b.AddBytes(suite) })
|
||||
fields[TagCertPQSuite] = b.BytesOrPanic()
|
||||
})
|
||||
_, err = unmarshalCertificateV3(bad, nil, Curve_CURVE25519)
|
||||
require.Error(t, err, "suite %x", suite)
|
||||
}
|
||||
}
|
||||
|
||||
func removeTag(order []asn1.Tag, tag asn1.Tag) []asn1.Tag {
|
||||
@@ -278,9 +289,9 @@ func TestCAPool_V3(t *testing.T) {
|
||||
// A second v3 extension of the same v2 CA is ambiguous.
|
||||
sk, err := mldsa.GenerateKey(mldsa.MLDSA87())
|
||||
require.NoError(t, err)
|
||||
signPQ, err := SignPQ(sk.Bytes())
|
||||
signPQ, err := SignPQ(DefaultPQSuite, sk.Bytes())
|
||||
require.NoError(t, err)
|
||||
again, err := ExtendToV3(V2Form(ca), sk.PublicKey().Bytes(), nil, signPQ)
|
||||
again, err := ExtendToV3(V2Form(ca), DefaultPQSuite, sk.PublicKey().Bytes(), nil, signPQ)
|
||||
require.NoError(t, err)
|
||||
require.ErrorContains(t, pool.AddCA(again), "two version 3 CAs extend the same version 2 CA")
|
||||
})
|
||||
@@ -343,17 +354,17 @@ func TestExtendToV3(t *testing.T) {
|
||||
|
||||
sk, err := mldsa.GenerateKey(mldsa.MLDSA87())
|
||||
require.NoError(t, err)
|
||||
signPQ, err := SignPQ(sk.Bytes())
|
||||
signPQ, err := SignPQ(DefaultPQSuite, sk.Bytes())
|
||||
require.NoError(t, err)
|
||||
|
||||
ca3, err := ExtendToV3(ca2, sk.PublicKey().Bytes(), nil, signPQ)
|
||||
ca3, err := ExtendToV3(ca2, DefaultPQSuite, sk.PublicKey().Bytes(), nil, signPQ)
|
||||
require.NoError(t, err)
|
||||
assertCertsEqual(t, ca2, V2Form(ca3))
|
||||
require.NoError(t, ca3.VerifyPrivateKey(curve, append(caKey2, sk.Bytes()...)))
|
||||
|
||||
dk, err := mlkem.GenerateKey1024()
|
||||
require.NoError(t, err)
|
||||
host3, err := ExtendToV3(host2, dk.EncapsulationKey().Bytes(), ca3, signPQ)
|
||||
host3, err := ExtendToV3(host2, DefaultPQSuite, dk.EncapsulationKey().Bytes(), ca3, signPQ)
|
||||
require.NoError(t, err)
|
||||
assertCertsEqual(t, host2, V2Form(host3))
|
||||
|
||||
@@ -364,26 +375,26 @@ func TestExtendToV3(t *testing.T) {
|
||||
|
||||
// A host must be extended by its own issuer, and with that issuer's key.
|
||||
other2, _, _, _ := NewTestCaCert(Version2, curve, time.Time{}, time.Time{}, nil, nil, nil)
|
||||
other3, err := ExtendToV3(other2, sk.PublicKey().Bytes(), nil, signPQ)
|
||||
other3, err := ExtendToV3(other2, DefaultPQSuite, sk.PublicKey().Bytes(), nil, signPQ)
|
||||
require.NoError(t, err)
|
||||
_, err = ExtendToV3(host2, dk.EncapsulationKey().Bytes(), other3, signPQ)
|
||||
_, err = ExtendToV3(host2, DefaultPQSuite, dk.EncapsulationKey().Bytes(), other3, signPQ)
|
||||
require.Error(t, err)
|
||||
|
||||
wrong, err := mldsa.GenerateKey(mldsa.MLDSA87())
|
||||
require.NoError(t, err)
|
||||
wrongSign, err := SignPQ(wrong.Bytes())
|
||||
wrongSign, err := SignPQ(DefaultPQSuite, wrong.Bytes())
|
||||
require.NoError(t, err)
|
||||
_, err = ExtendToV3(host2, dk.EncapsulationKey().Bytes(), ca3, wrongSign)
|
||||
_, err = ExtendToV3(host2, DefaultPQSuite, dk.EncapsulationKey().Bytes(), ca3, wrongSign)
|
||||
require.ErrorIs(t, err, ErrSignatureMismatch)
|
||||
|
||||
// A CA extends itself, and needs its own post-quantum key to.
|
||||
_, err = ExtendToV3(ca2, sk.PublicKey().Bytes(), nil, wrongSign)
|
||||
_, err = ExtendToV3(ca2, DefaultPQSuite, sk.PublicKey().Bytes(), nil, wrongSign)
|
||||
require.ErrorIs(t, err, ErrSignatureMismatch)
|
||||
_, err = ExtendToV3(ca2, sk.PublicKey().Bytes(), ca3, signPQ)
|
||||
_, err = ExtendToV3(ca2, DefaultPQSuite, sk.PublicKey().Bytes(), ca3, signPQ)
|
||||
require.Error(t, err)
|
||||
|
||||
// Only v2 certificates extend.
|
||||
_, err = ExtendToV3(host3, dk.EncapsulationKey().Bytes(), ca3, signPQ)
|
||||
_, err = ExtendToV3(host3, DefaultPQSuite, dk.EncapsulationKey().Bytes(), ca3, signPQ)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
@@ -398,13 +409,13 @@ func TestExtendToV3_Deterministic(t *testing.T) {
|
||||
host2, _, _, _ := NewTestCert(Version2, curve, ca2, caKey2, "host", time.Time{}, time.Time{}, []netip.Prefix{netip.MustParsePrefix("10.1.0.1/16")}, nil, nil)
|
||||
sk, err := mldsa.GenerateKey(mldsa.MLDSA87())
|
||||
require.NoError(t, err)
|
||||
signPQ, err := SignPQ(sk.Bytes())
|
||||
signPQ, err := SignPQ(DefaultPQSuite, sk.Bytes())
|
||||
require.NoError(t, err)
|
||||
dk, err := mlkem.GenerateKey1024()
|
||||
require.NoError(t, err)
|
||||
|
||||
extend := func(c Certificate, pq []byte, signer Certificate) ([]byte, string, Certificate) {
|
||||
c3, err := ExtendToV3(c, pq, signer, signPQ)
|
||||
c3, err := ExtendToV3(c, DefaultPQSuite, pq, signer, signPQ)
|
||||
require.NoError(t, err)
|
||||
b, err := c3.Marshal()
|
||||
require.NoError(t, err)
|
||||
@@ -446,18 +457,18 @@ func TestCertificateV3_KeyPEM(t *testing.T) {
|
||||
assert.Equal(t, curve, gotCurve)
|
||||
assert.Equal(t, ca.PublicKey(), pub)
|
||||
|
||||
key, _, gotCurve, err := UnmarshalPrivateKeyFromPEM(MarshalPrivateKeyToPEM(curve, hostKey))
|
||||
key, _, gotCurve, err := UnmarshalPrivateKeyFromPEM(MarshalPQPrivateKeyToPEM(curve, DefaultPQSuite, hostKey))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, curve, gotCurve)
|
||||
assert.Equal(t, hostKey, key)
|
||||
|
||||
key, _, gotCurve, err = UnmarshalSigningPrivateKeyFromPEM(MarshalSigningPrivateKeyToPEM(curve, caKey))
|
||||
key, _, gotCurve, err = UnmarshalSigningPrivateKeyFromPEM(MarshalPQSigningPrivateKeyToPEM(curve, DefaultPQSuite, caKey))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, curve, gotCurve)
|
||||
assert.Equal(t, caKey, key)
|
||||
|
||||
kdf := NewArgon2Parameters(64*1024, 4, 1)
|
||||
enc, err := EncryptAndMarshalSigningPrivateKey(curve, caKey, []byte("pw"), kdf)
|
||||
enc, err := EncryptAndMarshalPQSigningPrivateKey(curve, DefaultPQSuite, caKey, []byte("pw"), kdf)
|
||||
require.NoError(t, err)
|
||||
_, _, _, err = UnmarshalSigningPrivateKeyFromPEM(enc)
|
||||
require.ErrorIs(t, err, ErrPrivateKeyEncrypted)
|
||||
@@ -465,6 +476,18 @@ func TestCertificateV3_KeyPEM(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, curve, gotCurve)
|
||||
assert.Equal(t, caKey, key)
|
||||
|
||||
// The banners name both halves.
|
||||
assert.Contains(t, string(MarshalPQPrivateKeyToPEM(curve, DefaultPQSuite, hostKey)), " MLKEM1024 PRIVATE KEY-----")
|
||||
assert.Contains(t, string(MarshalPQSigningPrivateKeyToPEM(curve, DefaultPQSuite, caKey)), " MLDSA87 PRIVATE KEY-----")
|
||||
assert.Contains(t, string(enc), " MLDSA87 ENCRYPTED PRIVATE KEY-----")
|
||||
|
||||
// A key of the wrong size, or for an unknown suite, has no banner.
|
||||
assert.Nil(t, MarshalPQPrivateKeyToPEM(curve, DefaultPQSuite, hostKey[1:]))
|
||||
assert.Nil(t, MarshalPQPrivateKeyToPEM(curve, PQSuiteUnknown, hostKey))
|
||||
assert.Nil(t, MarshalPQSigningPrivateKeyToPEM(curve, DefaultPQSuite, caKey[1:]))
|
||||
_, err = EncryptAndMarshalPQSigningPrivateKey(curve, DefaultPQSuite, caKey[1:], []byte("pw"), kdf)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
+39
-29
@@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/argon2"
|
||||
"google.golang.org/protobuf/proto"
|
||||
@@ -158,7 +159,33 @@ func splitNonceCiphertext(blob []byte, nonceSize int) ([]byte, []byte, error) {
|
||||
|
||||
// EncryptAndMarshalSigningPrivateKey is a simple helper to encrypt and PEM encode a private key
|
||||
func EncryptAndMarshalSigningPrivateKey(curve Curve, b []byte, passphrase []byte, kdfParams *Argon2Parameters) ([]byte, error) {
|
||||
pq := len(b) == signingPrivateKeySize(curve)+MLDSA87PrivateKeySize
|
||||
switch curve {
|
||||
case Curve_CURVE25519:
|
||||
return encryptAndMarshalSigningPrivateKey(EncryptedEd25519PrivateKeyBanner, b, passphrase, kdfParams)
|
||||
case Curve_P256:
|
||||
return encryptAndMarshalSigningPrivateKey(EncryptedECDSAP256PrivateKeyBanner, b, passphrase, kdfParams)
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid curve: %v", curve)
|
||||
}
|
||||
}
|
||||
|
||||
// EncryptAndMarshalPQSigningPrivateKey encrypts and PEM encodes a composite CA private key of suite, the classical
|
||||
// signing key followed by the seed of the suite's signature algorithm.
|
||||
func EncryptAndMarshalPQSigningPrivateKey(curve Curve, suite PQSuite, b []byte, passphrase []byte, kdfParams *Argon2Parameters) ([]byte, error) {
|
||||
p := pqSuites[suite]
|
||||
if p == nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrUnknownPQSuite, suite)
|
||||
}
|
||||
if curve != Curve_CURVE25519 && curve != Curve_P256 {
|
||||
return nil, fmt.Errorf("invalid curve: %v", curve)
|
||||
}
|
||||
if len(b) != pqKeySize(pqSigningPrivateKey, curve, p) {
|
||||
return nil, ErrInvalidPrivateKey
|
||||
}
|
||||
return encryptAndMarshalSigningPrivateKey(pqBannerName(pqEncryptedSigningPrivateKey, curve, p), b, passphrase, kdfParams)
|
||||
}
|
||||
|
||||
func encryptAndMarshalSigningPrivateKey(banner string, b []byte, passphrase []byte, kdfParams *Argon2Parameters) ([]byte, error) {
|
||||
ciphertext, err := aes256Encrypt(passphrase, kdfParams, b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -181,21 +208,7 @@ func EncryptAndMarshalSigningPrivateKey(curve Curve, b []byte, passphrase []byte
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case pq && curve == Curve_CURVE25519:
|
||||
return pem.EncodeToMemory(&pem.Block{Type: EncryptedEd25519MLDSA87PrivateKeyBanner, Bytes: b}), nil
|
||||
case pq && curve == Curve_P256:
|
||||
return pem.EncodeToMemory(&pem.Block{Type: EncryptedECDSAP256MLDSA87PrivateKeyBanner, Bytes: b}), nil
|
||||
}
|
||||
|
||||
switch curve {
|
||||
case Curve_CURVE25519:
|
||||
return pem.EncodeToMemory(&pem.Block{Type: EncryptedEd25519PrivateKeyBanner, Bytes: b}), nil
|
||||
case Curve_P256:
|
||||
return pem.EncodeToMemory(&pem.Block{Type: EncryptedECDSAP256PrivateKeyBanner, Bytes: b}), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid curve: %v", curve)
|
||||
}
|
||||
return pem.EncodeToMemory(&pem.Block{Type: banner, Bytes: b}), nil
|
||||
}
|
||||
|
||||
// UnmarshalNebulaEncryptedData will unmarshal a protobuf byte representation of a nebula cert into its
|
||||
@@ -268,18 +281,15 @@ func DecryptAndUnmarshalSigningPrivateKey(passphrase, b []byte) (Curve, []byte,
|
||||
return curve, nil, r, fmt.Errorf("input did not contain a valid PEM encoded block")
|
||||
}
|
||||
|
||||
pq := false
|
||||
switch k.Type {
|
||||
case EncryptedEd25519PrivateKeyBanner:
|
||||
pq, pqKey := pqKeyBanners[k.Type]
|
||||
pqKey = pqKey && pq.kind == pqEncryptedSigningPrivateKey
|
||||
switch {
|
||||
case pqKey:
|
||||
curve = pq.curve
|
||||
case k.Type == EncryptedEd25519PrivateKeyBanner:
|
||||
curve = Curve_CURVE25519
|
||||
case EncryptedECDSAP256PrivateKeyBanner:
|
||||
case k.Type == EncryptedECDSAP256PrivateKeyBanner:
|
||||
curve = Curve_P256
|
||||
case EncryptedEd25519MLDSA87PrivateKeyBanner:
|
||||
curve = Curve_CURVE25519
|
||||
pq = true
|
||||
case EncryptedECDSAP256MLDSA87PrivateKeyBanner:
|
||||
curve = Curve_P256
|
||||
pq = true
|
||||
default:
|
||||
return curve, nil, r, fmt.Errorf("bytes did not contain a proper nebula encrypted Ed25519/ECDSA private key banner")
|
||||
}
|
||||
@@ -300,9 +310,9 @@ func DecryptAndUnmarshalSigningPrivateKey(passphrase, b []byte) (Curve, []byte,
|
||||
return curve, nil, r, fmt.Errorf("unsupported encryption algorithm: %s", ned.EncryptionMetadata.EncryptionAlgorithm)
|
||||
}
|
||||
|
||||
if pq {
|
||||
if len(bytes) != signingPrivateKeySize(curve)+MLDSA87PrivateKeySize {
|
||||
return curve, nil, r, fmt.Errorf("key was not %d bytes, is invalid %s ML-DSA-87 private key", signingPrivateKeySize(curve)+MLDSA87PrivateKeySize, curve)
|
||||
if pqKey {
|
||||
if len(bytes) != pq.size {
|
||||
return curve, nil, r, fmt.Errorf("key was not %d bytes, is invalid %s", pq.size, strings.ToLower(strings.TrimPrefix(k.Type, "NEBULA ")))
|
||||
}
|
||||
return curve, bytes, r, nil
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ var (
|
||||
ErrUnknownVersion = errors.New("certificate version unrecognized")
|
||||
ErrCertPubkeyPresent = errors.New("certificate has unexpected pubkey present")
|
||||
ErrCurveMismatch = errors.New("certificate curve does not match CA")
|
||||
ErrPQSuiteMismatch = errors.New("certificate post-quantum suite does not match CA")
|
||||
ErrUnknownPQSuite = errors.New("unknown post-quantum suite")
|
||||
|
||||
ErrInvalidPEMBlock = errors.New("input did not contain a valid PEM encoded block")
|
||||
ErrInvalidPEMCertificateBanner = errors.New("bytes did not contain a proper certificate banner")
|
||||
|
||||
+15
-10
@@ -4,8 +4,6 @@ import (
|
||||
"crypto/ecdh"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/mldsa"
|
||||
"crypto/mlkem"
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"net/netip"
|
||||
@@ -64,6 +62,9 @@ func NewTestCaCert(version Version, curve Curve, before, after time.Time, networ
|
||||
Groups: groups,
|
||||
IsCA: true,
|
||||
}
|
||||
if version == Version3 {
|
||||
t.PQSuite = DefaultPQSuite
|
||||
}
|
||||
|
||||
c, err := t.Sign(nil, curve, priv)
|
||||
if err != nil {
|
||||
@@ -104,7 +105,7 @@ func NewTestCert(v Version, curve Curve, ca Certificate, key []byte, name string
|
||||
}
|
||||
|
||||
if v == Version3 {
|
||||
pub, priv = appendPQKey(pub, priv)
|
||||
pub, priv = appendPQKey(ca, pub, priv)
|
||||
}
|
||||
|
||||
nc := &TBSCertificate{
|
||||
@@ -118,6 +119,7 @@ func NewTestCert(v Version, curve Curve, ca Certificate, key []byte, name string
|
||||
NotAfter: time.Unix(after.Unix(), 0),
|
||||
PublicKey: pub,
|
||||
IsCA: false,
|
||||
PQSuite: PQSuiteOf(ca),
|
||||
}
|
||||
|
||||
c, err := nc.Sign(ca, ca.Curve(), key)
|
||||
@@ -130,25 +132,28 @@ func NewTestCert(v Version, curve Curve, ca Certificate, key []byte, name string
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if v == Version3 {
|
||||
return c, pub, MarshalPQPrivateKeyToPEM(curve, PQSuiteOf(ca), priv), pem
|
||||
}
|
||||
return c, pub, MarshalPrivateKeyToPEM(curve, priv), pem
|
||||
}
|
||||
|
||||
// appendPQSigningKey makes a classical CA keypair into a version 3 composite one with a fresh ML-DSA-87 key.
|
||||
// appendPQSigningKey makes a classical CA keypair into a version 3 composite one with a fresh key of the default suite.
|
||||
func appendPQSigningKey(pub, priv []byte) ([]byte, []byte) {
|
||||
sk, err := mldsa.GenerateKey(mldsa.MLDSA87())
|
||||
pqPub, pqPriv, err := DefaultPQSuite.GenerateKey(true)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return append(pub, sk.PublicKey().Bytes()...), append(priv, sk.Bytes()...)
|
||||
return append(pub, pqPub...), append(priv, pqPriv...)
|
||||
}
|
||||
|
||||
// appendPQKey makes a classical host keypair into a version 3 composite one with a fresh ML-KEM-1024 key.
|
||||
func appendPQKey(pub, priv []byte) ([]byte, []byte) {
|
||||
dk, err := mlkem.GenerateKey1024()
|
||||
// appendPQKey makes a classical host keypair into a version 3 composite one with a fresh key of the CA's suite.
|
||||
func appendPQKey(ca Certificate, pub, priv []byte) ([]byte, []byte) {
|
||||
pqPub, pqPriv, err := PQSuiteOf(ca).GenerateKey(false)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return append(pub, dk.EncapsulationKey().Bytes()...), append(priv, dk.Bytes()...)
|
||||
return append(pub, pqPub...), append(priv, pqPriv...)
|
||||
}
|
||||
|
||||
func X25519Keypair() ([]byte, []byte) {
|
||||
|
||||
+149
-81
@@ -5,6 +5,8 @@ import (
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/ed25519"
|
||||
)
|
||||
@@ -74,23 +76,11 @@ const ( //key-agreement-key banners
|
||||
P256PublicKeyBanner = "NEBULA P256 PUBLIC KEY"
|
||||
)
|
||||
|
||||
// Version 3 keys are composite: the classical key followed by the post-quantum key, an ML-KEM-1024 seed or
|
||||
// encapsulation key for hosts and an ML-DSA-87 seed or public key for CAs.
|
||||
const ( //post-quantum key-agreement-key banners
|
||||
X25519MLKEM1024PrivateKeyBanner = "NEBULA X25519 MLKEM1024 PRIVATE KEY"
|
||||
X25519MLKEM1024PublicKeyBanner = "NEBULA X25519 MLKEM1024 PUBLIC KEY"
|
||||
P256MLKEM1024PrivateKeyBanner = "NEBULA P256 MLKEM1024 PRIVATE KEY"
|
||||
P256MLKEM1024PublicKeyBanner = "NEBULA P256 MLKEM1024 PUBLIC KEY"
|
||||
)
|
||||
|
||||
const ( //post-quantum signing key banners
|
||||
EncryptedECDSAP256MLDSA87PrivateKeyBanner = "NEBULA ECDSA P256 MLDSA87 ENCRYPTED PRIVATE KEY"
|
||||
ECDSAP256MLDSA87PrivateKeyBanner = "NEBULA ECDSA P256 MLDSA87 PRIVATE KEY"
|
||||
ECDSAP256MLDSA87PublicKeyBanner = "NEBULA ECDSA P256 MLDSA87 PUBLIC KEY"
|
||||
EncryptedEd25519MLDSA87PrivateKeyBanner = "NEBULA ED25519 MLDSA87 ENCRYPTED PRIVATE KEY"
|
||||
Ed25519MLDSA87PrivateKeyBanner = "NEBULA ED25519 MLDSA87 PRIVATE KEY"
|
||||
Ed25519MLDSA87PublicKeyBanner = "NEBULA ED25519 MLDSA87 PUBLIC KEY"
|
||||
)
|
||||
// Version 3 keys are composite: the classical key followed by the post-quantum key of the certificate's PQSuite, a
|
||||
// KEM seed or public key for hosts and a signature algorithm's seed or public key for CAs. Their banners name both
|
||||
// halves, "NEBULA X25519 MLKEM1024 PRIVATE KEY" or "NEBULA ED25519 MLDSA87 PUBLIC KEY" for example, because the
|
||||
// post-quantum algorithm can not be told from the key's size: ML-KEM and ML-DSA seeds are the same size at every
|
||||
// strength. The banners name algorithms, not suites, so suites sharing an algorithm share its banners too.
|
||||
|
||||
// Classical key sizes, the composite keys of version 3 add the post-quantum key to these.
|
||||
const (
|
||||
@@ -102,12 +92,91 @@ const (
|
||||
ecdsaP256PublicKeySize = 65 // uncompressed
|
||||
)
|
||||
|
||||
// signingPrivateKeySize returns the size of a classical signing private key.
|
||||
func signingPrivateKeySize(curve Curve) int {
|
||||
if curve == Curve_P256 {
|
||||
return p256PrivateKeySize
|
||||
// ClassicalPrivateKey returns the classical half of a host private key: all of a classical key, or the classical half
|
||||
// of a composite one, whatever its suite.
|
||||
func ClassicalPrivateKey(key []byte) []byte {
|
||||
// X25519 and P256 private keys are the same size.
|
||||
if len(key) > x25519KeySize {
|
||||
return key[:x25519KeySize]
|
||||
}
|
||||
return ed25519PrivateKeySize
|
||||
return key
|
||||
}
|
||||
|
||||
// pqKeyKind is what a composite key banner holds.
|
||||
type pqKeyKind int
|
||||
|
||||
const (
|
||||
pqPrivateKey pqKeyKind = iota
|
||||
pqPublicKey
|
||||
pqSigningPrivateKey
|
||||
pqSigningPublicKey
|
||||
pqEncryptedSigningPrivateKey
|
||||
)
|
||||
|
||||
// pqKeyBanner is what a composite key banner says about the key under it.
|
||||
type pqKeyBanner struct {
|
||||
kind pqKeyKind
|
||||
curve Curve
|
||||
size int // of the whole composite key; for an encrypted key, once decrypted
|
||||
}
|
||||
|
||||
var pqKeyKinds = []pqKeyKind{pqPrivateKey, pqPublicKey, pqSigningPrivateKey, pqSigningPublicKey, pqEncryptedSigningPrivateKey}
|
||||
|
||||
// pqKeyBanners maps every composite key banner of every suite to what it holds, see registerPQSuite.
|
||||
var pqKeyBanners = map[string]pqKeyBanner{}
|
||||
|
||||
// pqBannerName returns the banner for a composite key.
|
||||
func pqBannerName(kind pqKeyKind, curve Curve, suite *pqSuite) string {
|
||||
classical := map[Curve]string{Curve_CURVE25519: "X25519", Curve_P256: "P256"}[curve]
|
||||
alg := suite.kem.name
|
||||
if kind != pqPrivateKey && kind != pqPublicKey {
|
||||
classical = map[Curve]string{Curve_CURVE25519: "ED25519", Curve_P256: "ECDSA P256"}[curve]
|
||||
alg = suite.dsa.name
|
||||
}
|
||||
return "NEBULA " + classical + " " + alg + " " + map[pqKeyKind]string{
|
||||
pqPrivateKey: "PRIVATE KEY",
|
||||
pqPublicKey: "PUBLIC KEY",
|
||||
pqSigningPrivateKey: "PRIVATE KEY",
|
||||
pqSigningPublicKey: "PUBLIC KEY",
|
||||
pqEncryptedSigningPrivateKey: "ENCRYPTED PRIVATE KEY",
|
||||
}[kind]
|
||||
}
|
||||
|
||||
// pqKeySize returns the size of a composite key.
|
||||
func pqKeySize(kind pqKeyKind, curve Curve, suite *pqSuite) int {
|
||||
p256 := curve == Curve_P256
|
||||
switch kind {
|
||||
case pqPrivateKey:
|
||||
return x25519KeySize + suite.kem.privateKeySize // a P256 private key is the same size
|
||||
case pqPublicKey:
|
||||
return map[bool]int{false: x25519KeySize, true: p256PublicKeySize}[p256] + suite.kem.publicKeySize
|
||||
case pqSigningPublicKey:
|
||||
return map[bool]int{false: ed25519PublicKeySize, true: ecdsaP256PublicKeySize}[p256] + suite.dsa.publicKeySize
|
||||
default:
|
||||
return map[bool]int{false: ed25519PrivateKeySize, true: p256PrivateKeySize}[p256] + suite.dsa.privateKeySize
|
||||
}
|
||||
}
|
||||
|
||||
// marshalPQKeyToPEM PEM encodes a composite key of suite, nil for an unknown curve or suite or a key of the wrong size.
|
||||
func marshalPQKeyToPEM(kind pqKeyKind, curve Curve, suite PQSuite, b []byte) []byte {
|
||||
p := pqSuites[suite]
|
||||
if p == nil || (curve != Curve_CURVE25519 && curve != Curve_P256) || len(b) != pqKeySize(kind, curve, p) {
|
||||
return nil
|
||||
}
|
||||
return pem.EncodeToMemory(&pem.Block{Type: pqBannerName(kind, curve, p), Bytes: b})
|
||||
}
|
||||
|
||||
// unmarshalPQKeyBlock checks a PEM block that may hold a composite key of one of kinds. ok is false when the banner
|
||||
// is not a composite key banner of those kinds at all.
|
||||
func unmarshalPQKeyBlock(k *pem.Block, kinds ...pqKeyKind) (curve Curve, ok bool, err error) {
|
||||
b, found := pqKeyBanners[k.Type]
|
||||
if !found || !slices.Contains(kinds, b.kind) {
|
||||
return 0, false, nil
|
||||
}
|
||||
if b.kind != pqEncryptedSigningPrivateKey && len(k.Bytes) != b.size {
|
||||
return b.curve, true, fmt.Errorf("key was not %d bytes, is invalid %s", b.size, strings.ToLower(strings.TrimPrefix(k.Type, "NEBULA ")))
|
||||
}
|
||||
return b.curve, true, nil
|
||||
}
|
||||
|
||||
/* including "ECDSA" in the P256 banners is a clue that these keys should be used only for signing */
|
||||
@@ -154,6 +223,12 @@ func unmarshalCertificateBlock(block *pem.Block) (Certificate, error) {
|
||||
}
|
||||
|
||||
func marshalCertPublicKeyToPEM(c Certificate) []byte {
|
||||
if suite := PQSuiteOf(c); suite != PQSuiteUnknown {
|
||||
if c.IsCA() {
|
||||
return MarshalPQSigningPublicKeyToPEM(c.Curve(), suite, c.PublicKey())
|
||||
}
|
||||
return MarshalPQPublicKeyToPEM(c.Curve(), suite, c.PublicKey())
|
||||
}
|
||||
if c.IsCA() {
|
||||
return MarshalSigningPublicKeyToPEM(c.Curve(), c.PublicKey())
|
||||
} else {
|
||||
@@ -164,13 +239,6 @@ func marshalCertPublicKeyToPEM(c Certificate) []byte {
|
||||
// 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 {
|
||||
pq := len(b) > MLKEM1024PublicKeySize
|
||||
switch {
|
||||
case pq && curve == Curve_CURVE25519:
|
||||
return pem.EncodeToMemory(&pem.Block{Type: X25519MLKEM1024PublicKeyBanner, Bytes: b})
|
||||
case pq && curve == Curve_P256:
|
||||
return pem.EncodeToMemory(&pem.Block{Type: P256MLKEM1024PublicKeyBanner, Bytes: b})
|
||||
}
|
||||
switch curve {
|
||||
case Curve_CURVE25519:
|
||||
return pem.EncodeToMemory(&pem.Block{Type: X25519PublicKeyBanner, Bytes: b})
|
||||
@@ -181,16 +249,15 @@ func MarshalPublicKeyToPEM(curve Curve, b []byte) []byte {
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalPQPublicKeyToPEM returns a PEM representation of a composite host public key of suite, the classical ECDH
|
||||
// key followed by the suite's KEM public key, or nil if b is not one.
|
||||
func MarshalPQPublicKeyToPEM(curve Curve, suite PQSuite, b []byte) []byte {
|
||||
return marshalPQKeyToPEM(pqPublicKey, curve, suite, b)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
pq := len(b) > MLDSA87PublicKeySize
|
||||
switch {
|
||||
case pq && curve == Curve_CURVE25519:
|
||||
return pem.EncodeToMemory(&pem.Block{Type: Ed25519MLDSA87PublicKeyBanner, Bytes: b})
|
||||
case pq && curve == Curve_P256:
|
||||
return pem.EncodeToMemory(&pem.Block{Type: ECDSAP256MLDSA87PublicKeyBanner, Bytes: b})
|
||||
}
|
||||
switch curve {
|
||||
case Curve_CURVE25519:
|
||||
return pem.EncodeToMemory(&pem.Block{Type: Ed25519PublicKeyBanner, Bytes: b})
|
||||
@@ -201,6 +268,12 @@ func MarshalSigningPublicKeyToPEM(curve Curve, b []byte) []byte {
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalPQSigningPublicKeyToPEM returns a PEM representation of a composite CA public key of suite, the classical
|
||||
// signing key followed by the public key of the suite's signature algorithm, or nil if b is not one.
|
||||
func MarshalPQSigningPublicKeyToPEM(curve Curve, suite PQSuite, b []byte) []byte {
|
||||
return marshalPQKeyToPEM(pqSigningPublicKey, curve, suite, b)
|
||||
}
|
||||
|
||||
// UnmarshalPublicKeyFromPEM will try to unmarshal the first pem block in a byte array, returning any non
|
||||
// consumed data or an error on failure. Only key-agreement (ECDH) public key banners are accepted.
|
||||
// Use UnmarshalSigningPublicKeyFromPEM for Ed25519/ECDSA banners.
|
||||
@@ -209,6 +282,12 @@ func UnmarshalPublicKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) {
|
||||
if k == nil {
|
||||
return nil, r, 0, fmt.Errorf("input did not contain a valid PEM encoded block")
|
||||
}
|
||||
if curve, ok, err := unmarshalPQKeyBlock(k, pqPublicKey); ok {
|
||||
if err != nil {
|
||||
return nil, r, 0, err
|
||||
}
|
||||
return k.Bytes, r, curve, nil
|
||||
}
|
||||
var expectedLen int
|
||||
var curve Curve
|
||||
switch k.Type {
|
||||
@@ -219,12 +298,6 @@ func UnmarshalPublicKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) {
|
||||
// Uncompressed
|
||||
expectedLen = 65
|
||||
curve = Curve_P256
|
||||
case X25519MLKEM1024PublicKeyBanner:
|
||||
expectedLen = x25519KeySize + MLKEM1024PublicKeySize
|
||||
curve = Curve_CURVE25519
|
||||
case P256MLKEM1024PublicKeyBanner:
|
||||
expectedLen = p256PublicKeySize + MLKEM1024PublicKeySize
|
||||
curve = Curve_P256
|
||||
default:
|
||||
return nil, r, 0, fmt.Errorf("bytes did not contain a proper public key banner")
|
||||
}
|
||||
@@ -242,6 +315,12 @@ func UnmarshalSigningPublicKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) {
|
||||
if k == nil {
|
||||
return nil, r, 0, fmt.Errorf("input did not contain a valid PEM encoded block")
|
||||
}
|
||||
if curve, ok, err := unmarshalPQKeyBlock(k, pqSigningPublicKey); ok {
|
||||
if err != nil {
|
||||
return nil, r, 0, err
|
||||
}
|
||||
return k.Bytes, r, curve, nil
|
||||
}
|
||||
var expectedLen int
|
||||
var curve Curve
|
||||
switch k.Type {
|
||||
@@ -252,12 +331,6 @@ func UnmarshalSigningPublicKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) {
|
||||
// Uncompressed
|
||||
expectedLen = 65
|
||||
curve = Curve_P256
|
||||
case Ed25519MLDSA87PublicKeyBanner:
|
||||
expectedLen = ed25519PublicKeySize + MLDSA87PublicKeySize
|
||||
curve = Curve_CURVE25519
|
||||
case ECDSAP256MLDSA87PublicKeyBanner:
|
||||
expectedLen = ecdsaP256PublicKeySize + MLDSA87PublicKeySize
|
||||
curve = Curve_P256
|
||||
default:
|
||||
return nil, r, 0, fmt.Errorf("bytes did not contain a proper Ed25519/ECDSA public key banner")
|
||||
}
|
||||
@@ -268,13 +341,6 @@ func UnmarshalSigningPublicKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) {
|
||||
}
|
||||
|
||||
func MarshalPrivateKeyToPEM(curve Curve, b []byte) []byte {
|
||||
pq := len(b) > MLKEM1024PrivateKeySize
|
||||
switch {
|
||||
case pq && curve == Curve_CURVE25519:
|
||||
return pem.EncodeToMemory(&pem.Block{Type: X25519MLKEM1024PrivateKeyBanner, Bytes: b})
|
||||
case pq && curve == Curve_P256:
|
||||
return pem.EncodeToMemory(&pem.Block{Type: P256MLKEM1024PrivateKeyBanner, Bytes: b})
|
||||
}
|
||||
switch curve {
|
||||
case Curve_CURVE25519:
|
||||
return pem.EncodeToMemory(&pem.Block{Type: X25519PrivateKeyBanner, Bytes: b})
|
||||
@@ -285,14 +351,19 @@ func MarshalPrivateKeyToPEM(curve Curve, b []byte) []byte {
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalPQPrivateKeyToPEM returns a PEM representation of a composite host private key of suite, the classical ECDH
|
||||
// key followed by the seed of the suite's KEM, or nil if b is not one.
|
||||
func MarshalPQPrivateKeyToPEM(curve Curve, suite PQSuite, b []byte) []byte {
|
||||
return marshalPQKeyToPEM(pqPrivateKey, curve, suite, b)
|
||||
}
|
||||
|
||||
// MarshalPQSigningPrivateKeyToPEM returns a PEM representation of a composite CA private key of suite, the classical
|
||||
// signing key followed by the seed of the suite's signature algorithm, or nil if b is not one.
|
||||
func MarshalPQSigningPrivateKeyToPEM(curve Curve, suite PQSuite, b []byte) []byte {
|
||||
return marshalPQKeyToPEM(pqSigningPrivateKey, curve, suite, b)
|
||||
}
|
||||
|
||||
func MarshalSigningPrivateKeyToPEM(curve Curve, b []byte) []byte {
|
||||
pq := len(b) == signingPrivateKeySize(curve)+MLDSA87PrivateKeySize
|
||||
switch {
|
||||
case pq && curve == Curve_CURVE25519:
|
||||
return pem.EncodeToMemory(&pem.Block{Type: Ed25519MLDSA87PrivateKeyBanner, Bytes: b})
|
||||
case pq && curve == Curve_P256:
|
||||
return pem.EncodeToMemory(&pem.Block{Type: ECDSAP256MLDSA87PrivateKeyBanner, Bytes: b})
|
||||
}
|
||||
switch curve {
|
||||
case Curve_CURVE25519:
|
||||
return pem.EncodeToMemory(&pem.Block{Type: Ed25519PrivateKeyBanner, Bytes: b})
|
||||
@@ -310,6 +381,12 @@ func UnmarshalPrivateKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) {
|
||||
if k == nil {
|
||||
return nil, r, 0, fmt.Errorf("input did not contain a valid PEM encoded block")
|
||||
}
|
||||
if curve, ok, err := unmarshalPQKeyBlock(k, pqPrivateKey); ok {
|
||||
if err != nil {
|
||||
return nil, r, 0, err
|
||||
}
|
||||
return k.Bytes, r, curve, nil
|
||||
}
|
||||
var expectedLen int
|
||||
var curve Curve
|
||||
switch k.Type {
|
||||
@@ -319,12 +396,6 @@ func UnmarshalPrivateKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) {
|
||||
case P256PrivateKeyBanner:
|
||||
expectedLen = 32
|
||||
curve = Curve_P256
|
||||
case X25519MLKEM1024PrivateKeyBanner:
|
||||
expectedLen = x25519KeySize + MLKEM1024PrivateKeySize
|
||||
curve = Curve_CURVE25519
|
||||
case P256MLKEM1024PrivateKeyBanner:
|
||||
expectedLen = p256PrivateKeySize + MLKEM1024PrivateKeySize
|
||||
curve = Curve_P256
|
||||
default:
|
||||
return nil, r, 0, fmt.Errorf("bytes did not contain a proper private key banner")
|
||||
}
|
||||
@@ -339,24 +410,21 @@ func UnmarshalSigningPrivateKeyFromPEM(b []byte) ([]byte, []byte, Curve, error)
|
||||
if k == nil {
|
||||
return nil, r, 0, fmt.Errorf("input did not contain a valid PEM encoded block")
|
||||
}
|
||||
if curve, ok, err := unmarshalPQKeyBlock(k, pqSigningPrivateKey, pqEncryptedSigningPrivateKey); ok {
|
||||
switch {
|
||||
case err != nil:
|
||||
return nil, r, 0, err
|
||||
case pqKeyBanners[k.Type].kind == pqEncryptedSigningPrivateKey:
|
||||
return nil, nil, curve, ErrPrivateKeyEncrypted
|
||||
}
|
||||
return k.Bytes, r, curve, nil
|
||||
}
|
||||
var curve Curve
|
||||
switch k.Type {
|
||||
case EncryptedEd25519PrivateKeyBanner:
|
||||
return nil, nil, Curve_CURVE25519, ErrPrivateKeyEncrypted
|
||||
case EncryptedECDSAP256PrivateKeyBanner, EncryptedECDSAP256MLDSA87PrivateKeyBanner:
|
||||
case EncryptedECDSAP256PrivateKeyBanner:
|
||||
return nil, nil, Curve_P256, ErrPrivateKeyEncrypted
|
||||
case EncryptedEd25519MLDSA87PrivateKeyBanner:
|
||||
return nil, nil, Curve_CURVE25519, ErrPrivateKeyEncrypted
|
||||
case Ed25519MLDSA87PrivateKeyBanner:
|
||||
curve = Curve_CURVE25519
|
||||
if len(k.Bytes) != ed25519PrivateKeySize+MLDSA87PrivateKeySize {
|
||||
return nil, r, 0, fmt.Errorf("key was not %d bytes, is invalid Ed25519 ML-DSA-87 private key", ed25519PrivateKeySize+MLDSA87PrivateKeySize)
|
||||
}
|
||||
case ECDSAP256MLDSA87PrivateKeyBanner:
|
||||
curve = Curve_P256
|
||||
if len(k.Bytes) != p256PrivateKeySize+MLDSA87PrivateKeySize {
|
||||
return nil, r, 0, fmt.Errorf("key was not %d bytes, is invalid ECDSA P256 ML-DSA-87 private key", p256PrivateKeySize+MLDSA87PrivateKeySize)
|
||||
}
|
||||
case Ed25519PrivateKeyBanner:
|
||||
curve = Curve_CURVE25519
|
||||
if len(k.Bytes) != ed25519.PrivateKeySize {
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
package cert
|
||||
|
||||
import (
|
||||
"crypto/mldsa"
|
||||
"crypto/mlkem"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PQSuite names the post-quantum algorithms of a version 3 certificate, the way Curve names its classical ones: the
|
||||
// key encapsulation mechanism of a host's key, and the signature algorithm of a CA's key and of the certificates it
|
||||
// signs. A CA and every certificate it signs share a suite. Every version 3 certificate names its suite.
|
||||
type PQSuite uint8
|
||||
|
||||
const (
|
||||
// PQSuiteUnknown is never valid in a certificate.
|
||||
PQSuiteUnknown PQSuite = 0
|
||||
|
||||
// PQSuite_MLKEM1024_MLDSA87 gives hosts ML-KEM-1024 keys and CAs ML-DSA-87 keys and signatures.
|
||||
PQSuite_MLKEM1024_MLDSA87 PQSuite = 1
|
||||
)
|
||||
|
||||
// DefaultPQSuite is the suite tools use when none is asked for.
|
||||
const DefaultPQSuite = PQSuite_MLKEM1024_MLDSA87
|
||||
|
||||
// pqKEM is the key encapsulation mechanism of a suite, what host keys use. Private keys are seeds.
|
||||
type pqKEM struct {
|
||||
name string // as it appears in key banners
|
||||
publicKeySize int
|
||||
privateKeySize int
|
||||
generate func() (pub, priv []byte, err error)
|
||||
publicKey func(priv []byte) ([]byte, error)
|
||||
checkPublicKey func(pub []byte) bool
|
||||
}
|
||||
|
||||
// pqDSA is the signature algorithm of a suite, what CA keys and certificate signatures use. Private keys are seeds.
|
||||
type pqDSA struct {
|
||||
name string // as it appears in key banners
|
||||
publicKeySize int
|
||||
privateKeySize int
|
||||
signatureSize int
|
||||
generate func() (pub, priv []byte, err error)
|
||||
publicKey func(priv []byte) ([]byte, error)
|
||||
checkPublicKey func(pub []byte) bool
|
||||
signer func(priv []byte) (SignerLambda, error) // must sign deterministically
|
||||
verify func(pub, msg, sig []byte) bool
|
||||
}
|
||||
|
||||
type pqSuite struct {
|
||||
kem pqKEM
|
||||
dsa pqDSA
|
||||
}
|
||||
|
||||
// pqSuites holds every known suite, see registerPQSuite.
|
||||
var pqSuites = map[PQSuite]*pqSuite{}
|
||||
|
||||
func init() {
|
||||
registerPQSuite(PQSuite_MLKEM1024_MLDSA87, &pqSuite{kem: mlkem1024, dsa: mldsa87})
|
||||
}
|
||||
|
||||
// registerPQSuite makes a suite known, along with the banners of its composite keys.
|
||||
func registerPQSuite(id PQSuite, s *pqSuite) {
|
||||
pqSuites[id] = s
|
||||
for _, curve := range []Curve{Curve_CURVE25519, Curve_P256} {
|
||||
for _, kind := range pqKeyKinds {
|
||||
pqKeyBanners[pqBannerName(kind, curve, s)] = pqKeyBanner{kind: kind, curve: curve, size: pqKeySize(kind, curve, s)}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var mlkem1024 = pqKEM{
|
||||
name: "MLKEM1024",
|
||||
publicKeySize: mlkem.EncapsulationKeySize1024,
|
||||
privateKeySize: mlkem.SeedSize,
|
||||
generate: func() ([]byte, []byte, error) {
|
||||
dk, err := mlkem.GenerateKey1024()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return dk.EncapsulationKey().Bytes(), dk.Bytes(), nil
|
||||
},
|
||||
publicKey: func(priv []byte) ([]byte, error) {
|
||||
dk, err := mlkem.NewDecapsulationKey1024(priv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dk.EncapsulationKey().Bytes(), nil
|
||||
},
|
||||
checkPublicKey: func(pub []byte) bool {
|
||||
_, err := mlkem.NewEncapsulationKey1024(pub)
|
||||
return err == nil
|
||||
},
|
||||
}
|
||||
|
||||
var mldsa87 = pqDSA{
|
||||
name: "MLDSA87",
|
||||
publicKeySize: mldsa.MLDSA87PublicKeySize,
|
||||
privateKeySize: mldsa.PrivateKeySize,
|
||||
signatureSize: mldsa.MLDSA87SignatureSize,
|
||||
generate: func() ([]byte, []byte, error) {
|
||||
sk, err := mldsa.GenerateKey(mldsa.MLDSA87())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return sk.PublicKey().Bytes(), sk.Bytes(), nil
|
||||
},
|
||||
publicKey: func(priv []byte) ([]byte, error) {
|
||||
sk, err := mldsa.NewPrivateKey(mldsa.MLDSA87(), priv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sk.PublicKey().Bytes(), nil
|
||||
},
|
||||
checkPublicKey: func(pub []byte) bool {
|
||||
_, err := mldsa.NewPublicKey(mldsa.MLDSA87(), pub)
|
||||
return err == nil
|
||||
},
|
||||
// Deterministic signing (FIPS 204 section 3.4): the hedged variant guards signers an attacker can fault
|
||||
// physically, which a CA key in a file is not, and the Ed25519 signature beside it is deterministic too.
|
||||
signer: func(priv []byte) (SignerLambda, error) {
|
||||
sk, err := mldsa.NewPrivateKey(mldsa.MLDSA87(), priv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return func(b []byte) ([]byte, error) {
|
||||
return sk.SignDeterministic(b, &mldsa.Options{Context: pqSignatureContext})
|
||||
}, nil
|
||||
},
|
||||
verify: func(pub, msg, sig []byte) bool {
|
||||
pk, err := mldsa.NewPublicKey(mldsa.MLDSA87(), pub)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return mldsa.Verify(pk, msg, sig, &mldsa.Options{Context: pqSignatureContext}) == nil
|
||||
},
|
||||
}
|
||||
|
||||
// PQSuites returns every known suite.
|
||||
func PQSuites() []PQSuite {
|
||||
s := make([]PQSuite, 0, len(pqSuites))
|
||||
for k := range pqSuites {
|
||||
s = append(s, k)
|
||||
}
|
||||
slices.Sort(s)
|
||||
return s
|
||||
}
|
||||
|
||||
// ParsePQSuite returns the suite named by s, as String names it, ignoring case.
|
||||
func ParsePQSuite(s string) (PQSuite, error) {
|
||||
for _, k := range PQSuites() {
|
||||
if strings.EqualFold(s, k.String()) {
|
||||
return k, nil
|
||||
}
|
||||
}
|
||||
return PQSuiteUnknown, fmt.Errorf("%w: %q", ErrUnknownPQSuite, s)
|
||||
}
|
||||
|
||||
// PQSuiteOf returns the suite of a version 3 certificate, or PQSuiteUnknown for any other version.
|
||||
func PQSuiteOf(c Certificate) PQSuite {
|
||||
if c3, ok := c.(*certificateV3); ok {
|
||||
return c3.pqSuite
|
||||
}
|
||||
return PQSuiteUnknown
|
||||
}
|
||||
|
||||
func (s PQSuite) String() string {
|
||||
if p := pqSuites[s]; p != nil {
|
||||
return p.kem.name + "_" + p.dsa.name
|
||||
}
|
||||
return fmt.Sprintf("PQSuite(%d)", uint8(s))
|
||||
}
|
||||
|
||||
// Valid reports whether s is a known suite.
|
||||
func (s PQSuite) Valid() bool {
|
||||
return pqSuites[s] != nil
|
||||
}
|
||||
|
||||
// PublicKeySize returns the size of the post-quantum half of a composite public key, a CA's or a host's.
|
||||
func (s PQSuite) PublicKeySize(isCA bool) int {
|
||||
p := pqSuites[s]
|
||||
switch {
|
||||
case p == nil:
|
||||
return 0
|
||||
case isCA:
|
||||
return p.dsa.publicKeySize
|
||||
default:
|
||||
return p.kem.publicKeySize
|
||||
}
|
||||
}
|
||||
|
||||
// PrivateKeySize returns the size of the post-quantum half of a composite private key, a CA's or a host's.
|
||||
func (s PQSuite) PrivateKeySize(isCA bool) int {
|
||||
p := pqSuites[s]
|
||||
switch {
|
||||
case p == nil:
|
||||
return 0
|
||||
case isCA:
|
||||
return p.dsa.privateKeySize
|
||||
default:
|
||||
return p.kem.privateKeySize
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateKey makes a post-quantum keypair, a CA's signing key or a host's KEM key, to append to a classical one.
|
||||
func (s PQSuite) GenerateKey(isCA bool) (pub, priv []byte, err error) {
|
||||
p := pqSuites[s]
|
||||
switch {
|
||||
case p == nil:
|
||||
return nil, nil, fmt.Errorf("%w: %v", ErrUnknownPQSuite, s)
|
||||
case isCA:
|
||||
return p.dsa.generate()
|
||||
default:
|
||||
return p.kem.generate()
|
||||
}
|
||||
}
|
||||
|
||||
// publicKey derives the post-quantum public key from the post-quantum half of a private key.
|
||||
func (s PQSuite) publicKey(isCA bool, priv []byte) ([]byte, error) {
|
||||
p := pqSuites[s]
|
||||
switch {
|
||||
case p == nil:
|
||||
return nil, fmt.Errorf("%w: %v", ErrUnknownPQSuite, s)
|
||||
case isCA:
|
||||
return p.dsa.publicKey(priv)
|
||||
default:
|
||||
return p.kem.publicKey(priv)
|
||||
}
|
||||
}
|
||||
|
||||
// checkPublicKey reports whether pub is a well formed post-quantum public key, a CA's or a host's.
|
||||
func (s PQSuite) checkPublicKey(isCA bool, pub []byte) bool {
|
||||
p := pqSuites[s]
|
||||
switch {
|
||||
case p == nil, len(pub) != s.PublicKeySize(isCA):
|
||||
return false
|
||||
case isCA:
|
||||
return p.dsa.checkPublicKey(pub)
|
||||
default:
|
||||
return p.kem.checkPublicKey(pub)
|
||||
}
|
||||
}
|
||||
|
||||
// signatureSize returns the size of the post-quantum signature of a certificate in this suite.
|
||||
func (s PQSuite) signatureSize() int {
|
||||
if p := pqSuites[s]; p != nil {
|
||||
return p.dsa.signatureSize
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// SplitPublicKey splits a composite public key into its classical and post-quantum halves.
|
||||
func (s PQSuite) SplitPublicKey(isCA bool, key []byte) (classical, pq []byte, err error) {
|
||||
n := s.PublicKeySize(isCA)
|
||||
if n == 0 || len(key) <= n {
|
||||
return nil, nil, ErrInvalidPublicKey
|
||||
}
|
||||
return key[:len(key)-n], key[len(key)-n:], nil
|
||||
}
|
||||
|
||||
// SplitPrivateKey splits a composite private key into its classical and post-quantum halves.
|
||||
func (s PQSuite) SplitPrivateKey(isCA bool, key []byte) (classical, pq []byte, err error) {
|
||||
n := s.PrivateKeySize(isCA)
|
||||
if n == 0 || len(key) <= n {
|
||||
return nil, nil, ErrInvalidPrivateKey
|
||||
}
|
||||
return key[:len(key)-n], key[len(key)-n:], nil
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package cert
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testPQSuite stands in for a second suite. It reuses ML-KEM-1024 and ML-DSA-87 under other names, so it is
|
||||
// interchangeable with the default suite in everything but its identity, which is what has to keep them apart.
|
||||
const testPQSuite PQSuite = 200
|
||||
|
||||
func registerTestPQSuite(t *testing.T) {
|
||||
t.Helper()
|
||||
kem, dsa := mlkem1024, mldsa87
|
||||
kem.name, dsa.name = "TESTKEM", "TESTDSA"
|
||||
registerPQSuite(testPQSuite, &pqSuite{kem: kem, dsa: dsa})
|
||||
t.Cleanup(func() {
|
||||
delete(pqSuites, testPQSuite)
|
||||
for k := range pqKeyBanners {
|
||||
if strings.Contains(k, "TESTKEM") || strings.Contains(k, "TESTDSA") {
|
||||
delete(pqKeyBanners, k)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// newTestV3InSuite issues a v3 CA and host certificate in suite.
|
||||
func newTestV3InSuite(t *testing.T, curve Curve, suite PQSuite) (ca Certificate, caKey []byte, host Certificate, hostKey []byte) {
|
||||
t.Helper()
|
||||
ca2, _, caKey2, _ := NewTestCaCert(Version2, curve, time.Time{}, time.Time{}, nil, nil, nil)
|
||||
pqPub, pqPriv, err := suite.GenerateKey(true)
|
||||
require.NoError(t, err)
|
||||
signPQ, err := SignPQ(suite, pqPriv)
|
||||
require.NoError(t, err)
|
||||
ca, err = ExtendToV3(ca2, suite, pqPub, nil, signPQ)
|
||||
require.NoError(t, err)
|
||||
caKey = append(caKey2, pqPriv...)
|
||||
|
||||
var pub, priv []byte
|
||||
if curve == Curve_P256 {
|
||||
pub, priv = P256Keypair()
|
||||
} else {
|
||||
pub, priv = X25519Keypair()
|
||||
}
|
||||
hostPQPub, hostPQPriv, err := suite.GenerateKey(false)
|
||||
require.NoError(t, err)
|
||||
tbs := &TBSCertificate{
|
||||
Version: Version3,
|
||||
Curve: curve,
|
||||
PQSuite: suite,
|
||||
Name: "host",
|
||||
Networks: []netip.Prefix{netip.MustParsePrefix("10.1.0.1/16")},
|
||||
NotBefore: ca.NotBefore(),
|
||||
NotAfter: ca.NotAfter(),
|
||||
PublicKey: append(pub, hostPQPub...),
|
||||
}
|
||||
host, err = tbs.Sign(ca, curve, caKey)
|
||||
require.NoError(t, err)
|
||||
return ca, caKey, host, append(priv, hostPQPriv...)
|
||||
}
|
||||
|
||||
func TestPQSuite_Names(t *testing.T) {
|
||||
assert.Equal(t, "MLKEM1024_MLDSA87", PQSuite_MLKEM1024_MLDSA87.String())
|
||||
assert.Equal(t, []PQSuite{PQSuite_MLKEM1024_MLDSA87}, PQSuites())
|
||||
assert.False(t, PQSuiteUnknown.Valid())
|
||||
|
||||
s, err := ParsePQSuite("mlkem1024_mldsa87")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, PQSuite_MLKEM1024_MLDSA87, s)
|
||||
_, err = ParsePQSuite("mlkem768_mldsa65")
|
||||
require.ErrorIs(t, err, ErrUnknownPQSuite)
|
||||
|
||||
_, _, host, _ := newTestV3(t, Curve_CURVE25519)
|
||||
assert.Equal(t, DefaultPQSuite, PQSuiteOf(host))
|
||||
assert.Equal(t, PQSuiteUnknown, PQSuiteOf(V2Form(host)))
|
||||
}
|
||||
|
||||
// A second suite works end to end, and is never confused with the first, even with keys of the same sizes.
|
||||
func TestPQSuite_Second(t *testing.T) {
|
||||
registerTestPQSuite(t)
|
||||
|
||||
for _, curve := range testCurves {
|
||||
t.Run(curve.String(), func(t *testing.T) {
|
||||
ca, caKey, host, hostKey := newTestV3InSuite(t, curve, testPQSuite)
|
||||
assert.Equal(t, testPQSuite, PQSuiteOf(ca))
|
||||
assert.Equal(t, testPQSuite, PQSuiteOf(host))
|
||||
require.NoError(t, ca.VerifyPrivateKey(curve, caKey))
|
||||
require.NoError(t, host.VerifyPrivateKey(curve, hostKey))
|
||||
|
||||
pool := NewCAPool()
|
||||
require.NoError(t, pool.AddCA(ca))
|
||||
_, err := pool.VerifyCertificate(time.Now(), host)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The suite travels in the certificate, handshakes included.
|
||||
hs, err := host.MarshalForHandshakes()
|
||||
require.NoError(t, err)
|
||||
c, err := Recombine(Version3, hs, host.PublicKey(), curve)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testPQSuite, PQSuiteOf(c))
|
||||
assertCertsEqual(t, host, c)
|
||||
|
||||
// Keys get their algorithms' banners.
|
||||
b := MarshalPQPrivateKeyToPEM(curve, testPQSuite, hostKey)
|
||||
assert.Contains(t, string(b), " TESTKEM PRIVATE KEY-----")
|
||||
key, _, _, err := UnmarshalPrivateKeyFromPEM(b)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, hostKey, key)
|
||||
assert.Contains(t, string(host.MarshalPublicKeyPEM()), " TESTKEM PUBLIC KEY-----")
|
||||
assert.Contains(t, string(ca.MarshalPublicKeyPEM()), " TESTDSA PUBLIC KEY-----")
|
||||
|
||||
// The suite is signed: relabelling a certificate breaks it, though the keys fit either suite.
|
||||
_, _, other, _ := newTestV3(t, curve)
|
||||
relabelled := other.Copy().(*certificateV3)
|
||||
relabelled.pqSuite = testPQSuite
|
||||
assert.False(t, relabelled.CheckSignature(ca.PublicKey()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// A CA and the certificates it signs share a suite, like they share a curve.
|
||||
func TestPQSuite_Mismatch(t *testing.T) {
|
||||
registerTestPQSuite(t)
|
||||
|
||||
for _, curve := range testCurves {
|
||||
t.Run(curve.String(), func(t *testing.T) {
|
||||
ca, caKey, host, _ := newTestV3(t, curve)
|
||||
|
||||
// Signing refuses a certificate in another suite than its CA.
|
||||
tbs := &TBSCertificate{
|
||||
Version: Version3, Curve: curve, PQSuite: testPQSuite, Name: "x",
|
||||
Networks: host.Networks(), NotBefore: host.NotBefore(), NotAfter: host.NotAfter(), PublicKey: host.PublicKey(),
|
||||
}
|
||||
_, err := tbs.Sign(ca, curve, caKey)
|
||||
require.ErrorIs(t, err, ErrPQSuiteMismatch)
|
||||
|
||||
// Extending refuses it too.
|
||||
v2CA := V2Form(ca)
|
||||
v2Host := V2Form(host)
|
||||
_, pqHost, err := DefaultPQSuite.SplitPublicKey(false, host.PublicKey())
|
||||
require.NoError(t, err)
|
||||
signPQ, err := SignPQ(DefaultPQSuite, caKey[len(caKey)-DefaultPQSuite.PrivateKeySize(true):])
|
||||
require.NoError(t, err)
|
||||
_, err = ExtendToV3(v2Host, testPQSuite, pqHost, ca, signPQ)
|
||||
require.ErrorIs(t, err, ErrPQSuiteMismatch)
|
||||
|
||||
// The pool rejects a certificate whose CA, found by issuer, is in another suite: here the same v2 CA
|
||||
// extended once per suite, with only the default suite's extension trusted.
|
||||
pqPub, pqPriv, err := testPQSuite.GenerateKey(true)
|
||||
require.NoError(t, err)
|
||||
signOther, err := SignPQ(testPQSuite, pqPriv)
|
||||
require.NoError(t, err)
|
||||
otherCA, err := ExtendToV3(v2CA, testPQSuite, pqPub, nil, signOther)
|
||||
require.NoError(t, err)
|
||||
otherHost, err := ExtendToV3(v2Host, testPQSuite, pqHost, otherCA, signOther)
|
||||
require.NoError(t, err)
|
||||
|
||||
pool := NewCAPool()
|
||||
require.NoError(t, pool.AddCA(ca))
|
||||
_, err = pool.VerifyCertificate(time.Now(), otherHost)
|
||||
require.ErrorIs(t, err, ErrPQSuiteMismatch)
|
||||
})
|
||||
}
|
||||
}
|
||||
+20
-11
@@ -26,6 +26,7 @@ type TBSCertificate struct {
|
||||
NotAfter time.Time
|
||||
PublicKey []byte
|
||||
Curve Curve
|
||||
PQSuite PQSuite // version 3 only, the suite of the certificate and of its signing CA
|
||||
issuer string
|
||||
}
|
||||
|
||||
@@ -48,15 +49,16 @@ type SignerLambda func(certBytes []byte) ([]byte, error)
|
||||
// If the TBSCertificate is a CA then signer must be nil.
|
||||
func (t *TBSCertificate) Sign(signer Certificate, curve Curve, key []byte) (Certificate, error) {
|
||||
if t.Version == Version3 {
|
||||
// The signing key is a composite CA key, classical || ML-DSA-87 seed.
|
||||
if len(key) <= MLDSA87PrivateKeySize {
|
||||
return nil, ErrInvalidPrivateKey
|
||||
}
|
||||
classical, err := classicalSigner(t.Curve, key[:len(key)-MLDSA87PrivateKeySize])
|
||||
// The signing key is a composite CA key, classical || the seed of the suite's signature algorithm.
|
||||
classicalKey, pqKey, err := t.PQSuite.SplitPrivateKey(true, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pq, err := SignPQ(key[len(key)-MLDSA87PrivateKeySize:])
|
||||
classical, err := classicalSigner(t.Curve, classicalKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pq, err := SignPQ(t.PQSuite, pqKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -98,14 +100,18 @@ func classicalSigner(curve Curve, key []byte) (SignerLambda, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// SignHybridWith signs a version 3 certificate, using sp for the classical signature and pqSp for the ML-DSA-87
|
||||
// signature. t.PublicKey is the composite public key. signer must be a version 3 CA, or nil for a self signed CA.
|
||||
// The v2 form is signed exactly as SignWith would sign it, and so is the v2 form of signer.
|
||||
// SignHybridWith signs a version 3 certificate, using sp for the classical signature and pqSp for the post-quantum
|
||||
// signature of t.PQSuite. t.PublicKey is the composite public key. signer must be a version 3 CA in the same suite,
|
||||
// or nil for a self signed CA. The v2 form is signed exactly as SignWith would sign it, and so is the v2 form of
|
||||
// signer.
|
||||
func (t *TBSCertificate) SignHybridWith(signer Certificate, curve Curve, sp, pqSp SignerLambda) (Certificate, error) {
|
||||
if t.Version != Version3 {
|
||||
return nil, fmt.Errorf("SignHybridWith only signs version 3 certificates")
|
||||
}
|
||||
classical, pq, err := splitPQPublicKey(t.IsCA, t.PublicKey)
|
||||
if !t.PQSuite.Valid() {
|
||||
return nil, fmt.Errorf("%w: %v", ErrUnknownPQSuite, t.PQSuite)
|
||||
}
|
||||
classical, pq, err := t.PQSuite.SplitPublicKey(t.IsCA, t.PublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -115,6 +121,9 @@ func (t *TBSCertificate) SignHybridWith(signer Certificate, curve Curve, sp, pqS
|
||||
if signer.Version() != Version3 {
|
||||
return nil, fmt.Errorf("a version 3 certificate must be signed by a version 3 CA")
|
||||
}
|
||||
if PQSuiteOf(signer) != t.PQSuite {
|
||||
return nil, ErrPQSuiteMismatch
|
||||
}
|
||||
signerV2 = V2Form(signer)
|
||||
}
|
||||
|
||||
@@ -125,7 +134,7 @@ func (t *TBSCertificate) SignHybridWith(signer Certificate, curve Curve, sp, pqS
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ExtendToV3(c2, pq, signer, pqSp)
|
||||
return ExtendToV3(c2, t.PQSuite, pq, signer, pqSp)
|
||||
}
|
||||
|
||||
// SignWith does the same thing as sign, but uses the function in `sp` to calculate the signature.
|
||||
|
||||
+16
-10
@@ -4,8 +4,6 @@ import (
|
||||
"crypto/ecdh"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/mldsa"
|
||||
"crypto/mlkem"
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"net/netip"
|
||||
@@ -65,6 +63,9 @@ func NewTestCaCert(version cert.Version, curve cert.Curve, before, after time.Ti
|
||||
Groups: groups,
|
||||
IsCA: true,
|
||||
}
|
||||
if version == cert.Version3 {
|
||||
t.PQSuite = cert.DefaultPQSuite
|
||||
}
|
||||
|
||||
c, err := t.Sign(nil, curve, priv)
|
||||
if err != nil {
|
||||
@@ -101,7 +102,7 @@ func NewTestCert(v cert.Version, curve cert.Curve, ca cert.Certificate, key []by
|
||||
}
|
||||
|
||||
if v == cert.Version3 {
|
||||
pub, priv = appendPQKey(pub, priv)
|
||||
pub, priv = appendPQKey(ca, pub, priv)
|
||||
}
|
||||
|
||||
nc := &cert.TBSCertificate{
|
||||
@@ -115,6 +116,7 @@ func NewTestCert(v cert.Version, curve cert.Curve, ca cert.Certificate, key []by
|
||||
NotAfter: time.Unix(after.Unix(), 0),
|
||||
PublicKey: pub,
|
||||
IsCA: false,
|
||||
PQSuite: cert.PQSuiteOf(ca),
|
||||
}
|
||||
|
||||
c, err := nc.Sign(ca, ca.Curve(), key)
|
||||
@@ -127,6 +129,9 @@ func NewTestCert(v cert.Version, curve cert.Curve, ca cert.Certificate, key []by
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if v == cert.Version3 {
|
||||
return c, pub, cert.MarshalPQPrivateKeyToPEM(curve, cert.PQSuiteOf(ca), priv), pem
|
||||
}
|
||||
return c, pub, cert.MarshalPrivateKeyToPEM(curve, priv), pem
|
||||
}
|
||||
|
||||
@@ -142,6 +147,7 @@ func NewTestCertDifferentVersion(c cert.Certificate, v cert.Version, ca cert.Cer
|
||||
NotAfter: time.Unix(c.NotAfter().Unix(), 0),
|
||||
PublicKey: c.PublicKey(),
|
||||
IsCA: false,
|
||||
PQSuite: cert.PQSuiteOf(ca),
|
||||
}
|
||||
|
||||
c, err := nc.Sign(ca, ca.Curve(), key)
|
||||
@@ -157,22 +163,22 @@ func NewTestCertDifferentVersion(c cert.Certificate, v cert.Version, ca cert.Cer
|
||||
return c, pem
|
||||
}
|
||||
|
||||
// appendPQSigningKey makes a classical CA keypair into a version 3 composite one with a fresh ML-DSA-87 key.
|
||||
// appendPQSigningKey makes a classical CA keypair into a version 3 composite one with a fresh key of the default suite.
|
||||
func appendPQSigningKey(pub, priv []byte) ([]byte, []byte) {
|
||||
sk, err := mldsa.GenerateKey(mldsa.MLDSA87())
|
||||
pqPub, pqPriv, err := cert.DefaultPQSuite.GenerateKey(true)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return append(pub, sk.PublicKey().Bytes()...), append(priv, sk.Bytes()...)
|
||||
return append(pub, pqPub...), append(priv, pqPriv...)
|
||||
}
|
||||
|
||||
// appendPQKey makes a classical host keypair into a version 3 composite one with a fresh ML-KEM-1024 key.
|
||||
func appendPQKey(pub, priv []byte) ([]byte, []byte) {
|
||||
dk, err := mlkem.GenerateKey1024()
|
||||
// appendPQKey makes a classical host keypair into a version 3 composite one with a fresh key of the CA's suite.
|
||||
func appendPQKey(ca cert.Certificate, pub, priv []byte) ([]byte, []byte) {
|
||||
pqPub, pqPriv, err := cert.PQSuiteOf(ca).GenerateKey(false)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return append(pub, dk.EncapsulationKey().Bytes()...), append(priv, dk.Bytes()...)
|
||||
return append(pub, pqPub...), append(priv, pqPriv...)
|
||||
}
|
||||
|
||||
func X25519Keypair() ([]byte, []byte) {
|
||||
|
||||
+22
-8
@@ -37,6 +37,7 @@ type caFlags struct {
|
||||
argonParallelism *uint
|
||||
encryption *bool
|
||||
version *uint
|
||||
pqSuite *string
|
||||
|
||||
curve *string
|
||||
p11url *string
|
||||
@@ -71,7 +72,8 @@ func newCaFlags() *caFlags {
|
||||
cf := caFlags{set: flag.NewFlagSet("ca", flag.ContinueOnError)}
|
||||
cf.set.Usage = func() {}
|
||||
cf.name = cf.set.String("name", "", "Required: name of the certificate authority")
|
||||
cf.version = cf.set.Uint("version", uint(cert.Version2), "Optional: version of the certificate format to use. Version 3 adds an ML-DSA-87 key, for post-quantum certificates")
|
||||
cf.version = cf.set.Uint("version", uint(cert.Version2), "Optional: version of the certificate format to use. Version 3 adds a post-quantum key, for post-quantum certificates")
|
||||
cf.pqSuite = pqSuiteFlag(cf.set, "Optional: post-quantum suite of a version 3 CA, which the certificates it signs share")
|
||||
cf.duration = cf.set.Duration("duration", time.Duration(time.Hour*8760), "Optional: amount of time the certificate should be valid for. Valid time units are seconds: \"s\", minutes: \"m\", hours: \"h\"")
|
||||
cf.outKeyPath = cf.set.String("out-key", "ca.key", "Optional: path to write the private key to")
|
||||
cf.outCertPath = cf.set.String("out-crt", "ca.crt", "Optional: path to write the certificate to")
|
||||
@@ -164,7 +166,11 @@ func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error
|
||||
return newHelpErrorf("-version must be %v, %v or %v", cert.Version1, cert.Version2, cert.Version3)
|
||||
}
|
||||
if version == cert.Version3 && isP11 {
|
||||
return newHelpErrorf("-version 3 can not be used with -pkcs11, the ML-DSA-87 key has to be in out-key")
|
||||
return newHelpErrorf("-version 3 can not be used with -pkcs11, the post-quantum key has to be in out-key")
|
||||
}
|
||||
suite, err := parsePQSuiteFlag(cf.set, *cf.pqSuite, version == cert.Version3, "is only for -version 3")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var networks []netip.Prefix
|
||||
@@ -300,7 +306,7 @@ func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error
|
||||
}
|
||||
|
||||
if version == cert.Version3 {
|
||||
pub, rawPriv, err = appendPQSigningKey(pub, rawPriv)
|
||||
pub, rawPriv, err = appendPQSigningKey(suite, pub, rawPriv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -319,6 +325,9 @@ func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error
|
||||
IsCA: true,
|
||||
Curve: curve,
|
||||
}
|
||||
if version == cert.Version3 {
|
||||
t.PQSuite = suite
|
||||
}
|
||||
|
||||
if !isP11 && !isStdio(*cf.outKeyPath) {
|
||||
if _, err := os.Stat(*cf.outKeyPath); err == nil {
|
||||
@@ -346,14 +355,19 @@ func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error
|
||||
return fmt.Errorf("error while signing: %s", err)
|
||||
}
|
||||
|
||||
if *cf.encryption {
|
||||
switch {
|
||||
case *cf.encryption && version == cert.Version3:
|
||||
b, err = cert.EncryptAndMarshalPQSigningPrivateKey(curve, suite, rawPriv, passphrase, kdfParams)
|
||||
case *cf.encryption:
|
||||
b, err = cert.EncryptAndMarshalSigningPrivateKey(curve, rawPriv, passphrase, kdfParams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while encrypting out-key: %s", err)
|
||||
}
|
||||
} else {
|
||||
case version == cert.Version3:
|
||||
b = cert.MarshalPQSigningPrivateKeyToPEM(curve, suite, rawPriv)
|
||||
default:
|
||||
b = cert.MarshalSigningPrivateKeyToPEM(curve, rawPriv)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while encrypting out-key: %s", err)
|
||||
}
|
||||
|
||||
err = writeOutput(*cf.outKeyPath, b, 0600, out)
|
||||
if err != nil {
|
||||
|
||||
@@ -68,12 +68,14 @@ func Test_caHelp(t *testing.T) {
|
||||
" -out-qr string\n"+
|
||||
" \tOptional: output a qr code image (png) of the certificate\n"+
|
||||
optionalPkcs11String(" -pkcs11 string\n \tOptional: PKCS#11 URI to an existing private key\n")+
|
||||
" -pq-suite string\n"+
|
||||
" \tOptional: post-quantum suite of a version 3 CA, which the certificates it signs share (MLKEM1024_MLDSA87) (default \"MLKEM1024_MLDSA87\")\n"+
|
||||
" -subnets string\n"+
|
||||
" \tDeprecated, see -unsafe-networks\n"+
|
||||
" -unsafe-networks string\n"+
|
||||
" \tOptional: comma separated list of ip address and network in CIDR notation. This will limit which ip addresses and networks subordinate certs can use in unsafe networks\n"+
|
||||
" -version uint\n"+
|
||||
" \tOptional: version of the certificate format to use. Version 3 adds an ML-DSA-87 key, for post-quantum certificates (default 2)\n",
|
||||
" \tOptional: version of the certificate format to use. Version 3 adds a post-quantum key, for post-quantum certificates (default 2)\n",
|
||||
ob.String(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ type keygenFlags struct {
|
||||
inKeyPath *string
|
||||
curve *string
|
||||
version *uint
|
||||
pqSuite *string
|
||||
p11url *string
|
||||
}
|
||||
|
||||
@@ -29,8 +30,9 @@ func newKeygenFlags() *keygenFlags {
|
||||
cf.outPubPath = cf.set.String("out-pub", "", "Required: path to write the public key to")
|
||||
cf.outKeyPath = cf.set.String("out-key", "", "Required: path to write the private key to")
|
||||
cf.curve = cf.set.String("curve", defaultCurve(), "ECDH Curve (25519, P256)")
|
||||
cf.version = cf.set.Uint("version", 0, "Optional: 3 adds an ML-KEM-1024 key, for version 3 certificates")
|
||||
cf.inKeyPath = cf.set.String("in-key", "", "Optional: path to an existing private key to add an ML-KEM-1024 key to, keeping the existing key. Implies -version 3")
|
||||
cf.version = cf.set.Uint("version", 0, "Optional: 3 adds a post-quantum key, for version 3 certificates")
|
||||
cf.inKeyPath = cf.set.String("in-key", "", "Optional: path to an existing private key to add a post-quantum key to, keeping the existing key. Implies -version 3")
|
||||
cf.pqSuite = pqSuiteFlag(cf.set, "Optional: post-quantum suite of a version 3 key, which must be the suite of the CA that signs it")
|
||||
cf.p11url = p11Flag(cf.set)
|
||||
return &cf
|
||||
}
|
||||
@@ -63,7 +65,11 @@ func keygen(args []string, out io.Writer, errOut io.Writer) error {
|
||||
return newHelpErrorf("-version must be %v, or left out for keys that work with any other version", cert.Version3)
|
||||
}
|
||||
if version == cert.Version3 && isP11 {
|
||||
return newHelpErrorf("-version 3 can not be used with -pkcs11, the ML-KEM-1024 key has to be in out-key")
|
||||
return newHelpErrorf("-version 3 can not be used with -pkcs11, the post-quantum key has to be in out-key")
|
||||
}
|
||||
suite, err := parsePQSuiteFlag(cf.set, *cf.pqSuite, version == cert.Version3, "is only for -version 3")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var claims ioClaims
|
||||
@@ -113,7 +119,7 @@ func keygen(args []string, out io.Writer, errOut io.Writer) error {
|
||||
}
|
||||
|
||||
if version == cert.Version3 {
|
||||
pub, rawPriv, err = appendPQKey(pub, rawPriv)
|
||||
pub, rawPriv, err = appendPQKey(suite, pub, rawPriv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -139,12 +145,20 @@ func keygen(args []string, out io.Writer, errOut io.Writer) error {
|
||||
return fmt.Errorf("error while getting public key: %w", err)
|
||||
}
|
||||
} else {
|
||||
err = writeOutput(*cf.outKeyPath, cert.MarshalPrivateKeyToPEM(curve, rawPriv), 0600, out)
|
||||
b := cert.MarshalPrivateKeyToPEM(curve, rawPriv)
|
||||
if version == cert.Version3 {
|
||||
b = cert.MarshalPQPrivateKeyToPEM(curve, suite, rawPriv)
|
||||
}
|
||||
err = writeOutput(*cf.outKeyPath, b, 0600, out)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while writing out-key: %s", err)
|
||||
}
|
||||
}
|
||||
err = writeOutput(*cf.outPubPath, cert.MarshalPublicKeyToPEM(curve, pub), 0600, out)
|
||||
b := cert.MarshalPublicKeyToPEM(curve, pub)
|
||||
if version == cert.Version3 {
|
||||
b = cert.MarshalPQPublicKeyToPEM(curve, suite, pub)
|
||||
}
|
||||
err = writeOutput(*cf.outPubPath, b, 0600, out)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while writing out-pub: %s", err)
|
||||
}
|
||||
|
||||
@@ -24,14 +24,16 @@ func Test_keygenHelp(t *testing.T) {
|
||||
" -curve string\n"+
|
||||
" \tECDH Curve (25519, P256) (default \"25519\")\n"+
|
||||
" -in-key string\n"+
|
||||
" \tOptional: path to an existing private key to add an ML-KEM-1024 key to, keeping the existing key. Implies -version 3\n"+
|
||||
" \tOptional: path to an existing private key to add a post-quantum key to, keeping the existing key. Implies -version 3\n"+
|
||||
" -out-key string\n"+
|
||||
" \tRequired: path to write the private key to\n"+
|
||||
" -out-pub string\n"+
|
||||
" \tRequired: path to write the public key to\n"+
|
||||
optionalPkcs11String(" -pkcs11 string\n \tOptional: PKCS#11 URI to an existing private key\n")+
|
||||
" -pq-suite string\n"+
|
||||
" \tOptional: post-quantum suite of a version 3 key, which must be the suite of the CA that signs it (MLKEM1024_MLDSA87) (default \"MLKEM1024_MLDSA87\")\n"+
|
||||
" -version uint\n"+
|
||||
" \tOptional: 3 adds an ML-KEM-1024 key, for version 3 certificates\n",
|
||||
" \tOptional: 3 adds a post-quantum key, for version 3 certificates\n",
|
||||
ob.String(),
|
||||
)
|
||||
}
|
||||
|
||||
+68
-29
@@ -3,33 +3,60 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdh"
|
||||
"crypto/mldsa"
|
||||
"crypto/mlkem"
|
||||
"encoding/pem"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/slackhq/nebula/cert"
|
||||
)
|
||||
|
||||
// appendPQSigningKey makes a classical CA keypair into a version 3 composite one with a fresh ML-DSA-87 key.
|
||||
func appendPQSigningKey(pub, priv []byte) ([]byte, []byte, error) {
|
||||
sk, err := mldsa.GenerateKey(mldsa.MLDSA87())
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error while generating ML-DSA-87 key: %s", err)
|
||||
// pqSuiteFlag adds -pq-suite to a flag set.
|
||||
func pqSuiteFlag(set *flag.FlagSet, usage string) *string {
|
||||
var names []string
|
||||
for _, s := range cert.PQSuites() {
|
||||
names = append(names, s.String())
|
||||
}
|
||||
return append(pub, sk.PublicKey().Bytes()...), append(priv, sk.Bytes()...), nil
|
||||
return set.String("pq-suite", cert.DefaultPQSuite.String(), usage+" ("+strings.Join(names, ", ")+")")
|
||||
}
|
||||
|
||||
// appendPQKey makes a classical host keypair into a version 3 composite one with a fresh ML-KEM-1024 key.
|
||||
func appendPQKey(pub, priv []byte) ([]byte, []byte, error) {
|
||||
dk, err := mlkem.GenerateKey1024()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error while generating ML-KEM-1024 key: %s", err)
|
||||
// parsePQSuiteFlag returns the suite -pq-suite names. Setting it is an error unless allowed, when the suite has no use
|
||||
// or comes from elsewhere.
|
||||
func parsePQSuiteFlag(set *flag.FlagSet, value string, allowed bool, why string) (cert.PQSuite, error) {
|
||||
given := false
|
||||
set.Visit(func(f *flag.Flag) {
|
||||
if f.Name == "pq-suite" {
|
||||
given = true
|
||||
}
|
||||
})
|
||||
if given && !allowed {
|
||||
return cert.PQSuiteUnknown, newHelpErrorf("-pq-suite %s", why)
|
||||
}
|
||||
return append(pub, dk.EncapsulationKey().Bytes()...), append(priv, dk.Bytes()...), nil
|
||||
s, err := cert.ParsePQSuite(value)
|
||||
if err != nil {
|
||||
return cert.PQSuiteUnknown, newHelpErrorf("-pq-suite: %s", err)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// appendPQSigningKey makes a classical CA keypair into a version 3 composite one with a fresh key of suite.
|
||||
func appendPQSigningKey(suite cert.PQSuite, pub, priv []byte) ([]byte, []byte, error) {
|
||||
pqPub, pqPriv, err := suite.GenerateKey(true)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error while generating %v CA key: %s", suite, err)
|
||||
}
|
||||
return append(pub, pqPub...), append(priv, pqPriv...), nil
|
||||
}
|
||||
|
||||
// appendPQKey makes a classical host keypair into a version 3 composite one with a fresh key of suite.
|
||||
func appendPQKey(suite cert.PQSuite, pub, priv []byte) ([]byte, []byte, error) {
|
||||
pqPub, pqPriv, err := suite.GenerateKey(false)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error while generating %v key: %s", suite, err)
|
||||
}
|
||||
return append(pub, pqPub...), append(priv, pqPriv...), nil
|
||||
}
|
||||
|
||||
// publicKeyFor returns the public key of a classical host private key.
|
||||
@@ -45,7 +72,7 @@ func publicKeyFor(curve cert.Curve, priv []byte) ([]byte, error) {
|
||||
}
|
||||
k, err := c.NewPrivateKey(priv)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("not a classical %s private key, it may already have an ML-KEM-1024 key", curve)
|
||||
return nil, fmt.Errorf("not a classical %s private key, it may already have a post-quantum key", curve)
|
||||
}
|
||||
return k.PublicKey().Bytes(), nil
|
||||
}
|
||||
@@ -58,6 +85,7 @@ type pqExtendFlags struct {
|
||||
inPubPath *string
|
||||
outCertPath *string
|
||||
outKeyPath *string
|
||||
pqSuite *string
|
||||
}
|
||||
|
||||
func newPQExtendFlags() *pqExtendFlags {
|
||||
@@ -69,6 +97,7 @@ func newPQExtendFlags() *pqExtendFlags {
|
||||
pf.caCertPath = pf.set.String("ca-crt", "", "Required for a host in-crt: path to the version 3 form of the CA that issued it")
|
||||
pf.inPubPath = pf.set.String("in-pub", "", "Required for a host in-crt: path to the host's public key from keygen -in-key")
|
||||
pf.outKeyPath = pf.set.String("out-key", "", "Required for a CA in-crt: path to write the CA's version 3 key to, encrypted again if ca-key was")
|
||||
pf.pqSuite = pqSuiteFlag(pf.set, "Optional, for a CA in-crt: post-quantum suite of the version 3 CA. A host takes its CA's suite")
|
||||
return &pf
|
||||
}
|
||||
|
||||
@@ -134,9 +163,14 @@ func pqExtend(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
return err
|
||||
}
|
||||
|
||||
suite, err := parsePQSuiteFlag(pf.set, *pf.pqSuite, c.IsCA(), "is for CA certificates, a host takes its CA's suite")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var c3 cert.Certificate
|
||||
if c.IsCA() {
|
||||
c3, err = extendCA(pf, c, curve, caKey, rawCAKey, passphrase, out)
|
||||
c3, err = extendCA(pf, c, suite, curve, caKey, rawCAKey, passphrase, out)
|
||||
} else {
|
||||
c3, err = extendHost(pf, c, curve, caKey, &claims)
|
||||
}
|
||||
@@ -154,8 +188,8 @@ func pqExtend(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
return nil
|
||||
}
|
||||
|
||||
// extendCA gives a v2 CA an ML-DSA-87 key and writes out the composite key.
|
||||
func extendCA(pf *pqExtendFlags, ca cert.Certificate, curve cert.Curve, key, rawKey, passphrase []byte, out io.Writer) (cert.Certificate, error) {
|
||||
// extendCA gives a v2 CA a post-quantum key of suite and writes out the composite key.
|
||||
func extendCA(pf *pqExtendFlags, ca cert.Certificate, suite cert.PQSuite, curve cert.Curve, key, rawKey, passphrase []byte, out io.Writer) (cert.Certificate, error) {
|
||||
if *pf.caCertPath != "" || *pf.inPubPath != "" {
|
||||
return nil, newHelpErrorf("-ca-crt and -in-pub are for host certificates, a CA extends itself")
|
||||
}
|
||||
@@ -166,15 +200,15 @@ func extendCA(pf *pqExtendFlags, ca cert.Certificate, curve cert.Curve, key, raw
|
||||
return nil, fmt.Errorf("refusing to extend, the CA certificate does not match ca-key")
|
||||
}
|
||||
|
||||
pub, priv, err := appendPQSigningKey(nil, append([]byte(nil), key...))
|
||||
pub, priv, err := appendPQSigningKey(suite, nil, append([]byte(nil), key...))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signPQ, err := cert.SignPQ(priv[len(key):])
|
||||
signPQ, err := cert.SignPQ(suite, priv[len(key):])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c3, err := cert.ExtendToV3(ca, pub, nil, signPQ)
|
||||
c3, err := cert.ExtendToV3(ca, suite, pub, nil, signPQ)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while extending: %s", err)
|
||||
}
|
||||
@@ -192,12 +226,12 @@ func extendCA(pf *pqExtendFlags, ca cert.Certificate, curve cert.Curve, key, raw
|
||||
}
|
||||
a := ned.EncryptionMetadata.Argon2Parameters
|
||||
kdf := cert.NewArgon2Parameters(a.Memory, a.Parallelism, a.Iterations)
|
||||
b, err = cert.EncryptAndMarshalSigningPrivateKey(curve, priv, passphrase, kdf)
|
||||
b, err = cert.EncryptAndMarshalPQSigningPrivateKey(curve, suite, priv, passphrase, kdf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while encrypting out-key: %s", err)
|
||||
}
|
||||
} else {
|
||||
b = cert.MarshalSigningPrivateKeyToPEM(curve, priv)
|
||||
b = cert.MarshalPQSigningPrivateKeyToPEM(curve, suite, priv)
|
||||
}
|
||||
if err := writeOutput(*pf.outKeyPath, b, 0600, out); err != nil {
|
||||
return nil, fmt.Errorf("error while writing out-key: %s", err)
|
||||
@@ -205,7 +239,7 @@ func extendCA(pf *pqExtendFlags, ca cert.Certificate, curve cert.Curve, key, raw
|
||||
return c3, nil
|
||||
}
|
||||
|
||||
// extendHost adds a host's ML-KEM-1024 key to its certificate, signed by the v3 form of its CA.
|
||||
// extendHost adds a host's post-quantum key to its certificate, signed by the v3 form of its CA, in the CA's suite.
|
||||
func extendHost(pf *pqExtendFlags, c cert.Certificate, curve cert.Curve, caKey []byte, claims *ioClaims) (cert.Certificate, error) {
|
||||
if *pf.outKeyPath != "" {
|
||||
return nil, newHelpErrorf("-out-key is for CA certificates, a host's key comes from keygen -in-key")
|
||||
@@ -240,19 +274,24 @@ func extendHost(pf *pqExtendFlags, c cert.Certificate, curve cert.Curve, caKey [
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while parsing in-pub: %s", err)
|
||||
}
|
||||
if pubCurve != c.Curve() || len(pub) <= cert.MLKEM1024PublicKeySize {
|
||||
return nil, fmt.Errorf("in-pub must be the host's key from keygen -in-key")
|
||||
suite := cert.PQSuiteOf(ca)
|
||||
classical, pq, err := suite.SplitPublicKey(false, pub)
|
||||
if pubCurve != c.Curve() || err != nil {
|
||||
return nil, fmt.Errorf("in-pub must be the host's key from keygen -in-key -pq-suite %v", suite)
|
||||
}
|
||||
classical, pq := pub[:len(pub)-cert.MLKEM1024PublicKeySize], pub[len(pub)-cert.MLKEM1024PublicKeySize:]
|
||||
if !bytes.Equal(classical, c.PublicKey()) {
|
||||
return nil, fmt.Errorf("in-pub does not extend the key in in-crt")
|
||||
}
|
||||
|
||||
signPQ, err := cert.SignPQ(caKey[len(caKey)-cert.MLDSA87PrivateKeySize:])
|
||||
_, pqCAKey, err := suite.SplitPrivateKey(true, caKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c3, err := cert.ExtendToV3(c, pq, ca, signPQ)
|
||||
signPQ, err := cert.SignPQ(suite, pqCAKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c3, err := cert.ExtendToV3(c, suite, pq, ca, signPQ)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while extending: %s", err)
|
||||
}
|
||||
|
||||
@@ -100,9 +100,40 @@ func TestPQ_SignErrors(t *testing.T) {
|
||||
require.NoError(t, ca([]string{"-version", "3", "-name", "v3 ca", "-out-crt", p("ca3.crt"), "-out-key", p("ca3.key")}, ob, eb, nopw))
|
||||
require.ErrorContains(t, signCert([]string{"-ca-crt", p("ca3.crt"), "-ca-key", p("ca3.key"), "-name", "h", "-networks", "10.1.0.1/16", "-in-pub", p("k.pub"), "-out-crt", p("h3.crt")}, ob, eb, nopw), "need an in-pub from keygen -version 3")
|
||||
|
||||
// keygen -in-key refuses a key that already has an ML-KEM key.
|
||||
// keygen -in-key refuses a key that already has a post-quantum key.
|
||||
require.NoError(t, keygen([]string{"-version", "3", "-out-key", p("k3.key"), "-out-pub", p("k3.pub")}, ob, eb))
|
||||
require.ErrorContains(t, keygen([]string{"-in-key", p("k3.key"), "-out-key", p("k33.key"), "-out-pub", p("k33.pub")}, ob, eb), "already have an ML-KEM-1024 key")
|
||||
require.ErrorContains(t, keygen([]string{"-in-key", p("k3.key"), "-out-key", p("k33.key"), "-out-pub", p("k33.pub")}, ob, eb), "already have a post-quantum key")
|
||||
}
|
||||
|
||||
func TestPQ_SuiteFlag(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
ob, eb := &bytes.Buffer{}, &bytes.Buffer{}
|
||||
nopw := &StubPasswordReader{}
|
||||
p := func(n string) string { return filepath.Join(dir, n) }
|
||||
|
||||
// The suite can be named, case aside, and the certificates a CA signs share its suite.
|
||||
require.NoError(t, ca([]string{"-version", "3", "-pq-suite", "mlkem1024_mldsa87", "-name", "ca", "-out-crt", p("ca.crt"), "-out-key", p("ca.key")}, ob, eb, nopw))
|
||||
assert.Equal(t, cert.PQSuite_MLKEM1024_MLDSA87, cert.PQSuiteOf(readCert(t, p("ca.crt"))))
|
||||
require.NoError(t, keygen([]string{"-version", "3", "-pq-suite", "MLKEM1024_MLDSA87", "-out-key", p("k.key"), "-out-pub", p("k.pub")}, ob, eb))
|
||||
require.NoError(t, signCert([]string{"-ca-crt", p("ca.crt"), "-ca-key", p("ca.key"), "-name", "h", "-networks", "10.1.0.1/16", "-in-pub", p("k.pub"), "-out-crt", p("h.crt")}, ob, eb, nopw))
|
||||
assert.Equal(t, cert.PQSuite_MLKEM1024_MLDSA87, cert.PQSuiteOf(readCert(t, p("h.crt"))))
|
||||
|
||||
// Printing shows it.
|
||||
ob.Reset()
|
||||
require.NoError(t, printCert([]string{"-json", "-path", p("h.crt")}, ob, eb))
|
||||
assert.Contains(t, ob.String(), `"pqSuite":"MLKEM1024_MLDSA87"`)
|
||||
|
||||
// Unknown suites, and suites where there is no post-quantum key, are refused.
|
||||
require.ErrorContains(t, ca([]string{"-version", "3", "-pq-suite", "mlkem768_mldsa65", "-name", "x", "-out-crt", p("x.crt"), "-out-key", p("x.key")}, ob, eb, nopw), "unknown post-quantum suite")
|
||||
require.ErrorContains(t, ca([]string{"-pq-suite", "MLKEM1024_MLDSA87", "-name", "x", "-out-crt", p("x.crt"), "-out-key", p("x.key")}, ob, eb, nopw), "only for -version 3")
|
||||
require.ErrorContains(t, keygen([]string{"-pq-suite", "MLKEM1024_MLDSA87", "-out-key", p("x.key"), "-out-pub", p("x.pub")}, ob, eb), "only for -version 3")
|
||||
|
||||
// A host takes its CA's suite when extended.
|
||||
require.NoError(t, ca([]string{"-name", "ca2", "-out-crt", p("ca2.crt"), "-out-key", p("ca2.key")}, ob, eb, nopw))
|
||||
require.NoError(t, signCert([]string{"-ca-crt", p("ca2.crt"), "-ca-key", p("ca2.key"), "-name", "h2", "-networks", "10.1.0.2/16", "-out-crt", p("h2.crt"), "-out-key", p("h2.key")}, ob, eb, nopw))
|
||||
require.NoError(t, pqExtend([]string{"-in-crt", p("ca2.crt"), "-ca-key", p("ca2.key"), "-pq-suite", "MLKEM1024_MLDSA87", "-out-crt", p("ca23.crt"), "-out-key", p("ca23.key")}, ob, eb, nopw))
|
||||
require.NoError(t, keygen([]string{"-in-key", p("h2.key"), "-out-key", p("h23.key"), "-out-pub", p("h23.pub")}, ob, eb))
|
||||
require.ErrorContains(t, pqExtend([]string{"-in-crt", p("h2.crt"), "-in-pub", p("h23.pub"), "-ca-crt", p("ca23.crt"), "-ca-key", p("ca23.key"), "-pq-suite", "MLKEM1024_MLDSA87", "-out-crt", p("h23.crt")}, ob, eb, nopw), "a host takes its CA's suite")
|
||||
}
|
||||
|
||||
// TestPQ_Extend upgrades a v2 CA and a host in place, the way a running network migrates.
|
||||
@@ -145,7 +176,7 @@ func TestPQ_Extend(t *testing.T) {
|
||||
require.ErrorIs(t, err, cert.ErrPrivateKeyEncrypted, "an encrypted ca-key stays encrypted")
|
||||
}
|
||||
|
||||
// The host gets an ML-KEM key on top of the one it has, and the CA signs it.
|
||||
// The host gets a post-quantum key on top of the one it has, and the CA signs it.
|
||||
require.NoError(t, keygen([]string{"-in-key", p("host.key"), "-out-key", p("host3.key"), "-out-pub", p("host3.pub")}, ob, eb))
|
||||
require.NoError(t, pqExtend([]string{"-in-crt", p("host.crt"), "-in-pub", p("host3.pub"), "-ca-crt", p("ca3.crt"), "-ca-key", p("ca3.key"), "-out-crt", p("host3.crt")}, ob, eb, pw))
|
||||
host3 := readCert(t, p("host3.crt"))
|
||||
|
||||
+19
-6
@@ -169,16 +169,20 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
version = caCert.Version()
|
||||
}
|
||||
|
||||
// A version 3 certificate is in the suite of its CA.
|
||||
suite := cert.PQSuiteOf(caCert)
|
||||
switch {
|
||||
case version == cert.Version3 && caCert.Version() != cert.Version3:
|
||||
return newHelpErrorf("version 3 certificates must be signed by a version 3 CA")
|
||||
case version == cert.Version3 && isP11:
|
||||
return newHelpErrorf("version 3 certificates can not be signed with -pkcs11, the ML-DSA-87 key has to be in ca-key")
|
||||
return newHelpErrorf("version 3 certificates can not be signed with -pkcs11, the post-quantum key has to be in ca-key")
|
||||
case version != cert.Version3 && caCert.Version() == cert.Version3:
|
||||
// A v3 CA signs anything older as its v2 form, with the classical half of its key.
|
||||
caCert = cert.V2Form(caCert)
|
||||
if caKey != nil {
|
||||
caKey = caKey[:len(caKey)-cert.MLDSA87PrivateKeySize]
|
||||
if caKey, _, err = suite.SplitPrivateKey(true, caKey); err != nil {
|
||||
return fmt.Errorf("error while parsing ca-key: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,12 +276,16 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
if pubCurve != curve {
|
||||
return fmt.Errorf("curve of in-pub does not match ca")
|
||||
}
|
||||
if isPQ := len(pub) > cert.MLKEM1024PublicKeySize; isPQ != (version == cert.Version3) {
|
||||
// Composite keys are longer than any classical one, an uncompressed P256 key being the longest.
|
||||
if isPQ := len(pub) > 65; isPQ != (version == cert.Version3) {
|
||||
if isPQ {
|
||||
return fmt.Errorf("in-pub has an ML-KEM-1024 key, which only version 3 certificates carry")
|
||||
return fmt.Errorf("in-pub has a post-quantum key, which only version 3 certificates carry")
|
||||
}
|
||||
return fmt.Errorf("version 3 certificates need an in-pub from keygen -version 3")
|
||||
}
|
||||
if _, _, err := suite.SplitPublicKey(false, pub); version == cert.Version3 && err != nil {
|
||||
return fmt.Errorf("in-pub is not a key for the CA's post-quantum suite, %v", suite)
|
||||
}
|
||||
} else if isP11 {
|
||||
pub, err = p11Client.GetPubKey()
|
||||
if err != nil {
|
||||
@@ -286,7 +294,7 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
} else {
|
||||
pub, rawPriv = newKeypair(curve)
|
||||
if version == cert.Version3 {
|
||||
pub, rawPriv, err = appendPQKey(pub, rawPriv)
|
||||
pub, rawPriv, err = appendPQKey(suite, pub, rawPriv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -388,6 +396,7 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
PublicKey: pub,
|
||||
IsCA: false,
|
||||
Curve: curve,
|
||||
PQSuite: suite,
|
||||
}
|
||||
|
||||
nc, err := t.Sign(caCert, curve, caKey)
|
||||
@@ -408,7 +417,11 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
}
|
||||
}
|
||||
|
||||
err = writeOutput(*sf.outKeyPath, cert.MarshalPrivateKeyToPEM(curve, rawPriv), 0600, out)
|
||||
b := cert.MarshalPrivateKeyToPEM(curve, rawPriv)
|
||||
if version == cert.Version3 {
|
||||
b = cert.MarshalPQPrivateKeyToPEM(curve, suite, rawPriv)
|
||||
}
|
||||
err = writeOutput(*sf.outKeyPath, b, 0600, out)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while writing out-key: %s", err)
|
||||
}
|
||||
|
||||
+33
-12
@@ -77,12 +77,23 @@ CertificateV3 ::= SEQUENCE {
|
||||
curve [1] Curve DEFAULT curve25519,
|
||||
publicKey [2] OCTET STRING, -- classical: X25519 / P256 for hosts, Ed25519 / ECDSA P256 for CAs
|
||||
signature [3] OCTET STRING, -- classical CA signature over details || curve || publicKey, as in v2
|
||||
pqPublicKey [4] OCTET STRING, -- ML-KEM-1024 encapsulation key for hosts, ML-DSA-87 public key for CAs
|
||||
pqSignature [5] OCTET STRING -- ML-DSA-87 by the issuing CA, context "nebula certificate v3",
|
||||
-- over the DER of fields 0 to 4, curve always present
|
||||
pqSuite [4] OCTET STRING, -- one byte, the PQSuite; always present
|
||||
pqPublicKey [5] OCTET STRING, -- the suite's KEM public key for hosts, its signature public key for CAs
|
||||
pqSignature [6] OCTET STRING -- the suite's signature by the issuing CA, context "nebula certificate v3",
|
||||
-- over the DER of fields 0 to 5, curve always present
|
||||
}
|
||||
```
|
||||
|
||||
- **The suite.** `pqSuite` names the post-quantum algorithms the way `curve` names the classical
|
||||
ones: the KEM of host keys, and the signature algorithm of CA keys and of the certificates a CA
|
||||
signs. A CA and every certificate it signs share a suite, as they share a curve; the CA pool
|
||||
rejects a mismatch, and the post-quantum signature covers the suite, so a certificate can not be
|
||||
relabelled. The field is always present, in handshakes too, with no default, and 0 is never
|
||||
valid. There is one suite today, `MLKEM1024_MLDSA87` (1): ML-KEM-1024 hosts, ML-DSA-87 CAs. A
|
||||
new suite is an entry in the cert package's suite table plus, for a new KEM, a hybrid subtype; no
|
||||
new certificate version. Changing a CA's suite means a new CA, or a new v3 extension of its v2
|
||||
form that replaces the old one, since one v2 CA has one v3 extension.
|
||||
|
||||
- **The v2 form.** Fields 0 to 3 are a byte exact v2 certificate, signed by the v2 form of the CA.
|
||||
A v3 holder presents it in classical and phase 1 handshakes, so peers that only speak v2 accept
|
||||
it as they always have, and old parsers never see v3 bytes. A node therefore needs one
|
||||
@@ -109,7 +120,9 @@ CertificateV3 ::= SEQUENCE {
|
||||
- **Keys are composite**, classical followed by post-quantum, wherever a v3 certificate deals in
|
||||
them: `PublicKey()` returns `classical || pq`, and private keys are `classical || seed` (64 byte
|
||||
ML-KEM seed for hosts, 32 byte ML-DSA seed for CAs), in one PEM block in `pki.key` with banners
|
||||
such as `NEBULA X25519 MLKEM1024 PRIVATE KEY` and `NEBULA ED25519 MLDSA87 PRIVATE KEY`.
|
||||
such as `NEBULA X25519 MLKEM1024 PRIVATE KEY` and `NEBULA ED25519 MLDSA87 PRIVATE KEY`. The
|
||||
banner names the post-quantum algorithm, not the suite, because a seed's size does not tell the
|
||||
algorithm's strength.
|
||||
- **Both curves.** Curve25519 and P256; the post-quantum half does not depend on the curve.
|
||||
- **In-place upgrade.** `nebula-cert pq-extend` turns an existing v2 CA into a v3 CA whose v2 form
|
||||
is the old CA certificate, byte for byte, and extends existing host certificates the same way.
|
||||
@@ -118,8 +131,8 @@ CertificateV3 ::= SEQUENCE {
|
||||
|
||||
| Certificate | Full | In a handshake | v2 form |
|
||||
|---|---|---|---|
|
||||
| CA, Curve25519 | 7362 B | n/a | 134 B |
|
||||
| Host, Curve25519 | 6375 B | 4769 B | 171 B |
|
||||
| CA, Curve25519 | 7365 B | n/a | 134 B |
|
||||
| Host, Curve25519 | 6378 B | 4772 B | 171 B |
|
||||
|
||||
`crypto/mldsa` is new in Go 1.27, so `go.mod` moves to Go 1.27.
|
||||
|
||||
@@ -163,11 +176,17 @@ design rationale and security argument), with ML-KEM-1024 supplied by nebula:
|
||||
initiator sealed.
|
||||
- v3 certificates go with this subtype and nothing else: a v3 certificate on any other subtype, or
|
||||
a v2 one on this one, is a protocol error.
|
||||
- **One subtype per KEM.** A hybrid subtype names its KEM, and a v3 holder initiates the subtype of
|
||||
its suite's KEM. Peers need the same KEM, not the same suite: suites that differ only in their
|
||||
signature algorithm share a subtype, and each certificate carries its own suite. A responder
|
||||
whose suite uses another KEM drops msg1, so the pair behaves as if the responder did not speak
|
||||
PQ: classical fallback, or no tunnel under `require`. A suite's KEM must have a subtype for the
|
||||
pki to load a certificate in it.
|
||||
|
||||
| Message | Size (Curve25519) | Data fragments (k) |
|
||||
|---|---|---|
|
||||
| msg1 | 8006 B | 7 |
|
||||
| msg2 | 9625 B | 9 |
|
||||
| msg1 | 8009 B | 7 |
|
||||
| msg2 | 9628 B | 9 |
|
||||
| msg3 | 1616 B | 2 |
|
||||
|
||||
**Dependency.** DefinedNet/noise is a private repository today. `go.mod` points at a local clone
|
||||
@@ -484,10 +503,11 @@ in every scenario above, and within three at least 99.99%.
|
||||
|
||||
## Implementation (phase 2)
|
||||
|
||||
- `cert`: `cert_v3.go` (format, `V2Form`, `ExtendToV3`, `SignPQ`), `SignHybridWith` and the
|
||||
composite `Sign` path, the CA pool's v3 index and v2-form blocklisting, and the composite key
|
||||
PEM banners.
|
||||
- `cert`: `cert_v3.go` (format, `V2Form`, `ExtendToV3`, `SignPQ`), `pq_suite.go` (`PQSuite` and
|
||||
the suite table), `SignHybridWith` and the composite `Sign` path, the CA pool's v3 index, suite
|
||||
check and v2-form blocklisting, and the composite key PEM banners, generated per suite.
|
||||
- `handshake`: the `ix_hybrid_mlkem1024` subtype on `noise.HandshakeHybridIX`, `KEMMLKEM1024`,
|
||||
`HybridSubtype` mapping a suite to the subtype of its KEM,
|
||||
composite credentials, cipher states ordered by role for three message patterns, and
|
||||
`Machine.PeerCert` for a responder that has to act before completion.
|
||||
- `pki.go` loads a v3 certificate with its composite key and derives the v2 form; messages after
|
||||
@@ -496,7 +516,8 @@ in every scenario above, and within three at least 99.99%.
|
||||
initiator's msg3 resends and teardown; `outside.go` keeps quiet about pending responder indexes.
|
||||
- `nebula-cert`: `ca`, `keygen` and `sign` take `-version 3`, `keygen -in-key` extends an existing
|
||||
host key, `sign -version 2` with a v3 CA signs as its v2 form, and `pq-extend` upgrades v2
|
||||
certificates in place, keeping an encrypted CA key encrypted.
|
||||
certificates in place, keeping an encrypted CA key encrypted. `ca`, `keygen` and `pq-extend` of a
|
||||
CA take `-pq-suite`; `sign` and `pq-extend` of a host take the CA's suite.
|
||||
- Tests: certificate format, strictness, tampering, pool and blocklist rules and in-place
|
||||
extension on both curves; Machine-level hybrid handshakes including corrupted msg3 and
|
||||
impersonation with only the classical key; e2e tests for a clean hybrid handshake, a lost
|
||||
|
||||
+10
-13
@@ -15,7 +15,7 @@ type Credential struct {
|
||||
Bytes []byte // pre-marshaled certificate bytes
|
||||
privateKey []byte // static private key (public half lives in Cert)
|
||||
publicKey []byte // static public key, the classical half for a v3 Cert
|
||||
kemKey noise.KEMKey // static ML-KEM-1024 keypair, only for a v3 Cert
|
||||
kemKey noise.KEMKey // static KEM keypair of the Cert's suite, only for a v3 Cert
|
||||
cipherSuite noise.CipherSuite // pre-built cipher suite (DH + cipher + hash)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@ type Credential struct {
|
||||
// appropriate DH function, cipher, and hash.
|
||||
//
|
||||
// For a version 3 certificate privateKey is the composite private key,
|
||||
// classical || ML-KEM-1024 seed, matching the composite Cert.PublicKey().
|
||||
// classical || the seed of its suite's KEM, matching the composite
|
||||
// Cert.PublicKey().
|
||||
func NewCredential(
|
||||
c cert.Certificate,
|
||||
hsBytes []byte,
|
||||
@@ -38,14 +39,10 @@ func NewCredential(
|
||||
publicKey: c.PublicKey(),
|
||||
cipherSuite: cipherSuite,
|
||||
}
|
||||
if v2 := cert.V2Form(c); v2 != nil {
|
||||
classical := v2.PublicKey()
|
||||
hc.publicKey = classical
|
||||
hc.kemKey = noise.KEMKey{
|
||||
Private: privateKey[len(privateKey)-cert.MLKEM1024PrivateKeySize:],
|
||||
Public: c.PublicKey()[len(classical):],
|
||||
}
|
||||
hc.privateKey = privateKey[:len(privateKey)-cert.MLKEM1024PrivateKeySize]
|
||||
if suite := cert.PQSuiteOf(c); suite != cert.PQSuiteUnknown {
|
||||
// The pki checked the private key against the certificate, so both split.
|
||||
hc.publicKey, hc.kemKey.Public, _ = suite.SplitPublicKey(false, c.PublicKey())
|
||||
hc.privateKey, hc.kemKey.Private, _ = suite.SplitPrivateKey(false, privateKey)
|
||||
}
|
||||
return hc
|
||||
}
|
||||
@@ -53,7 +50,7 @@ func NewCredential(
|
||||
// buildHandshakeState creates a noise.HandshakeState from this credential.
|
||||
// pskPlacement selects the noise psk modifier; 0 means none. A psk for
|
||||
// placements 2 and up is supplied later with SetPresharedKey. A hybrid
|
||||
// pattern also gets ML-KEM-1024 and the static ML-KEM key.
|
||||
// pattern also gets its KEM and the static KEM key.
|
||||
func (hc *Credential) buildHandshakeState(initiator bool, info subtypeInfo) (*noise.HandshakeState, error) {
|
||||
cfg := noise.Config{
|
||||
CipherSuite: hc.cipherSuite,
|
||||
@@ -64,8 +61,8 @@ func (hc *Credential) buildHandshakeState(initiator bool, info subtypeInfo) (*no
|
||||
PresharedKey: []byte{},
|
||||
PresharedKeyPlacement: info.pskPlacement,
|
||||
}
|
||||
if info.hybrid {
|
||||
cfg.KEMFunc = KEMMLKEM1024
|
||||
if info.kem != nil {
|
||||
cfg.KEMFunc = info.kem
|
||||
cfg.StaticKEMKeypair = hc.kemKey
|
||||
}
|
||||
return noise.NewHandshakeState(cfg)
|
||||
|
||||
@@ -20,4 +20,5 @@ var (
|
||||
ErrMultiMessageUnsupported = errors.New("multi-message handshake patterns are not yet supported by the manager")
|
||||
ErrSubtypeMismatch = errors.New("packet subtype does not match handshake machine subtype")
|
||||
ErrCertVersionMismatch = errors.New("handshake subtype does not allow this certificate version")
|
||||
ErrPQSuiteMismatch = errors.New("handshake subtype does not use the KEM of this certificate's post-quantum suite")
|
||||
)
|
||||
@@ -524,7 +524,7 @@ func TestTableAnswered(t *testing.T) {
|
||||
r := rand.New(rand.NewPCG(3, 3))
|
||||
tb := NewTable[testKey, string](testConfig())
|
||||
now := time.Unix(1000, 0)
|
||||
const msgLen = 8006 // a hybrid msg1: K=7 data symbols, 3 repair
|
||||
const msgLen = 8009 // a hybrid msg1: K=7 data symbols, 3 repair
|
||||
|
||||
sendAll := func(key testKey, pkts [][]byte) []*Result {
|
||||
var out []*Result
|
||||
|
||||
+25
-2
@@ -94,7 +94,8 @@ func NewMachine(
|
||||
|
||||
// Version 3 certificates go with hybrid patterns and nothing else, the
|
||||
// classical and phase 1 subtypes use the v2 form instead.
|
||||
if info.hybrid != (version == cert.Version3) {
|
||||
hybrid := info.kem != nil
|
||||
if hybrid != (version == cert.Version3) {
|
||||
return nil, fmt.Errorf("%w: %v", ErrCertVersionMismatch, version)
|
||||
}
|
||||
|
||||
@@ -102,6 +103,11 @@ func NewMachine(
|
||||
if cred == nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrNoCredential, version)
|
||||
}
|
||||
if hybrid {
|
||||
if err := checkHybridSuite(subtype, cred.Cert); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
hs, err := cred.buildHandshakeState(initiator, info)
|
||||
if err != nil {
|
||||
@@ -112,7 +118,7 @@ func NewMachine(
|
||||
hs: hs,
|
||||
subtype: subtype,
|
||||
msgs: info.msgs,
|
||||
hybrid: info.hybrid,
|
||||
hybrid: hybrid,
|
||||
getCred: getCred,
|
||||
allocIndex: allocIndex,
|
||||
verifier: verifier,
|
||||
@@ -390,6 +396,16 @@ func (m *Machine) processPayload(msg []byte, flags msgFlags) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkHybridSuite checks that a version 3 certificate's suite uses the KEM of
|
||||
// the hybrid subtype.
|
||||
func checkHybridSuite(subtype header.MessageSubType, c cert.Certificate) error {
|
||||
suite := cert.PQSuiteOf(c)
|
||||
if st, ok := HybridSubtype(suite); !ok || st != subtype {
|
||||
return fmt.Errorf("%w: %v", ErrPQSuiteMismatch, suite)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Machine) validateCert(payload Payload) error {
|
||||
// A v3 certificate only ever comes with a hybrid pattern, and a hybrid
|
||||
// pattern only ever with one. The composite static key would not recombine
|
||||
@@ -420,6 +436,13 @@ func (m *Machine) validateCert(payload Payload) error {
|
||||
return ErrPublicKeyMismatch
|
||||
}
|
||||
|
||||
if m.hybrid {
|
||||
if err := checkHybridSuite(m.subtype, rc); err != nil {
|
||||
m.failed = true
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Version negotiation, if the peer sent a different version and we have it, switch
|
||||
if rc.Version() != m.myVersion {
|
||||
if m.getCred(rc.Version()) != nil {
|
||||
|
||||
+26
-4
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/DefinedNet/noise"
|
||||
"github.com/slackhq/nebula/cert"
|
||||
"github.com/slackhq/nebula/header"
|
||||
)
|
||||
|
||||
@@ -22,9 +23,10 @@ type subtypeInfo struct {
|
||||
pskPlacement int // noise psk modifier position, 0 means no psk token
|
||||
msgs []msgFlags
|
||||
|
||||
// hybrid patterns carry composite static keys and need version 3
|
||||
// certificates on both sides; every other pattern refuses them.
|
||||
hybrid bool
|
||||
// kem is the KEM of a hybrid pattern, nil for any other. Hybrid patterns
|
||||
// carry composite static keys and need version 3 certificates on both
|
||||
// sides, in suites using this KEM; every other pattern refuses them.
|
||||
kem noise.KEMFunc
|
||||
}
|
||||
|
||||
// subtypeInfos defines the noise pattern and message content layout for each
|
||||
@@ -68,7 +70,7 @@ var subtypeInfos = map[header.MessageSubType]subtypeInfo{
|
||||
// first packet under the new keys does.
|
||||
header.HandshakeIXHybridMLKEM1024: {
|
||||
pattern: noise.HandshakeHybridIX,
|
||||
hybrid: true,
|
||||
kem: KEMMLKEM1024,
|
||||
msgs: []msgFlags{
|
||||
{expectsPayload: true, expectsCert: true},
|
||||
{expectsPayload: true, expectsCert: true},
|
||||
@@ -90,6 +92,26 @@ var subtypeInfos = map[header.MessageSubType]subtypeInfo{
|
||||
//},
|
||||
}
|
||||
|
||||
// hybridSubtypes maps the suite of a version 3 certificate to the hybrid
|
||||
// subtype for its KEM. Peers need the same KEM, not the same suite, so suites
|
||||
// sharing a KEM share a subtype; the suite itself travels in the certificate.
|
||||
var hybridSubtypes = map[cert.PQSuite]header.MessageSubType{
|
||||
cert.PQSuite_MLKEM1024_MLDSA87: header.HandshakeIXHybridMLKEM1024,
|
||||
}
|
||||
|
||||
// HybridSubtype returns the hybrid handshake subtype for the KEM of a version 3
|
||||
// certificate's suite.
|
||||
func HybridSubtype(suite cert.PQSuite) (header.MessageSubType, bool) {
|
||||
st, ok := hybridSubtypes[suite]
|
||||
return st, ok
|
||||
}
|
||||
|
||||
// IsHybrid reports whether subtype is a hybrid handshake, one that needs version
|
||||
// 3 certificates.
|
||||
func IsHybrid(subtype header.MessageSubType) bool {
|
||||
return subtypeInfos[subtype].kem != nil
|
||||
}
|
||||
|
||||
func subtypeInfoFor(subtype header.MessageSubType) (subtypeInfo, error) {
|
||||
if info, ok := subtypeInfos[subtype]; ok {
|
||||
return info, nil
|
||||
|
||||
@@ -190,8 +190,8 @@ func (hm *HandshakeManager) HandleIncoming(via ViaSender, packet []byte, h *head
|
||||
// Gate on known handshake subtypes. Unknown subtypes (or future ones we
|
||||
// don't yet support) are dropped here rather than silently routed through
|
||||
// the IX path. Add a case when introducing a new pattern.
|
||||
switch h.Subtype {
|
||||
case header.HandshakeIXPSK0, header.HandshakeIXPSK2MLKEM1024, hybridSubtype:
|
||||
switch {
|
||||
case h.Subtype == header.HandshakeIXPSK0, h.Subtype == header.HandshakeIXPSK2MLKEM1024, handshake.IsHybrid(h.Subtype):
|
||||
// supported
|
||||
default:
|
||||
hm.l.Debug("dropping handshake with unsupported subtype",
|
||||
@@ -795,7 +795,7 @@ func (hm *HandshakeManager) beginHandshake(via ViaSender, packet []byte, h *head
|
||||
// A hybrid responder answers with its v3 certificate and has to wait for msg3,
|
||||
// so its index is registered right away for msg3 to find.
|
||||
var pending *HandshakeHostInfo
|
||||
if h.Subtype == hybridSubtype {
|
||||
if handshake.IsHybrid(h.Subtype) {
|
||||
v = cert.Version3
|
||||
pending = &HandshakeHostInfo{
|
||||
hostinfo: &HostInfo{
|
||||
|
||||
@@ -27,8 +27,6 @@ import (
|
||||
// within the handshake timeout the tunnel is closed.
|
||||
|
||||
const (
|
||||
hybridSubtype = header.HandshakeIXHybridMLKEM1024
|
||||
|
||||
// hybridMsg3MaxLen bounds the msg3 a pending responder reassembles. msg3 is
|
||||
// a ciphertext and two tags, about 1.6KB.
|
||||
hybridMsg3MaxLen = 4096
|
||||
@@ -45,6 +43,7 @@ type hybridResponder struct {
|
||||
// heard from the responder.
|
||||
type hybridConfirm struct {
|
||||
hostinfo *HostInfo
|
||||
subtype header.MessageSubType
|
||||
sender *fragment.Sender
|
||||
send func([]byte) error
|
||||
handshakeLen uint64 // messages the handshake took, which the replay window was seeded with
|
||||
@@ -323,6 +322,7 @@ func (hm *HandshakeManager) startHybridConfirm(via ViaSender, hostinfo *HostInfo
|
||||
now := time.Now()
|
||||
c := &hybridConfirm{
|
||||
hostinfo: hostinfo,
|
||||
subtype: h.Subtype,
|
||||
sender: s,
|
||||
send: hm.sendTo(via),
|
||||
handshakeLen: handshakeLen,
|
||||
@@ -341,7 +341,7 @@ func (hm *HandshakeManager) sendHybridMsg3(c *hybridConfirm) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
hm.messageMetrics.Tx(header.Handshake, hybridSubtype, int64(len(pkts)+len(later)))
|
||||
hm.messageMetrics.Tx(header.Handshake, c.subtype, int64(len(pkts)+len(later)))
|
||||
// No repair delay: the packets cached during the handshake go out right
|
||||
// behind msg3, and the responder drops whatever arrives before msg3
|
||||
// completes. With the repair trailing, losing either data fragment would
|
||||
|
||||
+17
-9
@@ -276,14 +276,15 @@ func (hm *HandshakeManager) relayEstablished(target netip.Addr, relayHI *HostInf
|
||||
}
|
||||
|
||||
// buildPQAttempt creates the initiator's PQ Machine and msg1 sender. A node with
|
||||
// a v3 certificate speaks the hybrid handshake, anyone else the ephemeral KEM one.
|
||||
// a v3 certificate speaks the hybrid handshake for its suite's KEM, anyone else
|
||||
// the ephemeral KEM one.
|
||||
func (hm *HandshakeManager) buildPQAttempt(hh *HandshakeHostInfo) bool {
|
||||
cs := hm.f.pki.getCertState()
|
||||
v := hm.initiatingVersion(hh, cs)
|
||||
subtype := header.HandshakeIXPSK2MLKEM1024
|
||||
if cs.GetCredential(cert.Version3) != nil {
|
||||
if st, ok := cs.hybridSubtype(); ok {
|
||||
v = cert.Version3
|
||||
subtype = hybridSubtype
|
||||
subtype = st
|
||||
}
|
||||
if cs.GetCredential(v) == nil {
|
||||
hm.f.l.Error("Unable to handshake with host because no certificate is available",
|
||||
@@ -409,13 +410,20 @@ func (hm *HandshakeManager) handleFragment(via ViaSender, packet []byte, h *head
|
||||
hm.l.Debug("dropping post-quantum handshake, not accepting them", "from", via)
|
||||
return
|
||||
}
|
||||
if h.Subtype == hybridSubtype || cfg.require {
|
||||
hasV3 := hm.f.pki.getCertState().GetCredential(cert.Version3) != nil
|
||||
if h.Subtype == hybridSubtype && !hasV3 {
|
||||
hybrid := handshake.IsHybrid(h.Subtype)
|
||||
if hybrid || cfg.require {
|
||||
mine, hasV3 := hm.f.pki.getCertState().hybridSubtype()
|
||||
switch {
|
||||
case hybrid && !hasV3:
|
||||
hm.l.Debug("dropping hybrid handshake, no v3 certificate to answer it with", "from", via)
|
||||
return
|
||||
}
|
||||
if h.Subtype != hybridSubtype && hasV3 {
|
||||
case hybrid && h.Subtype != mine:
|
||||
// The peer's suite uses another KEM. Like a peer without PQ, it
|
||||
// falls back to classical unless either side requires PQ.
|
||||
hm.l.Debug("dropping hybrid handshake, our v3 certificate's suite uses another KEM", "from", via,
|
||||
"style", header.SubTypeName(header.Handshake, h.Subtype), "ours", header.SubTypeName(header.Handshake, mine))
|
||||
return
|
||||
case !hybrid && hasV3:
|
||||
hm.l.Debug("dropping post-quantum handshake, hybrid handshakes are required", "from", via)
|
||||
return
|
||||
}
|
||||
@@ -463,7 +471,7 @@ func (hm *HandshakeManager) handleFragment(via ViaSender, packet []byte, h *head
|
||||
}
|
||||
|
||||
case 3:
|
||||
if h.Subtype == hybridSubtype {
|
||||
if handshake.IsHybrid(h.Subtype) {
|
||||
hm.continueHybridResponder(via, h, fh, sym)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/slackhq/nebula/cert"
|
||||
"github.com/slackhq/nebula/config"
|
||||
"github.com/slackhq/nebula/handshake"
|
||||
"github.com/slackhq/nebula/header"
|
||||
"github.com/slackhq/nebula/noiseutil"
|
||||
"github.com/slackhq/nebula/util"
|
||||
)
|
||||
@@ -222,6 +223,15 @@ func (cs *CertState) GetDefaultCertificate() cert.Certificate {
|
||||
func (cs *CertState) DefaultVersion() cert.Version { return cs.initiatingVersion }
|
||||
|
||||
// GetCredential returns the pre-computed handshake credential for the given version, or nil.
|
||||
// hybridSubtype returns the hybrid handshake subtype for the KEM of the v3
|
||||
// certificate's suite, false without a v3 certificate.
|
||||
func (cs *CertState) hybridSubtype() (header.MessageSubType, bool) {
|
||||
if cs.v3Cert == nil {
|
||||
return 0, false
|
||||
}
|
||||
return handshake.HybridSubtype(cert.PQSuiteOf(cs.v3Cert))
|
||||
}
|
||||
|
||||
func (cs *CertState) GetCredential(v cert.Version) *handshake.Credential {
|
||||
switch v {
|
||||
case cert.Version1:
|
||||
@@ -440,7 +450,7 @@ func newCertStateFromConfig(c *config.C, cipher string) (*CertState, error) {
|
||||
}
|
||||
|
||||
// newCertState builds the handshake credentials. v3, when present, must be what v2 is the v2 form of. privateKey is
|
||||
// then the composite key, classical || ML-KEM-1024 seed, and v1 and v2 get its classical half.
|
||||
// then the composite key, classical || the seed of the KEM of v3's suite, and v1 and v2 get its classical half.
|
||||
func newCertState(dv cert.Version, v1, v2, v3 cert.Certificate, pkcs11backed bool, privateKeyCurve cert.Curve, privateKey []byte, cipher string) (*CertState, error) {
|
||||
cs := CertState{
|
||||
privateKey: privateKey,
|
||||
@@ -468,8 +478,8 @@ func newCertState(dv cert.Version, v1, v2, v3 cert.Certificate, pkcs11backed boo
|
||||
}
|
||||
|
||||
classicalKey := privateKey
|
||||
if !pkcs11backed && len(privateKey) > cert.MLKEM1024PrivateKeySize {
|
||||
classicalKey = privateKey[:len(privateKey)-cert.MLKEM1024PrivateKeySize]
|
||||
if !pkcs11backed {
|
||||
classicalKey = cert.ClassicalPrivateKey(privateKey)
|
||||
}
|
||||
|
||||
if v3 != nil {
|
||||
@@ -479,6 +489,10 @@ func newCertState(dv cert.Version, v1, v2, v3 cert.Certificate, pkcs11backed boo
|
||||
if err := v3.VerifyPrivateKey(privateKeyCurve, privateKey); err != nil {
|
||||
return nil, fmt.Errorf("private key is not a pair with public key in v3 nebula cert: %w", err)
|
||||
}
|
||||
suite := cert.PQSuiteOf(v3)
|
||||
if _, ok := handshake.HybridSubtype(suite); !ok {
|
||||
return nil, fmt.Errorf("v3 nebula cert uses post-quantum suite %v, which no hybrid handshake supports", suite)
|
||||
}
|
||||
|
||||
v3hs, err := v3.MarshalForHandshakes()
|
||||
if err != nil {
|
||||
|
||||
Reference in new issue
Block a user