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:
parent
d16d3fd067
commit
afffbb899d
|
|
@ -39,7 +39,8 @@ func TestPrecertificateOCSP(t *testing.T) {
|
|||
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 {
|
||||
t.Errorf("requesting OCSP for rejected precertificate: %s", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,7 +122,8 @@ func TestPrecertificateRevocation(t *testing.T) {
|
|||
}
|
||||
|
||||
// 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")
|
||||
|
||||
// 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
|
||||
// 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")
|
||||
})
|
||||
}
|
||||
|
|
@ -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`)
|
||||
|
||||
// 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.AssertEquals(t, response.RevocationReason, 1)
|
||||
}
|
||||
|
|
@ -218,12 +221,13 @@ func TestBadKeyRevoker(t *testing.T) {
|
|||
ocsp.KeyCompromise,
|
||||
)
|
||||
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")
|
||||
|
||||
for _, cert := range certs {
|
||||
for i := 0; i < 5; i++ {
|
||||
_, err = ocsp_helper.ReqDER(cert.Raw, ocsp.Revoked)
|
||||
_, err = ocsp_helper.ReqDER(cert.Raw, ocspConfig)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,11 +12,13 @@ import (
|
|||
func main() {
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintf(os.Stderr, `
|
||||
OCSP-checking tool. Provide a list of filenames for certificates in PEM format, and
|
||||
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
|
||||
any request, if any response is invalid or has a bad signature, or if any
|
||||
response is too stale.
|
||||
checkocsp [OPTION]... FILE [FILE]...
|
||||
|
||||
OCSP-checking tool. Provide a list of filenames for certificates in PEM format,
|
||||
and this tool will check OCSP for each certificate based on its AIA field.
|
||||
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()
|
||||
|
|
@ -28,7 +30,7 @@ response is too stale.
|
|||
os.Exit(0)
|
||||
}
|
||||
for _, f := range flag.Args() {
|
||||
_, err := helper.Req(f)
|
||||
_, err := helper.Req(f, helper.ConfigFromFlags())
|
||||
if err != nil {
|
||||
log.Printf("error for %s: %s\n", f, err)
|
||||
errors = true
|
||||
|
|
|
|||
|
|
@ -11,17 +11,72 @@ import (
|
|||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ocsp"
|
||||
)
|
||||
|
||||
var method = flag.String("method", "GET", "Method to use for fetching OCSP")
|
||||
var urlOverride = flag.String("url", "", "URL of OCSP responder to override")
|
||||
var hostOverride = flag.String("host", "", "Host header to override in HTTP request")
|
||||
var tooSoon = flag.Int("too-soon", 76, "If NextUpdate is fewer than this many hours in future, warn.")
|
||||
var ignoreExpiredCerts = flag.Bool("ignore-expired-certs", false, "If a cert is expired, don't bother requesting OCSP.")
|
||||
var expectStatus = flag.Int("expect-status", 0, "Expect response to have this numeric status (0=good, 1=revoked)")
|
||||
var (
|
||||
method = flag.String("method", "GET", "Method to use for fetching OCSP")
|
||||
urlOverride = flag.String("url", "", "URL of OCSP responder to override")
|
||||
hostOverride = flag.String("host", "", "Host header to override in HTTP request")
|
||||
tooSoon = flag.Int("too-soon", 76, "If NextUpdate is fewer than this many hours in future, warn.")
|
||||
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) {
|
||||
if cert == nil {
|
||||
|
|
@ -92,21 +147,25 @@ func parseCMS(body []byte) (*x509.Certificate, error) {
|
|||
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)
|
||||
if err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing certificate: %s", err)
|
||||
}
|
||||
if time.Now().After(cert.NotAfter) {
|
||||
if *ignoreExpiredCerts {
|
||||
if config.ignoreExpiredCerts {
|
||||
return nil, nil
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
|
||||
ocspURL, err := getOCSPURL(cert)
|
||||
ocspURL, err := getOCSPURL(cert, config.urlOverride)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpResp, err := sendHTTPRequest(req, ocspURL)
|
||||
httpResp, err := sendHTTPRequest(req, ocspURL, config.method, config.hostOverride)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -149,30 +208,30 @@ func ReqDER(der []byte, expectStatus int) (*ocsp.Response, error) {
|
|||
if len(respBytes) == 0 {
|
||||
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)
|
||||
var httpRequest *http.Request
|
||||
var err error
|
||||
if *method == "GET" {
|
||||
if method == "GET" {
|
||||
ocspURL.Path = encodedReq
|
||||
fmt.Printf("Fetching %s\n", ocspURL.String())
|
||||
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",
|
||||
ocspURL, encodedReq)
|
||||
httpRequest, err = http.NewRequest("POST", ocspURL.String(), bytes.NewBuffer(req))
|
||||
} 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 {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Add("Content-Type", "application/ocsp-request")
|
||||
if *hostOverride != "" {
|
||||
httpRequest.Host = *hostOverride
|
||||
if host != "" {
|
||||
httpRequest.Host = host
|
||||
}
|
||||
client := http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
|
|
@ -181,10 +240,10 @@ func sendHTTPRequest(req []byte, ocspURL *url.URL) (*http.Response, error) {
|
|||
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
|
||||
if *urlOverride != "" {
|
||||
ocspServer = *urlOverride
|
||||
if urlOverride != "" {
|
||||
ocspServer = urlOverride
|
||||
} else if len(cert.OCSPServer) > 0 {
|
||||
ocspServer = cert.OCSPServer[0]
|
||||
} else {
|
||||
|
|
@ -227,17 +286,20 @@ func checkSignerTimes(resp *ocsp.Response, issuer *x509.Certificate) error {
|
|||
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))
|
||||
resp, err := ocsp.ParseResponseForCert(respBytes, cert, issuer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing response: %s", err)
|
||||
}
|
||||
if resp.Status != expectStatus {
|
||||
return nil, fmt.Errorf("wrong CertStatus %d, expected %d", resp.Status, expectStatus)
|
||||
if config.expectStatus != -1 && resp.Status != config.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)
|
||||
tooSoonDuration := time.Duration(*tooSoon) * time.Hour
|
||||
tooSoonDuration := time.Duration(config.tooSoon) * time.Hour
|
||||
if timeTilExpiry < tooSoonDuration {
|
||||
return nil, fmt.Errorf("NextUpdate is too soon: %s", timeTilExpiry)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ func init() {
|
|||
|
||||
func do(f string) {
|
||||
start := time.Now()
|
||||
resp, err := helper.Req(f)
|
||||
resp, err := helper.Req(f, helper.ConfigFromFlags())
|
||||
latency := time.Since(start)
|
||||
if err != nil {
|
||||
errors_count.With(prom.Labels{}).Inc()
|
||||
|
|
|
|||
Loading…
Reference in New Issue