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"