mirror of
https://github.com/slackhq/nebula.git
synced 2026-11-15 20:46: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
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user