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