m/p/pki: implement CRLs

This implements revokation and CRL watching functionality in the main
metropolis PKI library, in preparation for use in the consensus library
(which has full CRL support). In the future, this should also be
extended to be used in Metropolis authentication/authorization.

This also introduces a breaking change by changing the layout of etcd
storage for the PKI library - but we're pre-MVP, so this is fine.

Change-Id: If0775f5447a76949d8498d8853dd7b9c03e0e6dc
Reviewed-on: https://review.monogon.dev/c/monogon/+/465
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
diff --git a/metropolis/pkg/pki/crl_test.go b/metropolis/pkg/pki/crl_test.go
new file mode 100644
index 0000000..39a0b0e
--- /dev/null
+++ b/metropolis/pkg/pki/crl_test.go
@@ -0,0 +1,139 @@
+package pki
+
+import (
+	"context"
+	"crypto/x509"
+	"testing"
+
+	"go.etcd.io/etcd/integration"
+
+	"source.monogon.dev/metropolis/node/core/consensus/client"
+)
+
+// TestRevoke exercises the CRL revocation and watching functionality of a CA
+// certificate.
+func TestRevoke(t *testing.T) {
+	cluster := integration.NewClusterV3(nil, &integration.ClusterConfig{
+		Size: 1,
+	})
+	cl := client.NewLocal(cluster.Client(0))
+	defer cluster.Terminate(nil)
+	ctx, ctxC := context.WithCancel(context.Background())
+	defer ctxC()
+	ns := Namespaced("/test-managed/")
+
+	ca := &Certificate{
+		Namespace: &ns,
+		Issuer:    SelfSigned,
+		Name:      "ca",
+		Template:  CA("Test CA"),
+	}
+	sub := &Certificate{
+		Namespace: &ns,
+		Issuer:    ca,
+		Name:      "sub",
+		Template:  Server([]string{"server"}, nil),
+	}
+
+	caCertBytes, err := ca.Ensure(ctx, cl)
+	if err != nil {
+		t.Fatalf("Ensuring ca certificate failed: %v", err)
+	}
+	caCert, err := x509.ParseCertificate(caCertBytes)
+	if err != nil {
+		t.Fatalf("Loading newly emitted CA certificate failed: %v", err)
+	}
+
+	subCertBytes, err := sub.Ensure(ctx, cl)
+	if err != nil {
+		t.Fatalf("Ensuring sub certificate failed: %v", err)
+	}
+	subCert, err := x509.ParseCertificate(subCertBytes)
+	if err != nil {
+		t.Fatalf("Loading newly emitted sub certificate failed: %v", err)
+	}
+
+	// Ensure CRL is correctly signed and that subCert is not yet on it.
+	crlW := ca.WatchCRL(cl)
+	crl, err := crlW.Get(ctx)
+	if err != nil {
+		t.Fatalf("Retrieving initial CRL failed: %v", err)
+	}
+	if err := caCert.CheckCRLSignature(crl.List); err != nil {
+		t.Fatalf("Initial CRL not signed by CA: %v", err)
+	}
+	for _, el := range crl.List.TBSCertList.RevokedCertificates {
+		if el.SerialNumber.Cmp(subCert.SerialNumber) == 0 {
+			t.Fatalf("Newly emitted certificate is already on CRL.")
+		}
+	}
+
+	// Emit yet another certificate. Also shouldn't be on CRL.
+	bad := &Certificate{
+		Namespace: &ns,
+		Issuer:    ca,
+		Name:      "bad",
+		Template:  Server([]string{"badserver"}, nil),
+	}
+	badCertBytes, err := bad.Ensure(ctx, cl)
+	if err != nil {
+		t.Fatalf("Ensuring bad certificate failed: %v", err)
+	}
+	badCert, err := x509.ParseCertificate(badCertBytes)
+	if err != nil {
+		t.Fatalf("Loading newly emitted bad certificate failed: %v", err)
+	}
+	for _, el := range crl.List.TBSCertList.RevokedCertificates {
+		if el.SerialNumber.Cmp(badCert.SerialNumber) == 0 {
+			t.Fatalf("Newly emitted bad certificate is already on CRL.")
+		}
+	}
+
+	// Revoke bad certificate. Should now be present in CRL.
+	if err := ca.Revoke(ctx, cl, "badserver"); err != nil {
+		t.Fatalf("Revoke failed: %v", err)
+	}
+	// Get in a loop until found.
+	for {
+		crl, err = crlW.Get(ctx)
+		if err != nil {
+			t.Fatalf("Get failed: %v", err)
+		}
+		found := false
+		for _, el := range crl.List.TBSCertList.RevokedCertificates {
+			if el.SerialNumber.Cmp(badCert.SerialNumber) == 0 {
+				found = true
+			}
+			if el.SerialNumber.Cmp(subCert.SerialNumber) == 0 {
+				t.Errorf("Found non-revoked cert in CRL")
+			}
+		}
+		if found {
+			break
+		}
+	}
+	// Now revoke first certificate. Both should be now present in CRL.
+	if err := ca.Revoke(ctx, cl, "server"); err != nil {
+		t.Fatalf("Revoke failed: %v", err)
+	}
+	// Get in a loop until found.
+	for {
+		crl, err = crlW.Get(ctx)
+		if err != nil {
+			t.Fatalf("Get failed: %v", err)
+		}
+		foundSub := false
+		foundBad := false
+		for _, el := range crl.List.TBSCertList.RevokedCertificates {
+			if el.SerialNumber.Cmp(badCert.SerialNumber) == 0 {
+				foundBad = true
+			}
+			if el.SerialNumber.Cmp(subCert.SerialNumber) == 0 {
+				foundSub = true
+			}
+		}
+		if foundBad && foundSub {
+			break
+		}
+	}
+}