Skip to content

Commit 9fef15b

Browse files
redracmaraino
authored andcommitted
Add OCSP and CRL support to certificate verify
Add args and functionality to certificate verify to check a CRL and OCSP for a certificate based on the extensions. Users can pass flags to enable verification of each (CRL, OCSP). The command will try and get the CRL and OCSP server from the certifiacate and verify the certificate against each. I also moved functions from the crl command into internal/crlutil package so they can be re-used with the certificate verify command. Implements #845
1 parent cd22f47 commit 9fef15b

File tree

5 files changed

+586
-305
lines changed

5 files changed

+586
-305
lines changed

command/certificate/verify.go

Lines changed: 273 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
package certificate
22

33
import (
4+
"bytes"
5+
"crypto/tls"
46
"crypto/x509"
57
"encoding/pem"
8+
"fmt"
9+
"io"
10+
"net/http"
611
"os"
712

813
"github.com/pkg/errors"
914
"github.com/smallstep/cli/flags"
15+
"github.com/smallstep/cli/internal/crlutil"
1016
"github.com/urfave/cli"
1117
"go.step.sm/cli-utils/errs"
1218
"go.step.sm/crypto/x509util"
19+
"golang.org/x/crypto/ocsp"
1320
)
1421

1522
func verifyCommand() cli.Command {
@@ -18,7 +25,10 @@ func verifyCommand() cli.Command {
1825
Action: cli.ActionFunc(verifyAction),
1926
Usage: `verify a certificate`,
2027
UsageText: `**step certificate verify** <crt-file> [**--host**=<host>]
21-
[**--roots**=<root-bundle>] [**--servername**=<servername>]`,
28+
[**--roots**=<root-bundle>] [**--servername**=<servername>]
29+
[**--issuing-ca**=<ca-cert-file>] [**--verbose**]
30+
[**--verify-ocsp**]] [**--ocsp-endpoint**]=url
31+
[**--verify-crl**] [**--crl-endpoint**]=url`,
2232
Description: `**step certificate verify** executes the certificate path
2333
validation algorithm for x.509 certificates defined in RFC 5280. If the
2434
certificate is valid this command will return '0'. If validation fails, or if
@@ -65,7 +75,24 @@ Verify a certificate using a custom directory of root certificates for path vali
6575
'''
6676
$ step certificate verify ./certificate.crt --roots ./root-certificates/
6777
'''
68-
`,
78+
79+
Verify a certificate including OCSP and CRL using CRL and OCSP defined in the certificate
80+
81+
'''
82+
$ step certificate verify ./certificate.crt --verify-crl --verify-ocsp
83+
'''
84+
85+
Verify a certificate including OCSP and specifying an OCSP server
86+
87+
'''
88+
$ step certificate verify ./certificate.crt --verify-ocsp --ocsp-endpoint http://acme.com/ocsp
89+
'''
90+
91+
Verify a certificate including CRL and specificing a CRL server and providing the issuing CA certificate
92+
93+
'''
94+
$ step certificate verify ./certificate.crt --issuing-ca ./issuing_ca.pem --verify-crl --crl-endpoint http://acme.com/crl
95+
'''`,
6996
Flags: []cli.Flag{
7097
cli.StringFlag{
7198
Name: "host",
@@ -87,7 +114,32 @@ authenticity of the remote server.
87114
**directory**
88115
: Relative or full path to a directory. Every PEM encoded certificate from each file in the directory will be used for path validation.`,
89116
},
117+
cli.StringFlag{
118+
Name: "issuing-ca",
119+
Usage: `The certificate issuer CA <file> needed to communicate with OCSP and verify a CRL. By default the issuing CA will be taken from the cert Issuing Certificate URL extension.`,
120+
},
121+
cli.BoolFlag{
122+
Name: "verify-ocsp",
123+
Usage: "Verify the certificate against it's OCSP.",
124+
},
125+
cli.StringFlag{
126+
Name: "ocsp-endpoint",
127+
Usage: `The OCSP endpoint to use. If not provided step will attempt to check it against the certificate's OCSPServer AIA extension endpoints.`,
128+
},
129+
cli.BoolFlag{
130+
Name: "verify-crl",
131+
Usage: "Verify the certificate against it's CRL.",
132+
},
133+
cli.StringFlag{
134+
Name: "crl-endpoint",
135+
Usage: "The CRL endpoint to use. If not provided step will attempt to check it against the certificate's CRLDistributionPoints extension endpoints.",
136+
},
137+
cli.BoolFlag{
138+
Name: "verbose, v",
139+
Usage: "Print result of certificate verification to stdout on success",
140+
},
90141
flags.ServerName,
142+
flags.Insecure,
91143
},
92144
}
93145
}
@@ -102,9 +154,18 @@ func verifyAction(ctx *cli.Context) error {
102154
host = ctx.String("host")
103155
serverName = ctx.String("servername")
104156
roots = ctx.String("roots")
157+
verifyOCSP = ctx.Bool("verify-ocsp")
158+
ocspEndpoint = ctx.String("ocsp-endpoint")
159+
verifyCRL = ctx.Bool("verify-crl")
160+
crlEndpoint = ctx.String("crl-endpoint")
161+
verbose = ctx.Bool("verbose")
162+
issuerFile = ctx.String("issuing-ca")
163+
insecure = ctx.Bool("insecure")
105164
intermediatePool = x509.NewCertPool()
106165
rootPool *x509.CertPool
107166
cert *x509.Certificate
167+
issuer *x509.Certificate
168+
httpClient *http.Client
108169
)
109170

