Skip to content
Merged
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<serial>`, where
`<serial>` 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: <https://127.0.0.1:15000/dir>;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
Expand Down
7 changes: 7 additions & 0 deletions core/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 37 additions & 7 deletions db/memorystore.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"crypto/x509"
"encoding/hex"
"fmt"
"math/big"
"reflect"
"strconv"
"sync"
Expand Down Expand Up @@ -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 {
Expand All @@ -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),
}
}

Expand Down Expand Up @@ -280,23 +281,23 @@ 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
}
}

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

/*
Expand All @@ -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
}
60 changes: 59 additions & 1 deletion wfe/wfe.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"fmt"
"io/ioutil"
"log"
"math/big"
"math/rand"
"net"
"net/http"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}