From 6c126171118d4a493c2d51f892b84c51e4495dd3 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Fri, 12 Jul 2019 23:43:04 +0200 Subject: [PATCH 01/13] Add /cert-status-by-serial/ endpoint for management interface. --- db/memorystore.go | 30 ++++++++++++++++++++++++++++++ wfe/wfe.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/db/memorystore.go b/db/memorystore.go index ac3cd803..dbcf5834 100644 --- a/db/memorystore.go +++ b/db/memorystore.go @@ -6,6 +6,7 @@ import ( "crypto/x509" "encoding/hex" "fmt" + "math/big" "reflect" "strconv" "sync" @@ -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 +// 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.Certificate { + m.RLock() + defer m.RUnlock() + for _, c := range m.revokedCertificatesByID { + if serialNumber.Cmp(c.Cert.SerialNumber) == 0 { + return c + } + } + + return nil +} diff --git a/wfe/wfe.go b/wfe/wfe.go index 1eedd7ab..0b392ff0 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,45 @@ 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) + return + } + + var status string + var cert *core.Certificate + if cert = wfe.db.GetCertificateBySerial(serial); cert != nil { + status = "Valid" + } else if cert = wfe.db.GetRevokedCertificateBySerial(serial); cert != nil { + status = "Revoked" + } + + if status == "" { + response.WriteHeader(http.StatusNotFound) + return + } + result := make(map[string]interface{}) + result["Status"] = status + result["Serial"] = serial.Text(16) + result["Certificate"] = string(cert.PEM()) + + resultJSON, err := marshalIndent(result) + 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 @@ -360,6 +401,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 } From e79a42139381bee45a05613527ba5b69ab453369 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Fri, 12 Jul 2019 23:49:34 +0200 Subject: [PATCH 02/13] Document new endpoint. --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index e31184bc..c30c76c7 100644 --- a/README.md +++ b/README.md @@ -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 +a `GET` request to `https://localhost:15000/cert-status-by-serial/`, where +`` is the hexadecimal representation of the certificate's serial number. +It can be obtained via: + + openssl x509 -in cert.pem -noout -text | sed -En 's/.*Serial Number.*\(0x([0-9a-f]+)\)/\1/p' + +The endpoint returns the information as a JSON. + ### OCSP Responder URL Pebble does not support the OCSP protocol as a responder and so does not set From ad9245c3749213fbad1f50dc5a122f6d60765311 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sat, 13 Jul 2019 00:16:13 +0200 Subject: [PATCH 03/13] Return more information for revoked certificates. --- core/types.go | 6 ++++++ db/memorystore.go | 18 +++++++++--------- wfe/wfe.go | 17 +++++++++++++++-- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/core/types.go b/core/types.go index 678cdb46..93a09eac 100644 --- a/core/types.go +++ b/core/types.go @@ -192,6 +192,12 @@ func (c Certificate) Chain(no int) []byte { return bytes.Join(chain, nil) } +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 dbcf5834..449f6d5c 100644 --- a/db/memorystore.go +++ b/db/memorystore.go @@ -47,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 { @@ -59,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), } } @@ -281,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 } } @@ -293,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) } /* @@ -341,11 +341,11 @@ func (m *MemoryStore) GetCertificateBySerial(serialNumber *big.Int) *core.Certif // GetCertificateBySerial 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.Certificate { +func (m *MemoryStore) GetRevokedCertificateBySerial(serialNumber *big.Int) *core.RevokedCertificate { m.RLock() defer m.RUnlock() for _, c := range m.revokedCertificatesByID { - if serialNumber.Cmp(c.Cert.SerialNumber) == 0 { + if serialNumber.Cmp(c.Certificate.Cert.SerialNumber) == 0 { return c } } diff --git a/wfe/wfe.go b/wfe/wfe.go index 0b392ff0..fd8fb587 100644 --- a/wfe/wfe.go +++ b/wfe/wfe.go @@ -345,10 +345,13 @@ func (wfe *WebFrontEndImpl) handleCertStatusBySerial( var status string var cert *core.Certificate + var rcert *core.RevokedCertificate if cert = wfe.db.GetCertificateBySerial(serial); cert != nil { status = "Valid" - } else if cert = wfe.db.GetRevokedCertificateBySerial(serial); cert != nil { + } + if rcert = wfe.db.GetRevokedCertificateBySerial(serial); rcert != nil { status = "Revoked" + cert = rcert.Certificate } if status == "" { @@ -359,6 +362,12 @@ func (wfe *WebFrontEndImpl) handleCertStatusBySerial( 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 + } resultJSON, err := marshalIndent(result) if err != nil { @@ -2414,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 } From bf6b045b181b4d6775cb45286cf0146fa028eb51 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sat, 13 Jul 2019 00:22:49 +0200 Subject: [PATCH 04/13] Export timestamp as UTC. --- wfe/wfe.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wfe/wfe.go b/wfe/wfe.go index fd8fb587..21ae900a 100644 --- a/wfe/wfe.go +++ b/wfe/wfe.go @@ -366,7 +366,7 @@ func (wfe *WebFrontEndImpl) handleCertStatusBySerial( if rcert.Reason != nil { result["Reason"] = rcert.Reason } - result["RevokedAt"] = rcert.RevokedAt + result["RevokedAt"] = rcert.RevokedAt.UTC() } resultJSON, err := marshalIndent(result) From 7e0724327bc3fb711f20a751d8281b77c687e8cb Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Mon, 22 Jul 2019 21:07:22 +0200 Subject: [PATCH 05/13] Improve documentation. --- README.md | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c30c76c7..e1ef6d46 100644 --- a/README.md +++ b/README.md @@ -319,9 +319,26 @@ a `GET` request to `https://localhost:15000/cert-status-by-serial/`, whe `` is the hexadecimal representation of the certificate's serial number. It can be obtained via: - openssl x509 -in cert.pem -noout -text | sed -En 's/.*Serial Number.*\(0x([0-9a-f]+)\)/\1/p' + openssl x509 -in cert.pem -noout -serial | cut -d= -f2 + +The endpoint returns the information as a JSON: + + $ 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" + } -The endpoint returns the information as a JSON. ### OCSP Responder URL From 542e3200b800d6675938632270c0689d5d917704 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Mon, 22 Jul 2019 21:10:37 +0200 Subject: [PATCH 06/13] Add remark for hex encoding. --- README.md | 2 +- core/types.go | 1 + db/memorystore.go | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e1ef6d46..a6959fd8 100644 --- a/README.md +++ b/README.md @@ -316,7 +316,7 @@ intermediate certificates and keys. The certificate (in PEM format) and its revocation status can be queried by doing a `GET` request to `https://localhost:15000/cert-status-by-serial/`, where -`` is the hexadecimal representation of the certificate's serial number. +`` 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 diff --git a/core/types.go b/core/types.go index 93a09eac..67634a9c 100644 --- a/core/types.go +++ b/core/types.go @@ -192,6 +192,7 @@ 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 diff --git a/db/memorystore.go b/db/memorystore.go index 449f6d5c..5e683501 100644 --- a/db/memorystore.go +++ b/db/memorystore.go @@ -338,7 +338,7 @@ func (m *MemoryStore) GetCertificateBySerial(serialNumber *big.Int) *core.Certif return nil } -// GetCertificateBySerial loops over all revoked certificates to find the one that matches the +// 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 { From 13ccb4d4ada490fef17e96f0ed2e05ee1c91ae6e Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Mon, 22 Jul 2019 21:12:08 +0200 Subject: [PATCH 07/13] Invalid hex -> bad request. --- wfe/wfe.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wfe/wfe.go b/wfe/wfe.go index 21ae900a..bb8f4e1d 100644 --- a/wfe/wfe.go +++ b/wfe/wfe.go @@ -339,7 +339,7 @@ func (wfe *WebFrontEndImpl) handleCertStatusBySerial( serialStr := strings.TrimPrefix(request.URL.Path, certStatusBySerial) serial := big.NewInt(0) if _, ok := serial.SetString(serialStr, 16); !ok { - response.WriteHeader(http.StatusNotFound) + response.WriteHeader(http.StatusBadRequest) return } From 7d16a1a418a9947d2a54292a39d3ddfba7ed6dec Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Mon, 22 Jul 2019 21:13:21 +0200 Subject: [PATCH 08/13] Change query order. --- wfe/wfe.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/wfe/wfe.go b/wfe/wfe.go index bb8f4e1d..a640ff0e 100644 --- a/wfe/wfe.go +++ b/wfe/wfe.go @@ -346,12 +346,11 @@ func (wfe *WebFrontEndImpl) handleCertStatusBySerial( 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 { status = "Revoked" cert = rcert.Certificate + } else if cert = wfe.db.GetCertificateBySerial(serial); cert != nil { + status = "Valid" } if status == "" { From bd04016970e68f7f7d73188cf4569e75df4c78a1 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Mon, 22 Jul 2019 21:14:25 +0200 Subject: [PATCH 09/13] Improve check. --- wfe/wfe.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wfe/wfe.go b/wfe/wfe.go index a640ff0e..2052e277 100644 --- a/wfe/wfe.go +++ b/wfe/wfe.go @@ -353,7 +353,7 @@ func (wfe *WebFrontEndImpl) handleCertStatusBySerial( status = "Valid" } - if status == "" { + if status == "" || cert == nil { response.WriteHeader(http.StatusNotFound) return } From 10af09b8005c213129ad30e9ced92a83e2cc64df Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Mon, 22 Jul 2019 21:42:08 +0200 Subject: [PATCH 10/13] Using anonymous type instead of map. --- wfe/wfe.go | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/wfe/wfe.go b/wfe/wfe.go index 2052e277..38637b0a 100644 --- a/wfe/wfe.go +++ b/wfe/wfe.go @@ -357,15 +357,22 @@ func (wfe *WebFrontEndImpl) handleCertStatusBySerial( response.WriteHeader(http.StatusNotFound) return } - result := make(map[string]interface{}) - result["Status"] = status - result["Serial"] = serial.Text(16) - result["Certificate"] = string(cert.PEM()) + 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.Reason = rcert.Reason } - result["RevokedAt"] = rcert.RevokedAt.UTC() + result.RevokedAt = rcert.RevokedAt.UTC().String() } resultJSON, err := marshalIndent(result) From e44f584f6b3609dceda38e4f3a5b82ff9bbe77b1 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Fri, 26 Jul 2019 09:36:11 +0200 Subject: [PATCH 11/13] Update README.md Co-Authored-By: Daniel McCarney --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a6959fd8..e2b7a5ea 100644 --- a/README.md +++ b/README.md @@ -321,7 +321,7 @@ It can be obtained via: openssl x509 -in cert.pem -noout -serial | cut -d= -f2 -The endpoint returns the information as a JSON: +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 From e58cc6235718833d3a7f93f381db6cabaeddc0f0 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Fri, 26 Jul 2019 09:36:21 +0200 Subject: [PATCH 12/13] Update README.md Co-Authored-By: Daniel McCarney --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e2b7a5ea..8ea398bd 100644 --- a/README.md +++ b/README.md @@ -314,7 +314,7 @@ intermediate certificates and keys. #### Certificate Status -The certificate (in PEM format) and its revocation status can be queried by doing +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: From 7ac768ded72424190f0a1860ff5dfd4971085216 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Fri, 26 Jul 2019 09:39:35 +0200 Subject: [PATCH 13/13] Use writeJSONResponse. --- wfe/wfe.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/wfe/wfe.go b/wfe/wfe.go index 38637b0a..398fd986 100644 --- a/wfe/wfe.go +++ b/wfe/wfe.go @@ -375,14 +375,11 @@ func (wfe *WebFrontEndImpl) handleCertStatusBySerial( result.RevokedAt = rcert.RevokedAt.UTC().String() } - resultJSON, err := marshalIndent(result) + err := wfe.writeJSONResponse(response, http.StatusOK, result) 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 {