diff --git a/README.md b/README.md index e31184bc..8ea398bd 100644 --- a/README.md +++ b/README.md @@ -312,6 +312,34 @@ request to `https://localhost:15000/roots/0`, `https://localhost:15000/root-keys etc. These endpoints also send `Link` HTTP headers for all alternative root and intermediate certificates and keys. +#### Certificate Status + +The certificate (in PEM format) and its revocation status can be queried by sending +a `GET` request to `https://localhost:15000/cert-status-by-serial/`, where +`` is the hexadecimal representation of the certificate's serial number (no `0x` prefix). +It can be obtained via: + + openssl x509 -in cert.pem -noout -serial | cut -d= -f2 + +The endpoint returns the information as a JSON object: + + $ curl -ki https://127.0.0.1:15000/cert-status-by-serial/66317d2e02f5d3d6 + HTTP/2 200 + cache-control: public, max-age=0, no-cache + content-type: application/json; charset=utf-8 + link: ;rel="index" + content-length: 1740 + date: Fri, 12 Jul 2019 22:14:21 GMT + + { + "Certificate": "-----BEGIN CERTIFICATE-----\nMIIEVz...tcw=\n-----END CERTIFICATE-----\n", + "Reason": 4, + "RevokedAt": "2019-07-13T00:13:20.418489956+02:00", + "Serial": "66317d2e02f5d3d6", + "Status": "Revoked" + } + + ### OCSP Responder URL Pebble does not support the OCSP protocol as a responder and so does not set diff --git a/core/types.go b/core/types.go index 678cdb46..67634a9c 100644 --- a/core/types.go +++ b/core/types.go @@ -192,6 +192,13 @@ func (c Certificate) Chain(no int) []byte { return bytes.Join(chain, nil) } +// RevokedCertificate is a certificate together with information about its revocation. +type RevokedCertificate struct { + Certificate *Certificate + RevokedAt time.Time + Reason *uint +} + type ValidationRecord struct { URL string Error *acme.ProblemDetails diff --git a/db/memorystore.go b/db/memorystore.go index ac3cd803..5e683501 100644 --- a/db/memorystore.go +++ b/db/memorystore.go @@ -6,6 +6,7 @@ import ( "crypto/x509" "encoding/hex" "fmt" + "math/big" "reflect" "strconv" "sync" @@ -46,7 +47,7 @@ type MemoryStore struct { challengesByID map[string]*core.Challenge certificatesByID map[string]*core.Certificate - revokedCertificatesByID map[string]*core.Certificate + revokedCertificatesByID map[string]*core.RevokedCertificate } func NewMemoryStore() *MemoryStore { @@ -58,7 +59,7 @@ func NewMemoryStore() *MemoryStore { authorizationsByID: make(map[string]*core.Authorization), challengesByID: make(map[string]*core.Challenge), certificatesByID: make(map[string]*core.Certificate), - revokedCertificatesByID: make(map[string]*core.Certificate), + revokedCertificatesByID: make(map[string]*core.RevokedCertificate), } } @@ -280,11 +281,11 @@ func (m *MemoryStore) GetCertificateByDER(der []byte) *core.Certificate { // GetCertificateByDER loops over all revoked certificates to find the one that matches the provided // DER bytes. This method is linear and it's not optimized to give you a quick response. -func (m *MemoryStore) GetRevokedCertificateByDER(der []byte) *core.Certificate { +func (m *MemoryStore) GetRevokedCertificateByDER(der []byte) *core.RevokedCertificate { m.RLock() defer m.RUnlock() for _, c := range m.revokedCertificatesByID { - if reflect.DeepEqual(c.DER, der) { + if reflect.DeepEqual(c.Certificate.DER, der) { return c } } @@ -292,11 +293,11 @@ func (m *MemoryStore) GetRevokedCertificateByDER(der []byte) *core.Certificate { return nil } -func (m *MemoryStore) RevokeCertificate(cert *core.Certificate) { +func (m *MemoryStore) RevokeCertificate(cert *core.RevokedCertificate) { m.Lock() defer m.Unlock() - m.revokedCertificatesByID[cert.ID] = cert - delete(m.certificatesByID, cert.ID) + m.revokedCertificatesByID[cert.Certificate.ID] = cert + delete(m.certificatesByID, cert.Certificate.ID) } /* @@ -322,3 +323,32 @@ func keyToID(key crypto.PublicKey) (string, error) { return hex.EncodeToString(spkiDigest[:]), nil } } + +// GetCertificateBySerial loops over all certificates to find the one that matches the provided +// serial number. This method is linear and it's not optimized to give you a quick response. +func (m *MemoryStore) GetCertificateBySerial(serialNumber *big.Int) *core.Certificate { + m.RLock() + defer m.RUnlock() + for _, c := range m.certificatesByID { + if serialNumber.Cmp(c.Cert.SerialNumber) == 0 { + return c + } + } + + return nil +} + +// GetRevokedCertificateBySerial loops over all revoked certificates to find the one that matches the +// provided serial number. This method is linear and it's not optimized to give you a quick +// response. +func (m *MemoryStore) GetRevokedCertificateBySerial(serialNumber *big.Int) *core.RevokedCertificate { + m.RLock() + defer m.RUnlock() + for _, c := range m.revokedCertificatesByID { + if serialNumber.Cmp(c.Certificate.Cert.SerialNumber) == 0 { + return c + } + } + + return nil +} diff --git a/wfe/wfe.go b/wfe/wfe.go index 1eedd7ab..398fd986 100644 --- a/wfe/wfe.go +++ b/wfe/wfe.go @@ -13,6 +13,7 @@ import ( "fmt" "io/ioutil" "log" + "math/big" "math/rand" "net" "net/http" @@ -58,6 +59,7 @@ const ( rootKeyPath = "/root-keys/" intermediateCertPath = "/intermediates/" intermediateKeyPath = "/intermediate-keys/" + certStatusBySerial = "/cert-status-by-serial/" // How long do pending authorizations last before expiring? pendingAuthzExpire = time.Hour @@ -329,6 +331,57 @@ func (wfe *WebFrontEndImpl) handleKey( } } +func (wfe *WebFrontEndImpl) handleCertStatusBySerial( + ctx context.Context, + response http.ResponseWriter, + request *http.Request) { + + serialStr := strings.TrimPrefix(request.URL.Path, certStatusBySerial) + serial := big.NewInt(0) + if _, ok := serial.SetString(serialStr, 16); !ok { + response.WriteHeader(http.StatusBadRequest) + return + } + + var status string + var cert *core.Certificate + var rcert *core.RevokedCertificate + if rcert = wfe.db.GetRevokedCertificateBySerial(serial); rcert != nil { + status = "Revoked" + cert = rcert.Certificate + } else if cert = wfe.db.GetCertificateBySerial(serial); cert != nil { + status = "Valid" + } + + if status == "" || cert == nil { + response.WriteHeader(http.StatusNotFound) + return + } + result := struct { + Status string + Serial string + Certificate string + Reason *uint `json:",omitempty"` + RevokedAt string `json:",omitempty"` + }{ + Status: status, + Serial: serial.Text(16), + Certificate: string(cert.PEM()), + } + if rcert != nil { + if rcert.Reason != nil { + result.Reason = rcert.Reason + } + result.RevokedAt = rcert.RevokedAt.UTC().String() + } + + err := wfe.writeJSONResponse(response, http.StatusOK, result) + if err != nil { + response.WriteHeader(http.StatusInternalServerError) + return + } +} + func (wfe *WebFrontEndImpl) Handler() http.Handler { m := http.NewServeMux() // GET only handlers @@ -360,6 +413,7 @@ func (wfe *WebFrontEndImpl) ManagementHandler() http.Handler { wfe.HandleManagementFunc(m, rootKeyPath, wfe.handleKey(wfe.ca.GetRootKey, rootKeyPath)) wfe.HandleManagementFunc(m, intermediateCertPath, wfe.handleCert(wfe.ca.GetIntermediateCert, intermediateCertPath)) wfe.HandleManagementFunc(m, intermediateKeyPath, wfe.handleKey(wfe.ca.GetIntermediateKey, intermediateKeyPath)) + wfe.HandleManagementFunc(m, certStatusBySerial, wfe.handleCertStatusBySerial) return m } @@ -2372,6 +2426,10 @@ func (wfe *WebFrontEndImpl) processRevocation( return prob } - wfe.db.RevokeCertificate(cert) + wfe.db.RevokeCertificate(&core.RevokedCertificate{ + Certificate: cert, + RevokedAt: time.Now(), + Reason: revokeCertReq.Reason, + }) return nil }