Add -expect-reason flag to checkocsp (#4901)

Adds a new -expect-reason flag to the checkocsp binary to allow for
verifying the revocation reason of the certificate(s) in question.
This flag has a default value of -1, meaning that no particular
revocation reason will be expected or enforced.

Also updates the -expect-status flag to have the same default (-1) and
behavior, so that when the tool is run interactively it can simply
print the revocation status of each certificate.

Finally, refactors the way the ocsp/helper library declares flags and
accesses their values. This unifies the interface and makes it easy to
extend to allow tests to modify parameters other than expectStatus when
desired.

Fixes #4885
This commit is contained in:
Aaron Gable 2020-06-29 14:15:14 -07:00 committed by GitHub
parent d16d3fd067
commit afffbb899d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 108 additions and 39 deletions

View File

@ -39,7 +39,8 @@ func TestPrecertificateOCSP(t *testing.T) {
t.Fatalf("couldn't find rejected precert for %q", domain) t.Fatalf("couldn't find rejected precert for %q", domain)
} }
_, err = ocsp_helper.ReqDER(cert.Raw, ocsp.Good) ocspConfig := ocsp_helper.DefaultConfig.WithExpectStatus(ocsp.Good)
_, err = ocsp_helper.ReqDER(cert.Raw, ocspConfig)
if err != nil { if err != nil {
t.Errorf("requesting OCSP for rejected precertificate: %s", err) t.Errorf("requesting OCSP for rejected precertificate: %s", err)
} }

View File

