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)
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue