mirror of
https://github.com/slackhq/nebula.git
synced 2026-11-15 16:26: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
+22
-8
@@ -37,6 +37,7 @@ type caFlags struct {
|
||||
argonParallelism *uint
|
||||
encryption *bool
|
||||
version *uint
|
||||
pqSuite *string
|
||||
|
||||
curve *string
|
||||
p11url *string
|
||||
@@ -71,7 +72,8 @@ func newCaFlags() *caFlags {
|
||||
cf := caFlags{set: flag.NewFlagSet("ca", flag.ContinueOnError)}
|
||||
cf.set.Usage = func() {}
|
||||
cf.name = cf.set.String("name", "", "Required: name of the certificate authority")
|
||||
cf.version = cf.set.Uint("version", uint(cert.Version2), "Optional: version of the certificate format to use. Version 3 adds an ML-DSA-87 key, for post-quantum certificates")
|
||||
cf.version = cf.set.Uint("version", uint(cert.Version2), "Optional: version of the certificate format to use. Version 3 adds a post-quantum key, for post-quantum certificates")
|
||||
cf.pqSuite = pqSuiteFlag(cf.set, "Optional: post-quantum suite of a version 3 CA, which the certificates it signs share")
|
||||
cf.duration = cf.set.Duration("duration", time.Duration(time.Hour*8760), "Optional: amount of time the certificate should be valid for. Valid time units are seconds: \"s\", minutes: \"m\", hours: \"h\"")
|
||||
cf.outKeyPath = cf.set.String("out-key", "ca.key", "Optional: path to write the private key to")
|
||||
cf.outCertPath = cf.set.String("out-crt", "ca.crt", "Optional: path to write the certificate to")
|
||||
@@ -164,7 +166,11 @@ func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error
|
||||
return newHelpErrorf("-version must be %v, %v or %v", cert.Version1, cert.Version2, cert.Version3)
|
||||
}
|
||||
if version == cert.Version3 && isP11 {
|
||||
return newHelpErrorf("-version 3 can not be used with -pkcs11, the ML-DSA-87 key has to be in out-key")
|
||||
return newHelpErrorf("-version 3 can not be used with -pkcs11, the post-quantum key has to be in out-key")
|
||||
}
|
||||
suite, err := parsePQSuiteFlag(cf.set, *cf.pqSuite, version == cert.Version3, "is only for -version 3")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var networks []netip.Prefix
|
||||
@@ -300,7 +306,7 @@ func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error
|
||||
}
|
||||
|
||||
if version == cert.Version3 {
|
||||
pub, rawPriv, err = appendPQSigningKey(pub, rawPriv)
|
||||
pub, rawPriv, err = appendPQSigningKey(suite, pub, rawPriv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -319,6 +325,9 @@ func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error
|
||||
IsCA: true,
|
||||
Curve: curve,
|
||||
}
|
||||
if version == cert.Version3 {
|
||||
t.PQSuite = suite
|
||||
}
|
||||
|
||||
if !isP11 && !isStdio(*cf.outKeyPath) {
|
||||
if _, err := os.Stat(*cf.outKeyPath); err == nil {
|
||||
@@ -346,14 +355,19 @@ func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error
|
||||
return fmt.Errorf("error while signing: %s", err)
|
||||
}
|
||||
|
||||
if *cf.encryption {
|
||||
switch {
|
||||
case *cf.encryption && version == cert.Version3:
|
||||
b, err = cert.EncryptAndMarshalPQSigningPrivateKey(curve, suite, rawPriv, passphrase, kdfParams)
|
||||
case *cf.encryption:
|
||||
b, err = cert.EncryptAndMarshalSigningPrivateKey(curve, rawPriv, passphrase, kdfParams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while encrypting out-key: %s", err)
|
||||
}
|
||||
} else {
|
||||
case version == cert.Version3:
|
||||
b = cert.MarshalPQSigningPrivateKeyToPEM(curve, suite, rawPriv)
|
||||
default:
|
||||
b = cert.MarshalSigningPrivateKeyToPEM(curve, rawPriv)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while encrypting out-key: %s", err)
|
||||
}
|
||||
|
||||
err = writeOutput(*cf.outKeyPath, b, 0600, out)
|
||||
if err != nil {
|
||||
|
||||
@@ -68,12 +68,14 @@ func Test_caHelp(t *testing.T) {
|
||||
" -out-qr string\n"+
|
||||
" \tOptional: output a qr code image (png) of the certificate\n"+
|
||||
optionalPkcs11String(" -pkcs11 string\n \tOptional: PKCS#11 URI to an existing private key\n")+
|
||||
" -pq-suite string\n"+
|
||||
" \tOptional: post-quantum suite of a version 3 CA, which the certificates it signs share (MLKEM1024_MLDSA87) (default \"MLKEM1024_MLDSA87\")\n"+
|
||||
" -subnets string\n"+
|
||||
" \tDeprecated, see -unsafe-networks\n"+
|
||||
" -unsafe-networks string\n"+
|
||||
" \tOptional: comma separated list of ip address and network in CIDR notation. This will limit which ip addresses and networks subordinate certs can use in unsafe networks\n"+
|
||||
" -version uint\n"+
|
||||
" \tOptional: version of the certificate format to use. Version 3 adds an ML-DSA-87 key, for post-quantum certificates (default 2)\n",
|
||||
" \tOptional: version of the certificate format to use. Version 3 adds a post-quantum key, for post-quantum certificates (default 2)\n",
|
||||
ob.String(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ type keygenFlags struct {
|
||||
inKeyPath *string
|
||||
curve *string
|
||||
version *uint
|
||||
pqSuite *string
|
||||
p11url *string
|
||||
}
|
||||
|
||||
@@ -29,8 +30,9 @@ func newKeygenFlags() *keygenFlags {
|
||||
cf.outPubPath = cf.set.String("out-pub", "", "Required: path to write the public key to")
|
||||
cf.outKeyPath = cf.set.String("out-key", "", "Required: path to write the private key to")
|
||||
cf.curve = cf.set.String("curve", defaultCurve(), "ECDH Curve (25519, P256)")
|
||||
cf.version = cf.set.Uint("version", 0, "Optional: 3 adds an ML-KEM-1024 key, for version 3 certificates")
|
||||
cf.inKeyPath = cf.set.String("in-key", "", "Optional: path to an existing private key to add an ML-KEM-1024 key to, keeping the existing key. Implies -version 3")
|
||||
cf.version = cf.set.Uint("version", 0, "Optional: 3 adds a post-quantum key, for version 3 certificates")
|
||||
cf.inKeyPath = cf.set.String("in-key", "", "Optional: path to an existing private key to add a post-quantum key to, keeping the existing key. Implies -version 3")
|
||||
cf.pqSuite = pqSuiteFlag(cf.set, "Optional: post-quantum suite of a version 3 key, which must be the suite of the CA that signs it")
|
||||
cf.p11url = p11Flag(cf.set)
|
||||
return &cf
|
||||
}
|
||||
@@ -63,7 +65,11 @@ func keygen(args []string, out io.Writer, errOut io.Writer) error {
|
||||
return newHelpErrorf("-version must be %v, or left out for keys that work with any other version", cert.Version3)
|
||||
}
|
||||
if version == cert.Version3 && isP11 {
|
||||
return newHelpErrorf("-version 3 can not be used with -pkcs11, the ML-KEM-1024 key has to be in out-key")
|
||||
return newHelpErrorf("-version 3 can not be used with -pkcs11, the post-quantum key has to be in out-key")
|
||||
}
|
||||
suite, err := parsePQSuiteFlag(cf.set, *cf.pqSuite, version == cert.Version3, "is only for -version 3")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var claims ioClaims
|
||||
@@ -113,7 +119,7 @@ func keygen(args []string, out io.Writer, errOut io.Writer) error {
|
||||
}
|
||||
|
||||
if version == cert.Version3 {
|
||||
pub, rawPriv, err = appendPQKey(pub, rawPriv)
|
||||
pub, rawPriv, err = appendPQKey(suite, pub, rawPriv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -139,12 +145,20 @@ func keygen(args []string, out io.Writer, errOut io.Writer) error {
|
||||
return fmt.Errorf("error while getting public key: %w", err)
|
||||
}
|
||||
} else {
|
||||
err = writeOutput(*cf.outKeyPath, cert.MarshalPrivateKeyToPEM(curve, rawPriv), 0600, out)
|
||||
b := cert.MarshalPrivateKeyToPEM(curve, rawPriv)
|
||||
if version == cert.Version3 {
|
||||
b = cert.MarshalPQPrivateKeyToPEM(curve, suite, rawPriv)
|
||||
}
|
||||
err = writeOutput(*cf.outKeyPath, b, 0600, out)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while writing out-key: %s", err)
|
||||
}
|
||||
}
|
||||
err = writeOutput(*cf.outPubPath, cert.MarshalPublicKeyToPEM(curve, pub), 0600, out)
|
||||
b := cert.MarshalPublicKeyToPEM(curve, pub)
|
||||
if version == cert.Version3 {
|
||||
b = cert.MarshalPQPublicKeyToPEM(curve, suite, pub)
|
||||
}
|
||||
err = writeOutput(*cf.outPubPath, b, 0600, out)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while writing out-pub: %s", err)
|
||||
}
|
||||
|
||||
@@ -24,14 +24,16 @@ func Test_keygenHelp(t *testing.T) {
|
||||
" -curve string\n"+
|
||||
" \tECDH Curve (25519, P256) (default \"25519\")\n"+
|
||||
" -in-key string\n"+
|
||||
" \tOptional: path to an existing private key to add an ML-KEM-1024 key to, keeping the existing key. Implies -version 3\n"+
|
||||
" \tOptional: path to an existing private key to add a post-quantum key to, keeping the existing key. Implies -version 3\n"+
|
||||
" -out-key string\n"+
|
||||
" \tRequired: path to write the private key to\n"+
|
||||
" -out-pub string\n"+
|
||||
" \tRequired: path to write the public key to\n"+
|
||||
optionalPkcs11String(" -pkcs11 string\n \tOptional: PKCS#11 URI to an existing private key\n")+
|
||||
" -pq-suite string\n"+
|
||||
" \tOptional: post-quantum suite of a version 3 key, which must be the suite of the CA that signs it (MLKEM1024_MLDSA87) (default \"MLKEM1024_MLDSA87\")\n"+
|
||||
" -version uint\n"+
|
||||
" \tOptional: 3 adds an ML-KEM-1024 key, for version 3 certificates\n",
|
||||
" \tOptional: 3 adds a post-quantum key, for version 3 certificates\n",
|
||||
ob.String(),
|
||||
)
|
||||
}
|
||||
|
||||
+68
-29
@@ -3,33 +3,60 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdh"
|
||||
"crypto/mldsa"
|
||||
"crypto/mlkem"
|
||||
"encoding/pem"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/slackhq/nebula/cert"
|
||||
)
|
||||
|
||||
// appendPQSigningKey makes a classical CA keypair into a version 3 composite one with a fresh ML-DSA-87 key.
|
||||
func appendPQSigningKey(pub, priv []byte) ([]byte, []byte, error) {
|
||||
sk, err := mldsa.GenerateKey(mldsa.MLDSA87())
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error while generating ML-DSA-87 key: %s", err)
|
||||
// pqSuiteFlag adds -pq-suite to a flag set.
|
||||
func pqSuiteFlag(set *flag.FlagSet, usage string) *string {
|
||||
var names []string
|
||||
for _, s := range cert.PQSuites() {
|
||||
names = append(names, s.String())
|
||||
}
|
||||
return append(pub, sk.PublicKey().Bytes()...), append(priv, sk.Bytes()...), nil
|
||||
return set.String("pq-suite", cert.DefaultPQSuite.String(), usage+" ("+strings.Join(names, ", ")+")")
|
||||
}
|
||||
|
||||
// appendPQKey makes a classical host keypair into a version 3 composite one with a fresh ML-KEM-1024 key.
|
||||
func appendPQKey(pub, priv []byte) ([]byte, []byte, error) {
|
||||
dk, err := mlkem.GenerateKey1024()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error while generating ML-KEM-1024 key: %s", err)
|
||||
// parsePQSuiteFlag returns the suite -pq-suite names. Setting it is an error unless allowed, when the suite has no use
|
||||
// or comes from elsewhere.
|
||||
func parsePQSuiteFlag(set *flag.FlagSet, value string, allowed bool, why string) (cert.PQSuite, error) {
|
||||
given := false
|
||||
set.Visit(func(f *flag.Flag) {
|
||||
if f.Name == "pq-suite" {
|
||||
given = true
|
||||
}
|
||||
})
|
||||
if given && !allowed {
|
||||
return cert.PQSuiteUnknown, newHelpErrorf("-pq-suite %s", why)
|
||||
}
|
||||
return append(pub, dk.EncapsulationKey().Bytes()...), append(priv, dk.Bytes()...), nil
|
||||
s, err := cert.ParsePQSuite(value)
|
||||
if err != nil {
|
||||
return cert.PQSuiteUnknown, newHelpErrorf("-pq-suite: %s", err)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// appendPQSigningKey makes a classical CA keypair into a version 3 composite one with a fresh key of suite.
|
||||
func appendPQSigningKey(suite cert.PQSuite, pub, priv []byte) ([]byte, []byte, error) {
|
||||
pqPub, pqPriv, err := suite.GenerateKey(true)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error while generating %v CA key: %s", suite, err)
|
||||
}
|
||||
return append(pub, pqPub...), append(priv, pqPriv...), nil
|
||||
}
|
||||
|
||||
// appendPQKey makes a classical host keypair into a version 3 composite one with a fresh key of suite.
|
||||
func appendPQKey(suite cert.PQSuite, pub, priv []byte) ([]byte, []byte, error) {
|
||||
pqPub, pqPriv, err := suite.GenerateKey(false)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error while generating %v key: %s", suite, err)
|
||||
}
|
||||
return append(pub, pqPub...), append(priv, pqPriv...), nil
|
||||
}
|
||||
|
||||
// publicKeyFor returns the public key of a classical host private key.
|
||||
@@ -45,7 +72,7 @@ func publicKeyFor(curve cert.Curve, priv []byte) ([]byte, error) {
|
||||
}
|
||||
k, err := c.NewPrivateKey(priv)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("not a classical %s private key, it may already have an ML-KEM-1024 key", curve)
|
||||
return nil, fmt.Errorf("not a classical %s private key, it may already have a post-quantum key", curve)
|
||||
}
|
||||
return k.PublicKey().Bytes(), nil
|
||||
}
|
||||
@@ -58,6 +85,7 @@ type pqExtendFlags struct {
|
||||
inPubPath *string
|
||||
outCertPath *string
|
||||
outKeyPath *string
|
||||
pqSuite *string
|
||||
}
|
||||
|
||||
func newPQExtendFlags() *pqExtendFlags {
|
||||
@@ -69,6 +97,7 @@ func newPQExtendFlags() *pqExtendFlags {
|
||||
pf.caCertPath = pf.set.String("ca-crt", "", "Required for a host in-crt: path to the version 3 form of the CA that issued it")
|
||||
pf.inPubPath = pf.set.String("in-pub", "", "Required for a host in-crt: path to the host's public key from keygen -in-key")
|
||||
pf.outKeyPath = pf.set.String("out-key", "", "Required for a CA in-crt: path to write the CA's version 3 key to, encrypted again if ca-key was")
|
||||
pf.pqSuite = pqSuiteFlag(pf.set, "Optional, for a CA in-crt: post-quantum suite of the version 3 CA. A host takes its CA's suite")
|
||||
return &pf
|
||||
}
|
||||
|
||||
@@ -134,9 +163,14 @@ func pqExtend(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
return err
|
||||
}
|
||||
|
||||
suite, err := parsePQSuiteFlag(pf.set, *pf.pqSuite, c.IsCA(), "is for CA certificates, a host takes its CA's suite")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var c3 cert.Certificate
|
||||
if c.IsCA() {
|
||||
c3, err = extendCA(pf, c, curve, caKey, rawCAKey, passphrase, out)
|
||||
c3, err = extendCA(pf, c, suite, curve, caKey, rawCAKey, passphrase, out)
|
||||
} else {
|
||||
c3, err = extendHost(pf, c, curve, caKey, &claims)
|
||||
}
|
||||
@@ -154,8 +188,8 @@ func pqExtend(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
return nil
|
||||
}
|
||||
|
||||
// extendCA gives a v2 CA an ML-DSA-87 key and writes out the composite key.
|
||||
func extendCA(pf *pqExtendFlags, ca cert.Certificate, curve cert.Curve, key, rawKey, passphrase []byte, out io.Writer) (cert.Certificate, error) {
|
||||
// extendCA gives a v2 CA a post-quantum key of suite and writes out the composite key.
|
||||
func extendCA(pf *pqExtendFlags, ca cert.Certificate, suite cert.PQSuite, curve cert.Curve, key, rawKey, passphrase []byte, out io.Writer) (cert.Certificate, error) {
|
||||
if *pf.caCertPath != "" || *pf.inPubPath != "" {
|
||||
return nil, newHelpErrorf("-ca-crt and -in-pub are for host certificates, a CA extends itself")
|
||||
}
|
||||
@@ -166,15 +200,15 @@ func extendCA(pf *pqExtendFlags, ca cert.Certificate, curve cert.Curve, key, raw
|
||||
return nil, fmt.Errorf("refusing to extend, the CA certificate does not match ca-key")
|
||||
}
|
||||
|
||||
pub, priv, err := appendPQSigningKey(nil, append([]byte(nil), key...))
|
||||
pub, priv, err := appendPQSigningKey(suite, nil, append([]byte(nil), key...))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signPQ, err := cert.SignPQ(priv[len(key):])
|
||||
signPQ, err := cert.SignPQ(suite, priv[len(key):])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c3, err := cert.ExtendToV3(ca, pub, nil, signPQ)
|
||||
c3, err := cert.ExtendToV3(ca, suite, pub, nil, signPQ)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while extending: %s", err)
|
||||
}
|
||||
@@ -192,12 +226,12 @@ func extendCA(pf *pqExtendFlags, ca cert.Certificate, curve cert.Curve, key, raw
|
||||
}
|
||||
a := ned.EncryptionMetadata.Argon2Parameters
|
||||
kdf := cert.NewArgon2Parameters(a.Memory, a.Parallelism, a.Iterations)
|
||||
b, err = cert.EncryptAndMarshalSigningPrivateKey(curve, priv, passphrase, kdf)
|
||||
b, err = cert.EncryptAndMarshalPQSigningPrivateKey(curve, suite, priv, passphrase, kdf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while encrypting out-key: %s", err)
|
||||
}
|
||||
} else {
|
||||
b = cert.MarshalSigningPrivateKeyToPEM(curve, priv)
|
||||
b = cert.MarshalPQSigningPrivateKeyToPEM(curve, suite, priv)
|
||||
}
|
||||
if err := writeOutput(*pf.outKeyPath, b, 0600, out); err != nil {
|
||||
return nil, fmt.Errorf("error while writing out-key: %s", err)
|
||||
@@ -205,7 +239,7 @@ func extendCA(pf *pqExtendFlags, ca cert.Certificate, curve cert.Curve, key, raw
|
||||
return c3, nil
|
||||
}
|
||||
|
||||
// extendHost adds a host's ML-KEM-1024 key to its certificate, signed by the v3 form of its CA.
|
||||
// extendHost adds a host's post-quantum key to its certificate, signed by the v3 form of its CA, in the CA's suite.
|
||||
func extendHost(pf *pqExtendFlags, c cert.Certificate, curve cert.Curve, caKey []byte, claims *ioClaims) (cert.Certificate, error) {
|
||||
if *pf.outKeyPath != "" {
|
||||
return nil, newHelpErrorf("-out-key is for CA certificates, a host's key comes from keygen -in-key")
|
||||
@@ -240,19 +274,24 @@ func extendHost(pf *pqExtendFlags, c cert.Certificate, curve cert.Curve, caKey [
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while parsing in-pub: %s", err)
|
||||
}
|
||||
if pubCurve != c.Curve() || len(pub) <= cert.MLKEM1024PublicKeySize {
|
||||
return nil, fmt.Errorf("in-pub must be the host's key from keygen -in-key")
|
||||
suite := cert.PQSuiteOf(ca)
|
||||
classical, pq, err := suite.SplitPublicKey(false, pub)
|
||||
if pubCurve != c.Curve() || err != nil {
|
||||
return nil, fmt.Errorf("in-pub must be the host's key from keygen -in-key -pq-suite %v", suite)
|
||||
}
|
||||
classical, pq := pub[:len(pub)-cert.MLKEM1024PublicKeySize], pub[len(pub)-cert.MLKEM1024PublicKeySize:]
|
||||
if !bytes.Equal(classical, c.PublicKey()) {
|
||||
return nil, fmt.Errorf("in-pub does not extend the key in in-crt")
|
||||
}
|
||||
|
||||
signPQ, err := cert.SignPQ(caKey[len(caKey)-cert.MLDSA87PrivateKeySize:])
|
||||
_, pqCAKey, err := suite.SplitPrivateKey(true, caKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c3, err := cert.ExtendToV3(c, pq, ca, signPQ)
|
||||
signPQ, err := cert.SignPQ(suite, pqCAKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c3, err := cert.ExtendToV3(c, suite, pq, ca, signPQ)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while extending: %s", err)
|
||||
}
|
||||
|
||||
@@ -100,9 +100,40 @@ func TestPQ_SignErrors(t *testing.T) {
|
||||
require.NoError(t, ca([]string{"-version", "3", "-name", "v3 ca", "-out-crt", p("ca3.crt"), "-out-key", p("ca3.key")}, ob, eb, nopw))
|
||||
require.ErrorContains(t, signCert([]string{"-ca-crt", p("ca3.crt"), "-ca-key", p("ca3.key"), "-name", "h", "-networks", "10.1.0.1/16", "-in-pub", p("k.pub"), "-out-crt", p("h3.crt")}, ob, eb, nopw), "need an in-pub from keygen -version 3")
|
||||
|
||||
// keygen -in-key refuses a key that already has an ML-KEM key.
|
||||
// keygen -in-key refuses a key that already has a post-quantum key.
|
||||
require.NoError(t, keygen([]string{"-version", "3", "-out-key", p("k3.key"), "-out-pub", p("k3.pub")}, ob, eb))
|
||||
require.ErrorContains(t, keygen([]string{"-in-key", p("k3.key"), "-out-key", p("k33.key"), "-out-pub", p("k33.pub")}, ob, eb), "already have an ML-KEM-1024 key")
|
||||
require.ErrorContains(t, keygen([]string{"-in-key", p("k3.key"), "-out-key", p("k33.key"), "-out-pub", p("k33.pub")}, ob, eb), "already have a post-quantum key")
|
||||
}
|
||||
|
||||
func TestPQ_SuiteFlag(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
ob, eb := &bytes.Buffer{}, &bytes.Buffer{}
|
||||
nopw := &StubPasswordReader{}
|
||||
p := func(n string) string { return filepath.Join(dir, n) }
|
||||
|
||||
// The suite can be named, case aside, and the certificates a CA signs share its suite.
|
||||
require.NoError(t, ca([]string{"-version", "3", "-pq-suite", "mlkem1024_mldsa87", "-name", "ca", "-out-crt", p("ca.crt"), "-out-key", p("ca.key")}, ob, eb, nopw))
|
||||
assert.Equal(t, cert.PQSuite_MLKEM1024_MLDSA87, cert.PQSuiteOf(readCert(t, p("ca.crt"))))
|
||||
require.NoError(t, keygen([]string{"-version", "3", "-pq-suite", "MLKEM1024_MLDSA87", "-out-key", p("k.key"), "-out-pub", p("k.pub")}, ob, eb))
|
||||
require.NoError(t, signCert([]string{"-ca-crt", p("ca.crt"), "-ca-key", p("ca.key"), "-name", "h", "-networks", "10.1.0.1/16", "-in-pub", p("k.pub"), "-out-crt", p("h.crt")}, ob, eb, nopw))
|
||||
assert.Equal(t, cert.PQSuite_MLKEM1024_MLDSA87, cert.PQSuiteOf(readCert(t, p("h.crt"))))
|
||||
|
||||
// Printing shows it.
|
||||
ob.Reset()
|
||||
require.NoError(t, printCert([]string{"-json", "-path", p("h.crt")}, ob, eb))
|
||||
assert.Contains(t, ob.String(), `"pqSuite":"MLKEM1024_MLDSA87"`)
|
||||
|
||||
// Unknown suites, and suites where there is no post-quantum key, are refused.
|
||||
require.ErrorContains(t, ca([]string{"-version", "3", "-pq-suite", "mlkem768_mldsa65", "-name", "x", "-out-crt", p("x.crt"), "-out-key", p("x.key")}, ob, eb, nopw), "unknown post-quantum suite")
|
||||
require.ErrorContains(t, ca([]string{"-pq-suite", "MLKEM1024_MLDSA87", "-name", "x", "-out-crt", p("x.crt"), "-out-key", p("x.key")}, ob, eb, nopw), "only for -version 3")
|
||||
require.ErrorContains(t, keygen([]string{"-pq-suite", "MLKEM1024_MLDSA87", "-out-key", p("x.key"), "-out-pub", p("x.pub")}, ob, eb), "only for -version 3")
|
||||
|
||||
// A host takes its CA's suite when extended.
|
||||
require.NoError(t, ca([]string{"-name", "ca2", "-out-crt", p("ca2.crt"), "-out-key", p("ca2.key")}, ob, eb, nopw))
|
||||
require.NoError(t, signCert([]string{"-ca-crt", p("ca2.crt"), "-ca-key", p("ca2.key"), "-name", "h2", "-networks", "10.1.0.2/16", "-out-crt", p("h2.crt"), "-out-key", p("h2.key")}, ob, eb, nopw))
|
||||
require.NoError(t, pqExtend([]string{"-in-crt", p("ca2.crt"), "-ca-key", p("ca2.key"), "-pq-suite", "MLKEM1024_MLDSA87", "-out-crt", p("ca23.crt"), "-out-key", p("ca23.key")}, ob, eb, nopw))
|
||||
require.NoError(t, keygen([]string{"-in-key", p("h2.key"), "-out-key", p("h23.key"), "-out-pub", p("h23.pub")}, ob, eb))
|
||||
require.ErrorContains(t, pqExtend([]string{"-in-crt", p("h2.crt"), "-in-pub", p("h23.pub"), "-ca-crt", p("ca23.crt"), "-ca-key", p("ca23.key"), "-pq-suite", "MLKEM1024_MLDSA87", "-out-crt", p("h23.crt")}, ob, eb, nopw), "a host takes its CA's suite")
|
||||
}
|
||||
|
||||
// TestPQ_Extend upgrades a v2 CA and a host in place, the way a running network migrates.
|
||||
@@ -145,7 +176,7 @@ func TestPQ_Extend(t *testing.T) {
|
||||
require.ErrorIs(t, err, cert.ErrPrivateKeyEncrypted, "an encrypted ca-key stays encrypted")
|
||||
}
|
||||
|
||||
// The host gets an ML-KEM key on top of the one it has, and the CA signs it.
|
||||
// The host gets a post-quantum key on top of the one it has, and the CA signs it.
|
||||
require.NoError(t, keygen([]string{"-in-key", p("host.key"), "-out-key", p("host3.key"), "-out-pub", p("host3.pub")}, ob, eb))
|
||||
require.NoError(t, pqExtend([]string{"-in-crt", p("host.crt"), "-in-pub", p("host3.pub"), "-ca-crt", p("ca3.crt"), "-ca-key", p("ca3.key"), "-out-crt", p("host3.crt")}, ob, eb, pw))
|
||||
host3 := readCert(t, p("host3.crt"))
|
||||
|
||||
+19
-6
@@ -169,16 +169,20 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
version = caCert.Version()
|
||||
}
|
||||
|
||||
// A version 3 certificate is in the suite of its CA.
|
||||
suite := cert.PQSuiteOf(caCert)
|
||||
switch {
|
||||
case version == cert.Version3 && caCert.Version() != cert.Version3:
|
||||
return newHelpErrorf("version 3 certificates must be signed by a version 3 CA")
|
||||
case version == cert.Version3 && isP11:
|
||||
return newHelpErrorf("version 3 certificates can not be signed with -pkcs11, the ML-DSA-87 key has to be in ca-key")
|
||||
return newHelpErrorf("version 3 certificates can not be signed with -pkcs11, the post-quantum key has to be in ca-key")
|
||||
case version != cert.Version3 && caCert.Version() == cert.Version3:
|
||||
// A v3 CA signs anything older as its v2 form, with the classical half of its key.
|
||||
caCert = cert.V2Form(caCert)
|
||||
if caKey != nil {
|
||||
caKey = caKey[:len(caKey)-cert.MLDSA87PrivateKeySize]
|
||||
if caKey, _, err = suite.SplitPrivateKey(true, caKey); err != nil {
|
||||
return fmt.Errorf("error while parsing ca-key: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,12 +276,16 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
if pubCurve != curve {
|
||||
return fmt.Errorf("curve of in-pub does not match ca")
|
||||
}
|
||||
if isPQ := len(pub) > cert.MLKEM1024PublicKeySize; isPQ != (version == cert.Version3) {
|
||||
// Composite keys are longer than any classical one, an uncompressed P256 key being the longest.
|
||||
if isPQ := len(pub) > 65; isPQ != (version == cert.Version3) {
|
||||
if isPQ {
|
||||
return fmt.Errorf("in-pub has an ML-KEM-1024 key, which only version 3 certificates carry")
|
||||
return fmt.Errorf("in-pub has a post-quantum key, which only version 3 certificates carry")
|
||||
}
|
||||
return fmt.Errorf("version 3 certificates need an in-pub from keygen -version 3")
|
||||
}
|
||||
if _, _, err := suite.SplitPublicKey(false, pub); version == cert.Version3 && err != nil {
|
||||
return fmt.Errorf("in-pub is not a key for the CA's post-quantum suite, %v", suite)
|
||||
}
|
||||
} else if isP11 {
|
||||
pub, err = p11Client.GetPubKey()
|
||||
if err != nil {
|
||||
@@ -286,7 +294,7 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
} else {
|
||||
pub, rawPriv = newKeypair(curve)
|
||||
if version == cert.Version3 {
|
||||
pub, rawPriv, err = appendPQKey(pub, rawPriv)
|
||||
pub, rawPriv, err = appendPQKey(suite, pub, rawPriv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -388,6 +396,7 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
PublicKey: pub,
|
||||
IsCA: false,
|
||||
Curve: curve,
|
||||
PQSuite: suite,
|
||||
}
|
||||
|
||||
nc, err := t.Sign(caCert, curve, caKey)
|
||||
@@ -408,7 +417,11 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
}
|
||||
}
|
||||
|
||||
err = writeOutput(*sf.outKeyPath, cert.MarshalPrivateKeyToPEM(curve, rawPriv), 0600, out)
|
||||
b := cert.MarshalPrivateKeyToPEM(curve, rawPriv)
|
||||
if version == cert.Version3 {
|
||||
b = cert.MarshalPQPrivateKeyToPEM(curve, suite, rawPriv)
|
||||
}
|
||||
err = writeOutput(*sf.outKeyPath, b, 0600, out)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while writing out-key: %s", err)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user