@ -122,7 +122,8 @@ func TestPrecertificateRevocation(t *testing.T) {
} }
// To start with the precertificate should have a Good OCSP response. // To start with the precertificate should have a Good OCSP response.
_, err = ocsp_helper.ReqDER(cert.Raw, ocsp.Good) ocspConfig := ocsp_helper.DefaultConfig.WithExpectStatus(ocsp.Good)
_, err = ocsp_helper.ReqDER(cert.Raw, ocspConfig)
test.AssertNotError(t, err, "requesting OCSP for precert") test.AssertNotError(t, err, "requesting OCSP for precert")
// Revoke the precertificate using the specified key and client // Revoke the precertificate using the specified key and client
@ -135,7 +136,8 @@ func TestPrecertificateRevocation(t *testing.T) {
// Check the OCSP response for the precertificate again. It should now be // Check the OCSP response for the precertificate again. It should now be
// revoked. // revoked.
_, err = ocsp_helper.ReqDER(cert.Raw, ocsp.Revoked) ocspConfig = ocsp_helper.DefaultConfig.WithExpectStatus(ocsp.Revoked)
_, err = ocsp_helper.ReqDER(cert.Raw, ocspConfig)
test.AssertNotError(t, err, "requesting OCSP for revoked precert") test.AssertNotError(t, err, "requesting OCSP for revoked precert")
}) })
} }
@ -173,7 +175,8 @@ func TestRevokeWithKeyCompromise(t *testing.T) {
test.AssertEquals(t, err.Error(), `acme: error code 400 "urn:ietf:params:acme:error:badPublicKey": public key is forbidden`) test.AssertEquals(t, err.Error(), `acme: error code 400 "urn:ietf:params:acme:error:badPublicKey": public key is forbidden`)
// Check the OCSP response. It should be revoked with reason = 1 (keyCompromise) // Check the OCSP response. It should be revoked with reason = 1 (keyCompromise)
response, err := ocsp_helper.ReqDER(cert.Raw, ocsp.Revoked) ocspConfig := ocsp_helper.DefaultConfig.WithExpectStatus(ocsp.Revoked)
response, err := ocsp_helper.ReqDER(cert.Raw, ocspConfig)
test.AssertNotError(t, err, "requesting OCSP for revoked cert") test.AssertNotError(t, err, "requesting OCSP for revoked cert")
test.AssertEquals(t, response.RevocationReason, 1) test.AssertEquals(t, response.RevocationReason, 1)
} }
@ -218,12 +221,13 @@ func TestBadKeyRevoker(t *testing.T) {
ocsp.KeyCompromise, ocsp.KeyCompromise,
) )
test.AssertNotError(t, err, "failed to revoke certificate") test.AssertNotError(t, err, "failed to revoke certificate")
_, err = ocsp_helper.ReqDER(badCert.certs[0].Raw, ocsp.Revoked) ocspConfig := ocsp_helper.DefaultConfig.WithExpectStatus(ocsp.Revoked)
_, err = ocsp_helper.ReqDER(badCert.certs[0].Raw, ocspConfig)
test.AssertNotError(t, err, "ReqDER failed") test.AssertNotError(t, err, "ReqDER failed")
for _, cert := range certs { for _, cert := range certs {
for i := 0; i < 5; i++ { for i := 0; i < 5; i++ {
_, err = ocsp_helper.ReqDER(cert.Raw, ocsp.Revoked) _, err = ocsp_helper.ReqDER(cert.Raw, ocspConfig)
if err == nil { if err == nil {
break break
} }

View File

@ -12,11 +12,13 @@ import (
func main() { func main() {
flag.Usage = func() { flag.Usage = func() {
fmt.Fprintf(os.Stderr, ` fmt.Fprintf(os.Stderr, `
OCSP-checking tool. Provide a list of filenames for certificates in PEM format, and checkocsp [OPTION]... FILE [FILE]...
this tool will check OCSP for each certificate based on the AIA field in the
certificates. It will return an error if the OCSP server fails to respond for OCSP-checking tool. Provide a list of filenames for certificates in PEM format,
any request, if any response is invalid or has a bad signature, or if any and this tool will check OCSP for each certificate based on its AIA field.
response is too stale. It will return an error if the OCSP server fails to respond for any request,
if any response is invalid or has a bad signature, or if any response is too
stale.
`) `)
flag.PrintDefaults() flag.PrintDefaults()
@ -28,7 +30,7 @@ response is too stale.
os.Exit(0) os.Exit(0)
} }
for _, f := range flag.Args() { for _, f := range flag.Args() {
_, err := helper.Req(f) _, err := helper.Req(f, helper.ConfigFromFlags())
if err != nil { if err != nil {
log.Printf("error for %s: %s\n", f, err) log.Printf("error for %s: %s\n", f, err)
errors = true errors = true

View File

@ -11,17 +11,72 @@ import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"sync"
"time" "time"
"golang.org/x/crypto/ocsp" "golang.org/x/crypto/ocsp"
) )
var method = flag.String("method", "GET", "Method to use for fetching OCSP") var (
var urlOverride = flag.String("url", "", "URL of OCSP responder to override") method = flag.String("method", "GET", "Method to use for fetching OCSP")
var hostOverride = flag.String("host", "", "Host header to override in HTTP request") urlOverride = flag.String("url", "", "URL of OCSP responder to override")
var tooSoon = flag.Int("too-soon", 76, "If NextUpdate is fewer than this many hours in future, warn.") hostOverride = flag.String("host", "", "Host header to override in HTTP request")
var ignoreExpiredCerts = flag.Bool("ignore-expired-certs", false, "If a cert is expired, don't bother requesting OCSP.") tooSoon = flag.Int("too-soon", 76, "If NextUpdate is fewer than this many hours in future, warn.")
var expectStatus = flag.Int("expect-status", 0, "Expect response to have this numeric status (0=good, 1=revoked)") ignoreExpiredCerts = flag.Bool("ignore-expired-certs", false, "If a cert is expired, don't bother requesting OCSP.")
expectStatus = flag.Int("expect-status", 0, "Expect response to have this numeric status (0=Good, 1=Revoked, 2=Unknown); or -1 for no enforcement.")
expectReason = flag.Int("expect-reason", -1, "Expect response to have this numeric revocation reason (0=Unspecified, 1=KeyCompromise, etc); or -1 for no enforcement.")
)
// Config contains fields which control various behaviors of the
// checker's behavior.
type Config struct {
method string
urlOverride string
hostOverride string
tooSoon int
ignoreExpiredCerts bool
expectStatus int
expectReason int
}
// DefaultConfig is a Config populated with the same defaults as if no
// command-line had been provided, so all retain their default value.
var DefaultConfig = Config{
method: *method,
urlOverride: *urlOverride,
hostOverride: *hostOverride,
tooSoon: *tooSoon,
ignoreExpiredCerts: *ignoreExpiredCerts,
expectStatus: *expectStatus,
expectReason: *expectReason,
}
var parseFlagsOnce sync.Once
// ConfigFromFlags returns a Config whose values are populated from
// any command line flags passed by the user, or default values if not passed.
func ConfigFromFlags() Config {
parseFlagsOnce.Do(func() {
flag.Parse()
})
return Config{
method: *method,
urlOverride: *urlOverride,
hostOverride: *hostOverride,
tooSoon: *tooSoon,
ignoreExpiredCerts: *ignoreExpiredCerts,
expectStatus: *expectStatus,
expectReason: *expectReason,
}
}
// WithExpectStatus returns a new Config with the given expectStatus,
// and all other fields the same as the receiver.
func (template Config) WithExpectStatus(status int) Config {
ret := template
ret.expectStatus = status
return ret
}
func getIssuer(cert *x509.Certificate) (*x509.Certificate, error) { func getIssuer(cert *x509.Certificate) (*x509.Certificate, error) {
if cert == nil { if cert == nil {
@ -92,21 +147,25 @@ func parseCMS(body []byte) (*x509.Certificate, error) {
return cert, nil return cert, nil
} }
func Req(fileName string) (*ocsp.Response, error) { // Req makes an OCSP request using the given config for the PEM certificate in
// fileName, and returns the response.
func Req(fileName string, config Config) (*ocsp.Response, error) {
contents, err := ioutil.ReadFile(fileName) contents, err := ioutil.ReadFile(fileName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return ReqDER(contents, *expectStatus) return ReqDER(contents, config)
} }
func ReqDER(der []byte, expectStatus int) (*ocsp.Response, error) { // ReqDER makes an OCSP request using the given config for the given DER-encoded
// certificate, and returns the response.
func ReqDER(der []byte, config Config) (*ocsp.Response, error) {
cert, err := parse(der) cert, err := parse(der)
if err != nil { if err != nil {
return nil, fmt.Errorf("parsing certificate: %s", err) return nil, fmt.Errorf("parsing certificate: %s", err)
} }
if time.Now().After(cert.NotAfter) { if time.Now().After(cert.NotAfter) {
if *ignoreExpiredCerts { if config.ignoreExpiredCerts {
return nil, nil return nil, nil
} else { } else {
return nil, fmt.Errorf("certificate expired %s ago: %s", return nil, fmt.Errorf("certificate expired %s ago: %s",
@ -123,12 +182,12 @@ func ReqDER(der []byte, expectStatus int) (*ocsp.Response, error) {
return nil, fmt.Errorf("creating OCSP request: %s", err) return nil, fmt.Errorf("creating OCSP request: %s", err)
} }
ocspURL, err := getOCSPURL(cert) ocspURL, err := getOCSPURL(cert, config.urlOverride)
if err != nil { if err != nil {
return nil, err return nil, err
} }
httpResp, err := sendHTTPRequest(req, ocspURL) httpResp, err := sendHTTPRequest(req, ocspURL, config.method, config.hostOverride)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -149,30 +208,30 @@ func ReqDER(der []byte, expectStatus int) (*ocsp.Response, error) {
if len(respBytes) == 0 { if len(respBytes) == 0 {
return nil, fmt.Errorf("empty response body") return nil, fmt.Errorf("empty response body")
} }
return parseAndPrint(respBytes, cert, issuer, expectStatus) return parseAndPrint(respBytes, cert, issuer, config)
} }
func sendHTTPRequest(req []byte, ocspURL *url.URL) (*http.Response, error) { func sendHTTPRequest(req []byte, ocspURL *url.URL, method string, host string) (*http.Response, error) {
encodedReq := base64.StdEncoding.EncodeToString(req) encodedReq := base64.StdEncoding.EncodeToString(req)
var httpRequest *http.Request var httpRequest *http.Request
var err error var err error
if *method == "GET" { if method == "GET" {
ocspURL.Path = encodedReq ocspURL.Path = encodedReq
fmt.Printf("Fetching %s\n", ocspURL.String()) fmt.Printf("Fetching %s\n", ocspURL.String())
httpRequest, err = http.NewRequest("GET", ocspURL.String(), http.NoBody) httpRequest, err = http.NewRequest("GET", ocspURL.String(), http.NoBody)
} else if *method == "POST" { } else if method == "POST" {
fmt.Printf("POSTing request, reproduce with: curl -i --data-binary @- %s < <(base64 -d <<<%s)\n", fmt.Printf("POSTing request, reproduce with: curl -i --data-binary @- %s < <(base64 -d <<<%s)\n",
ocspURL, encodedReq) ocspURL, encodedReq)
httpRequest, err = http.NewRequest("POST", ocspURL.String(), bytes.NewBuffer(req)) httpRequest, err = http.NewRequest("POST", ocspURL.String(), bytes.NewBuffer(req))
} else { } else {
return nil, fmt.Errorf("invalid method %s, expected GET or POST", *method) return nil, fmt.Errorf("invalid method %s, expected GET or POST", method)
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
httpRequest.Header.Add("Content-Type", "application/ocsp-request") httpRequest.Header.Add("Content-Type", "application/ocsp-request")
if *hostOverride != "" { if host != "" {
httpRequest.Host = *hostOverride httpRequest.Host = host
} }
client := http.Client{ client := http.Client{
Timeout: 5 * time.Second, Timeout: 5 * time.Second,
@ -181,10 +240,10 @@ func sendHTTPRequest(req []byte, ocspURL *url.URL) (*http.Response, error) {
return client.Do(httpRequest) return client.Do(httpRequest)
} }
func getOCSPURL(cert *x509.Certificate) (*url.URL, error) { func getOCSPURL(cert *x509.Certificate, urlOverride string) (*url.URL, error) {
var ocspServer string var ocspServer string
if *urlOverride != "" { if urlOverride != "" {
ocspServer = *urlOverride ocspServer = urlOverride
} else if len(cert.OCSPServer) > 0 { } else if len(cert.OCSPServer) > 0 {
ocspServer = cert.OCSPServer[0] ocspServer = cert.OCSPServer[0]
} else { } else {
@ -227,17 +286,20 @@ func checkSignerTimes(resp *ocsp.Response, issuer *x509.Certificate) error {
return nil return nil
} }
func parseAndPrint(respBytes []byte, cert, issuer *x509.Certificate, expectStatus int) (*ocsp.Response, error) { func parseAndPrint(respBytes []byte, cert, issuer *x509.Certificate, config Config) (*ocsp.Response, error) {
fmt.Printf("\nDecoding body: %s\n", base64.StdEncoding.EncodeToString(respBytes)) fmt.Printf("\nDecoding body: %s\n", base64.StdEncoding.EncodeToString(respBytes))
resp, err := ocsp.ParseResponseForCert(respBytes, cert, issuer) resp, err := ocsp.ParseResponseForCert(respBytes, cert, issuer)
if err != nil { if err != nil {
return nil, fmt.Errorf("parsing response: %s", err) return nil, fmt.Errorf("parsing response: %s", err)
} }
if resp.Status != expectStatus { if config.expectStatus != -1 && resp.Status != config.expectStatus {
return nil, fmt.Errorf("wrong CertStatus %d, expected %d", resp.Status, expectStatus) return nil, fmt.Errorf("wrong CertStatus %d, expected %d", resp.Status, config.expectStatus)
}
if config.expectReason != -1 && resp.RevocationReason != config.expectReason {
return nil, fmt.Errorf("wrong RevocationReason %d, expected %d", resp.RevocationReason, config.expectReason)
} }
timeTilExpiry := time.Until(resp.NextUpdate) timeTilExpiry := time.Until(resp.NextUpdate)
tooSoonDuration := time.Duration(*tooSoon) * time.Hour tooSoonDuration := time.Duration(config.tooSoon) * time.Hour
if timeTilExpiry < tooSoonDuration { if timeTilExpiry < tooSoonDuration {
return nil, fmt.Errorf("NextUpdate is too soon: %s", timeTilExpiry) return nil, fmt.Errorf("NextUpdate is too soon: %s", timeTilExpiry)
} }

View File

@ -57,7 +57,7 @@ func init() {
func do(f string) { func do(f string) {
start := time.Now() start := time.Now()
resp, err := helper.Req(f) resp, err := helper.Req(f, helper.ConfigFromFlags())
latency := time.Since(start) latency := time.Since(start)
if err != nil { if err != nil {
errors_count.With(prom.Labels{}).Inc() errors_count.With(prom.Labels{}).Inc()