cert: Add version 3 certificates, a v2 certificate extended with ML-KEM-1024 or ML-DSA-87 keys and an ML-DSA-87 CA signature

This commit is contained in:
JackDoan committed 2026-10-14 10:47:41 -05:00
1 parent 73ddb56c34
commit 1a8fb8d67e
9 files changed
+1255 -10

No files matched your search

+51 -1
View File
@@ -15,6 +15,11 @@ import (
type CAPool struct {
CAs map[string]*CachedCertificate
certBlocklist map[string]struct{}
// v3CAs indexes the version 3 CAs in CAs by the fingerprint of their v2 form, which is what the certificates
// they issue name as issuer. A version 3 CA only verifies version 3 certificates: version 2 certificates find
// their issuer in CAs, where only the v2 form of the CA, if listed itself, answers to that fingerprint.
v3CAs map[string]*CachedCertificate
}
// NewCAPool creates an empty CAPool
@@ -22,6 +27,7 @@ func NewCAPool() *CAPool {
ca := CAPool{
CAs: make(map[string]*CachedCertificate),
certBlocklist: make(map[string]struct{}),
v3CAs: make(map[string]*CachedCertificate),
}
return &ca
@@ -122,6 +128,17 @@ func (ncp *CAPool) AddCA(c Certificate) error {
cc.InvertedGroups[g] = struct{}{}
}
if v2 := V2Form(c); v2 != nil {
v2Sum, err := v2.Fingerprint()
if err != nil {
return fmt.Errorf("could not calculate fingerprint for provided CA; error: %w; %s", err, c.Name())
}
if other, ok := ncp.v3CAs[v2Sum]; ok && other.Fingerprint != sum {
return fmt.Errorf("%s: two version 3 CAs extend the same version 2 CA %s", c.Name(), v2Sum)
}
ncp.v3CAs[v2Sum] = cc
}
ncp.CAs[sum] = cc
if c.Expired(time.Now()) {
@@ -180,11 +197,34 @@ func (ncp *CAPool) VerifyCertificate(now time.Time, c Certificate) (*CachedCerti
return nil, ErrBlockListed
}
// Blocking the v2 form of a version 3 certificate blocks the certificate too.
var v2Fps []string
if v2 := V2Form(c); v2 != nil {
v2Fp, err := v2.Fingerprint()
if err != nil {
return nil, fmt.Errorf("could not calculate v2 fingerprint to verify: %w", err)
}
v2Fp2, err := CalculateAlternateFingerprint(v2)
if err != nil {
return nil, fmt.Errorf("could not calculate alternate v2 fingerprint to verify: %w", err)
}
v2Fps = append(v2Fps, v2Fp)
if v2Fp2 != "" {
v2Fps = append(v2Fps, v2Fp2)
}
for _, f := range v2Fps {
if ncp.IsBlocklisted(f) {
return nil, ErrBlockListed
}
}
}
cc := CachedCertificate{
Certificate: c,
InvertedGroups: make(map[string]struct{}),
Fingerprint: fp,
fingerprint2: fp2,
v2Fingerprints: v2Fps,
signerFingerprint: signer.Fingerprint,
}
@@ -202,6 +242,11 @@ func (ncp *CAPool) VerifyCachedCertificate(now time.Time, c *CachedCertificate)
if c.fingerprint2 != "" && ncp.IsBlocklisted(c.fingerprint2) {
return ErrBlockListed
}
for _, f := range c.v2Fingerprints {
if ncp.IsBlocklisted(f) {
return ErrBlockListed
}
}
_, err := ncp.verify(c.Certificate, now, c.Fingerprint, c.signerFingerprint)
return err
@@ -257,7 +302,12 @@ func (ncp *CAPool) GetCAForCert(c Certificate) (*CachedCertificate, error) {
return nil, fmt.Errorf("no issuer in certificate")
}
signer, ok := ncp.CAs[issuer]
cas := ncp.CAs
if c.Version() == Version3 {
cas = ncp.v3CAs
}
signer, ok := cas[issuer]
if ok {
return signer, nil
}
+13
View File
@@ -14,12 +14,14 @@ const (
VersionPre1 Version = 0
Version1 Version = 1
Version2 Version = 2
Version3 Version = 3
)
type Certificate interface {
// Version defines the underlying certificate structure and wire protocol version
// Version1 certificates are ipv4 only and uses protobuf serialization
// Version2 certificates are ipv4 or ipv6 and uses asn.1 serialization
// Version3 certificates extend a Version2 certificate with post-quantum keys and signatures
Version() Version
// Name is the human-readable name that identifies this certificate.
@@ -115,6 +117,9 @@ type CachedCertificate struct {
// A place to store a 2nd fingerprint if the certificate could have one, such as with P256
fingerprint2 string
// The fingerprints of a version 3 certificate's v2 form, which the blocklist also applies to
v2Fingerprints []string
}
func (cc *CachedCertificate) String() string {
@@ -143,6 +148,8 @@ func Recombine(v Version, rawCertBytes, publicKey []byte, curve Curve) (Certific
c, err = unmarshalCertificateV1(rawCertBytes, publicKey)
case Version2:
c, err = unmarshalCertificateV2(rawCertBytes, publicKey, curve)
case Version3:
c, err = unmarshalCertificateV3(rawCertBytes, publicKey, curve)
default:
return nil, ErrUnknownVersion
}
@@ -165,6 +172,12 @@ func CalculateAlternateFingerprint(c Certificate) (string, error) {
return "", nil
}
// The post-quantum signature of a version 3 certificate covers its classical signature, so it has no second form.
// The pool checks the fingerprints of its v2 form separately.
if c.Version() == Version3 {
return "", nil
}
nc := c.Copy()
b, err := p256.Swap(nc.Signature())
if err != nil {
+520
View File
@@ -0,0 +1,520 @@
package cert
import (
"bytes"
"crypto/mldsa"
"crypto/mlkem"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"encoding/pem"
"fmt"
"net/netip"
"time"
"golang.org/x/crypto/cryptobyte"
"golang.org/x/crypto/cryptobyte/asn1"
)
// A version 3 certificate is a version 2 certificate extended with post-quantum keys. Its first fields are exactly
// 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.
//
// 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
)
const (
// MLKEM1024PublicKeySize is the size of the ML-KEM-1024 encapsulation key in a v3 host certificate.
MLKEM1024PublicKeySize = mlkem.EncapsulationKeySize1024
// MLKEM1024PrivateKeySize is the size of the ML-KEM-1024 decapsulation key seed in a v3 host private key.
MLKEM1024PrivateKeySize = mlkem.SeedSize
// MLDSA87PublicKeySize is the size of the ML-DSA-87 public key in a v3 CA certificate.
MLDSA87PublicKeySize = mldsa.MLDSA87PublicKeySize
// MLDSA87PrivateKeySize is the size of the ML-DSA-87 private key seed in a v3 CA private key.
MLDSA87PrivateKeySize = mldsa.PrivateKeySize
// MLDSA87SignatureSize is the size of the ML-DSA-87 signature in every v3 certificate.
MLDSA87SignatureSize = mldsa.MLDSA87SignatureSize
// pqSignatureContext is the ML-DSA context string for certificate signatures.
pqSignatureContext = "nebula certificate v3"
)
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.
pqPublicKey []byte
// pqSignature is the issuing CA's ML-DSA-87 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 {
if c3, ok := c.(*certificateV3); ok {
return &c3.v2
}
return nil
}
func (c *certificateV3) Version() Version {
return Version3
}
func (c *certificateV3) Curve() Curve {
return c.v2.curve
}
func (c *certificateV3) Groups() []string {
return c.v2.details.groups
}
func (c *certificateV3) IsCA() bool {
return c.v2.details.isCA
}
func (c *certificateV3) Issuer() string {
return c.v2.details.issuer
}
func (c *certificateV3) Name() string {
return c.v2.details.name
}
func (c *certificateV3) Networks() []netip.Prefix {
return c.v2.details.networks
}
func (c *certificateV3) NotAfter() time.Time {
return c.v2.details.notAfter
}
func (c *certificateV3) NotBefore() time.Time {
return c.v2.details.notBefore
}
// PublicKey returns the composite public key, classical || post-quantum.
func (c *certificateV3) PublicKey() []byte {
k := make([]byte, 0, len(c.v2.publicKey)+len(c.pqPublicKey))
k = append(k, c.v2.publicKey...)
return append(k, c.pqPublicKey...)
}
func (c *certificateV3) MarshalPublicKeyPEM() []byte {
return marshalCertPublicKeyToPEM(c)
}
// Signature returns the classical signature. The post-quantum signature covers it.
func (c *certificateV3) Signature() []byte {
return c.v2.signature
}
func (c *certificateV3) UnsafeNetworks() []netip.Prefix {
return c.v2.details.unsafeNetworks
}
// Fingerprint is the sha256 of the whole certificate. It differs from the fingerprint of the v2 form.
func (c *certificateV3) Fingerprint() (string, error) {
b, err := c.Marshal()
if err != nil {
return "", err
}
sum := sha256.Sum256(b)
return hex.EncodeToString(sum[:]), nil
}
// CheckSignature checks both signatures against the composite public key of the signing CA.
func (c *certificateV3) CheckSignature(key []byte) bool {
classical, pq, err := splitPQPublicKey(true, key)
if err != nil || len(pq) != MLDSA87PublicKeySize {
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
}
func (c *certificateV3) Expired(t time.Time) bool {
return c.v2.Expired(t)
}
// 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
}
if err := c.v2.VerifyPrivateKey(curve, key[:len(key)-n]); 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()
}
if !bytes.Equal(pub, c.pqPublicKey) {
return ErrPublicPrivateKeyMismatch
}
return nil
}
func (c *certificateV3) String() string {
mb, err := c.marshalJSON()
if err != nil {
return fmt.Sprintf("<error marshalling certificate: %v>", err)
}
b, err := json.MarshalIndent(mb, "", "\t")
if err != nil {
return fmt.Sprintf("<error marshalling certificate: %v>", err)
}
return string(b)
}
// MarshalForHandshakes leaves out both public keys, the handshake carries them as the composite static key.
func (c *certificateV3) MarshalForHandshakes() ([]byte, error) {
return c.marshal(false, false)
}
func (c *certificateV3) Marshal() ([]byte, error) {
return c.marshal(true, false)
}
// marshalForPQSigning returns the bytes the post-quantum signature covers: every field but that signature, with the
// curve always present.
func (c *certificateV3) marshalForPQSigning() ([]byte, error) {
return c.marshal(true, true)
}
func (c *certificateV3) marshal(withPublicKeys, forSigning bool) ([]byte, error) {
if c.v2.rawDetails == nil {
return nil, ErrEmptyRawDetails
}
var b cryptobyte.Builder
b.AddASN1(asn1.SEQUENCE, func(b *cryptobyte.Builder) {
b.AddBytes(c.v2.rawDetails)
if withPublicKeys {
if forSigning || c.v2.curve != Curve_CURVE25519 {
b.AddASN1(TagCertCurve, func(b *cryptobyte.Builder) {
b.AddBytes([]byte{byte(c.v2.curve)})
})
}
b.AddASN1(TagCertPublicKey, func(b *cryptobyte.Builder) {
b.AddBytes(c.v2.publicKey)
})
}
b.AddASN1(TagCertSignature, func(b *cryptobyte.Builder) {
b.AddBytes(c.v2.signature)
})
if withPublicKeys {
b.AddASN1(TagCertPQPublicKey, func(b *cryptobyte.Builder) {
b.AddBytes(c.pqPublicKey)
})
}
if !forSigning {
b.AddASN1(TagCertPQSignature, func(b *cryptobyte.Builder) {
b.AddBytes(c.pqSignature)
})
}
})
return b.Bytes()
}
func (c *certificateV3) MarshalPEM() ([]byte, error) {
b, err := c.Marshal()
if err != nil {
return nil, err
}
return pem.EncodeToMemory(&pem.Block{Type: CertificateV3Banner, Bytes: b}), nil
}
func (c *certificateV3) MarshalJSON() ([]byte, error) {
b, err := c.marshalJSON()
if err != nil {
return nil, err
}
return json.Marshal(b)
}
func (c *certificateV3) marshalJSON() (m, error) {
mb, err := c.v2.marshalJSON()
if err != nil {
return nil, err
}
fp, err := c.Fingerprint()
if err != nil {
return nil, err
}
mb["v2Fingerprint"] = mb["fingerprint"]
mb["fingerprint"] = fp
mb["version"] = Version3
mb["pqPublicKey"] = fmt.Sprintf("%x", c.pqPublicKey)
mb["pqSignature"] = fmt.Sprintf("%x", c.pqSignature)
return mb, nil
}
func (c *certificateV3) Copy() Certificate {
nc := &certificateV3{
v2: *c.v2.Copy().(*certificateV2),
pqPublicKey: make([]byte, len(c.pqPublicKey)),
pqSignature: make([]byte, len(c.pqSignature)),
}
copy(nc.pqPublicKey, c.pqPublicKey)
copy(nc.pqSignature, c.pqSignature)
return nc
}
func (c *certificateV3) validate() error {
if err := c.v2.validate(); err != nil {
return err
}
if len(c.pqPublicKey) != pqPublicKeySize(c.IsCA()) {
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 {
return ErrBadFormat
}
return nil
}
// unmarshalCertificateV3 parses a v3 certificate. publicKey is the composite public key when the certificate came
// from a handshake, which then must not carry either public key itself.
func unmarshalCertificateV3(b []byte, publicKey []byte, curve Curve) (*certificateV3, error) {
l := len(b)
if l == 0 || l > MaxCertificateSize {
return nil, ErrBadFormat
}
input := cryptobyte.String(b)
var body cryptobyte.String
if !input.ReadASN1(&body, asn1.SEQUENCE) || body.Empty() || !input.Empty() {
return nil, ErrBadFormat
}
var rawDetails cryptobyte.String
if !body.ReadASN1Element(&rawDetails, TagCertDetails) || rawDetails.Empty() {
return nil, ErrBadFormat
}
var rawCurve byte
if !readOptionalASN1Byte(&body, &rawCurve, TagCertCurve, byte(curve)) {
return nil, ErrBadFormat
}
curve = Curve(rawCurve)
fromHandshake := len(publicKey) > 0
var classical, pq cryptobyte.String
if fromHandshake {
if body.PeekASN1Tag(TagCertPublicKey) {
return nil, ErrCertPubkeyPresent
}
} else if !body.ReadASN1(&classical, TagCertPublicKey) || classical.Empty() {
return nil, ErrBadFormat
}
var signature cryptobyte.String
if !body.ReadASN1(&signature, TagCertSignature) || signature.Empty() {
return nil, ErrBadFormat
}
if fromHandshake {
if body.PeekASN1Tag(TagCertPQPublicKey) {
return nil, ErrCertPubkeyPresent
}
} else if !body.ReadASN1(&pq, TagCertPQPublicKey) || pq.Empty() {
return nil, ErrBadFormat
}
var pqSignature cryptobyte.String
if !body.ReadASN1(&pqSignature, TagCertPQSignature) || pqSignature.Empty() {
return nil, ErrBadFormat
}
if !body.Empty() {
return nil, ErrBadFormat
}
details, err := unmarshalDetails(rawDetails)
if err != nil {
return nil, err
}
if fromHandshake {
k, p, err := splitPQPublicKey(details.isCA, publicKey)
if err != nil {
return nil, err
}
classical = bytes.Clone(k)
pq = bytes.Clone(p)
}
c := &certificateV3{
v2: certificateV2{
details: details,
rawDetails: rawDetails,
curve: curve,
publicKey: classical,
signature: signature,
},
pqPublicKey: pq,
pqSignature: pqSignature,
}
if err := c.validate(); err != nil {
return nil, err
}
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.
//
// 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) {
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())
}
var signingKey []byte
if c2.IsCA() {
if signer != nil {
return nil, fmt.Errorf("a CA certificate must extend itself, signer must be nil")
}
if !c2.CheckSignature(c2.publicKey) {
return nil, ErrNotSelfSigned
}
signingKey = append(bytes.Clone(c2.publicKey), pqPublicKey...)
} else {
if signer == nil || signer.Version() != Version3 || !signer.IsCA() {
return nil, fmt.Errorf("a host certificate must be extended by a version 3 CA")
}
signerV2 := V2Form(signer)
issuer, err := signerV2.Fingerprint()
if err != nil {
return nil, err
}
if c2.Issuer() != issuer {
return nil, fmt.Errorf("certificate was not issued by the signing CA")
}
if !c2.CheckSignature(signerV2.PublicKey()) {
return nil, ErrSignatureMismatch
}
signingKey = signer.PublicKey()
}
c3 := &certificateV3{
v2: *c2.Copy().(*certificateV2),
pqPublicKey: bytes.Clone(pqPublicKey),
}
if len(c3.pqPublicKey) != pqPublicKeySize(c3.IsCA()) {
return nil, ErrInvalidPublicKey
}
b, err := c3.marshalForPQSigning()
if err != nil {
return nil, err
}
sig, err := pqSign(b)
if err != nil {
return nil, err
}
c3.pqSignature = sig
if err := c3.validate(); err != nil {
return nil, err
}
// Catch a pqSign that used the wrong key before the certificate goes anywhere.
if !c3.CheckSignature(signingKey) {
return nil, ErrSignatureMismatch
}
return c3, nil
}
// SignPQ returns a SignerLambda that signs certificates with the ML-DSA-87 private key seed.
func SignPQ(seed []byte) (SignerLambda, error) {
sk, err := mldsa.NewPrivateKey(mldsa.MLDSA87(), seed)
if err != nil {
return nil, err
}
return func(b []byte) ([]byte, error) {
return sk.Sign(nil, b, &mldsa.Options{Context: pqSignatureContext})
}, nil
}
+429
View File
@@ -0,0 +1,429 @@
package cert
import (
"crypto/mldsa"
"crypto/mlkem"
"net/netip"
"testing"
"time"
"github.com/slackhq/nebula/cert/p256"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/cryptobyte"
"golang.org/x/crypto/cryptobyte/asn1"
)
var testCurves = []Curve{Curve_CURVE25519, Curve_P256}
func newTestV3(t *testing.T, curve Curve) (ca Certificate, caKey []byte, host Certificate, hostKey []byte) {
t.Helper()
ca, _, caKey, _ = NewTestCaCert(Version3, curve, time.Time{}, time.Time{}, nil, nil, []string{"g"})
host, _, hostKeyPEM, _ := NewTestCert(Version3, curve, ca, caKey, "host", time.Time{}, time.Time{}, []netip.Prefix{netip.MustParsePrefix("10.1.0.1/16")}, nil, []string{"g"})
hostKey, _, _, err := UnmarshalPrivateKeyFromPEM(hostKeyPEM)
require.NoError(t, err)
return ca, caKey, host, hostKey
}
func TestCertificateV3_Sign(t *testing.T) {
for _, curve := range testCurves {
t.Run(curve.String(), func(t *testing.T) {
ca, caKey, host, hostKey := newTestV3(t, curve)
assert.Equal(t, Version3, ca.Version())
assert.Equal(t, Version3, host.Version())
assert.True(t, ca.CheckSignature(ca.PublicKey()))
assert.True(t, host.CheckSignature(ca.PublicKey()))
require.NoError(t, ca.VerifyPrivateKey(curve, caKey))
require.NoError(t, host.VerifyPrivateKey(curve, hostKey))
// The classical half of the key alone is not the key.
require.Error(t, host.VerifyPrivateKey(curve, hostKey[:len(hostKey)-MLKEM1024PrivateKeySize]))
// The issuer is the v2 form of the CA, which old peers know it by.
caV2Fp, err := V2Form(ca).Fingerprint()
require.NoError(t, err)
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)
require.Error(t, err)
})
}
}
// The v2 form must be exactly the certificate the CA's v2 form would have signed, so peers that only speak v2
// accept it. Ed25519 signatures are deterministic, so the bytes can be compared outright.
func TestCertificateV3_V2FormIsExact(t *testing.T) {
ca, caKey, host, _ := newTestV3(t, Curve_CURVE25519)
classicalCAKey := caKey[:len(caKey)-MLDSA87PrivateKeySize]
classicalPub := V2Form(host).PublicKey()
tbs := &TBSCertificate{
Version: Version2,
Curve: Curve_CURVE25519,
Name: host.Name(),
Networks: host.Networks(),
Groups: host.Groups(),
NotBefore: host.NotBefore(),
NotAfter: host.NotAfter(),
PublicKey: classicalPub,
}
v2, err := tbs.Sign(V2Form(ca), Curve_CURVE25519, classicalCAKey)
require.NoError(t, err)
want, err := v2.Marshal()
require.NoError(t, err)
got, err := V2Form(host).Marshal()
require.NoError(t, err)
assert.Equal(t, want, got)
wantHS, err := v2.MarshalForHandshakes()
require.NoError(t, err)
gotHS, err := V2Form(host).MarshalForHandshakes()
require.NoError(t, err)
assert.Equal(t, wantHS, gotHS)
assert.Nil(t, V2Form(v2))
}
func TestCertificateV3_MarshalRoundTrip(t *testing.T) {
for _, curve := range testCurves {
t.Run(curve.String(), func(t *testing.T) {
ca, _, host, _ := newTestV3(t, curve)
for _, c := range []Certificate{ca, host} {
b, err := c.Marshal()
require.NoError(t, err)
c2, err := unmarshalCertificateV3(b, nil, Curve_CURVE25519)
require.NoError(t, err)
assertCertsEqual(t, c, c2)
p, err := c.MarshalPEM()
require.NoError(t, err)
c3, rest, err := UnmarshalCertificateFromPEM(p)
require.NoError(t, err)
assert.Empty(t, rest)
assertCertsEqual(t, c, c3)
// Handshakes carry the composite public key in the noise static, not in the certificate.
hs, err := c.MarshalForHandshakes()
require.NoError(t, err)
assert.Less(t, len(hs), len(b)-len(c.PublicKey()))
c4, err := Recombine(Version3, hs, c.PublicKey(), curve)
require.NoError(t, err)
assertCertsEqual(t, c, c4)
_, err = Recombine(Version3, b, c.PublicKey(), curve)
require.ErrorIs(t, err, ErrCertPubkeyPresent)
}
})
}
}
func assertCertsEqual(t *testing.T, want, got Certificate) {
t.Helper()
wb, err := want.Marshal()
require.NoError(t, err)
gb, err := got.Marshal()
require.NoError(t, err)
assert.Equal(t, wb, gb)
wf, err := want.Fingerprint()
require.NoError(t, err)
gf, err := got.Fingerprint()
require.NoError(t, err)
assert.Equal(t, wf, gf)
assert.Equal(t, want.PublicKey(), got.PublicKey())
}
// rebuildV3 re-encodes a v3 certificate's envelope after edit has had a go at its fields.
func rebuildV3(t *testing.T, c Certificate, edit func(fields map[asn1.Tag][]byte, order *[]asn1.Tag)) []byte {
t.Helper()
b, err := c.Marshal()
require.NoError(t, err)
in := cryptobyte.String(b)
var body cryptobyte.String
require.True(t, in.ReadASN1(&body, asn1.SEQUENCE))
fields := map[asn1.Tag][]byte{}
var order []asn1.Tag
for !body.Empty() {
var el cryptobyte.String
var tag asn1.Tag
require.True(t, body.ReadAnyASN1Element(&el, &tag))
fields[tag] = el
order = append(order, tag)
}
edit(fields, &order)
var out cryptobyte.Builder
out.AddASN1(asn1.SEQUENCE, func(b *cryptobyte.Builder) {
for _, tag := range order {
b.AddBytes(fields[tag])
}
})
return out.BytesOrPanic()
}
func TestCertificateV3_StrictParse(t *testing.T) {
_, _, host, _ := newTestV3(t, Curve_CURVE25519)
good, err := host.Marshal()
require.NoError(t, err)
_, err = unmarshalCertificateV3(append(good, 0), nil, Curve_CURVE25519)
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)
})
_, err = unmarshalCertificateV3(trailing, nil, Curve_CURVE25519)
require.ErrorIs(t, err, ErrBadFormat, "unknown field after the pq signature")
for _, tag := range []asn1.Tag{TagCertPQPublicKey, TagCertPQSignature} {
missing := rebuildV3(t, host, func(fields map[asn1.Tag][]byte, order *[]asn1.Tag) {
*order = removeTag(*order, tag)
})
_, err = unmarshalCertificateV3(missing, nil, Curve_CURVE25519)
require.Error(t, err, "missing %x", tag)
}
// A v2 certificate is not a v3 one.
v2, err := V2Form(host).Marshal()
require.NoError(t, err)
_, err = unmarshalCertificateV3(v2, nil, Curve_CURVE25519)
require.Error(t, err)
// A wrong sized post-quantum key fails validation.
short := rebuildV3(t, host, func(fields map[asn1.Tag][]byte, order *[]asn1.Tag) {
var b cryptobyte.Builder
b.AddASN1(TagCertPQPublicKey, func(b *cryptobyte.Builder) { b.AddBytes(make([]byte, MLKEM1024PublicKeySize-1)) })
fields[TagCertPQPublicKey] = b.BytesOrPanic()
})
_, err = unmarshalCertificateV3(short, nil, Curve_CURVE25519)
require.ErrorIs(t, err, ErrInvalidPublicKey)
}
func removeTag(order []asn1.Tag, tag asn1.Tag) []asn1.Tag {
var out []asn1.Tag
for _, t := range order {
if t != tag {
out = append(out, t)
}
}
return out
}
func TestCertificateV3_Tamper(t *testing.T) {
for _, curve := range testCurves {
t.Run(curve.String(), func(t *testing.T) {
ca, _, host, _ := newTestV3(t, curve)
// Swapping the post-quantum key breaks only the post-quantum signature, which must still catch it.
dk, err := mlkem.GenerateKey1024()
require.NoError(t, err)
swapped := host.Copy().(*certificateV3)
swapped.pqPublicKey = dk.EncapsulationKey().Bytes()
assert.True(t, V2Form(swapped).CheckSignature(V2Form(ca).PublicKey()))
assert.False(t, swapped.CheckSignature(ca.PublicKey()))
// The post-quantum signature covers the classical one.
resigned := host.Copy().(*certificateV3)
if curve == Curve_P256 {
sig, err := p256.Swap(resigned.v2.signature)
require.NoError(t, err)
resigned.v2.signature = sig
assert.True(t, V2Form(resigned).CheckSignature(V2Form(ca).PublicKey()))
} else {
resigned.v2.signature[0] ^= 1
}
assert.False(t, resigned.CheckSignature(ca.PublicKey()))
// Another CA's post-quantum key does not verify.
other, _, _, _ := NewTestCaCert(Version3, curve, time.Time{}, time.Time{}, nil, nil, nil)
k := append(V2Form(ca).PublicKey(), other.PublicKey()[len(V2Form(other).PublicKey()):]...)
assert.False(t, host.CheckSignature(k))
})
}
}
func TestCAPool_V3(t *testing.T) {
for _, curve := range testCurves {
t.Run(curve.String(), func(t *testing.T) {
ca, _, host, _ := newTestV3(t, curve)
now := time.Now()
// A v3 CA verifies v3 certificates, and only those.
pool := NewCAPool()
require.NoError(t, pool.AddCA(ca))
cc, err := pool.VerifyCertificate(now, host)
require.NoError(t, err)
require.NoError(t, pool.VerifyCachedCertificate(now, cc))
_, err = pool.VerifyCertificate(now, V2Form(host))
require.ErrorIs(t, err, ErrCaNotFound)
// Listing the v2 form of the CA as well accepts the v2 form of the certificate.
require.NoError(t, pool.AddCA(V2Form(ca)))
_, err = pool.VerifyCertificate(now, V2Form(host))
require.NoError(t, err)
_, err = pool.VerifyCertificate(now, host)
require.NoError(t, err)
// The v2 form of the CA alone does not verify a v3 certificate.
v2Pool := NewCAPool()
require.NoError(t, v2Pool.AddCA(V2Form(ca)))
_, err = v2Pool.VerifyCertificate(now, host)
require.ErrorIs(t, err, ErrCaNotFound)
// 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())
require.NoError(t, err)
again, err := ExtendToV3(V2Form(ca), 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")
})
}
}
func TestCAPool_V3Blocklist(t *testing.T) {
for _, curve := range testCurves {
t.Run(curve.String(), func(t *testing.T) {
ca, _, host, _ := newTestV3(t, curve)
now := time.Now()
pool := NewCAPool()
require.NoError(t, pool.AddCA(ca))
require.NoError(t, pool.AddCA(V2Form(ca)))
cc, err := pool.VerifyCertificate(now, host)
require.NoError(t, err)
fp, err := host.Fingerprint()
require.NoError(t, err)
v2Fp, err := V2Form(host).Fingerprint()
require.NoError(t, err)
assert.NotEqual(t, fp, v2Fp)
// Blocking the v3 fingerprint leaves the v2 form alone, which is why operators list both.
pool.BlocklistFingerprint(fp)
_, err = pool.VerifyCertificate(now, host)
require.ErrorIs(t, err, ErrBlockListed)
require.ErrorIs(t, pool.VerifyCachedCertificate(now, cc), ErrBlockListed)
_, err = pool.VerifyCertificate(now, V2Form(host))
require.NoError(t, err)
// Blocking the v2 fingerprint blocks both.
pool.ResetCertBlocklist()
pool.BlocklistFingerprint(v2Fp)
_, err = pool.VerifyCertificate(now, host)
require.ErrorIs(t, err, ErrBlockListed)
require.ErrorIs(t, pool.VerifyCachedCertificate(now, cc), ErrBlockListed)
_, err = pool.VerifyCertificate(now, V2Form(host))
require.ErrorIs(t, err, ErrBlockListed)
if curve == Curve_P256 {
// So does the other form of a P256 v2 signature.
alt, err := CalculateAlternateFingerprint(V2Form(host))
require.NoError(t, err)
pool.ResetCertBlocklist()
pool.BlocklistFingerprint(alt)
_, err = pool.VerifyCertificate(now, host)
require.ErrorIs(t, err, ErrBlockListed)
}
})
}
}
// Certificates already in use extend in place: their v2 forms, and fingerprints, do not change.
func TestExtendToV3(t *testing.T) {
for _, curve := range testCurves {
t.Run(curve.String(), func(t *testing.T) {
ca2, _, caKey2, _ := NewTestCaCert(Version2, curve, time.Time{}, time.Time{}, nil, nil, nil)
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())
require.NoError(t, err)
ca3, err := ExtendToV3(ca2, 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)
require.NoError(t, err)
assertCertsEqual(t, host2, V2Form(host3))
pool := NewCAPool()
require.NoError(t, pool.AddCA(ca3))
_, err = pool.VerifyCertificate(time.Now(), host3)
require.NoError(t, err)
// 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)
require.NoError(t, err)
_, err = ExtendToV3(host2, dk.EncapsulationKey().Bytes(), other3, signPQ)
require.Error(t, err)
wrong, err := mldsa.GenerateKey(mldsa.MLDSA87())
require.NoError(t, err)
wrongSign, err := SignPQ(wrong.Bytes())
require.NoError(t, err)
_, err = ExtendToV3(host2, 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)
require.ErrorIs(t, err, ErrSignatureMismatch)
_, err = ExtendToV3(ca2, sk.PublicKey().Bytes(), ca3, signPQ)
require.Error(t, err)
// Only v2 certificates extend.
_, err = ExtendToV3(host3, dk.EncapsulationKey().Bytes(), ca3, signPQ)
require.Error(t, err)
})
}
}
func TestCertificateV3_KeyPEM(t *testing.T) {
for _, curve := range testCurves {
t.Run(curve.String(), func(t *testing.T) {
ca, caKey, host, hostKey := newTestV3(t, curve)
pub, rest, gotCurve, err := UnmarshalPublicKeyFromPEM(host.MarshalPublicKeyPEM())
require.NoError(t, err)
assert.Empty(t, rest)
assert.Equal(t, curve, gotCurve)
assert.Equal(t, host.PublicKey(), pub)
pub, _, gotCurve, err = UnmarshalSigningPublicKeyFromPEM(ca.MarshalPublicKeyPEM())
require.NoError(t, err)
assert.Equal(t, curve, gotCurve)
assert.Equal(t, ca.PublicKey(), pub)
key, _, gotCurve, err := UnmarshalPrivateKeyFromPEM(MarshalPrivateKeyToPEM(curve, hostKey))
require.NoError(t, err)
assert.Equal(t, curve, gotCurve)
assert.Equal(t, hostKey, key)
key, _, gotCurve, err = UnmarshalSigningPrivateKeyFromPEM(MarshalSigningPrivateKeyToPEM(curve, 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)
require.NoError(t, err)
_, _, _, err = UnmarshalSigningPrivateKeyFromPEM(enc)
require.ErrorIs(t, err, ErrPrivateKeyEncrypted)
gotCurve, key, _, err = DecryptAndUnmarshalSigningPrivateKey([]byte("pw"), enc)
require.NoError(t, err)
assert.Equal(t, curve, gotCurve)
assert.Equal(t, caKey, key)
})
}
}
+22
View File
@@ -158,6 +158,7 @@ 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
ciphertext, err := aes256Encrypt(passphrase, kdfParams, b)
if err != nil {
return nil, err
@@ -180,6 +181,13 @@ 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
@@ -260,11 +268,18 @@ 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:
curve = Curve_CURVE25519
case 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")
}
@@ -285,6 +300,13 @@ 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)
}
return curve, bytes, r, nil
}
switch curve {
case Curve_CURVE25519:
if len(bytes) != ed25519.PrivateKeySize {
+28
View File
@@ -4,6 +4,8 @@ import (
"crypto/ecdh"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/mldsa"
"crypto/mlkem"
"crypto/rand"
"io"
"net/netip"
@@ -39,6 +41,10 @@ func NewTestCaCert(version Version, curve Curve, before, after time.Time, networ
// There is no default to allow the underlying lib to respond with an error
}
if version == Version3 {
pub, priv = appendPQSigningKey(pub, priv)
}
if before.IsZero() {
before = testCertNow.Add(time.Second * -60)
}
@@ -97,6 +103,10 @@ func NewTestCert(v Version, curve Curve, ca Certificate, key []byte, name string
panic("unknown curve")
}
if v == Version3 {
pub, priv = appendPQKey(pub, priv)
}
nc := &TBSCertificate{
Version: v,
Curve: curve,
@@ -123,6 +133,24 @@ func NewTestCert(v Version, curve Curve, ca Certificate, key []byte, name string
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.
func appendPQSigningKey(pub, priv []byte) ([]byte, []byte) {
sk, err := mldsa.GenerateKey(mldsa.MLDSA87())
if err != nil {
panic(err)
}
return append(pub, sk.PublicKey().Bytes()...), append(priv, sk.Bytes()...)
}
// 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()
if err != nil {
panic(err)
}
return append(pub, dk.EncapsulationKey().Bytes()...), append(priv, dk.Bytes()...)
}
func X25519Keypair() ([]byte, []byte) {
privkey := make([]byte, 32)
if _, err := io.ReadFull(rand.Reader, privkey); err != nil {
+98 -1
View File
@@ -64,6 +64,7 @@ func SplitPEM(data []byte, atEOF bool) (advance int, token []byte, err error) {
const ( //cert banners
CertificateBanner = "NEBULA CERTIFICATE"
CertificateV2Banner = "NEBULA CERTIFICATE V2"
CertificateV3Banner = "NEBULA CERTIFICATE V3"
)
const ( //key-agreement-key banners
@@ -73,6 +74,42 @@ 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"
)
// Classical key sizes, the composite keys of version 3 add the post-quantum key to these.
const (
x25519KeySize = 32
p256PublicKeySize = 65 // uncompressed
p256PrivateKeySize = 32
ed25519PublicKeySize = 32
ed25519PrivateKeySize = 64
ecdsaP256PublicKeySize = 65 // uncompressed
)
// signingPrivateKeySize returns the size of a classical signing private key.
func signingPrivateKeySize(curve Curve) int {
if curve == Curve_P256 {
return p256PrivateKeySize
}
return ed25519PrivateKeySize
}
/* including "ECDSA" in the P256 banners is a clue that these keys should be used only for signing */
const ( //signing key banners
EncryptedECDSAP256PrivateKeyBanner = "NEBULA ECDSA P256 ENCRYPTED PRIVATE KEY"
@@ -109,6 +146,8 @@ func unmarshalCertificateBlock(block *pem.Block) (Certificate, error) {
return unmarshalCertificateV1(block.Bytes, nil)
case CertificateV2Banner:
return unmarshalCertificateV2(block.Bytes, nil, Curve_CURVE25519)
case CertificateV3Banner:
return unmarshalCertificateV3(block.Bytes, nil, Curve_CURVE25519)
default:
return nil, ErrInvalidPEMCertificateBanner
}
@@ -125,6 +164,13 @@ 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})
@@ -138,6 +184,13 @@ func MarshalPublicKeyToPEM(curve Curve, b []byte) []byte {
// MarshalSigningPublicKeyToPEM returns a PEM representation of a public key used for signing.
// if your public key came from a certificate, prefer Certificate.PublicKeyPEM() if possible, to avoid mistakes!
func MarshalSigningPublicKeyToPEM(curve Curve, b []byte) []byte {
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})
@@ -166,6 +219,12 @@ 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")
}
@@ -193,6 +252,12 @@ 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")
}
@@ -203,6 +268,13 @@ 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})
@@ -214,6 +286,13 @@ func MarshalPrivateKeyToPEM(curve Curve, b []byte) []byte {
}
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})
@@ -240,6 +319,12 @@ 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")
}
@@ -258,8 +343,20 @@ func UnmarshalSigningPrivateKeyFromPEM(b []byte) ([]byte, []byte, Curve, error)
switch k.Type {
case EncryptedEd25519PrivateKeyBanner:
return nil, nil, Curve_CURVE25519, ErrPrivateKeyEncrypted
case EncryptedECDSAP256PrivateKeyBanner:
case EncryptedECDSAP256PrivateKeyBanner, EncryptedECDSAP256MLDSA87PrivateKeyBanner:
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 {
+66 -8
View File
@@ -47,31 +47,87 @@ type SignerLambda func(certBytes []byte) ([]byte, error)
// details do not violate constraints of the signing certificate.
// If the TBSCertificate is a CA then signer must be nil.
func (t *TBSCertificate) Sign(signer Certificate, curve Curve, key []byte) (Certificate, error) {
switch t.Curve {
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])
if err != nil {
return nil, err
}
pq, err := SignPQ(key[len(key)-MLDSA87PrivateKeySize:])
if err != nil {
return nil, err
}
return t.SignHybridWith(signer, curve, classical, pq)
}
sp, err := classicalSigner(t.Curve, key)
if err != nil {
return nil, err
}
return t.SignWith(signer, curve, sp)
}
// classicalSigner returns a SignerLambda for a classical signing private key.
func classicalSigner(curve Curve, key []byte) (SignerLambda, error) {
switch curve {
case Curve_CURVE25519:
if len(key) != ed25519.PrivateKeySize {
return nil, ErrInvalidPrivateKey
}
pk := ed25519.PrivateKey(key)
sp := func(certBytes []byte) ([]byte, error) {
return func(certBytes []byte) ([]byte, error) {
sig := ed25519.Sign(pk, certBytes)
return sig, nil
}
return t.SignWith(signer, curve, sp)
}, nil
case Curve_P256:
pk, err := ecdsa.ParseRawPrivateKey(elliptic.P256(), key)
if err != nil {
return nil, err
}
sp := func(certBytes []byte) ([]byte, error) {
return func(certBytes []byte) ([]byte, error) {
// We need to hash first for ECDSA
// - https://pkg.go.dev/crypto/ecdsa#SignASN1
hashed := sha256.Sum256(certBytes)
return ecdsa.SignASN1(rand.Reader, pk, hashed[:])
}
return t.SignWith(signer, curve, sp)
}, nil
default:
return nil, fmt.Errorf("invalid curve: %s", t.Curve)
return nil, fmt.Errorf("invalid curve: %s", curve)
}
}
// 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.
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 err != nil {
return nil, err
}
var signerV2 Certificate
if signer != nil {
if signer.Version() != Version3 {
return nil, fmt.Errorf("a version 3 certificate must be signed by a version 3 CA")
}
signerV2 = V2Form(signer)
}
t2 := *t
t2.Version = Version2
t2.PublicKey = classical
c2, err := t2.SignWith(signerV2, curve, sp)
if err != nil {
return nil, err
}
return ExtendToV3(c2, pq, signer, pqSp)
}
// SignWith does the same thing as sign, but uses the function in `sp` to calculate the signature.
// You should only use SignWith if you do not have direct access to your private key.
func (t *TBSCertificate) SignWith(signer Certificate, curve Curve, sp SignerLambda) (Certificate, error) {
@@ -114,6 +170,8 @@ func (t *TBSCertificate) SignWith(signer Certificate, curve Curve, sp SignerLamb
if err != nil {
return nil, err
}
case Version3:
return nil, fmt.Errorf("version 3 certificates carry two signatures, use SignHybridWith")
default:
return nil, fmt.Errorf("unknown cert version %d", t.Version)
}
+28
View File
@@ -4,6 +4,8 @@ import (
"crypto/ecdh"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/mldsa"
"crypto/mlkem"
"crypto/rand"
"io"
"net/netip"
@@ -40,6 +42,10 @@ func NewTestCaCert(version cert.Version, curve cert.Curve, before, after time.Ti
// There is no default to allow the underlying lib to respond with an error
}
if version == cert.Version3 {
pub, priv = appendPQSigningKey(pub, priv)
}
if before.IsZero() {
before = testCertNow.Add(time.Second * -60)
}
@@ -94,6 +100,10 @@ func NewTestCert(v cert.Version, curve cert.Curve, ca cert.Certificate, key []by
panic("unknown curve")
}
if v == cert.Version3 {
pub, priv = appendPQKey(pub, priv)
}
nc := &cert.TBSCertificate{
Version: v,
Curve: curve,
@@ -147,6 +157,24 @@ 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.
func appendPQSigningKey(pub, priv []byte) ([]byte, []byte) {
sk, err := mldsa.GenerateKey(mldsa.MLDSA87())
if err != nil {
panic(err)
}
return append(pub, sk.PublicKey().Bytes()...), append(priv, sk.Bytes()...)
}
// 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()
if err != nil {
panic(err)
}
return append(pub, dk.EncapsulationKey().Bytes()...), append(priv, dk.Bytes()...)
}
func X25519Keypair() ([]byte, []byte) {
privkey := make([]byte, 32)
if _, err := io.ReadFull(rand.Reader, privkey); err != nil {