treewide: introduce osbase package and move things around

All except localregistry moved from metropolis/pkg to osbase,
localregistry moved to metropolis/test as its only used there anyway.

Change-Id: If1a4bf377364bef0ac23169e1b90379c71b06d72
Reviewed-on: https://review.monogon.dev/c/monogon/+/3079
Tested-by: Jenkins CI
Reviewed-by: Serge Bazanski <serge@monogon.tech>
diff --git a/osbase/pki/crl.go b/osbase/pki/crl.go
new file mode 100644
index 0000000..40118aa
--- /dev/null
+++ b/osbase/pki/crl.go
@@ -0,0 +1,164 @@
+package pki
+
+import (
+	"context"
+	"crypto/rand"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"fmt"
+	"math/big"
+	"time"
+
+	clientv3 "go.etcd.io/etcd/client/v3"
+
+	"source.monogon.dev/osbase/event"
+	"source.monogon.dev/osbase/event/etcd"
+)
+
+// crlPath returns the etcd path under which the marshaled X.509 Certificate
+// Revocation List is stored.
+//
+// TODO(q3k): use etcd keyspace API from
+func (c *Certificate) crlPath() string {
+	return c.Namespace.etcdPath("%s-crl.der", c.Name)
+}
+
+// Revoke performs a CRL-based revocation of a given certificate by this CA,
+// looking it up by DNS name. The revocation is immediately written to the
+// backing etcd store and will be available to consumers through the WatchCRL
+// API.
+//
+// An error is returned if the CRL could not be emitted (eg. due to an etcd
+// communication error, a conflicting CRL write) or if the given hostname
+// matches no emitted certificate.
+//
+// Only Managed and External certificates can be revoked.
+func (c Certificate) Revoke(ctx context.Context, kv clientv3.KV, hostname string) error {
+	crlPath := c.crlPath()
+	issuedCerts := c.Namespace.etcdPath("issued/")
+
+	res, err := kv.Txn(ctx).Then(
+		clientv3.OpGet(crlPath),
+		clientv3.OpGet(issuedCerts, clientv3.WithPrefix())).Commit()
+	if err != nil {
+		return fmt.Errorf("failed to retrieve certificates and CRL from etcd: %w", err)
+	}
+
+	// Parse certs, CRL and CRL revision from state.
+	var certs []*x509.Certificate
+	var crlRevision int64
+	var crl *pkix.CertificateList
+	for _, el := range res.Responses {
+		for _, kv := range el.GetResponseRange().GetKvs() {
+			if string(kv.Key) == crlPath {
+				crl, err = x509.ParseCRL(kv.Value)
+				if err != nil {
+					return fmt.Errorf("could not parse CRL from etcd: %w", err)
+				}
+				crlRevision = kv.CreateRevision
+			} else {
+				cert, err := x509.ParseCertificate(kv.Value)
+				if err != nil {
+					return fmt.Errorf("could not parse certificate %q from etcd: %w", string(kv.Key), err)
+				}
+				certs = append(certs, cert)
+			}
+		}
+	}
+	if crl == nil {
+		return fmt.Errorf("could not find CRL in etcd")
+	}
+	revoked := crl.TBSCertList.RevokedCertificates
+
+	// Find requested hostname in issued certificates.
+	var serial *big.Int
+	for _, cert := range certs {
+		for _, dnsName := range cert.DNSNames {
+			if dnsName == hostname {
+				serial = cert.SerialNumber
+				break
+			}
+		}
+		if serial != nil {
+			break
+		}
+	}
+	if serial == nil {
+		return fmt.Errorf("could not find requested hostname")
+	}
+
+	// Check if certificate has already been revoked.
+	for _, revokedCert := range revoked {
+		if revokedCert.SerialNumber.Cmp(serial) == 0 {
+			return nil // Already revoked
+		}
+	}
+
+	// Otherwise, revoke and save new CRL.
+	revoked = append(revoked, pkix.RevokedCertificate{
+		SerialNumber:   serial,
+		RevocationTime: time.Now(),
+	})
+
+	crlRaw, err := c.makeCRL(ctx, kv, revoked)
+	if err != nil {
+		return fmt.Errorf("when generating new CRL for revocation: %w", err)
+	}
+
+	res, err = kv.Txn(ctx).If(
+		clientv3.Compare(clientv3.CreateRevision(crlPath), "=", crlRevision),
+	).Then(
+		clientv3.OpPut(crlPath, string(crlRaw)),
+	).Commit()
+	if err != nil {
+		return fmt.Errorf("when saving new CRL: %w", err)
+	}
+	if !res.Succeeded {
+		return fmt.Errorf("CRL save transaction failed, retry possible")
+	}
+
+	return nil
+}
+
+// makeCRL returns a valid CRL for a given list of certificates to be revoked.
+// The given etcd client is used to ensure this CA certificate exists in etcd,
+// but is not used to write any CRL to etcd.
+func (c *Certificate) makeCRL(ctx context.Context, kv clientv3.KV, revoked []pkix.RevokedCertificate) ([]byte, error) {
+	if c.Mode != CertificateManaged {
+		return nil, fmt.Errorf("only managed certificates can issue CRLs")
+	}
+	certBytes, err := c.ensure(ctx, kv)
+	if err != nil {
+		return nil, fmt.Errorf("when ensuring certificate: %w", err)
+	}
+	cert, err := x509.ParseCertificate(certBytes)
+	if err != nil {
+		return nil, fmt.Errorf("when parsing issuing certificate: %w", err)
+	}
+	crl, err := cert.CreateCRL(rand.Reader, c.PrivateKey, revoked, time.Now(), UnknownNotAfter)
+	if err != nil {
+		return nil, fmt.Errorf("failed to generate CRL: %w", err)
+	}
+	return crl, nil
+}
+
+// WatchCRL returns and Event Value compatible CRLWatcher which can be used to
+// retrieve and watch for the newest CRL available from this CA certificate.
+func (c *Certificate) WatchCRL(cl etcd.ThinClient) event.Watcher[*CRL] {
+	value := etcd.NewValue(cl, c.crlPath(), func(_, data []byte) (*CRL, error) {
+		crl, err := x509.ParseCRL(data)
+		if err != nil {
+			return nil, fmt.Errorf("could not parse CRL from etcd: %w", err)
+		}
+		return &CRL{
+			Raw:  data,
+			List: crl,
+		}, nil
+	})
+	return value.Watch()
+}
+
+type CRL struct {
+	Raw  []byte
+	List *pkix.CertificateList
+}