blob: 26277764979c66bd262b40c590d17767a669d6dd [file] [log] [blame]
package pki
import (
"context"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"math/big"
"time"
"go.etcd.io/etcd/clientv3"
"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
}