|
|
@@ -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)
|
|
|
+}
|