110171
switch addr, isURL, err := trimURL(crtFile); {
@@ -180,5 +241,215 @@ func verifyAction(ctx *cli.Context) error {
180241
return errors.Wrapf(err, "failed to verify certificate")
181242
}
182243

244+
verboseMSG := "certificate validated against roots\n"
245+
if host != "" {
246+
verboseMSG += "certificate host name validated\n"
247+
}
248+
249+
switch {
250+
case (verifyCRL || verifyOCSP) && roots != "":
251+
//nolint:gosec // using default configuration for 3rd party endpoints
252+
tlsConfig := &tls.Config{
253+
RootCAs: rootPool,
254+
}
255+
256+
transport := &http.Transport{
257+
TLSClientConfig: tlsConfig,
258+
}
259+
260+
httpClient = &http.Client{
261+
Transport: transport,
262+
}
263+
case verifyCRL || verifyOCSP:
264+
httpClient = &http.Client{}
265+
default:
266+
}
267+
268+
switch {
269+
case (verifyCRL || verifyOCSP) && issuerFile == "":
270+
if len(cert.IssuingCertificateURL) == 0 && issuerFile == "" {
271+
return errors.Errorf("could not get the issuing CA from the cert and no issuing CA certificate provided")
272+
}
273+
274+
resp, err := httpClient.Get(cert.IssuingCertificateURL[0])
275+
if err != nil {
276+
return errors.Errorf("failed to download the issuing CA")
277+
}
278+
defer resp.Body.Close()
279+
280+
body, err := io.ReadAll(resp.Body)
281+
if err != nil {
282+
return errors.Errorf("failed to read the response body from the issuing CA url")
283+
}
284+
285+
issuer, err = x509.ParseCertificate(body)
286+
if err != nil {
287+
return errors.Errorf("failed to parse the issuing CA")
288+
}
289+
case issuerFile != "":
290+
issuerCertPEM, err := os.ReadFile(issuerFile)
291+
if err != nil {
292+
return errors.Errorf("unable to load the issuing CA certificate file")
293+
}
294+
295+
issuerBlock, _ := pem.Decode(issuerCertPEM)
296+
if issuerBlock == nil || issuerBlock.Type != "CERTIFICATE" {
297+
return errors.Errorf("failed to decode the issuing CA certificate")
298+
}
299+
300+
issuer, err = x509.ParseCertificate(issuerBlock.Bytes)
301+
if err != nil {
302+
return errors.Errorf("failed to parse the issuing CA certificate")
303+
}
304+
default:
305+
}
306+
307+
if verifyCRL {
308+
var endpoints []string
309+
switch {
310+
case crlEndpoint != "":
311+
endpoints = []string{crlEndpoint}
312+
case len(cert.CRLDistributionPoints) == 0:
313+
return errors.Errorf("CRL distribution endpoint not found in certificate")
314+
default:
315+
endpoints = cert.CRLDistributionPoints
316+
}
317+
318+
crlVerified := false
319+
crlOut:
320+
for _, endpoint := range endpoints {
321+
respReceived, err := VerifyCRLEndpoint(endpoint, cert, issuer, httpClient, insecure)
322+
switch {
323+
case err == nil:
324+
verboseMSG += fmt.Sprintf("certificate not revoked in CRL %s\n", endpoint)
325+
crlVerified = true
326+
break crlOut
327+
case respReceived:
328+
return err
329+
case verbose:
330+
fmt.Println(err)
331+
default:
332+
}
333+
}
334+
335+
if !crlVerified {
336+
return errors.Errorf("could not verify certificate against CRL distribution point(s)")
337+
}
338+
}
339+
340+
if verifyOCSP {
341+
var endpoints []string
342+
switch {
343+
case ocspEndpoint != "":
344+
endpoints = []string{ocspEndpoint}
345+
case len(cert.OCSPServer) == 0:
346+
return errors.Errorf("no OCSP AIA extension found")
347+
default:
348+
endpoints = cert.OCSPServer
349+
}
350+
351+
ocspVerified := false
352+
ocspOut:
353+
for _, endpoint := range endpoints {
354+
respReceived, err := VerifyOCSPEndpoint(endpoint, cert, issuer, httpClient)
355+
switch {
356+
case err == nil:
357+
verboseMSG += fmt.Sprintf("certificate status is good according OCSP %s\n", endpoint)
358+
ocspVerified = true
359+
break ocspOut
360+
case respReceived:
361+
return err
362+
case verbose:
363+
fmt.Println(err)
364+
default:
365+
}
366+
}
367+
368+
if !ocspVerified {
369+
return errors.Errorf("could not verify certificate against OCSP server(s)")
370+
}
371+
}
372+
373+
if verbose {
374+
fmt.Println(verboseMSG + "certficiate is valid")
375+
}
183376
return nil
184377
}
378+
379+
func VerifyOCSPEndpoint(endpoint string, cert, issuer *x509.Certificate, httpClient *http.Client) (bool, error) {
380+
req, err := ocsp.CreateRequest(cert, issuer, nil)
381+
if err != nil {
382+
return false, errors.Errorf("error creating OCSP request")
383+
}
384+
385+
httpReq, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(req))
386+
if err != nil {
387+
return false, errors.Errorf("error contacting OCSP server: %s", endpoint)
388+
}
389+
httpReq.Header.Add("Content-Type", "application/ocsp-request")
390+
httpResp, err := httpClient.Do(httpReq)
391+
if err != nil {
392+
return false, errors.Errorf("error contacting OCSP server: %s", endpoint)
393+
}
394+
defer httpResp.Body.Close()
395+
respBytes, err := io.ReadAll(httpResp.Body)
396+
if err != nil {
397+
return false, errors.Errorf("error reading response from OCSP server: %s", endpoint)
398+
}
399+
400+
resp, err := ocsp.ParseResponse(respBytes, issuer)
401+
if err != nil {
402+
return false, errors.Errorf("error parsing response from OCSP server: %s", endpoint)
403+
}
404+
405+
switch resp.Status {
406+
case ocsp.Revoked:
407+
return true, errors.Errorf("certificate has been revoked according to OCSP %s", endpoint)
408+
case ocsp.Good:
409+
return true, nil
410+
default:
411+
return true, errors.Errorf("certificate status is unknown according to OCSP %s", endpoint)
412+
}
413+
}
414+
415+
func VerifyCRLEndpoint(endpoint string, cert, issuer *x509.Certificate, httpClient *http.Client, insecure bool) (bool, error) {
416+
resp, err := httpClient.Get(endpoint)
417+
if err != nil {
418+
return false, errors.Wrap(err, "error downloading crl")
419+
}
420+
defer resp.Body.Close()
421+
422+
if resp.StatusCode >= 400 {
423+
return false, errors.Errorf("error downloading crl: status code %d", resp.StatusCode)
424+
}
425+
426+
b, err := io.ReadAll(resp.Body)
427+
if err != nil {
428+
return false, errors.Wrap(err, "error downloading crl")
429+
}
430+
431+
crl, err := x509.ParseRevocationList(b)
432+
if err != nil {
433+
return false, errors.Wrap(err, "error parsing crl")
434+
}
435+
436+
crlJSON, err := crlutil.ParseCRL(b)
437+
if err != nil {
438+
return false, errors.Wrap(err, "error parsing crl into json")
439+
}
440+
441+
if issuer != nil && !insecure {
442+
err = crl.CheckSignatureFrom(issuer)
443+
if err != nil {
444+
return false, errors.Wrap(err, "error validating the CRL against the CA issuer")
445+
}
446+
}
447+
448+
for _, revoked := range crlJSON.RevokedCertificates {
449+
if cert.SerialNumber.String() == revoked.SerialNumber {
450+
return true, errors.Errorf("certificate marked as revoked in CRL %s", endpoint)
451+
}
452+
}
453+
454+
return true, nil
455+
}

0 commit comments

Comments
 (0)