11package certificate
22
33import (
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
1522func 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
2333validation algorithm for x.509 certificates defined in RFC 5280. If the
2434certificate 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