Skip to content

Commit

Permalink
Add OCSP and CRL support to certificate verify
Browse files Browse the repository at this point in the history
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
  • Loading branch information
redrac committed Apr 28, 2024
1 parent a7b2b3a commit 0642c10
Show file tree
Hide file tree
Showing 5 changed files with 453 additions and 304 deletions.
141 changes: 140 additions & 1 deletion command/certificate/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,20 @@ package certificate
import (
"crypto/x509"
"encoding/pem"
"net/http"
"os"
"io"
"io/ioutil"
"bytes"
"fmt"

"github.com/pkg/errors"
"github.com/smallstep/cli/flags"
"github.com/smallstep/cli/internal/crlutil"
"github.com/urfave/cli"
"go.step.sm/cli-utils/errs"
"go.step.sm/crypto/x509util"
"golang.org/x/crypto/ocsp"
)

func verifyCommand() cli.Command {
Expand All @@ -18,7 +25,9 @@ func verifyCommand() cli.Command {
Action: cli.ActionFunc(verifyAction),
Usage: `verify a certificate`,
UsageText: `**step certificate verify** <crt-file> [**--host**=<host>]
[**--roots**=<root-bundle>] [**--servername**=<servername>]`,
[**--roots**=<root-bundle>] [**--servername**=<servername>]
[**--ca**]=file [**--verify-verbose**]
[**--verify-ocsp**] [**-verify-crl**]`,
Description: `**step certificate verify** executes the certificate path
validation algorithm for x.509 certificates defined in RFC 5280. If the
certificate is valid this command will return '0'. If validation fails, or if
Expand Down Expand Up @@ -65,6 +74,12 @@ Verify a certificate using a custom directory of root certificates for path vali
'''
$ step certificate verify ./certificate.crt --roots ./root-certificates/
'''
Verify a certificate including OCSP and CRL
'''
$ step certificate verify ./certificate.crt --ca ./issuing_ca.pem --verify-crl --verify-ocsp
'''
`,
Flags: []cli.Flag{
cli.StringFlag{
Expand All @@ -87,6 +102,26 @@ authenticity of the remote server.
**directory**
: Relative or full path to a directory. Every PEM encoded certificate from each file in the directory will be used for path validation.`,
},
cli.StringFlag{
Name: "ca",
Usage: `The certificate issuer CA <file> needed to communicate with OCSP.`,
},
cli.BoolFlag{
Name: "verify-ocsp",
Usage: "Verify the certificate against it's OCSP if the certificate has the OCSP server is defined in the AIA extension.",
},
cli.BoolFlag{
Name: "verify-crl",
Usage: "Verify the certificate against it's CRL if the certificate has a CRL Distribution Point defined",
},
cli.StringFlag{
Name: "issuing-cert",
Usage: `The issuing CA certificate of the leaf certificate you want to verify.`,
},
cli.BoolFlag{
Name: "verify-verbose",
Usage: "Print result of certificate verification to stdout on success",
},
flags.ServerName,
},
}
Expand All @@ -102,11 +137,19 @@ func verifyAction(ctx *cli.Context) error {
host = ctx.String("host")
serverName = ctx.String("servername")
roots = ctx.String("roots")
verifyOCSP = ctx.Bool("verify-ocsp")
verifyCRL = ctx.Bool("verify-crl")
verbose = ctx.Bool("verify-verbose")
issuerFile = ctx.String("ca")
intermediatePool = x509.NewCertPool()
rootPool *x509.CertPool
cert *x509.Certificate
)

if issuerFile == "" && verifyOCSP {
return errors.Errorf("You must provide the issuinge CA certificate if you want to verify the certificate with OCSP")
}

switch addr, isURL, err := trimURL(crtFile); {
case err != nil:
return err
Expand Down Expand Up @@ -180,5 +223,101 @@ func verifyAction(ctx *cli.Context) error {
return errors.Wrapf(err, "failed to verify certificate")
}

verboseMSG := "certificate validated against roots\n"
if host != "" {
verboseMSG = verboseMSG + "certificate host name validated\n"
}

if verifyCRL {
if len(cert.CRLDistributionPoints) == 0 {
return errors.Errorf("CRL distribution endpoint not found in certificate")
}

httpClient := http.Client{}

resp, err := httpClient.Get(cert.CRLDistributionPoints[0])
if err != nil {
return errors.Wrap(err, "error downloading crl")
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return errors.Errorf("error downloading crl: status code %d", resp.StatusCode)
}
b, err := io.ReadAll(resp.Body)
if err != nil {
return errors.Wrap(err, "error downloading crl")
}

crl, err := crlutil.ParseCRL(b)
if err != nil {
return errors.Wrap(err, "error parsing crl")
}

for _, revoked := range crl.RevokedCertificates {
if cert.SerialNumber.String() == revoked.SerialNumber {
return errors.Errorf("certificate marked as revoked in CRL")
}
}
verboseMSG = verboseMSG + "certificate not revoked in CRL\n"
}

if verifyOCSP {
if len(cert.OCSPServer) == 0 {
return errors.Errorf("no OCSP AIA extension found")
}

issuerCertPEM, err := ioutil.ReadFile(issuerFile)
if err != nil {
return errors.Errorf("unable to load the issuing CA certificate file")
}

issuerBlock, _ := pem.Decode(issuerCertPEM)
if issuerBlock == nil || issuerBlock.Type != "CERTIFICATE" {
return errors.Errorf("failed to decode the issuing CA certificate")
}

issuer, err := x509.ParseCertificate(issuerBlock.Bytes)
if err != nil {
return errors.Errorf("failed to parse the issuing CA certificate")
}

req, err := ocsp.CreateRequest(cert, issuer, nil)
if err != nil {
return errors.Errorf("error creating OCSP request")
}

httpReq, err := http.NewRequest(http.MethodPost, cert.OCSPServer[0], bytes.NewReader(req))
if err != nil {
return errors.Errorf("error contacting OCSP server: %s", cert.OCSPServer[0])
}
httpReq.Header.Add("Content-Type", "application/ocsp-request")
httpResp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return errors.Errorf("error contacting OCSP server: %s", cert.OCSPServer[0])
}
defer httpResp.Body.Close()
respBytes, err := ioutil.ReadAll(httpResp.Body)
if err != nil {
return errors.Errorf("error reading response from OCSP server: %s", cert.OCSPServer[0])
}

resp, err := ocsp.ParseResponse(respBytes, issuer)
if err != nil {
return errors.Errorf("error parsing repsonse from OCSP server: %s", cert.OCSPServer[0])
}

switch resp.Status {
case ocsp.Revoked:
return errors.Errorf("certificate has been revoked according to OCSP")
case ocsp.Good:
default:
return errors.Errorf("certificate status is unknown")
}
verboseMSG = verboseMSG + "certificate status shows good in OCSP\n"
}

if verbose {
fmt.Println(verboseMSG + "certficiate is valid")
}
return nil
}
Loading

0 comments on commit 0642c10

Please sign in to comment.