| 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/metropolis/node/core/consensus/client" | 
 | 	"source.monogon.dev/metropolis/pkg/event" | 
 | 	"source.monogon.dev/metropolis/pkg/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 client.Namespaced) CRLWatcher { | 
 | 	value := etcd.NewValue(cl, c.crlPath(), func(_, data []byte) (interface{}, 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 CRLWatcher{value.Watch()} | 
 | } | 
 |  | 
 | // CRLWatcher is a Event Value compatible Watcher which will be updated any time | 
 | // a given CA certificate's CRL gets updated. | 
 | type CRLWatcher struct { | 
 | 	event.Watcher | 
 | } | 
 |  | 
 | type CRL struct { | 
 | 	Raw  []byte | 
 | 	List *pkix.CertificateList | 
 | } | 
 |  | 
 | // Retrieve the newest available CRL from etcd, blocking until one is available | 
 | // or updated. | 
 | // | 
 | // The first call will block until a CRL is available, which happens the first | 
 | // time a given CA certificate is stored in etcd (eg. through an Ensure call). | 
 | func (c *CRLWatcher) Get(ctx context.Context, opts ...event.GetOption) (*CRL, error) { | 
 | 	v, err := c.Watcher.Get(ctx, opts...) | 
 | 	if err != nil { | 
 | 		return nil, err | 
 | 	} | 
 | 	return v.(*CRL), nil | 
 | } |