mirror of
https://github.com/slackhq/nebula.git
synced 2026-11-15 14:26:57 +01:00
nebula-cert: Make version 3 CAs, keys and certificates, and pq-extend v2 certificates in place
This commit is contained in:
1 parent
b26c1e9c3e
commit
4d37a3140f
10 files changed
+610
-42
No files matched your search
+13
-3
@@ -71,7 +71,7 @@ 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")
|
||||
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.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")
|
||||
@@ -160,8 +160,11 @@ func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error
|
||||
}
|
||||
|
||||
version := cert.Version(*cf.version)
|
||||
if version != cert.Version1 && version != cert.Version2 {
|
||||
return newHelpErrorf("-version must be either %v or %v", cert.Version1, cert.Version2)
|
||||
if version != cert.Version1 && version != cert.Version2 && version != cert.Version3 {
|
||||
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")
|
||||
}
|
||||
|
||||
var networks []netip.Prefix
|
||||
@@ -295,6 +298,13 @@ func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error
|
||||
default:
|
||||
return fmt.Errorf("invalid curve: %s", *cf.curve)
|
||||
}
|
||||
|
||||
if version == cert.Version3 {
|
||||
pub, rawPriv, err = appendPQSigningKey(pub, rawPriv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t := &cert.TBSCertificate{
|
||||
|
||||
@@ -73,7 +73,7 @@ func Test_caHelp(t *testing.T) {
|
||||
" -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 (default 2)\n",
|
||||
" \tOptional: version of the certificate format to use. Version 3 adds an ML-DSA-87 key, for post-quantum certificates (default 2)\n",
|
||||
ob.String(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ type keygenFlags struct {
|
||||
set *flag.FlagSet
|
||||
outKeyPath *string
|
||||
outPubPath *string
|
||||
inKeyPath *string
|
||||
curve *string
|
||||
version *uint
|
||||
p11url *string
|
||||
}
|
||||
|
||||
@@ -27,6 +29,8 @@ 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.p11url = p11Flag(cf.set)
|
||||
return &cf
|
||||
}
|
||||
@@ -51,9 +55,41 @@ func keygen(args []string, out io.Writer, errOut io.Writer) error {
|
||||
return err
|
||||
}
|
||||
|
||||
version := cert.Version(*cf.version)
|
||||
if *cf.inKeyPath != "" {
|
||||
version = cert.Version3
|
||||
}
|
||||
if version != 0 && version != cert.Version3 {
|
||||
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")
|
||||
}
|
||||
|
||||
var claims ioClaims
|
||||
if err := reserveInputs(&claims, "in-key", *cf.inKeyPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var pub, rawPriv []byte
|
||||
var curve cert.Curve
|
||||
if isP11 {
|
||||
if *cf.inKeyPath != "" {
|
||||
raw, err := readInput("in-key", *cf.inKeyPath, &claims)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while reading in-key: %s", err)
|
||||
}
|
||||
rawPriv, _, curve, err = cert.UnmarshalPrivateKeyFromPEM(raw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while parsing in-key: %s", err)
|
||||
}
|
||||
pub, err = publicKeyFor(curve, rawPriv)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while parsing in-key: %s", err)
|
||||
}
|
||||
if fips140.Enforced() && curve == cert.Curve_CURVE25519 {
|
||||
return errors.New("use of Curve25519 is not allowed in FIPS 140-only mode")
|
||||
}
|
||||
} else if isP11 {
|
||||
switch *cf.curve {
|
||||
case "P256":
|
||||
curve = cert.Curve_P256
|
||||
@@ -76,7 +112,13 @@ func keygen(args []string, out io.Writer, errOut io.Writer) error {
|
||||
}
|
||||
}
|
||||
|
||||
var claims ioClaims
|
||||
if version == cert.Version3 {
|
||||
pub, rawPriv, err = appendPQKey(pub, rawPriv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := reserveOutputs(&claims,
|
||||
"out-key", *cf.outKeyPath,
|
||||
"out-pub", *cf.outPubPath,
|
||||
|
||||
@@ -23,11 +23,15 @@ func Test_keygenHelp(t *testing.T) {
|
||||
" Pass \"-\" to any path flag to read from stdin or write to stdout.\n"+
|
||||
" -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"+
|
||||
" -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"),
|
||||
optionalPkcs11String(" -pkcs11 string\n \tOptional: PKCS#11 URI to an existing private key\n")+
|
||||
" -version uint\n"+
|
||||
" \tOptional: 3 adds an ML-KEM-1024 key, for version 3 certificates\n",
|
||||
ob.String(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -89,6 +89,8 @@ func main() {
|
||||
err = printCert(args[1:], os.Stdout, os.Stderr)
|
||||
case "verify":
|
||||
err = verify(args[1:], os.Stdout, os.Stderr)
|
||||
case "pq-extend":
|
||||
err = pqExtend(args[1:], os.Stdout, os.Stderr, StdinPasswordReader{})
|
||||
default:
|
||||
err = fmt.Errorf("unknown mode: %s", args[0])
|
||||
}
|
||||
@@ -122,6 +124,8 @@ func handleError(mode string, e error, out io.Writer) int {
|
||||
printHelp(out)
|
||||
case "verify":
|
||||
verifyHelp(out)
|
||||
case "pq-extend":
|
||||
pqExtendHelp(out)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,6 +149,7 @@ func help(err string, out io.Writer) {
|
||||
fmt.Fprintln(out, " "+signSummary())
|
||||
fmt.Fprintln(out, " "+printSummary())
|
||||
fmt.Fprintln(out, " "+verifySummary())
|
||||
fmt.Fprintln(out, " "+pqExtendSummary())
|
||||
fmt.Fprintln(out, "")
|
||||
fmt.Fprintf(out, " To see usage for a given mode, use %s <mode> -h\n", os.Args[0])
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ func Test_help(t *testing.T) {
|
||||
" " + signSummary() + "\n" +
|
||||
" " + printSummary() + "\n" +
|
||||
" " + verifySummary() + "\n" +
|
||||
" " + pqExtendSummary() + "\n" +
|
||||
"\n" +
|
||||
" To see usage for a given mode, use " + os.Args[0] + " <mode> -h\n"
|
||||
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdh"
|
||||
"crypto/mldsa"
|
||||
"crypto/mlkem"
|
||||
"encoding/pem"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"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)
|
||||
}
|
||||
return append(pub, sk.PublicKey().Bytes()...), append(priv, sk.Bytes()...), nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
return append(pub, dk.EncapsulationKey().Bytes()...), append(priv, dk.Bytes()...), nil
|
||||
}
|
||||
|
||||
// publicKeyFor returns the public key of a classical host private key.
|
||||
func publicKeyFor(curve cert.Curve, priv []byte) ([]byte, error) {
|
||||
var c ecdh.Curve
|
||||
switch curve {
|
||||
case cert.Curve_CURVE25519:
|
||||
c = ecdh.X25519()
|
||||
case cert.Curve_P256:
|
||||
c = ecdh.P256()
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid curve: %s", curve)
|
||||
}
|
||||
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 k.PublicKey().Bytes(), nil
|
||||
}
|
||||
|
||||
type pqExtendFlags struct {
|
||||
set *flag.FlagSet
|
||||
caKeyPath *string
|
||||
caCertPath *string
|
||||
inCertPath *string
|
||||
inPubPath *string
|
||||
outCertPath *string
|
||||
outKeyPath *string
|
||||
}
|
||||
|
||||
func newPQExtendFlags() *pqExtendFlags {
|
||||
pf := pqExtendFlags{set: flag.NewFlagSet("pq-extend", flag.ContinueOnError)}
|
||||
pf.set.Usage = func() {}
|
||||
pf.inCertPath = pf.set.String("in-crt", "", "Required: path to the version 2 certificate to extend, a CA or a host")
|
||||
pf.outCertPath = pf.set.String("out-crt", "", "Required: path to write the version 3 certificate to")
|
||||
pf.caKeyPath = pf.set.String("ca-key", "ca.key", "Optional: path to the CA key. For a CA in-crt, its own version 2 key. For a host in-crt, the version 3 key of the CA that issued it")
|
||||
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")
|
||||
return &pf
|
||||
}
|
||||
|
||||
// pqExtend turns a version 2 certificate into a version 3 one in place: the v2 form, and so its fingerprint and
|
||||
// existing trust in it, stays exactly as it was.
|
||||
func pqExtend(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error {
|
||||
pf := newPQExtendFlags()
|
||||
if err := pf.set.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := mustFlagString("in-crt", pf.inCertPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := mustFlagString("out-crt", pf.outCertPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := mustFlagString("ca-key", pf.caKeyPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var claims ioClaims
|
||||
if err := reserveInputs(&claims,
|
||||
"in-crt", *pf.inCertPath,
|
||||
"ca-key", *pf.caKeyPath,
|
||||
"ca-crt", *pf.caCertPath,
|
||||
"in-pub", *pf.inPubPath,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := reserveOutputs(&claims,
|
||||
"out-crt", *pf.outCertPath,
|
||||
"out-key", *pf.outKeyPath,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rawCert, err := readInput("in-crt", *pf.inCertPath, &claims)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while reading in-crt: %s", err)
|
||||
}
|
||||
c, _, err := cert.UnmarshalCertificateFromPEM(rawCert)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while parsing in-crt: %s", err)
|
||||
}
|
||||
if c.Version() != cert.Version2 {
|
||||
return fmt.Errorf("only version 2 certificates extend to version 3, in-crt is version %d", c.Version())
|
||||
}
|
||||
|
||||
for _, p := range []string{*pf.outCertPath, *pf.outKeyPath} {
|
||||
if p != "" && !isStdio(p) {
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
return fmt.Errorf("refusing to overwrite existing file: %s", p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rawCAKey, err := readInput("ca-key", *pf.caKeyPath, &claims)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while reading ca-key: %s", err)
|
||||
}
|
||||
curve, caKey, passphrase, err := readSigningKey(rawCAKey, errOut, pr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var c3 cert.Certificate
|
||||
if c.IsCA() {
|
||||
c3, err = extendCA(pf, c, curve, caKey, rawCAKey, passphrase, out)
|
||||
} else {
|
||||
c3, err = extendHost(pf, c, curve, caKey, &claims)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b, err := c3.MarshalPEM()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while marshalling certificate: %s", err)
|
||||
}
|
||||
if err := writeOutput(*pf.outCertPath, b, 0600, out); err != nil {
|
||||
return fmt.Errorf("error while writing out-crt: %s", err)
|
||||
}
|
||||
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) {
|
||||
if *pf.caCertPath != "" || *pf.inPubPath != "" {
|
||||
return nil, newHelpErrorf("-ca-crt and -in-pub are for host certificates, a CA extends itself")
|
||||
}
|
||||
if err := mustFlagString("out-key", pf.outKeyPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ca.VerifyPrivateKey(curve, key); err != nil {
|
||||
return nil, fmt.Errorf("refusing to extend, the CA certificate does not match ca-key")
|
||||
}
|
||||
|
||||
pub, priv, err := appendPQSigningKey(nil, append([]byte(nil), key...))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signPQ, err := cert.SignPQ(priv[len(key):])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c3, err := cert.ExtendToV3(ca, pub, nil, signPQ)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while extending: %s", err)
|
||||
}
|
||||
|
||||
var b []byte
|
||||
if passphrase != nil {
|
||||
// Encrypt it the way ca-key was, with a fresh salt.
|
||||
block, _ := pem.Decode(rawKey)
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("error while parsing ca-key: input did not contain a valid PEM encoded block")
|
||||
}
|
||||
ned, err := cert.UnmarshalNebulaEncryptedData(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while parsing encrypted ca-key: %s", err)
|
||||
}
|
||||
a := ned.EncryptionMetadata.Argon2Parameters
|
||||
kdf := cert.NewArgon2Parameters(a.Memory, a.Parallelism, a.Iterations)
|
||||
b, err = cert.EncryptAndMarshalSigningPrivateKey(curve, priv, passphrase, kdf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while encrypting out-key: %s", err)
|
||||
}
|
||||
} else {
|
||||
b = cert.MarshalSigningPrivateKeyToPEM(curve, priv)
|
||||
}
|
||||
if err := writeOutput(*pf.outKeyPath, b, 0600, out); err != nil {
|
||||
return nil, fmt.Errorf("error while writing out-key: %s", err)
|
||||
}
|
||||
return c3, nil
|
||||
}
|
||||
|
||||
// extendHost adds a host's ML-KEM-1024 key to its certificate, signed by the v3 form of its CA.
|
||||
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")
|
||||
}
|
||||
if err := mustFlagString("ca-crt", pf.caCertPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := mustFlagString("in-pub", pf.inPubPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawCA, err := readInput("ca-crt", *pf.caCertPath, claims)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while reading ca-crt: %s", err)
|
||||
}
|
||||
ca, _, err := cert.UnmarshalCertificateFromPEM(rawCA)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while parsing ca-crt: %s", err)
|
||||
}
|
||||
if ca.Version() != cert.Version3 {
|
||||
return nil, fmt.Errorf("ca-crt must be a version 3 CA, extend the CA first")
|
||||
}
|
||||
if err := ca.VerifyPrivateKey(curve, caKey); err != nil {
|
||||
return nil, fmt.Errorf("refusing to extend, ca-crt does not match ca-key")
|
||||
}
|
||||
|
||||
rawPub, err := readInput("in-pub", *pf.inPubPath, claims)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while reading in-pub: %s", err)
|
||||
}
|
||||
pub, _, pubCurve, err := cert.UnmarshalPublicKeyFromPEM(rawPub)
|
||||
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")
|
||||
}
|
||||
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:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c3, err := cert.ExtendToV3(c, pq, ca, signPQ)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while extending: %s", err)
|
||||
}
|
||||
return c3, nil
|
||||
}
|
||||
|
||||
func pqExtendSummary() string {
|
||||
return "pq-extend <flags>: extend a version 2 CA or host certificate to version 3 in place, keeping its v2 form and fingerprint"
|
||||
}
|
||||
|
||||
func pqExtendHelp(out io.Writer) {
|
||||
pf := newPQExtendFlags()
|
||||
out.Write([]byte("Usage of " + os.Args[0] + " " + pqExtendSummary() + "\n"))
|
||||
out.Write([]byte(stdioHelpText))
|
||||
pf.set.SetOutput(out)
|
||||
pf.set.PrintDefaults()
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/slackhq/nebula/cert"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func readCert(t *testing.T, path string) cert.Certificate {
|
||||
t.Helper()
|
||||
b, err := os.ReadFile(path)
|
||||
require.NoError(t, err)
|
||||
c, _, err := cert.UnmarshalCertificateFromPEM(b)
|
||||
require.NoError(t, err)
|
||||
return c
|
||||
}
|
||||
|
||||
func readSigningKeyFile(t *testing.T, path string) (cert.Curve, []byte) {
|
||||
t.Helper()
|
||||
b, err := os.ReadFile(path)
|
||||
require.NoError(t, err)
|
||||
k, _, curve, err := cert.UnmarshalSigningPrivateKeyFromPEM(b)
|
||||
require.NoError(t, err)
|
||||
return curve, k
|
||||
}
|
||||
|
||||
func readKeyFile(t *testing.T, path string) (cert.Curve, []byte) {
|
||||
t.Helper()
|
||||
b, err := os.ReadFile(path)
|
||||
require.NoError(t, err)
|
||||
k, _, curve, err := cert.UnmarshalPrivateKeyFromPEM(b)
|
||||
require.NoError(t, err)
|
||||
return curve, k
|
||||
}
|
||||
|
||||
func verifiedBy(t *testing.T, c cert.Certificate, cas ...cert.Certificate) error {
|
||||
t.Helper()
|
||||
pool := cert.NewCAPool()
|
||||
for _, ca := range cas {
|
||||
require.NoError(t, pool.AddCA(ca))
|
||||
}
|
||||
_, err := pool.VerifyCertificate(time.Now(), c)
|
||||
return err
|
||||
}
|
||||
|
||||
func TestPQ_CAAndSign(t *testing.T) {
|
||||
for _, curve := range []string{"25519", "P256"} {
|
||||
t.Run(curve, func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
ob, eb := &bytes.Buffer{}, &bytes.Buffer{}
|
||||
nopw := &StubPasswordReader{}
|
||||
p := func(n string) string { return filepath.Join(dir, n) }
|
||||
|
||||
require.NoError(t, ca([]string{"-version", "3", "-curve", curve, "-name", "pq ca", "-out-crt", p("ca.crt"), "-out-key", p("ca.key")}, ob, eb, nopw))
|
||||
caCrt := readCert(t, p("ca.crt"))
|
||||
assert.Equal(t, cert.Version3, caCrt.Version())
|
||||
caCurve, caKey := readSigningKeyFile(t, p("ca.key"))
|
||||
require.NoError(t, caCrt.VerifyPrivateKey(caCurve, caKey))
|
||||
|
||||
// A v3 CA signs v3 by default.
|
||||
require.NoError(t, signCert([]string{"-ca-crt", p("ca.crt"), "-ca-key", p("ca.key"), "-name", "host", "-networks", "10.1.0.1/16", "-out-crt", p("host.crt"), "-out-key", p("host.key")}, ob, eb, nopw))
|
||||
host := readCert(t, p("host.crt"))
|
||||
assert.Equal(t, cert.Version3, host.Version())
|
||||
hostCurve, hostKey := readKeyFile(t, p("host.key"))
|
||||
require.NoError(t, host.VerifyPrivateKey(hostCurve, hostKey))
|
||||
require.NoError(t, verifiedBy(t, host, caCrt))
|
||||
|
||||
// And v2 as its v2 form.
|
||||
require.NoError(t, signCert([]string{"-version", "2", "-ca-crt", p("ca.crt"), "-ca-key", p("ca.key"), "-name", "old", "-networks", "10.1.0.2/16", "-out-crt", p("old.crt"), "-out-key", p("old.key")}, ob, eb, nopw))
|
||||
old := readCert(t, p("old.crt"))
|
||||
assert.Equal(t, cert.Version2, old.Version())
|
||||
require.NoError(t, verifiedBy(t, old, cert.V2Form(caCrt)))
|
||||
|
||||
// keygen -version 3 makes keys for v3 certificates, and only those.
|
||||
require.NoError(t, keygen([]string{"-version", "3", "-curve", curve, "-out-key", p("k3.key"), "-out-pub", p("k3.pub")}, ob, eb))
|
||||
require.NoError(t, signCert([]string{"-ca-crt", p("ca.crt"), "-ca-key", p("ca.key"), "-name", "k3", "-networks", "10.1.0.3/16", "-in-pub", p("k3.pub"), "-out-crt", p("k3.crt")}, ob, eb, nopw))
|
||||
_, k3 := readKeyFile(t, p("k3.key"))
|
||||
require.NoError(t, readCert(t, p("k3.crt")).VerifyPrivateKey(hostCurve, k3))
|
||||
require.ErrorContains(t, signCert([]string{"-version", "2", "-ca-crt", p("ca.crt"), "-ca-key", p("ca.key"), "-name", "x", "-networks", "10.1.0.4/16", "-in-pub", p("k3.pub"), "-out-crt", p("x.crt")}, ob, eb, nopw), "only version 3 certificates carry")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPQ_SignErrors(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
ob, eb := &bytes.Buffer{}, &bytes.Buffer{}
|
||||
nopw := &StubPasswordReader{}
|
||||
p := func(n string) string { return filepath.Join(dir, n) }
|
||||
|
||||
require.NoError(t, ca([]string{"-name", "v2 ca", "-out-crt", p("ca.crt"), "-out-key", p("ca.key")}, ob, eb, nopw))
|
||||
require.ErrorContains(t, signCert([]string{"-version", "3", "-ca-crt", p("ca.crt"), "-ca-key", p("ca.key"), "-name", "h", "-networks", "10.1.0.1/16", "-out-crt", p("h.crt"), "-out-key", p("h.key")}, ob, eb, nopw), "must be signed by a version 3 CA")
|
||||
|
||||
require.NoError(t, keygen([]string{"-out-key", p("k.key"), "-out-pub", p("k.pub")}, ob, eb))
|
||||
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.
|
||||
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")
|
||||
}
|
||||
|
||||
// TestPQ_Extend upgrades a v2 CA and a host in place, the way a running network migrates.
|
||||
func TestPQ_Extend(t *testing.T) {
|
||||
for _, encrypt := range []bool{false, true} {
|
||||
name := "plain"
|
||||
if encrypt {
|
||||
name = "encrypted"
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
ob, eb := &bytes.Buffer{}, &bytes.Buffer{}
|
||||
pw := &StubPasswordReader{}
|
||||
if encrypt {
|
||||
t.Setenv("NEBULA_CA_PASSPHRASE", "hunter2")
|
||||
}
|
||||
p := func(n string) string { return filepath.Join(dir, n) }
|
||||
|
||||
caArgs := []string{"-name", "ca", "-out-crt", p("ca.crt"), "-out-key", p("ca.key"), "-argon-memory", "1024"}
|
||||
if encrypt {
|
||||
caArgs = append(caArgs, "-encrypt")
|
||||
}
|
||||
require.NoError(t, ca(caArgs, ob, eb, pw))
|
||||
require.NoError(t, signCert([]string{"-ca-crt", p("ca.crt"), "-ca-key", p("ca.key"), "-name", "host", "-networks", "10.1.0.1/16", "-out-crt", p("host.crt"), "-out-key", p("host.key")}, ob, eb, pw))
|
||||
ca2 := readCert(t, p("ca.crt"))
|
||||
host2 := readCert(t, p("host.crt"))
|
||||
|
||||
// The CA extends itself.
|
||||
require.NoError(t, pqExtend([]string{"-in-crt", p("ca.crt"), "-ca-key", p("ca.key"), "-out-crt", p("ca3.crt"), "-out-key", p("ca3.key")}, ob, eb, pw))
|
||||
ca3 := readCert(t, p("ca3.crt"))
|
||||
assert.Equal(t, cert.Version3, ca3.Version())
|
||||
assertSameCert(t, ca2, cert.V2Form(ca3))
|
||||
raw, err := os.ReadFile(p("ca3.key"))
|
||||
require.NoError(t, err)
|
||||
curve, ca3Key, _, err := readSigningKey(raw, eb, pw)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, ca3.VerifyPrivateKey(curve, ca3Key))
|
||||
if encrypt {
|
||||
_, _, _, err = cert.UnmarshalSigningPrivateKeyFromPEM(raw)
|
||||
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.
|
||||
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"))
|
||||
assert.Equal(t, cert.Version3, host3.Version())
|
||||
assertSameCert(t, host2, cert.V2Form(host3))
|
||||
hostCurve, host3Key := readKeyFile(t, p("host3.key"))
|
||||
require.NoError(t, host3.VerifyPrivateKey(hostCurve, host3Key))
|
||||
require.NoError(t, verifiedBy(t, host3, ca3))
|
||||
require.NoError(t, verifiedBy(t, host2, ca2), "existing trust is untouched")
|
||||
|
||||
// Someone else's key does not extend this host.
|
||||
require.NoError(t, keygen([]string{"-version", "3", "-out-key", p("other.key"), "-out-pub", p("other.pub")}, ob, eb))
|
||||
require.ErrorContains(t, pqExtend([]string{"-in-crt", p("host.crt"), "-in-pub", p("other.pub"), "-ca-crt", p("ca3.crt"), "-ca-key", p("ca3.key"), "-out-crt", p("bad.crt")}, ob, eb, pw), "does not extend the key")
|
||||
|
||||
// Nor does the v2 CA key, or a v3 certificate.
|
||||
require.ErrorContains(t, pqExtend([]string{"-in-crt", p("host.crt"), "-in-pub", p("host3.pub"), "-ca-crt", p("ca3.crt"), "-ca-key", p("ca.key"), "-out-crt", p("bad.crt")}, ob, eb, pw), "does not match ca-key")
|
||||
require.ErrorContains(t, pqExtend([]string{"-in-crt", p("host3.crt"), "-in-pub", p("host3.pub"), "-ca-crt", p("ca3.crt"), "-ca-key", p("ca3.key"), "-out-crt", p("bad.crt")}, ob, eb, pw), "only version 2 certificates")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertSameCert(t *testing.T, want, got cert.Certificate) {
|
||||
t.Helper()
|
||||
wb, err := want.Marshal()
|
||||
require.NoError(t, err)
|
||||
gb, err := got.Marshal()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, wb, gb)
|
||||
}
|
||||
+91
-34
@@ -44,7 +44,7 @@ type signFlags struct {
|
||||
func newSignFlags() *signFlags {
|
||||
sf := signFlags{set: flag.NewFlagSet("sign", flag.ContinueOnError)}
|
||||
sf.set.Usage = func() {}
|
||||
sf.version = sf.set.Uint("version", 0, "Optional: version of the certificate format to use. The default is to match the version of the signing CA")
|
||||
sf.version = sf.set.Uint("version", 0, "Optional: version of the certificate format to use. The default is to match the version of the signing CA. A version 3 CA also signs version 2 certificates, as its v2 form")
|
||||
sf.caKeyPath = sf.set.String("ca-key", "ca.key", "Optional: path to the signing CA key")
|
||||
sf.caCertPath = sf.set.String("ca-crt", "ca.crt", "Optional: path to the signing CA cert")
|
||||
sf.name = sf.set.String("name", "", "Required: name of the cert, usually a hostname")
|
||||
@@ -102,8 +102,8 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
}
|
||||
|
||||
version := cert.Version(*sf.version)
|
||||
if version != 0 && version != cert.Version1 && version != cert.Version2 {
|
||||
return newHelpErrorf("-version must be either %v or %v", cert.Version1, cert.Version2)
|
||||
if version != 0 && version != cert.Version1 && version != cert.Version2 && version != cert.Version3 {
|
||||
return newHelpErrorf("-version must be %v, %v or %v", cert.Version1, cert.Version2, cert.Version3)
|
||||
}
|
||||
|
||||
if *sf.outKeyPath == "" {
|
||||
@@ -139,37 +139,9 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
return fmt.Errorf("error while reading ca-key: %s", err)
|
||||
}
|
||||
|
||||
// naively attempt to decode the private key as though it is not encrypted
|
||||
caKey, _, curve, err = cert.UnmarshalSigningPrivateKeyFromPEM(rawCAKey)
|
||||
if errors.Is(err, cert.ErrPrivateKeyEncrypted) {
|
||||
var passphrase []byte
|
||||
passphrase = []byte(os.Getenv("NEBULA_CA_PASSPHRASE"))
|
||||
if len(passphrase) == 0 {
|
||||
// ask for a passphrase until we get one
|
||||
for range 5 {
|
||||
errOut.Write([]byte("Enter passphrase: "))
|
||||
passphrase, err = pr.ReadPassword()
|
||||
|
||||
if errors.Is(err, ErrNoTerminal) {
|
||||
return fmt.Errorf("ca-key is encrypted and must be decrypted interactively")
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("error reading password: %s", err)
|
||||
}
|
||||
|
||||
if len(passphrase) > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(passphrase) == 0 {
|
||||
return fmt.Errorf("cannot open encrypted ca-key without passphrase")
|
||||
}
|
||||
}
|
||||
curve, caKey, _, err = cert.DecryptAndUnmarshalSigningPrivateKey(passphrase, rawCAKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while parsing encrypted ca-key: %s", err)
|
||||
}
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("error while parsing ca-key: %s", err)
|
||||
curve, caKey, _, err = readSigningKey(rawCAKey, errOut, pr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,6 +169,19 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
version = caCert.Version()
|
||||
}
|
||||
|
||||
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")
|
||||
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 no duration is given, expire one second before the root expires
|
||||
if *sf.duration <= 0 {
|
||||
*sf.duration = time.Until(caCert.NotAfter()) - time.Second*1
|
||||
@@ -287,6 +272,12 @@ 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) {
|
||||
if isPQ {
|
||||
return fmt.Errorf("in-pub has an ML-KEM-1024 key, which only version 3 certificates carry")
|
||||
}
|
||||
return fmt.Errorf("version 3 certificates need an in-pub from keygen -version 3")
|
||||
}
|
||||
} else if isP11 {
|
||||
pub, err = p11Client.GetPubKey()
|
||||
if err != nil {
|
||||
@@ -294,6 +285,12 @@ 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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !isStdio(*sf.outCertPath) {
|
||||
@@ -378,6 +375,27 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
}
|
||||
|
||||
crts = append(crts, nc)
|
||||
|
||||
case cert.Version3:
|
||||
t := &cert.TBSCertificate{
|
||||
Version: cert.Version3,
|
||||
Name: *sf.name,
|
||||
Networks: append(v4Networks, v6Networks...),
|
||||
Groups: groups,
|
||||
UnsafeNetworks: append(v4UnsafeNetworks, v6UnsafeNetworks...),
|
||||
NotBefore: notBefore,
|
||||
NotAfter: notAfter,
|
||||
PublicKey: pub,
|
||||
IsCA: false,
|
||||
Curve: curve,
|
||||
}
|
||||
|
||||
nc, err := t.Sign(caCert, curve, caKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while signing: %w", err)
|
||||
}
|
||||
crts = append(crts, nc)
|
||||
|
||||
default:
|
||||
// this should be unreachable
|
||||
return fmt.Errorf("invalid version: %d", version)
|
||||
@@ -470,3 +488,42 @@ func signHelp(out io.Writer) {
|
||||
sf.set.SetOutput(out)
|
||||
sf.set.PrintDefaults()
|
||||
}
|
||||
|
||||
// readSigningKey decodes a CA key, asking for its passphrase if it is encrypted.
|
||||
// The passphrase is returned too, nil for a key that was not encrypted.
|
||||
func readSigningKey(rawCAKey []byte, errOut io.Writer, pr PasswordReader) (cert.Curve, []byte, []byte, error) {
|
||||
// naively attempt to decode the private key as though it is not encrypted
|
||||
caKey, _, curve, err := cert.UnmarshalSigningPrivateKeyFromPEM(rawCAKey)
|
||||
if errors.Is(err, cert.ErrPrivateKeyEncrypted) {
|
||||
var passphrase []byte
|
||||
passphrase = []byte(os.Getenv("NEBULA_CA_PASSPHRASE"))
|
||||
if len(passphrase) == 0 {
|
||||
// ask for a passphrase until we get one
|
||||
for range 5 {
|
||||
errOut.Write([]byte("Enter passphrase: "))
|
||||
passphrase, err = pr.ReadPassword()
|
||||
|
||||
if errors.Is(err, ErrNoTerminal) {
|
||||
return 0, nil, nil, fmt.Errorf("ca-key is encrypted and must be decrypted interactively")
|
||||
} else if err != nil {
|
||||
return 0, nil, nil, fmt.Errorf("error reading password: %s", err)
|
||||
}
|
||||
|
||||
if len(passphrase) > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(passphrase) == 0 {
|
||||
return 0, nil, nil, fmt.Errorf("cannot open encrypted ca-key without passphrase")
|
||||
}
|
||||
}
|
||||
curve, caKey, _, err = cert.DecryptAndUnmarshalSigningPrivateKey(passphrase, rawCAKey)
|
||||
if err != nil {
|
||||
return 0, nil, nil, fmt.Errorf("error while parsing encrypted ca-key: %s", err)
|
||||
}
|
||||
return curve, caKey, passphrase, nil
|
||||
} else if err != nil {
|
||||
return 0, nil, nil, fmt.Errorf("error while parsing ca-key: %s", err)
|
||||
}
|
||||
return curve, caKey, nil, nil
|
||||
}
|
||||
@@ -55,7 +55,7 @@ func Test_signHelp(t *testing.T) {
|
||||
" -unsafe-networks string\n"+
|
||||
" \tOptional: comma separated list of ip address and network in CIDR notation. Unsafe networks this cert can route for\n"+
|
||||
" -version uint\n"+
|
||||
" \tOptional: version of the certificate format to use. The default is to match the version of the signing CA\n",
|
||||
" \tOptional: version of the certificate format to use. The default is to match the version of the signing CA. A version 3 CA also signs version 2 certificates, as its v2 form\n",
|
||||
ob.String(),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user