capi/lib/service/interpretation.go (271 lines of code) (raw):

/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package service import ( "fmt" "github.com/mozilla/CCADB-Tools/capi/lib/expiration" "github.com/mozilla/CCADB-Tools/capi/lib/model" "github.com/mozilla/CCADB-Tools/capi/lib/revocation/crl" "github.com/mozilla/CCADB-Tools/capi/lib/revocation/ocsp" ) type Expectation int const ( None Expectation = iota Valid Expired Revoked ) func (e Expectation) String() string { switch e { case None: return "none" case Valid: return "valid" case Expired: return "expired" case Revoked: return "revoked" } return "" } func InterpretResult(result *model.TestWebsiteResult, expectation Expectation) { switch expectation { case Valid: //////// Expiration checks // Leaf must NOT be expired result.Opinion.Append(assertNotExpired(result.Chain.Leaf, Leaf)) // Intermediates must NOT be expired for _, intermediate := range result.Chain.Intermediates { result.Opinion.Append(assertNotExpired(intermediate, Intermediate)) } // Root must NOT be expired. result.Opinion.Append(assertNotExpired(result.Chain.Root, Root)) /////// Revocation checks // Leaf MUST be Good result.Opinion.Append(assertNotRevoked(result.Chain.Leaf, Leaf)) // Intermediates MUST be Good. for _, intermediate := range result.Chain.Intermediates { result.Opinion.Append(assertNotRevoked(intermediate, Intermediate)) } // Root must be Good result.Opinion.Append(assertNotRevoked(result.Chain.Root, Root)) case Expired: //////// Expiration checks // Leaf MUST be expired result.Opinion.Append(assertExpired(result.Chain.Leaf, Leaf)) // Intermediates MAY be expired for _, intermediate := range result.Chain.Intermediates { result.Opinion.Append(assertMayBeExpired(intermediate, Intermediate)) } // Root must NOT be expired. result.Opinion.Append(assertNotExpired(result.Chain.Root, Root)) /////// Revocation checks // // By policy, we do not care whether or not the leaf certificate // is revoked by any CRL or OCSP responder. // // Intermediates may be good (or Unauthorized iff they are expired) for _, intermediate := range result.Chain.Intermediates { result.Opinion.Append(assertNotRevoked(intermediate, Intermediate)) } // Root must be Good result.Opinion.Append(assertNotRevoked(result.Chain.Root, Root)) case Revoked: //////// Expiration checks // Leaf must not be expired. result.Opinion.Append(assertNotExpired(result.Chain.Leaf, Leaf)) // Intermediates must not be expired for _, intermediate := range result.Chain.Intermediates { result.Opinion.Append(assertNotExpired(intermediate, Intermediate)) } // Root must not be expired result.Opinion.Append(assertNotExpired(result.Chain.Root, Root)) /////// Revocation checks // Leaf MUST be revoked. result.Opinion.Append(assertRevoked(result.Chain.Leaf, Leaf)) // Intermediates MAY be revoked for _, intermediate := range result.Chain.Intermediates { result.Opinion.Append(assertMayBeRevoked(intermediate, Intermediate)) } // Root must NOT be revoked result.Opinion.Append(assertNotRevoked(result.Chain.Root, Root)) } switch len(result.Opinion.Errors) == 0 { case true: result.Opinion.Result = model.PASS case false: result.Opinion.Result = model.FAIL } } type CertType string const ( Root CertType = "root" Intermediate = "intermediate" Leaf = "leaf" ) func assertNotRevoked(cert model.CertificateResult, t CertType) (opinion model.Opinion) { for _, response := range cert.OCSP { if cert.Expiration.Status == expiration.Expired && response.Status == ocsp.Unauthorized && t != Root { continue } if response.Error != "" { interpretation := "" switch response.Status { case ocsp.CryptoVerifcationError: interpretation = fmt.Sprintf("OCSP responder %s could not verify the provided chain at the %s. This is usually accompanied by a verification error thrown by certutil.", response.Responder, t) case ocsp.BadResponse: interpretation = fmt.Sprintf("OCSP responder %s gave a bad response for the %s.", response.Responder, t) } opinion.Errors = append(opinion.Errors, model.Concern{ Raw: response.Error, Interpretation: interpretation, Advise: cert.CrtSh, }) } else if response.Status != ocsp.Good { opinion.Errors = append(opinion.Errors, model.Concern{ Raw: response.Error, Interpretation: fmt.Sprintf("%s is `%s` by OCSP responder %s", t, response.Status.String(), response.Responder), Advise: cert.CrtSh, }) } } for _, crlStatus := range cert.CRL { if crlStatus.Status == crl.Unchecked { continue } if crlStatus.Status == crl.Revoked { opinion.Errors = append(opinion.Errors, model.Concern{ Raw: "", Interpretation: fmt.Sprintf("%s is revoked by CRL endpoint %s", t, crlStatus.Endpoint), Advise: cert.CrtSh, }) } if crlStatus.Error != "" { opinion.Errors = append(opinion.Errors, model.Concern{ Raw: crlStatus.Error, Interpretation: "An error occurred while retrieving the CRL. This is usually a networking error", Advise: fmt.Sprintf("If this is a networking error, attempt to verify that CRL endpoint at %s is active and available", crlStatus.Endpoint), }) } } return } func assertNotExpired(cert model.CertificateResult, t CertType) (opinion model.Opinion) { if cert.Expiration.Status == expiration.Expired { opinion.Errors = append(opinion.Errors, model.Concern{ Raw: cert.Expiration.Error, Interpretation: fmt.Sprintf("%s is expired", t), Advise: cert.CrtSh, }) } if cert.Expiration.Status == expiration.IssuerUnknown { opinion.Errors = append(opinion.Errors, model.Concern{ Raw: cert.Expiration.Raw, Interpretation: fmt.Sprintf("bad chain at %s", t), Advise: cert.CrtSh, }) } if cert.Expiration.Error != "" { opinion.Errors = append(opinion.Errors, model.Concern{ Raw: cert.Expiration.Error, Interpretation: fmt.Sprintf("fatal error at %s", t), Advise: cert.CrtSh, }) } return } func assertExpired(cert model.CertificateResult, t CertType) (opinion model.Opinion) { if cert.Expiration.Status != expiration.Expired { opinion.Errors = append(opinion.Errors, model.Concern{ Raw: cert.Expiration.Error, Interpretation: fmt.Sprintf("%s is not expired", t), Advise: cert.CrtSh, }) } if cert.Expiration.Status == expiration.IssuerUnknown { opinion.Errors = append(opinion.Errors, model.Concern{ Raw: cert.Expiration.Raw, Interpretation: fmt.Sprintf("bad chain at %s", t), Advise: cert.CrtSh, }) } if cert.Expiration.Error != "" { opinion.Errors = append(opinion.Errors, model.Concern{ Raw: cert.Expiration.Error, Interpretation: fmt.Sprintf("certutil encountered a fatal error when attempting to verify the %s certificate, %s", t, cert.Fingerprint), Advise: "This is likely an error in CAPI", }) } return } func assertMayBeExpired(cert model.CertificateResult, t CertType) (opinion model.Opinion) { if cert.Expiration.Status == expiration.IssuerUnknown { opinion.Errors = append(opinion.Errors, model.Concern{ Raw: cert.Expiration.Raw, Interpretation: fmt.Sprintf("bad chain at %s", t), Advise: cert.CrtSh, }) } if cert.Expiration.Error != "" { opinion.Errors = append(opinion.Errors, model.Concern{ Raw: cert.Expiration.Error, Interpretation: fmt.Sprintf("certutil encountered a fatal error when attempting to verify the %s certificate, %s", t, cert.Fingerprint), Advise: "This is likely an error in CAPI", }) } return } func assertRevoked(cert model.CertificateResult, t CertType) (opinion model.Opinion) { for _, response := range cert.OCSP { if cert.Expiration.Status == expiration.Expired && response.Status == ocsp.Unauthorized && t != Root { continue } if response.Status == ocsp.Revoked { continue } if response.Error != "" { interpretation := "" switch response.Status { case ocsp.CryptoVerifcationError: interpretation = fmt.Sprintf("OCSP responder %s could not verify the provided chain at the %s. This is usually accompanied by a verification error thrown by certutil.", response.Responder, t) case ocsp.BadResponse: interpretation = fmt.Sprintf("OCSP responder %s gave a bad response for the %s.", response.Responder, t) } opinion.Errors = append(opinion.Errors, model.Concern{ Raw: response.Error, Interpretation: interpretation, Advise: cert.CrtSh, }) } else { opinion.Errors = append(opinion.Errors, model.Concern{ Raw: response.Status.String(), Interpretation: fmt.Sprintf("%s is `%s` by OCSP responder %s", t, response.Status.String(), response.Responder), Advise: cert.CrtSh, }) } } for _, crlStatus := range cert.CRL { if crlStatus.Status == crl.Unchecked { continue } if crlStatus.Status != crl.Revoked { opinion.Errors = append(opinion.Errors, model.Concern{ Raw: crlStatus.Error, Interpretation: fmt.Sprintf("%s is not revoked by CRL endpoint %s", t, crlStatus.Endpoint), Advise: cert.CrtSh, }) } if crlStatus.Error != "" { opinion.Errors = append(opinion.Errors, model.Concern{ Raw: crlStatus.Error, Interpretation: "An error occurred while retrieving the CRL. This is usually a networking error", Advise: fmt.Sprintf("If this is a networking error, attempt to verify that CRL endpoint at %s is active and available", crlStatus.Endpoint), }) } } return } func assertMayBeRevoked(cert model.CertificateResult, t CertType) (opinion model.Opinion) { for _, response := range cert.OCSP { if response.Status == ocsp.Revoked { continue } if response.Error != "" { interpretation := "" switch response.Status { case ocsp.CryptoVerifcationError: interpretation = fmt.Sprintf("OCSP responder %s could not verify the provided chain at the %s. This is usually accompanied by a verification error thrown by certutil.", response.Responder, t) case ocsp.BadResponse: interpretation = fmt.Sprintf("OCSP responder %s gave a bad response for the %s.", response.Responder, t) } opinion.Errors = append(opinion.Errors, model.Concern{ Raw: response.Error, Interpretation: interpretation, Advise: cert.CrtSh, }) } } for _, crlStatus := range cert.CRL { if crlStatus.Error != "" { opinion.Errors = append(opinion.Errors, model.Concern{ Raw: crlStatus.Error, Interpretation: "An error occurred while retrieving the CRL. This is usually a networking error", Advise: fmt.Sprintf("If this is a networking error, attempt to verify that CRL endpoint at %s is active and available", crlStatus.Endpoint), }) } } return }