From d64d33a6fbffccab1ffab2ced8546084bb7aa530 Mon Sep 17 00:00:00 2001 From: Alex Carder Date: Tue, 23 Apr 2024 14:45:24 -0700 Subject: [PATCH] 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 --- command/certificate/verify.go | 275 +++++++++++++++- command/crl/inspect.go | 305 +---------------- .../crlutil}/crl_extensions.go | 2 +- internal/crlutil/crlutil.go | 307 ++++++++++++++++++ .../crlutil}/signature_algorithms.go | 2 +- 5 files changed, 586 insertions(+), 305 deletions(-) rename {command/crl => internal/crlutil}/crl_extensions.go (99%) create mode 100644 internal/crlutil/crlutil.go rename {command/crl => internal/crlutil}/signature_algorithms.go (99%) diff --git a/command/certificate/verify.go b/command/certificate/verify.go index 9ec25d139..b0a4fffd7 100644 --- a/command/certificate/verify.go +++ b/command/certificate/verify.go @@ -1,15 +1,22 @@ package certificate import ( + "bytes" + "crypto/tls" "crypto/x509" "encoding/pem" + "fmt" + "io" + "net/http" "os" "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 { @@ -18,7 +25,10 @@ func verifyCommand() cli.Command { Action: cli.ActionFunc(verifyAction), Usage: `verify a certificate`, UsageText: `**step certificate verify** [**--host**=] -[**--roots**=] [**--servername**=]`, +[**--roots**=] [**--servername**=] +[**--issuing-ca**=] [**--verbose**] +[**--verify-ocsp**]] [**--ocsp-endpoint**]=url +[**--verify-crl**] [**--crl-endpoint**]=url`, 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 @@ -65,7 +75,24 @@ 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 using CRL and OCSP defined in the certificate + +''' +$ step certificate verify ./certificate.crt --verify-crl --verify-ocsp +''' + +Verify a certificate including OCSP and specifying an OCSP server + +''' +$ step certificate verify ./certificate.crt --verify-ocsp --ocsp-endpoint http://acme.com/ocsp +''' + +Verify a certificate including CRL and specificing a CRL server and providing the issuing CA certificate + +''' +$ step certificate verify ./certificate.crt --issuing-ca ./issuing_ca.pem --verify-crl --crl-endpoint http://acme.com/crl +'''`, Flags: []cli.Flag{ cli.StringFlag{ Name: "host", @@ -87,7 +114,32 @@ 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: "issuing-ca", + Usage: `The certificate issuer CA needed to communicate with OCSP and verify a CRL. By default the issuing CA will be taken from the cert Issuing Certificate URL extension.`, + }, + cli.BoolFlag{ + Name: "verify-ocsp", + Usage: "Verify the certificate against it's OCSP.", + }, + cli.StringFlag{ + Name: "ocsp-endpoint", + Usage: `The OCSP endpoint to use. If not provided step will attempt to check it against the certificate's OCSPServer AIA extension endpoints.`, + }, + cli.BoolFlag{ + Name: "verify-crl", + Usage: "Verify the certificate against it's CRL.", + }, + cli.StringFlag{ + Name: "crl-endpoint", + Usage: "The CRL endpoint to use. If not provided step will attempt to check it against the certificate's CRLDistributionPoints extension endpoints.", + }, + cli.BoolFlag{ + Name: "verbose, v", + Usage: "Print result of certificate verification to stdout on success", + }, flags.ServerName, + flags.Insecure, }, } } @@ -102,9 +154,18 @@ func verifyAction(ctx *cli.Context) error { host = ctx.String("host") serverName = ctx.String("servername") roots = ctx.String("roots") + verifyOCSP = ctx.Bool("verify-ocsp") + ocspEndpoint = ctx.String("ocsp-endpoint") + verifyCRL = ctx.Bool("verify-crl") + crlEndpoint = ctx.String("crl-endpoint") + verbose = ctx.Bool("verbose") + issuerFile = ctx.String("issuing-ca") + insecure = ctx.Bool("insecure") intermediatePool = x509.NewCertPool() rootPool *x509.CertPool cert *x509.Certificate + issuer *x509.Certificate + httpClient *http.Client ) switch addr, isURL, err := trimURL(crtFile); { @@ -180,5 +241,215 @@ func verifyAction(ctx *cli.Context) error { return errors.Wrapf(err, "failed to verify certificate") } + verboseMSG := "certificate validated against roots\n" + if host != "" { + verboseMSG += "certificate host name validated\n" + } + + switch { + case (verifyCRL || verifyOCSP) && roots != "": + //nolint:gosec // using default configuration for 3rd party endpoints + tlsConfig := &tls.Config{ + RootCAs: rootPool, + } + + transport := &http.Transport{ + TLSClientConfig: tlsConfig, + } + + httpClient = &http.Client{ + Transport: transport, + } + case verifyCRL || verifyOCSP: + httpClient = &http.Client{} + default: + } + + switch { + case (verifyCRL || verifyOCSP) && issuerFile == "": + if len(cert.IssuingCertificateURL) == 0 && issuerFile == "" { + return errors.Errorf("could not get the issuing CA from the cert and no issuing CA certificate provided") + } + + resp, err := httpClient.Get(cert.IssuingCertificateURL[0]) + if err != nil { + return errors.Errorf("failed to download the issuing CA") + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return errors.Errorf("failed to read the response body from the issuing CA url") + } + + issuer, err = x509.ParseCertificate(body) + if err != nil { + return errors.Errorf("failed to parse the issuing CA") + } + case issuerFile != "": + issuerCertPEM, err := os.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") + } + default: + } + + if verifyCRL { + var endpoints []string + switch { + case crlEndpoint != "": + endpoints = []string{crlEndpoint} + case len(cert.CRLDistributionPoints) == 0: + return errors.Errorf("CRL distribution endpoint not found in certificate") + default: + endpoints = cert.CRLDistributionPoints + } + + crlVerified := false + crlOut: + for _, endpoint := range endpoints { + respReceived, err := VerifyCRLEndpoint(endpoint, cert, issuer, httpClient, insecure) + switch { + case err == nil: + verboseMSG += fmt.Sprintf("certificate not revoked in CRL %s\n", endpoint) + crlVerified = true + break crlOut + case respReceived: + return err + case verbose: + fmt.Println(err) + default: + } + } + + if !crlVerified { + return errors.Errorf("could not verify certificate against CRL distribution point(s)") + } + } + + if verifyOCSP { + var endpoints []string + switch { + case ocspEndpoint != "": + endpoints = []string{ocspEndpoint} + case len(cert.OCSPServer) == 0: + return errors.Errorf("no OCSP AIA extension found") + default: + endpoints = cert.OCSPServer + } + + ocspVerified := false + ocspOut: + for _, endpoint := range endpoints { + respReceived, err := VerifyOCSPEndpoint(endpoint, cert, issuer, httpClient) + switch { + case err == nil: + verboseMSG += fmt.Sprintf("certificate status is good according OCSP %s\n", endpoint) + ocspVerified = true + break ocspOut + case respReceived: + return err + case verbose: + fmt.Println(err) + default: + } + } + + if !ocspVerified { + return errors.Errorf("could not verify certificate against OCSP server(s)") + } + } + + if verbose { + fmt.Println(verboseMSG + "certficiate is valid") + } return nil } + +func VerifyOCSPEndpoint(endpoint string, cert, issuer *x509.Certificate, httpClient *http.Client) (bool, error) { + req, err := ocsp.CreateRequest(cert, issuer, nil) + if err != nil { + return false, errors.Errorf("error creating OCSP request") + } + + httpReq, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(req)) + if err != nil { + return false, errors.Errorf("error contacting OCSP server: %s", endpoint) + } + httpReq.Header.Add("Content-Type", "application/ocsp-request") + httpResp, err := httpClient.Do(httpReq) + if err != nil { + return false, errors.Errorf("error contacting OCSP server: %s", endpoint) + } + defer httpResp.Body.Close() + respBytes, err := io.ReadAll(httpResp.Body) + if err != nil { + return false, errors.Errorf("error reading response from OCSP server: %s", endpoint) + } + + resp, err := ocsp.ParseResponse(respBytes, issuer) + if err != nil { + return false, errors.Errorf("error parsing response from OCSP server: %s", endpoint) + } + + switch resp.Status { + case ocsp.Revoked: + return true, errors.Errorf("certificate has been revoked according to OCSP %s", endpoint) + case ocsp.Good: + return true, nil + default: + return true, errors.Errorf("certificate status is unknown according to OCSP %s", endpoint) + } +} + +func VerifyCRLEndpoint(endpoint string, cert, issuer *x509.Certificate, httpClient *http.Client, insecure bool) (bool, error) { + resp, err := httpClient.Get(endpoint) + if err != nil { + return false, errors.Wrap(err, "error downloading crl") + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + return false, errors.Errorf("error downloading crl: status code %d", resp.StatusCode) + } + + b, err := io.ReadAll(resp.Body) + if err != nil { + return false, errors.Wrap(err, "error downloading crl") + } + + crl, err := x509.ParseRevocationList(b) + if err != nil { + return false, errors.Wrap(err, "error parsing crl") + } + + crlJSON, err := crlutil.ParseCRL(b) + if err != nil { + return false, errors.Wrap(err, "error parsing crl into json") + } + + if issuer != nil && !insecure { + err = crl.CheckSignatureFrom(issuer) + if err != nil { + return false, errors.Wrap(err, "error validating the CRL against the CA issuer") + } + } + + for _, revoked := range crlJSON.RevokedCertificates { + if cert.SerialNumber.String() == revoked.SerialNumber { + return true, errors.Errorf("certificate marked as revoked in CRL %s", endpoint) + } + } + + return true, nil +} diff --git a/command/crl/inspect.go b/command/crl/inspect.go index ff7277d67..63b376110 100644 --- a/command/crl/inspect.go +++ b/command/crl/inspect.go @@ -2,30 +2,21 @@ package crl import ( "bytes" - "crypto" - "crypto/ecdsa" - "crypto/ed25519" - "crypto/rsa" "crypto/tls" "crypto/x509" - "crypto/x509/pkix" - "encoding/asn1" "encoding/json" "encoding/pem" - "fmt" "io" - "math/big" "net" "net/http" "net/url" "os" - "strconv" "strings" - "time" "github.com/pkg/errors" "github.com/smallstep/cli/flags" "github.com/smallstep/cli/utils" + "github.com/smallstep/cli/internal/crlutil" "github.com/urfave/cli" "go.step.sm/cli-utils/command" "go.step.sm/cli-utils/errs" @@ -226,7 +217,7 @@ func inspectAction(ctx *cli.Context) error { } } - crl, err := ParseCRL(b) + crl, err := crlutil.ParseCRL(b) if err != nil { return errors.Wrap(err, "error parsing crl") } @@ -236,7 +227,7 @@ func inspectAction(ctx *cli.Context) error { if (crt.KeyUsage&x509.KeyUsageCRLSign) == 0 || len(crt.SubjectKeyId) == 0 { continue } - if crl.authorityKeyID == nil || bytes.Equal(crt.SubjectKeyId, crl.authorityKeyID) { + if crl.AuthorityKeyID == nil || bytes.Equal(crt.SubjectKeyId, crl.AuthorityKeyID) { if crl.Verify(crt) { crl.Signature.Valid = true } @@ -257,296 +248,8 @@ func inspectAction(ctx *cli.Context) error { Bytes: b, }) default: - printCRL(crl) + crlutil.PrintCRL(crl) } return nil } - -// CRL is the JSON representation of a certificate revocation list. -type CRL struct { - Version *big.Int `json:"version"` - SignatureAlgorithm SignatureAlgorithm `json:"signature_algorithm"` - Issuer DistinguishedName `json:"issuer"` - ThisUpdate time.Time `json:"this_update"` - NextUpdate time.Time `json:"next_update"` - RevokedCertificates []RevokedCertificate `json:"revoked_certificates"` - Extensions []Extension `json:"extensions,omitempty"` - Signature *Signature `json:"signature"` - authorityKeyID []byte - raw []byte -} - -// pemCRLPrefix is the magic string that indicates that we have a PEM encoded -// CRL. -var pemCRLPrefix = []byte("-----BEGIN X509 CRL") - -// pemType is the type of a PEM encoded CRL. -var pemType = "X509 CRL" - -func ParseCRL(b []byte) (*CRL, error) { - if bytes.HasPrefix(b, pemCRLPrefix) { - block, _ := pem.Decode(b) - if block != nil && block.Type == pemType { - b = block.Bytes - } - } - - crl, err := x509.ParseRevocationList(b) - if err != nil { - return nil, errors.Wrap(err, "error parsing crl") - } - - certs := make([]RevokedCertificate, len(crl.RevokedCertificateEntries)) - for i, c := range crl.RevokedCertificateEntries { - certs[i] = newRevokedCertificate(c) - } - - var issuerKeyID []byte - extensions := make([]Extension, len(crl.Extensions)) - for i, e := range crl.Extensions { - extensions[i] = newExtension(e) - if e.Id.Equal(oidExtensionAuthorityKeyID) { - var v authorityKeyID - if _, err := asn1.Unmarshal(e.Value, &v); err == nil { - issuerKeyID = v.ID - } - } - } - - sa := newSignatureAlgorithm(crl.SignatureAlgorithm) - - return &CRL{ - Version: crl.Number.Add(crl.Number, big.NewInt(1)), - SignatureAlgorithm: sa, - Issuer: newDistinguishedName(crl.Issuer), - ThisUpdate: crl.ThisUpdate, - NextUpdate: crl.NextUpdate, - RevokedCertificates: certs, - Extensions: extensions, - Signature: &Signature{ - SignatureAlgorithm: sa, - Value: crl.Signature, - Valid: false, - Reason: "", - }, - authorityKeyID: issuerKeyID, - raw: crl.RawTBSRevocationList, - }, nil -} - -func (c *CRL) Verify(ca *x509.Certificate) bool { - now := time.Now() - if now.After(c.NextUpdate) { - c.Signature.Reason = "CRL has expired" - return false - } - if now.After(ca.NotAfter) { - c.Signature.Reason = "CA certificate has expired" - return false - } - - if !c.VerifySignature(ca) { - c.Signature.Reason = "Signature does not match" - return false - } - - return true -} - -func (c *CRL) VerifySignature(ca *x509.Certificate) bool { - var sum []byte - var hash crypto.Hash - if hash = c.SignatureAlgorithm.hash; hash > 0 { - h := hash.New() - h.Write(c.raw) - sum = h.Sum(nil) - } - - sig := c.Signature.Value - switch pub := ca.PublicKey.(type) { - case *ecdsa.PublicKey: - return ecdsa.VerifyASN1(pub, sum, sig) - case *rsa.PublicKey: - switch c.SignatureAlgorithm.algo { - case x509.SHA256WithRSAPSS, x509.SHA384WithRSAPSS, x509.SHA512WithRSAPSS: - return rsa.VerifyPSS(pub, hash, sum, sig, &rsa.PSSOptions{ - SaltLength: rsa.PSSSaltLengthAuto, - }) == nil - default: - return rsa.VerifyPKCS1v15(pub, hash, sum, sig) == nil - } - case ed25519.PublicKey: - return ed25519.Verify(pub, c.raw, sig) - default: - return false - } -} - -func printCRL(crl *CRL) { - fmt.Println("Certificate Revocation List (CRL):") - fmt.Println(" Data:") - fmt.Printf(" Valid: %v\n", crl.Signature.Valid) - if crl.Signature.Reason != "" { - fmt.Printf(" Reason: %s\n", crl.Signature.Reason) - } - fmt.Printf(" Version: %d (0x%x)\n", crl.Version, crl.Version.Add(crl.Version, big.NewInt(-1))) - fmt.Println(" Signature algorithm:", crl.SignatureAlgorithm) - fmt.Println(" Issuer:", crl.Issuer) - fmt.Println(" Last Update:", crl.ThisUpdate.UTC()) - fmt.Println(" Next Update:", crl.NextUpdate.UTC()) - fmt.Println(" CRL Extensions:") - for _, e := range crl.Extensions { - fmt.Println(spacer(12) + e.Name) - for _, s := range e.Details { - fmt.Println(spacer(16) + s) - } - } - if len(crl.RevokedCertificates) == 0 { - fmt.Println(spacer(8) + "No Revoked Certificates.") - } else { - fmt.Println(spacer(8) + "Revoked Certificates:") - for _, crt := range crl.RevokedCertificates { - fmt.Printf(spacer(12)+"Serial Number: %s (0x%X)\n", crt.SerialNumber, crt.SerialNumberBytes) - fmt.Println(spacer(16)+"Revocation Date:", crt.RevocationTime.UTC()) - if len(crt.Extensions) > 0 { - fmt.Println(spacer(16) + "CRL Entry Extensions:") - for _, e := range crt.Extensions { - fmt.Println(spacer(20) + e.Name) - for _, s := range e.Details { - fmt.Println(spacer(24) + s) - } - } - } - } - } - - fmt.Println(" Signature Algorithm:", crl.Signature.SignatureAlgorithm) - printBytes(crl.Signature.Value, spacer(8)) -} - -// Signature is the JSON representation of a CRL signature. -type Signature struct { - SignatureAlgorithm SignatureAlgorithm `json:"signature_algorithm"` - Value []byte `json:"value"` - Valid bool `json:"valid"` - Reason string `json:"reason,omitempty"` -} - -// DistinguishedName is the JSON representation of the CRL issuer. -type DistinguishedName struct { - Country []string `json:"country,omitempty"` - Organization []string `json:"organization,omitempty"` - OrganizationalUnit []string `json:"organizational_unit,omitempty"` - Locality []string `json:"locality,omitempty"` - Province []string `json:"province,omitempty"` - StreetAddress []string `json:"street_address,omitempty"` - PostalCode []string `json:"postal_code,omitempty"` - SerialNumber string `json:"serial_number,omitempty"` - CommonName string `json:"common_name,omitempty"` - ExtraNames map[string][]interface{} `json:"extra_names,omitempty"` - dn pkix.Name -} - -// String returns the one line representation of the distinguished name. -func (d DistinguishedName) String() string { - return d.dn.String() -} - -func newDistinguishedName(dn pkix.Name) DistinguishedName { - var extraNames map[string][]interface{} - if len(dn.ExtraNames) > 0 { - extraNames = make(map[string][]interface{}) - for _, tv := range dn.ExtraNames { - oid := tv.Type.String() - if s, ok := tv.Value.(string); ok { - extraNames[oid] = append(extraNames[oid], s) - continue - } - if b, err := asn1.Marshal(tv.Value); err == nil { - extraNames[oid] = append(extraNames[oid], b) - continue - } - extraNames[oid] = append(extraNames[oid], escapeValue(tv.Value)) - } - } - - return DistinguishedName{ - Country: dn.Country, - Organization: dn.Organization, - OrganizationalUnit: dn.OrganizationalUnit, - Locality: dn.Locality, - Province: dn.Province, - StreetAddress: dn.StreetAddress, - PostalCode: dn.PostalCode, - SerialNumber: dn.SerialNumber, - CommonName: dn.CommonName, - ExtraNames: extraNames, - } -} - -// RevokedCertificate is the JSON representation of a certificate in a CRL. -type RevokedCertificate struct { - SerialNumber string `json:"serial_number"` - RevocationTime time.Time `json:"revocation_time"` - Extensions []Extension `json:"extensions,omitempty"` - SerialNumberBytes []byte `json:"-"` -} - -func newRevokedCertificate(c x509.RevocationListEntry) RevokedCertificate { - var extensions []Extension - - return RevokedCertificate{ - SerialNumber: c.SerialNumber.String(), - RevocationTime: c.RevocationTime.UTC(), - Extensions: extensions, - SerialNumberBytes: c.SerialNumber.Bytes(), - } -} - -func spacer(i int) string { - return fmt.Sprintf("%"+strconv.Itoa(i)+"s", "") -} - -func printBytes(bs []byte, prefix string) { - for i, b := range bs { - if i == 0 { - fmt.Print(prefix) - } else if (i % 16) == 0 { - fmt.Print("\n" + prefix) - } - fmt.Printf("%02x", b) - if i != len(bs)-1 { - fmt.Print(":") - } - } - fmt.Println() -} - -func escapeValue(v interface{}) string { - s := fmt.Sprint(v) - escaped := make([]rune, 0, len(s)) - - for k, c := range s { - escape := false - - switch c { - case ',', '+', '"', '\\', '<', '>', ';': - escape = true - - case ' ': - escape = k == 0 || k == len(s)-1 - - case '#': - escape = k == 0 - } - - if escape { - escaped = append(escaped, '\\', c) - } else { - escaped = append(escaped, c) - } - } - - return string(escaped) -} diff --git a/command/crl/crl_extensions.go b/internal/crlutil/crl_extensions.go similarity index 99% rename from command/crl/crl_extensions.go rename to internal/crlutil/crl_extensions.go index 22bc98be5..c03b0d266 100644 --- a/command/crl/crl_extensions.go +++ b/internal/crlutil/crl_extensions.go @@ -1,4 +1,4 @@ -package crl +package crlutil import ( "bytes" diff --git a/internal/crlutil/crlutil.go b/internal/crlutil/crlutil.go new file mode 100644 index 000000000..8179d10d8 --- /dev/null +++ b/internal/crlutil/crlutil.go @@ -0,0 +1,307 @@ +package crlutil + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/pem" + "fmt" + "math/big" + "strconv" + "time" + + "github.com/pkg/errors" +) + +// CRL is the JSON representation of a certificate revocation list. +type CRL struct { + Version *big.Int `json:"version"` + SignatureAlgorithm SignatureAlgorithm `json:"signature_algorithm"` + Issuer DistinguishedName `json:"issuer"` + ThisUpdate time.Time `json:"this_update"` + NextUpdate time.Time `json:"next_update"` + RevokedCertificates []RevokedCertificate `json:"revoked_certificates"` + Extensions []Extension `json:"extensions,omitempty"` + Signature *Signature `json:"signature"` + AuthorityKeyID []byte + Raw []byte +} + +// pemCRLPrefix is the magic string that indicates that we have a PEM encoded +// CRL. +var pemCRLPrefix = []byte("-----BEGIN X509 CRL") + +// pemType is the type of a PEM encoded CRL. +var pemType = "X509 CRL" + +func ParseCRL(b []byte) (*CRL, error) { + if bytes.HasPrefix(b, pemCRLPrefix) { + block, _ := pem.Decode(b) + if block != nil && block.Type == pemType { + b = block.Bytes + } + } + + crl, err := x509.ParseRevocationList(b) + if err != nil { + return nil, errors.Wrap(err, "error parsing crl") + } + + certs := make([]RevokedCertificate, len(crl.RevokedCertificateEntries)) + for i, c := range crl.RevokedCertificateEntries { + certs[i] = newRevokedCertificate(c) + } + + var issuerKeyID []byte + extensions := make([]Extension, len(crl.Extensions)) + for i, e := range crl.Extensions { + extensions[i] = newExtension(e) + if e.Id.Equal(oidExtensionAuthorityKeyID) { + var v authorityKeyID + if _, err := asn1.Unmarshal(e.Value, &v); err == nil { + issuerKeyID = v.ID + } + } + } + + sa := newSignatureAlgorithm(crl.SignatureAlgorithm) + + return &CRL{ + Version: crl.Number.Add(crl.Number, big.NewInt(1)), + SignatureAlgorithm: sa, + Issuer: newDistinguishedName(crl.Issuer), + ThisUpdate: crl.ThisUpdate, + NextUpdate: crl.NextUpdate, + RevokedCertificates: certs, + Extensions: extensions, + Signature: &Signature{ + SignatureAlgorithm: sa, + Value: crl.Signature, + Valid: false, + Reason: "", + }, + AuthorityKeyID: issuerKeyID, + Raw: crl.RawTBSRevocationList, + }, nil +} + +func (c *CRL) Verify(ca *x509.Certificate) bool { + now := time.Now() + if now.After(c.NextUpdate) { + c.Signature.Reason = "CRL has expired" + return false + } + if now.After(ca.NotAfter) { + c.Signature.Reason = "CA certificate has expired" + return false + } + + if !c.VerifySignature(ca) { + c.Signature.Reason = "Signature does not match" + return false + } + + return true +} + +func (c *CRL) VerifySignature(ca *x509.Certificate) bool { + var sum []byte + var hash crypto.Hash + if hash = c.SignatureAlgorithm.hash; hash > 0 { + h := hash.New() + h.Write(c.Raw) + sum = h.Sum(nil) + } + + sig := c.Signature.Value + switch pub := ca.PublicKey.(type) { + case *ecdsa.PublicKey: + return ecdsa.VerifyASN1(pub, sum, sig) + case *rsa.PublicKey: + switch c.SignatureAlgorithm.algo { + case x509.SHA256WithRSAPSS, x509.SHA384WithRSAPSS, x509.SHA512WithRSAPSS: + return rsa.VerifyPSS(pub, hash, sum, sig, &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthAuto, + }) == nil + default: + return rsa.VerifyPKCS1v15(pub, hash, sum, sig) == nil + } + case ed25519.PublicKey: + return ed25519.Verify(pub, c.Raw, sig) + default: + return false + } +} + +func PrintCRL(crl *CRL) { + fmt.Println("Certificate Revocation List (CRL):") + fmt.Println(" Data:") + fmt.Printf(" Valid: %v\n", crl.Signature.Valid) + if crl.Signature.Reason != "" { + fmt.Printf(" Reason: %s\n", crl.Signature.Reason) + } + fmt.Printf(" Version: %d (0x%x)\n", crl.Version, crl.Version.Add(crl.Version, big.NewInt(-1))) + fmt.Println(" Signature algorithm:", crl.SignatureAlgorithm) + fmt.Println(" Issuer:", crl.Issuer) + fmt.Println(" Last Update:", crl.ThisUpdate.UTC()) + fmt.Println(" Next Update:", crl.NextUpdate.UTC()) + fmt.Println(" CRL Extensions:") + for _, e := range crl.Extensions { + fmt.Println(spacer(12) + e.Name) + for _, s := range e.Details { + fmt.Println(spacer(16) + s) + } + } + if len(crl.RevokedCertificates) == 0 { + fmt.Println(spacer(8) + "No Revoked Certificates.") + } else { + fmt.Println(spacer(8) + "Revoked Certificates:") + for _, crt := range crl.RevokedCertificates { + fmt.Printf(spacer(12)+"Serial Number: %s (0x%X)\n", crt.SerialNumber, crt.SerialNumberBytes) + fmt.Println(spacer(16)+"Revocation Date:", crt.RevocationTime.UTC()) + if len(crt.Extensions) > 0 { + fmt.Println(spacer(16) + "CRL Entry Extensions:") + for _, e := range crt.Extensions { + fmt.Println(spacer(20) + e.Name) + for _, s := range e.Details { + fmt.Println(spacer(24) + s) + } + } + } + } + } + + fmt.Println(" Signature Algorithm:", crl.Signature.SignatureAlgorithm) + printBytes(crl.Signature.Value, spacer(8)) +} + +// Signature is the JSON representation of a CRL signature. +type Signature struct { + SignatureAlgorithm SignatureAlgorithm `json:"signature_algorithm"` + Value []byte `json:"value"` + Valid bool `json:"valid"` + Reason string `json:"reason,omitempty"` +} + +// DistinguishedName is the JSON representation of the CRL issuer. +type DistinguishedName struct { + Country []string `json:"country,omitempty"` + Organization []string `json:"organization,omitempty"` + OrganizationalUnit []string `json:"organizational_unit,omitempty"` + Locality []string `json:"locality,omitempty"` + Province []string `json:"province,omitempty"` + StreetAddress []string `json:"street_address,omitempty"` + PostalCode []string `json:"postal_code,omitempty"` + SerialNumber string `json:"serial_number,omitempty"` + CommonName string `json:"common_name,omitempty"` + ExtraNames map[string][]interface{} `json:"extra_names,omitempty"` + dn pkix.Name +} + +// String returns the one line representation of the distinguished name. +func (d DistinguishedName) String() string { + return d.dn.String() +} + +func newDistinguishedName(dn pkix.Name) DistinguishedName { + var extraNames map[string][]interface{} + if len(dn.ExtraNames) > 0 { + extraNames = make(map[string][]interface{}) + for _, tv := range dn.ExtraNames { + oid := tv.Type.String() + if s, ok := tv.Value.(string); ok { + extraNames[oid] = append(extraNames[oid], s) + continue + } + if b, err := asn1.Marshal(tv.Value); err == nil { + extraNames[oid] = append(extraNames[oid], b) + continue + } + extraNames[oid] = append(extraNames[oid], escapeValue(tv.Value)) + } + } + + return DistinguishedName{ + Country: dn.Country, + Organization: dn.Organization, + OrganizationalUnit: dn.OrganizationalUnit, + Locality: dn.Locality, + Province: dn.Province, + StreetAddress: dn.StreetAddress, + PostalCode: dn.PostalCode, + SerialNumber: dn.SerialNumber, + CommonName: dn.CommonName, + ExtraNames: extraNames, + } +} + +// RevokedCertificate is the JSON representation of a certificate in a CRL. +type RevokedCertificate struct { + SerialNumber string `json:"serial_number"` + RevocationTime time.Time `json:"revocation_time"` + Extensions []Extension `json:"extensions,omitempty"` + SerialNumberBytes []byte `json:"-"` +} + +func newRevokedCertificate(c x509.RevocationListEntry) RevokedCertificate { + var extensions []Extension + + return RevokedCertificate{ + SerialNumber: c.SerialNumber.String(), + RevocationTime: c.RevocationTime.UTC(), + Extensions: extensions, + SerialNumberBytes: c.SerialNumber.Bytes(), + } +} + +func spacer(i int) string { + return fmt.Sprintf("%"+strconv.Itoa(i)+"s", "") +} + +func printBytes(bs []byte, prefix string) { + for i, b := range bs { + if i == 0 { + fmt.Print(prefix) + } else if (i % 16) == 0 { + fmt.Print("\n" + prefix) + } + fmt.Printf("%02x", b) + if i != len(bs)-1 { + fmt.Print(":") + } + } + fmt.Println() +} + +func escapeValue(v interface{}) string { + s := fmt.Sprint(v) + escaped := make([]rune, 0, len(s)) + + for k, c := range s { + escape := false + + switch c { + case ',', '+', '"', '\\', '<', '>', ';': + escape = true + + case ' ': + escape = k == 0 || k == len(s)-1 + + case '#': + escape = k == 0 + } + + if escape { + escaped = append(escaped, '\\', c) + } else { + escaped = append(escaped, c) + } + } + + return string(escaped) +} diff --git a/command/crl/signature_algorithms.go b/internal/crlutil/signature_algorithms.go similarity index 99% rename from command/crl/signature_algorithms.go rename to internal/crlutil/signature_algorithms.go index 5effbef6c..c09884e7f 100644 --- a/command/crl/signature_algorithms.go +++ b/internal/crlutil/signature_algorithms.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package crl +package crlutil import ( "crypto"