Skip to content
Merged
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,17 @@ 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 doing
Comment thread
felixfontein marked this conversation as resolved.
Outdated
a `GET` request to `https://localhost:15000/cert-status-by-serial/<serial>`, where
`<serial>` is the hexadecimal representation of the certificate's serial number.
Comment thread
felixfontein marked this conversation as resolved.
Outdated
It can be obtained via:

openssl x509 -in cert.pem -noout -text | sed -En 's/.*Serial Number.*\(0x([0-9a-f]+)\)/\1/p'
Comment thread
felixfontein marked this conversation as resolved.
Outdated

The endpoint returns the information as a JSON.
Comment thread
felixfontein marked this conversation as resolved.
Outdated

### OCSP Responder URL

Pebble does not support the OCSP protocol as a responder and so does not set
Expand Down
6 changes: 6 additions & 0 deletions core/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,12 @@ func (c Certificate) Chain(no int) []byte {
return bytes.Join(chain, nil)
}

type RevokedCertificate struct {
Comment thread
felixfontein marked this conversation as resolved.
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
}

// GetCertificateBySerial loops over all revoked certificates to find the one that matches the
Comment thread
felixfontein marked this conversation as resolved.
Outdated
// 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
}
57 changes: 56 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,54 @@ 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.StatusNotFound)
Comment thread
felixfontein marked this conversation as resolved.
Outdated
return
}

var status string
var cert *core.Certificate
var rcert *core.RevokedCertificate
if cert = wfe.db.GetCertificateBySerial(serial); cert != nil {
status = "Valid"
}
if rcert = wfe.db.GetRevokedCertificateBySerial(serial); rcert != nil {
Comment thread
felixfontein marked this conversation as resolved.
status = "Revoked"
cert = rcert.Certificate
}

if status == "" {
Comment thread
felixfontein marked this conversation as resolved.
Outdated
response.WriteHeader(http.StatusNotFound)
return
}
result := make(map[string]interface{})
Comment thread
felixfontein marked this conversation as resolved.
Outdated
result["Status"] = status
result["Serial"] = serial.Text(16)
result["Certificate"] = string(cert.PEM())
if rcert != nil {
if rcert.Reason != nil {
result["Reason"] = rcert.Reason
}
result["RevokedAt"] = rcert.RevokedAt.UTC()
}

resultJSON, err := marshalIndent(result)
Comment thread
cpu marked this conversation as resolved.
Outdated
if err != nil {
response.WriteHeader(http.StatusInternalServerError)
return
}
response.Header().Set("Content-Type", "application/json; charset=utf-8")
response.WriteHeader(http.StatusOK)
_, _ = response.Write(resultJSON)
}

func (wfe *WebFrontEndImpl) Handler() http.Handler {
m := http.NewServeMux()
// GET only handlers
Expand Down Expand Up @@ -360,6 +410,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 +2423,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
}