Use zlint to check our CRLs (#6972)
Update zlint to v3.5.0, which introduces scaffolding for running lints over CRLs. Convert all of our existing CRL checks to structs which match the zlint interface, and add them to the registry. Then change our linter's CheckCRL function, and crl-checker's Validate function, to run all lints in the zlint registry. Finally, update the ceremony tool to run these lints as well. This change touches a lot of files, but involves almost no logic changes. It's all just infrastructure, changing the way our lints and their tests are shaped, and moving test files into new homes. Fixes https://github.com/letsencrypt/boulder/issues/6934 Fixes https://github.com/letsencrypt/boulder/issues/6979
This commit is contained in:
parent
0051277c71
commit
b090ffbd2e
|
@ -336,7 +336,7 @@ func makeTemplate(randReader io.Reader, profile *certProfile, pubKey []byte, ct
|
|||
type failReader struct{}
|
||||
|
||||
func (fr *failReader) Read([]byte) (int, error) {
|
||||
return 0, errors.New("Empty reader used by x509.CreateCertificate")
|
||||
return 0, errors.New("empty reader used by x509.CreateCertificate")
|
||||
}
|
||||
|
||||
func generateCSR(profile *certProfile, signer crypto.Signer) ([]byte, error) {
|
||||
|
|
|
@ -3,15 +3,18 @@ package notmain
|
|||
import (
|
||||
"crypto"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"github.com/letsencrypt/boulder/crl/crl_x509"
|
||||
"github.com/letsencrypt/boulder/linter"
|
||||
)
|
||||
|
||||
func generateCRL(signer crypto.Signer, issuer *x509.Certificate, thisUpdate, nextUpdate time.Time, number int64, revokedCertificates []pkix.RevokedCertificate) ([]byte, error) {
|
||||
template := &x509.RevocationList{
|
||||
func generateCRL(signer crypto.Signer, issuer *x509.Certificate, thisUpdate, nextUpdate time.Time, number int64, revokedCertificates []crl_x509.RevokedCertificate) ([]byte, error) {
|
||||
template := &crl_x509.RevocationList{
|
||||
RevokedCertificates: revokedCertificates,
|
||||
Number: big.NewInt(number),
|
||||
ThisUpdate: thisUpdate,
|
||||
|
@ -33,12 +36,26 @@ func generateCRL(signer crypto.Signer, issuer *x509.Certificate, thisUpdate, nex
|
|||
return nil, errors.New("nextUpdate must be less than 12 months after thisUpdate")
|
||||
}
|
||||
|
||||
err := linter.CheckCRL(template, issuer, signer, []string{
|
||||
// We skip this lint because our ceremony tooling issues CRLs with validity
|
||||
// periods up to 12 months, but the lint only allows up to 10 days (which
|
||||
// is the limit for CRLs containing Subscriber Certificates).
|
||||
"e_crl_validity_period",
|
||||
// We skip this lint because it is only applicable for sharded/partitioned
|
||||
// CRLs, which our Subscriber CRLs are, but our higher-level CRLs issued by
|
||||
// this tool are not.
|
||||
"e_crl_has_idp",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("crl failed pre-issuance lint: %w", err)
|
||||
}
|
||||
|
||||
// x509.CreateRevocationList uses an io.Reader here for signing methods that require
|
||||
// a source of randomness. Since PKCS#11 based signing generates needed randomness
|
||||
// at the HSM we don't need to pass a real reader. Instead of passing a nil reader
|
||||
// we use one that always returns errors in case the internal usage of this reader
|
||||
// changes.
|
||||
crlBytes, err := x509.CreateRevocationList(&failReader{}, template, issuer, signer)
|
||||
crlBytes, err := crl_x509.CreateRevocationList(&failReader{}, template, issuer, signer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -14,34 +14,33 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/letsencrypt/boulder/crl/crl_x509"
|
||||
"github.com/letsencrypt/boulder/test"
|
||||
)
|
||||
|
||||
func TestGenerateCRLTimeBounds(t *testing.T) {
|
||||
_, err := generateCRL(nil, nil, time.Time{}.Add(time.Hour), time.Time{}, 1, nil)
|
||||
_, err := generateCRL(nil, nil, time.Now().Add(time.Hour), time.Now(), 1, nil)
|
||||
test.AssertError(t, err, "generateCRL did not fail")
|
||||
test.AssertEquals(t, err.Error(), "thisUpdate must be before nextUpdate")
|
||||
|
||||
_, err = generateCRL(nil, &x509.Certificate{
|
||||
NotBefore: time.Time{}.Add(time.Hour),
|
||||
NotAfter: time.Time{},
|
||||
}, time.Time{}, time.Time{}, 1, nil)
|
||||
NotBefore: time.Now().Add(time.Hour),
|
||||
NotAfter: time.Now(),
|
||||
}, time.Now(), time.Now(), 1, nil)
|
||||
test.AssertError(t, err, "generateCRL did not fail")
|
||||
test.AssertEquals(t, err.Error(), "thisUpdate is before issuing certificate's notBefore")
|
||||
|
||||
_, err = generateCRL(nil, &x509.Certificate{
|
||||
NotBefore: time.Time{},
|
||||
NotAfter: time.Time{}.Add(time.Hour * 2),
|
||||
}, time.Time{}.Add(time.Hour), time.Time{}.Add(time.Hour*3), 1, nil)
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(time.Hour * 2),
|
||||
}, time.Now().Add(time.Hour), time.Now().Add(time.Hour*3), 1, nil)
|
||||
test.AssertError(t, err, "generateCRL did not fail")
|
||||
test.AssertEquals(t, err.Error(), "nextUpdate is after issuing certificate's notAfter")
|
||||
}
|
||||
|
||||
func TestGenerateCRLLength(t *testing.T) {
|
||||
_, err := generateCRL(nil, &x509.Certificate{
|
||||
NotBefore: time.Time{},
|
||||
NotAfter: time.Time{}.Add(time.Hour * 24 * 366),
|
||||
}, time.Time{}, time.Time{}.Add(time.Hour*24*366), 1, nil)
|
||||
_, err = generateCRL(nil, &x509.Certificate{
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(time.Hour * 24 * 370),
|
||||
}, time.Now(), time.Now().Add(time.Hour*24*366), 1, nil)
|
||||
test.AssertError(t, err, "generateCRL did not fail")
|
||||
test.AssertEquals(t, err.Error(), "nextUpdate must be less than 12 months after thisUpdate")
|
||||
}
|
||||
|
@ -62,17 +61,58 @@ func (p wrappedSigner) Public() crypto.PublicKey {
|
|||
return p.k.Public()
|
||||
}
|
||||
|
||||
func TestGenerateCRLLints(t *testing.T) {
|
||||
k, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
test.AssertNotError(t, err, "failed to generate test key")
|
||||
|
||||
cert := &x509.Certificate{
|
||||
Subject: pkix.Name{CommonName: "asd"},
|
||||
SerialNumber: big.NewInt(7),
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(365 * 24 * time.Hour),
|
||||
IsCA: true,
|
||||
KeyUsage: x509.KeyUsageCRLSign,
|
||||
SubjectKeyId: []byte{1, 2, 3},
|
||||
}
|
||||
|
||||
certBytes, err := x509.CreateCertificate(rand.Reader, cert, cert, k.Public(), k)
|
||||
test.AssertNotError(t, err, "failed to generate test cert")
|
||||
cert, err = x509.ParseCertificate(certBytes)
|
||||
test.AssertNotError(t, err, "failed to parse test cert")
|
||||
|
||||
// This CRL should fail the following lints:
|
||||
// - e_crl_has_idp (because our ceremony CRLs don't have the IDP extension)
|
||||
// - e_crl_validity_period (because our ceremony CRLs are valid for a long time)
|
||||
// - e_crl_acceptable_reason_codes (because 6 is forbidden)
|
||||
// However, only the last of those should show up in the error message,
|
||||
// because the first two should be explicitly removed from the lint registry
|
||||
// by the ceremony tool.
|
||||
six := 6
|
||||
_, err = generateCRL(&wrappedSigner{k}, cert, time.Now().Add(time.Hour), time.Now().Add(100*24*time.Hour), 1, []crl_x509.RevokedCertificate{
|
||||
{
|
||||
SerialNumber: big.NewInt(12345),
|
||||
ReasonCode: &six,
|
||||
},
|
||||
})
|
||||
test.AssertError(t, err, "generateCRL did not fail")
|
||||
test.AssertNotContains(t, err.Error(), "e_crl_has_idp")
|
||||
test.AssertNotContains(t, err.Error(), "e_crl_validity_period")
|
||||
test.AssertContains(t, err.Error(), "e_crl_acceptable_reason_codes")
|
||||
}
|
||||
|
||||
func TestGenerateCRL(t *testing.T) {
|
||||
k, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
test.AssertNotError(t, err, "failed to generate test key")
|
||||
|
||||
template := &x509.Certificate{
|
||||
Subject: pkix.Name{CommonName: "asd"},
|
||||
SerialNumber: big.NewInt(7),
|
||||
NotBefore: time.Time{},
|
||||
NotAfter: time.Time{}.Add(time.Hour * 3),
|
||||
KeyUsage: x509.KeyUsageCRLSign,
|
||||
SubjectKeyId: []byte{1, 2, 3},
|
||||
Subject: pkix.Name{CommonName: "asd"},
|
||||
SerialNumber: big.NewInt(7),
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(365 * 24 * time.Hour),
|
||||
IsCA: true,
|
||||
BasicConstraintsValid: true,
|
||||
KeyUsage: x509.KeyUsageCRLSign,
|
||||
SubjectKeyId: []byte{1, 2, 3},
|
||||
}
|
||||
|
||||
certBytes, err := x509.CreateCertificate(rand.Reader, template, template, k.Public(), k)
|
||||
|
@ -80,18 +120,18 @@ func TestGenerateCRL(t *testing.T) {
|
|||
cert, err := x509.ParseCertificate(certBytes)
|
||||
test.AssertNotError(t, err, "failed to parse test cert")
|
||||
|
||||
crlPEM, err := generateCRL(&wrappedSigner{k}, cert, time.Time{}.Add(time.Hour), time.Time{}.Add(time.Hour*2), 1, nil)
|
||||
crlPEM, err := generateCRL(&wrappedSigner{k}, cert, time.Now().Add(time.Hour), time.Now().Add(time.Hour*2), 1, nil)
|
||||
test.AssertNotError(t, err, "generateCRL failed with valid profile")
|
||||
|
||||
pemBlock, _ := pem.Decode(crlPEM)
|
||||
crlDER := pemBlock.Bytes
|
||||
|
||||
// use crypto/x509 to check signature is valid and list is empty
|
||||
goCRL, err := x509.ParseCRL(crlDER)
|
||||
goCRL, err := x509.ParseRevocationList(crlDER)
|
||||
test.AssertNotError(t, err, "failed to parse CRL")
|
||||
err = cert.CheckCRLSignature(goCRL)
|
||||
err = goCRL.CheckSignatureFrom(cert)
|
||||
test.AssertNotError(t, err, "CRL signature check failed")
|
||||
test.AssertEquals(t, len(goCRL.TBSCertList.RevokedCertificates), 0)
|
||||
test.AssertEquals(t, len(goCRL.RevokedCertificates), 0)
|
||||
|
||||
// fully parse the CRL to check that the version is correct, and that
|
||||
// it contains the CRL number extension containing the number we expect
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/letsencrypt/boulder/cmd"
|
||||
"github.com/letsencrypt/boulder/crl/crl_x509"
|
||||
"github.com/letsencrypt/boulder/linter"
|
||||
"github.com/letsencrypt/boulder/pkcs11helpers"
|
||||
"github.com/letsencrypt/boulder/strictyaml"
|
||||
|
@ -721,7 +722,7 @@ func crlCeremony(configBytes []byte) error {
|
|||
return fmt.Errorf("unable to parse crl-profile.next-update: %s", err)
|
||||
}
|
||||
|
||||
var revokedCertificates []pkix.RevokedCertificate
|
||||
var revokedCertificates []crl_x509.RevokedCertificate
|
||||
for _, rc := range config.CRLProfile.RevokedCertificates {
|
||||
cert, err := loadCert(rc.CertificatePath)
|
||||
if err != nil {
|
||||
|
@ -731,7 +732,7 @@ func crlCeremony(configBytes []byte) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("unable to parse crl-profile.revoked-certificates.revocation-date")
|
||||
}
|
||||
revokedCert := pkix.RevokedCertificate{
|
||||
revokedCert := crl_x509.RevokedCertificate{
|
||||
SerialNumber: cert.SerialNumber,
|
||||
RevocationTime: revokedAt,
|
||||
}
|
||||
|
|
|
@ -100,7 +100,14 @@ func main() {
|
|||
|
||||
totalBytes += len(crl.Raw)
|
||||
|
||||
err = checker.Validate(crl, issuer, ageLimit)
|
||||
zcrl, err := crl_x509.ParseRevocationList(crl.Raw)
|
||||
if err != nil {
|
||||
errCount += 1
|
||||
logger.Errf("parsing CRL %q failed: %s", u, err)
|
||||
continue
|
||||
}
|
||||
|
||||
err = checker.Validate(zcrl, issuer, ageLimit)
|
||||
if err != nil {
|
||||
errCount += 1
|
||||
logger.Errf("checking CRL %q failed: %s", u, err)
|
||||
|
|
|
@ -8,9 +8,11 @@ import (
|
|||
"sort"
|
||||
"time"
|
||||
|
||||
zlint_x509 "github.com/zmap/zcrypto/x509"
|
||||
"github.com/zmap/zlint/v3"
|
||||
|
||||
"github.com/letsencrypt/boulder/crl/crl_x509"
|
||||
"github.com/letsencrypt/boulder/linter"
|
||||
crlint "github.com/letsencrypt/boulder/linter/lints/crl"
|
||||
)
|
||||
|
||||
// Validate runs the given CRL through our set of lints, ensures its signature
|
||||
|
@ -18,7 +20,12 @@ import (
|
|||
// less than ageLimit old. It returns an error if any of these conditions are
|
||||
// not met.
|
||||
func Validate(crl *crl_x509.RevocationList, issuer *x509.Certificate, ageLimit time.Duration) error {
|
||||
err := linter.ProcessResultSet(crlint.LintCRL(crl))
|
||||
zcrl, err := zlint_x509.ParseRevocationList(crl.Raw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing CRL: %w", err)
|
||||
}
|
||||
|
||||
err = linter.ProcessResultSet(zlint.LintRevocationList(zcrl))
|
||||
if err != nil {
|
||||
return fmt.Errorf("linting CRL: %w", err)
|
||||
}
|
||||
|
|
|
@ -39,9 +39,15 @@ func TestValidate(t *testing.T) {
|
|||
test.AssertError(t, err, "validating crl from wrong issuer")
|
||||
test.AssertContains(t, err.Error(), "signature")
|
||||
|
||||
crl.Number = nil
|
||||
crlFile, err = os.Open("../../linter/lints/cabf_br/testdata/crl_long_validity.pem")
|
||||
test.AssertNotError(t, err, "opening test crl file")
|
||||
crlPEM, err = io.ReadAll(crlFile)
|
||||
test.AssertNotError(t, err, "reading test crl file")
|
||||
crlDER, _ = pem.Decode(crlPEM)
|
||||
crl, err = crl_x509.ParseRevocationList(crlDER.Bytes)
|
||||
test.AssertNotError(t, err, "parsing test crl")
|
||||
err = Validate(crl, issuer, 100*365*24*time.Hour)
|
||||
test.AssertError(t, err, "validaint crl with lint error")
|
||||
test.AssertError(t, err, "validating crl with lint error")
|
||||
test.AssertContains(t, err.Error(), "linting")
|
||||
}
|
||||
|
||||
|
|
|
@ -14,10 +14,11 @@ import (
|
|||
"github.com/zmap/zlint/v3/lint"
|
||||
|
||||
"github.com/letsencrypt/boulder/crl/crl_x509"
|
||||
crllints "github.com/letsencrypt/boulder/linter/lints/crl"
|
||||
|
||||
_ "github.com/letsencrypt/boulder/linter/lints/cabf_br"
|
||||
_ "github.com/letsencrypt/boulder/linter/lints/chrome"
|
||||
_ "github.com/letsencrypt/boulder/linter/lints/cpcps"
|
||||
_ "github.com/letsencrypt/boulder/linter/lints/rfc"
|
||||
)
|
||||
|
||||
var ErrLinting = fmt.Errorf("failed lint(s)")
|
||||
|
@ -37,6 +38,15 @@ func Check(tbs *x509.Certificate, subjectPubKey crypto.PublicKey, realIssuer *x5
|
|||
return err
|
||||
}
|
||||
|
||||
// CheckCRL is like Check, but for CRLs.
|
||||
func CheckCRL(tbs *crl_x509.RevocationList, realIssuer *x509.Certificate, realSigner crypto.Signer, skipLints []string) error {
|
||||
linter, err := New(realIssuer, realSigner, skipLints)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return linter.CheckCRL(tbs)
|
||||
}
|
||||
|
||||
// Linter is capable of linting a to-be-signed (TBS) certificate. It does so by
|
||||
// signing that certificate with a throwaway private key and a fake issuer whose
|
||||
// public key matches the throwaway private key, and then running the resulting
|
||||
|
@ -93,7 +103,7 @@ func (l Linter) CheckCRL(tbs *crl_x509.RevocationList) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lintRes := crllints.LintCRL(crl)
|
||||
lintRes := zlint.LintRevocationListEx(crl, l.registry)
|
||||
return ProcessResultSet(lintRes)
|
||||
}
|
||||
|
||||
|
@ -209,12 +219,12 @@ func ProcessResultSet(lintRes *zlint.ResultSet) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func makeLintCRL(tbs *crl_x509.RevocationList, issuer *x509.Certificate, signer crypto.Signer) (*crl_x509.RevocationList, error) {
|
||||
func makeLintCRL(tbs *crl_x509.RevocationList, issuer *x509.Certificate, signer crypto.Signer) (*zlintx509.RevocationList, error) {
|
||||
lintCRLBytes, err := crl_x509.CreateRevocationList(rand.Reader, tbs, issuer, signer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lintCRL, err := crl_x509.ParseRevocationList(lintCRLBytes)
|
||||
lintCRL, err := zlintx509.ParseRevocationList(lintCRLBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
package cabfbr
|
||||
|
||||
import (
|
||||
"github.com/zmap/zcrypto/x509"
|
||||
"github.com/zmap/zlint/v3/lint"
|
||||
|
||||
"github.com/letsencrypt/boulder/linter/lints"
|
||||
)
|
||||
|
||||
type crlAcceptableReasonCodes struct{}
|
||||
|
||||
/************************************************
|
||||
Baseline Requirements: 7.2.2.1:
|
||||
The CRLReason indicated MUST NOT be unspecified (0).
|
||||
The CRLReason MUST NOT be certificateHold (6).
|
||||
|
||||
When the CRLReason code is not one of the following, then the reasonCode extension MUST NOT be provided:
|
||||
- keyCompromise (RFC 5280 CRLReason #1);
|
||||
- privilegeWithdrawn (RFC 5280 CRLReason #9);
|
||||
- cessationOfOperation (RFC 5280 CRLReason #5);
|
||||
- affiliationChanged (RFC 5280 CRLReason #3); or
|
||||
- superseded (RFC 5280 CRLReason #4).
|
||||
************************************************/
|
||||
|
||||
func init() {
|
||||
lint.RegisterRevocationListLint(&lint.RevocationListLint{
|
||||
LintMetadata: lint.LintMetadata{
|
||||
Name: "e_crl_acceptable_reason_codes",
|
||||
Description: "CRL entry Reason Codes must be 1, 3, 4, 5, or 9",
|
||||
Citation: "BRs: 7.2.2.1",
|
||||
Source: lint.CABFBaselineRequirements,
|
||||
// We use the Mozilla Root Store Policy v2.8.1 effective date here
|
||||
// because, although this lint enforces requirements from the BRs, those
|
||||
// same requirements were in the MRSP first.
|
||||
EffectiveDate: lints.MozillaPolicy281Date,
|
||||
},
|
||||
Lint: NewCrlAcceptableReasonCodes,
|
||||
})
|
||||
}
|
||||
|
||||
func NewCrlAcceptableReasonCodes() lint.RevocationListLintInterface {
|
||||
return &crlAcceptableReasonCodes{}
|
||||
}
|
||||
|
||||
func (l *crlAcceptableReasonCodes) CheckApplies(c *x509.RevocationList) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (l *crlAcceptableReasonCodes) Execute(c *x509.RevocationList) *lint.LintResult {
|
||||
for _, rc := range c.RevokedCertificates {
|
||||
if rc.ReasonCode == nil {
|
||||
continue
|
||||
}
|
||||
switch *rc.ReasonCode {
|
||||
case 1: // keyCompromise
|
||||
case 3: // affiliationChanged
|
||||
case 4: // superseded
|
||||
case 5: // cessationOfOperation
|
||||
case 9: // privilegeWithdrawn
|
||||
continue
|
||||
default:
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "CRLs MUST NOT include reasonCodes other than 1, 3, 4, 5, and 9",
|
||||
}
|
||||
}
|
||||
}
|
||||
return &lint.LintResult{Status: lint.Pass}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
package cabfbr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/zmap/zlint/v3/lint"
|
||||
|
||||
"github.com/letsencrypt/boulder/linter/lints/test"
|
||||
)
|
||||
|
||||
func TestCrlAcceptableReasonCodes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
want lint.LintStatus
|
||||
wantSubStr string
|
||||
}{
|
||||
{
|
||||
// crl_good.pem contains a revocation entry with no reason code extension.
|
||||
name: "good",
|
||||
want: lint.Pass,
|
||||
},
|
||||
{
|
||||
name: "reason_0",
|
||||
want: lint.Error,
|
||||
wantSubStr: "MUST NOT include reasonCodes other than",
|
||||
},
|
||||
{
|
||||
name: "reason_1",
|
||||
want: lint.Pass,
|
||||
},
|
||||
{
|
||||
name: "reason_2",
|
||||
want: lint.Error,
|
||||
wantSubStr: "MUST NOT include reasonCodes other than",
|
||||
},
|
||||
{
|
||||
name: "reason_3",
|
||||
want: lint.Pass,
|
||||
},
|
||||
{
|
||||
name: "reason_4",
|
||||
want: lint.Pass,
|
||||
},
|
||||
{
|
||||
name: "reason_5",
|
||||
want: lint.Pass,
|
||||
},
|
||||
{
|
||||
name: "reason_6",
|
||||
want: lint.Error,
|
||||
wantSubStr: "MUST NOT include reasonCodes other than",
|
||||
},
|
||||
{
|
||||
name: "reason_8",
|
||||
want: lint.Error,
|
||||
wantSubStr: "MUST NOT include reasonCodes other than",
|
||||
},
|
||||
{
|
||||
name: "reason_9",
|
||||
want: lint.Pass,
|
||||
},
|
||||
{
|
||||
name: "reason_10",
|
||||
want: lint.Error,
|
||||
wantSubStr: "MUST NOT include reasonCodes other than",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
l := NewCrlAcceptableReasonCodes()
|
||||
c := test.LoadPEMCRL(t, fmt.Sprintf("testdata/crl_%s.pem", tc.name))
|
||||
r := l.Execute(c)
|
||||
|
||||
if r.Status != tc.want {
|
||||
t.Errorf("expected %q, got %q", tc.want, r.Status)
|
||||
}
|
||||
if !strings.Contains(r.Details, tc.wantSubStr) {
|
||||
t.Errorf("expected %q, got %q", tc.wantSubStr, r.Details)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package cabfbr
|
||||
|
||||
import (
|
||||
"github.com/zmap/zcrypto/encoding/asn1"
|
||||
"github.com/zmap/zcrypto/x509"
|
||||
"github.com/zmap/zlint/v3/lint"
|
||||
"github.com/zmap/zlint/v3/util"
|
||||
)
|
||||
|
||||
type crlCriticalReasonCodes struct{}
|
||||
|
||||
/************************************************
|
||||
Baseline Requirements: 7.2.2.1:
|
||||
If present, [the reasonCode] extension MUST NOT be marked critical.
|
||||
************************************************/
|
||||
|
||||
func init() {
|
||||
lint.RegisterRevocationListLint(&lint.RevocationListLint{
|
||||
LintMetadata: lint.LintMetadata{
|
||||
Name: "e_crl_no_critical_reason_codes",
|
||||
Description: "CRL entry reasonCode extension MUST NOT be marked critical",
|
||||
Citation: "BRs: 7.2.2.1",
|
||||
Source: lint.CABFBaselineRequirements,
|
||||
EffectiveDate: util.CABFBRs_1_8_0_Date,
|
||||
},
|
||||
Lint: NewCrlCriticalReasonCodes,
|
||||
})
|
||||
}
|
||||
|
||||
func NewCrlCriticalReasonCodes() lint.RevocationListLintInterface {
|
||||
return &crlCriticalReasonCodes{}
|
||||
}
|
||||
|
||||
func (l *crlCriticalReasonCodes) CheckApplies(c *x509.RevocationList) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (l *crlCriticalReasonCodes) Execute(c *x509.RevocationList) *lint.LintResult {
|
||||
reasonCodeOID := asn1.ObjectIdentifier{2, 5, 29, 21} // id-ce-reasonCode
|
||||
for _, rc := range c.RevokedCertificates {
|
||||
for _, ext := range rc.Extensions {
|
||||
if ext.Id.Equal(reasonCodeOID) && ext.Critical {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "CRL entry reasonCode extension MUST NOT be marked critical",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return &lint.LintResult{Status: lint.Pass}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package cabfbr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/zmap/zlint/v3/lint"
|
||||
|
||||
"github.com/letsencrypt/boulder/linter/lints/test"
|
||||
)
|
||||
|
||||
func TestCrlCriticalReasonCodes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
want lint.LintStatus
|
||||
wantSubStr string
|
||||
}{
|
||||
{
|
||||
name: "good",
|
||||
want: lint.Pass,
|
||||
},
|
||||
{
|
||||
name: "critical_reason",
|
||||
want: lint.Error,
|
||||
wantSubStr: "reasonCode extension MUST NOT be marked critical",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
l := NewCrlCriticalReasonCodes()
|
||||
c := test.LoadPEMCRL(t, fmt.Sprintf("testdata/crl_%s.pem", tc.name))
|
||||
r := l.Execute(c)
|
||||
|
||||
if r.Status != tc.want {
|
||||
t.Errorf("expected %q, got %q", tc.want, r.Status)
|
||||
}
|
||||
if !strings.Contains(r.Details, tc.wantSubStr) {
|
||||
t.Errorf("expected %q, got %q", tc.wantSubStr, r.Details)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package cabfbr
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/zmap/zcrypto/x509"
|
||||
"github.com/zmap/zlint/v3/lint"
|
||||
"github.com/zmap/zlint/v3/util"
|
||||
)
|
||||
|
||||
type crlValidityPeriod struct{}
|
||||
|
||||
/************************************************
|
||||
Baseline Requirements, Section 4.9.7:
|
||||
For the status of Subscriber Certificates [...] the value of the nextUpdate
|
||||
field MUST NOT be more than ten days beyond the value of the thisUpdate field.
|
||||
|
||||
Although the validity period for CRLs covering the status of Subordinate CA
|
||||
certificates is longer (up to 12 months), Boulder does not produce such CRLs,
|
||||
so this lint only covers the Subscriber Certificate case.
|
||||
************************************************/
|
||||
|
||||
func init() {
|
||||
lint.RegisterRevocationListLint(&lint.RevocationListLint{
|
||||
LintMetadata: lint.LintMetadata{
|
||||
Name: "e_crl_validity_period",
|
||||
Description: "CRLs must have an acceptable validity period",
|
||||
Citation: "BRs: 4.9.7",
|
||||
Source: lint.CABFBaselineRequirements,
|
||||
EffectiveDate: util.CABFBRs_1_2_1_Date,
|
||||
},
|
||||
Lint: NewCrlValidityPeriod,
|
||||
})
|
||||
}
|
||||
|
||||
func NewCrlValidityPeriod() lint.RevocationListLintInterface {
|
||||
return &crlValidityPeriod{}
|
||||
}
|
||||
|
||||
func (l *crlValidityPeriod) CheckApplies(c *x509.RevocationList) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (l *crlValidityPeriod) Execute(c *x509.RevocationList) *lint.LintResult {
|
||||
validity := c.NextUpdate.Sub(c.ThisUpdate)
|
||||
if validity <= 0 {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "CRL has NextUpdate at or before ThisUpdate",
|
||||
}
|
||||
}
|
||||
if validity > 10*24*time.Hour {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "CRL has validity period greater than ten days",
|
||||
}
|
||||
}
|
||||
return &lint.LintResult{Status: lint.Pass}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package cabfbr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/zmap/zlint/v3/lint"
|
||||
|
||||
"github.com/letsencrypt/boulder/linter/lints/test"
|
||||
)
|
||||
|
||||
func TestCrlValidityPeriod(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
want lint.LintStatus
|
||||
wantSubStr string
|
||||
}{
|
||||
{
|
||||
name: "good",
|
||||
want: lint.Pass,
|
||||
},
|
||||
{
|
||||
name: "negative_validity",
|
||||
want: lint.Error,
|
||||
wantSubStr: "at or before",
|
||||
},
|
||||
{
|
||||
name: "long_validity",
|
||||
want: lint.Error,
|
||||
wantSubStr: "greater than ten days",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
l := NewCrlValidityPeriod()
|
||||
c := test.LoadPEMCRL(t, fmt.Sprintf("testdata/crl_%s.pem", tc.name))
|
||||
r := l.Execute(c)
|
||||
|
||||
if r.Status != tc.want {
|
||||
t.Errorf("expected %q, got %q", tc.want, r.Status)
|
||||
}
|
||||
if !strings.Contains(r.Details, tc.wantSubStr) {
|
||||
t.Errorf("expected %q, got %q", tc.wantSubStr, r.Details)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -3,6 +3,8 @@ package lints
|
|||
import (
|
||||
"time"
|
||||
|
||||
"github.com/zmap/zcrypto/encoding/asn1"
|
||||
"github.com/zmap/zcrypto/x509/pkix"
|
||||
"github.com/zmap/zlint/v3/lint"
|
||||
)
|
||||
|
||||
|
@ -19,5 +21,17 @@ const (
|
|||
)
|
||||
|
||||
var (
|
||||
CPSV33Date = time.Date(2021, time.June, 8, 0, 0, 0, 0, time.UTC)
|
||||
CPSV33Date = time.Date(2021, time.June, 8, 0, 0, 0, 0, time.UTC)
|
||||
MozillaPolicy281Date = time.Date(2023, time.February, 15, 0, 0, 0, 0, time.UTC)
|
||||
)
|
||||
|
||||
// GetExtWithOID is a helper for several of our custom lints. It returns the
|
||||
// extension with the given OID if it exists, or nil otherwise.
|
||||
func GetExtWithOID(exts []pkix.Extension, oid asn1.ObjectIdentifier) *pkix.Extension {
|
||||
for _, ext := range exts {
|
||||
if ext.Id.Equal(oid) {
|
||||
return &ext
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,161 @@
|
|||
package cpcps
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/zmap/zcrypto/encoding/asn1"
|
||||
"github.com/zmap/zcrypto/x509"
|
||||
"github.com/zmap/zlint/v3/lint"
|
||||
"golang.org/x/crypto/cryptobyte"
|
||||
cryptobyte_asn1 "golang.org/x/crypto/cryptobyte/asn1"
|
||||
|
||||
"github.com/letsencrypt/boulder/linter/lints"
|
||||
)
|
||||
|
||||
type crlHasIDP struct{}
|
||||
|
||||
/************************************************
|
||||
Various root programs (and the BRs, after Ballot SC-063 passes) require that
|
||||
sharded/partitioned CRLs have a specifically-encoded Issuing Distribution Point
|
||||
extension. Since there's no way to tell from the CRL itself whether or not it
|
||||
is sharded, we apply this lint universally to all CRLs, but as part of the Let's
|
||||
Encrypt-specific suite of lints.
|
||||
************************************************/
|
||||
|
||||
func init() {
|
||||
lint.RegisterRevocationListLint(&lint.RevocationListLint{
|
||||
LintMetadata: lint.LintMetadata{
|
||||
Name: "e_crl_has_idp",
|
||||
Description: "Let's Encrypt issues sharded CRLs; therefore our CRLs must have an Issuing Distribution Point",
|
||||
Citation: "",
|
||||
Source: lints.LetsEncryptCPS,
|
||||
EffectiveDate: lints.CPSV33Date,
|
||||
},
|
||||
Lint: NewCrlHasIDP,
|
||||
})
|
||||
}
|
||||
|
||||
func NewCrlHasIDP() lint.RevocationListLintInterface {
|
||||
return &crlHasIDP{}
|
||||
}
|
||||
|
||||
func (l *crlHasIDP) CheckApplies(c *x509.RevocationList) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (l *crlHasIDP) Execute(c *x509.RevocationList) *lint.LintResult {
|
||||
idpOID := asn1.ObjectIdentifier{2, 5, 29, 28} // id-ce-issuingDistributionPoint
|
||||
idpe := lints.GetExtWithOID(c.Extensions, idpOID)
|
||||
if idpe == nil {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Warn,
|
||||
Details: "CRL missing IDP",
|
||||
}
|
||||
}
|
||||
if !idpe.Critical {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "IDP MUST be critical",
|
||||
}
|
||||
}
|
||||
|
||||
// Step inside the outer issuingDistributionPoint sequence to get access to
|
||||
// its constituent fields, DistributionPoint and OnlyContainsUserCerts.
|
||||
idpv := cryptobyte.String(idpe.Value)
|
||||
if !idpv.ReadASN1(&idpv, cryptobyte_asn1.SEQUENCE) {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Warn,
|
||||
Details: "Failed to read issuingDistributionPoint",
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that the DistributionPoint is a reasonable URI. To get to the URI,
|
||||
// we have to step inside the DistributionPointName, then step inside that's
|
||||
// FullName, and finally read the singular SEQUENCE OF GeneralName element.
|
||||
if !idpv.PeekASN1Tag(cryptobyte_asn1.Tag(0).ContextSpecific().Constructed()) {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Warn,
|
||||
Details: "IDP should contain distributionPoint",
|
||||
}
|
||||
}
|
||||
|
||||
var dpName cryptobyte.String
|
||||
if !idpv.ReadASN1(&dpName, cryptobyte_asn1.Tag(0).ContextSpecific().Constructed()) {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Warn,
|
||||
Details: "Failed to read IDP distributionPoint",
|
||||
}
|
||||
}
|
||||
|
||||
if !dpName.ReadASN1(&dpName, cryptobyte_asn1.Tag(0).ContextSpecific().Constructed()) {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Warn,
|
||||
Details: "Failed to read IDP distributionPoint fullName",
|
||||
}
|
||||
}
|
||||
|
||||
uriBytes := make([]byte, 0)
|
||||
if !dpName.ReadASN1Bytes(&uriBytes, cryptobyte_asn1.Tag(6).ContextSpecific()) {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Warn,
|
||||
Details: "Failed to read IDP URI",
|
||||
}
|
||||
}
|
||||
|
||||
uri, err := url.Parse(string(uriBytes))
|
||||
if err != nil {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "Failed to parse IDP URI",
|
||||
}
|
||||
}
|
||||
|
||||
if uri.Scheme != "http" {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "IDP URI MUST use http scheme",
|
||||
}
|
||||
}
|
||||
|
||||
if !dpName.Empty() {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Warn,
|
||||
Details: "IDP should contain only one distributionPoint",
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that OnlyContainsUserCerts is True. We have to read this boolean as
|
||||
// a byte and ensure its value is 0xFF because cryptobyte.ReadASN1Boolean
|
||||
// can't handle custom encoding rules like this field's [1] tag.
|
||||
if !idpv.PeekASN1Tag(cryptobyte_asn1.Tag(1).ContextSpecific()) {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Warn,
|
||||
Details: "IDP should contain onlyContainsUserCerts",
|
||||
}
|
||||
}
|
||||
|
||||
onlyContainsUserCerts := make([]byte, 0)
|
||||
if !idpv.ReadASN1Bytes(&onlyContainsUserCerts, cryptobyte_asn1.Tag(1).ContextSpecific()) {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "Failed to read IDP onlyContainsUserCerts",
|
||||
}
|
||||
}
|
||||
|
||||
if len(onlyContainsUserCerts) != 1 || onlyContainsUserCerts[0] != 0xFF {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "IDP should set onlyContainsUserCerts: TRUE",
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that no other fields are set.
|
||||
if !idpv.Empty() {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Warn,
|
||||
Details: "IDP should not contain fields other than distributionPoint and onlyContainsUserCerts",
|
||||
}
|
||||
}
|
||||
|
||||
return &lint.LintResult{Status: lint.Pass}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package cpcps
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/zmap/zlint/v3/lint"
|
||||
|
||||
"github.com/letsencrypt/boulder/linter/lints/test"
|
||||
)
|
||||
|
||||
func TestCrlHasIDP(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
want lint.LintStatus
|
||||
wantSubStr string
|
||||
}{
|
||||
{
|
||||
name: "good",
|
||||
want: lint.Pass,
|
||||
},
|
||||
{
|
||||
name: "no_idp",
|
||||
want: lint.Warn,
|
||||
},
|
||||
{
|
||||
name: "idp_no_uri",
|
||||
want: lint.Warn,
|
||||
wantSubStr: "should contain distributionPoint",
|
||||
},
|
||||
{
|
||||
name: "idp_two_uris",
|
||||
want: lint.Warn,
|
||||
wantSubStr: "only one distributionPoint",
|
||||
},
|
||||
{
|
||||
name: "idp_no_usercerts",
|
||||
want: lint.Warn,
|
||||
wantSubStr: "should contain onlyContainsUserCerts",
|
||||
},
|
||||
{
|
||||
name: "idp_some_reasons",
|
||||
want: lint.Warn,
|
||||
wantSubStr: "should not contain fields other than",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
l := NewCrlHasIDP()
|
||||
c := test.LoadPEMCRL(t, fmt.Sprintf("testdata/crl_%s.pem", tc.name))
|
||||
r := l.Execute(c)
|
||||
|
||||
if r.Status != tc.want {
|
||||
t.Errorf("expected %q, got %q", tc.want, r.Status)
|
||||
}
|
||||
if !strings.Contains(r.Details, tc.wantSubStr) {
|
||||
t.Errorf("expected %q, got %q", tc.wantSubStr, r.Details)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package cpcps
|
||||
|
||||
import (
|
||||
"github.com/zmap/zcrypto/encoding/asn1"
|
||||
"github.com/zmap/zcrypto/x509"
|
||||
"github.com/zmap/zlint/v3/lint"
|
||||
|
||||
"github.com/letsencrypt/boulder/linter/lints"
|
||||
)
|
||||
|
||||
type crlHasNoAIA struct{}
|
||||
|
||||
/************************************************
|
||||
RFC 5280: 5.2.7
|
||||
|
||||
The requirements around the Authority Information Access extension are extensive.
|
||||
Therefore we do not include one.
|
||||
Conforming CRL issuers MUST include the nextUpdate field in all CRLs.
|
||||
************************************************/
|
||||
|
||||
func init() {
|
||||
lint.RegisterRevocationListLint(&lint.RevocationListLint{
|
||||
LintMetadata: lint.LintMetadata{
|
||||
Name: "e_crl_has_no_aia",
|
||||
Description: "Let's Encrypt does not include the CRL AIA extension",
|
||||
Citation: "",
|
||||
Source: lints.LetsEncryptCPS,
|
||||
EffectiveDate: lints.CPSV33Date,
|
||||
},
|
||||
Lint: NewCrlHasNoAIA,
|
||||
})
|
||||
}
|
||||
|
||||
func NewCrlHasNoAIA() lint.RevocationListLintInterface {
|
||||
return &crlHasNoAIA{}
|
||||
}
|
||||
|
||||
func (l *crlHasNoAIA) CheckApplies(c *x509.RevocationList) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (l *crlHasNoAIA) Execute(c *x509.RevocationList) *lint.LintResult {
|
||||
aiaOID := asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 1} // id-pe-authorityInfoAccess
|
||||
if lints.GetExtWithOID(c.Extensions, aiaOID) != nil {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Notice,
|
||||
Details: "CRL has an Authority Information Access url",
|
||||
}
|
||||
}
|
||||
return &lint.LintResult{Status: lint.Pass}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package cpcps
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/zmap/zlint/v3/lint"
|
||||
|
||||
"github.com/letsencrypt/boulder/linter/lints/test"
|
||||
)
|
||||
|
||||
func TestCrlHasNoAIA(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
want lint.LintStatus
|
||||
wantSubStr string
|
||||
}{
|
||||
{
|
||||
name: "good",
|
||||
want: lint.Pass,
|
||||
},
|
||||
{
|
||||
name: "aia",
|
||||
want: lint.Notice,
|
||||
wantSubStr: "Authority Information Access",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
l := NewCrlHasNoAIA()
|
||||
c := test.LoadPEMCRL(t, fmt.Sprintf("testdata/crl_%s.pem", tc.name))
|
||||
r := l.Execute(c)
|
||||
|
||||
if r.Status != tc.want {
|
||||
t.Errorf("expected %q, got %q", tc.want, r.Status)
|
||||
}
|
||||
if !strings.Contains(r.Details, tc.wantSubStr) {
|
||||
t.Errorf("expected %q, got %q", tc.wantSubStr, r.Details)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package cpcps
|
||||
|
||||
import (
|
||||
"github.com/zmap/zcrypto/encoding/asn1"
|
||||
"github.com/zmap/zcrypto/x509"
|
||||
"github.com/zmap/zlint/v3/lint"
|
||||
|
||||
"github.com/letsencrypt/boulder/linter/lints"
|
||||
)
|
||||
|
||||
type crlHasNoCertIssuers struct{}
|
||||
|
||||
/************************************************
|
||||
RFC 5280: 5.3.3
|
||||
|
||||
Section 5.3.3 defines the Certificate Issuer entry extension. The presence of
|
||||
this extension means that the CRL is an "indirect CRL", including certificates
|
||||
which were issued by a different issuer than the one issuing the CRL itself.
|
||||
We do not issue indirect CRLs, so our CRL entries should not have this extension.
|
||||
************************************************/
|
||||
|
||||
func init() {
|
||||
lint.RegisterRevocationListLint(&lint.RevocationListLint{
|
||||
LintMetadata: lint.LintMetadata{
|
||||
Name: "e_crl_has_no_cert_issuers",
|
||||
Description: "Let's Encrypt does not issue indirect CRLs",
|
||||
Citation: "",
|
||||
Source: lints.LetsEncryptCPS,
|
||||
EffectiveDate: lints.CPSV33Date,
|
||||
},
|
||||
Lint: NewCrlHasNoCertIssuers,
|
||||
})
|
||||
}
|
||||
|
||||
func NewCrlHasNoCertIssuers() lint.RevocationListLintInterface {
|
||||
return &crlHasNoCertIssuers{}
|
||||
}
|
||||
|
||||
func (l *crlHasNoCertIssuers) CheckApplies(c *x509.RevocationList) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (l *crlHasNoCertIssuers) Execute(c *x509.RevocationList) *lint.LintResult {
|
||||
certIssuerOID := asn1.ObjectIdentifier{2, 5, 29, 29} // id-ce-certificateIssuer
|
||||
for _, entry := range c.RevokedCertificates {
|
||||
if lints.GetExtWithOID(entry.Extensions, certIssuerOID) != nil {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Notice,
|
||||
Details: "CRL has an entry with a Certificate Issuer extension",
|
||||
}
|
||||
}
|
||||
}
|
||||
return &lint.LintResult{Status: lint.Pass}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package cpcps
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/zmap/zlint/v3/lint"
|
||||
|
||||
"github.com/letsencrypt/boulder/linter/lints/test"
|
||||
)
|
||||
|
||||
func TestCrlHasNoCertIssuers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
want lint.LintStatus
|
||||
wantSubStr string
|
||||
}{
|
||||
{
|
||||
name: "good",
|
||||
want: lint.Pass,
|
||||
},
|
||||
{
|
||||
name: "cert_issuer",
|
||||
want: lint.Notice,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
l := NewCrlHasNoCertIssuers()
|
||||
c := test.LoadPEMCRL(t, fmt.Sprintf("testdata/crl_%s.pem", tc.name))
|
||||
r := l.Execute(c)
|
||||
|
||||
if r.Status != tc.want {
|
||||
t.Errorf("expected %q, got %q", tc.want, r.Status)
|
||||
}
|
||||
if !strings.Contains(r.Details, tc.wantSubStr) {
|
||||
t.Errorf("expected %q, got %q", tc.wantSubStr, r.Details)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package cpcps
|
||||
|
||||
import (
|
||||
"github.com/zmap/zcrypto/encoding/asn1"
|
||||
"github.com/zmap/zcrypto/x509"
|
||||
"github.com/zmap/zlint/v3/lint"
|
||||
|
||||
"github.com/letsencrypt/boulder/linter/lints"
|
||||
)
|
||||
|
||||
type crlIsNotDelta struct{}
|
||||
|
||||
/************************************************
|
||||
RFC 5280: 5.2.4
|
||||
|
||||
Section 5.2.4 defines a Delta CRL, and all the requirements that come with it.
|
||||
These requirements are complex and do not serve our purpose, so we ensure that
|
||||
we never issue a CRL which could be construed as a Delta CRL.
|
||||
|
||||
RFC 5280: 5.2.6
|
||||
|
||||
Similarly, Section 5.2.6 defines the Freshest CRL extension, which is only
|
||||
applicable in the case that the CRL is a Delta CRL.
|
||||
************************************************/
|
||||
|
||||
func init() {
|
||||
lint.RegisterRevocationListLint(&lint.RevocationListLint{
|
||||
LintMetadata: lint.LintMetadata{
|
||||
Name: "e_crl_is_not_delta",
|
||||
Description: "Let's Encrypt does not issue delta CRLs",
|
||||
Citation: "",
|
||||
Source: lints.LetsEncryptCPS,
|
||||
EffectiveDate: lints.CPSV33Date,
|
||||
},
|
||||
Lint: NewCrlIsNotDelta,
|
||||
})
|
||||
}
|
||||
|
||||
func NewCrlIsNotDelta() lint.RevocationListLintInterface {
|
||||
return &crlIsNotDelta{}
|
||||
}
|
||||
|
||||
func (l *crlIsNotDelta) CheckApplies(c *x509.RevocationList) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (l *crlIsNotDelta) Execute(c *x509.RevocationList) *lint.LintResult {
|
||||
deltaCRLIndicatorOID := asn1.ObjectIdentifier{2, 5, 29, 27} // id-ce-deltaCRLIndicator
|
||||
if lints.GetExtWithOID(c.Extensions, deltaCRLIndicatorOID) != nil {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Notice,
|
||||
Details: "CRL is a Delta CRL",
|
||||
}
|
||||
}
|
||||
|
||||
freshestCRLOID := asn1.ObjectIdentifier{2, 5, 29, 46} // id-ce-freshestCRL
|
||||
if lints.GetExtWithOID(c.Extensions, freshestCRLOID) != nil {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Notice,
|
||||
Details: "CRL has a Freshest CRL url",
|
||||
}
|
||||
}
|
||||
|
||||
return &lint.LintResult{Status: lint.Pass}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package cpcps
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/zmap/zlint/v3/lint"
|
||||
|
||||
"github.com/letsencrypt/boulder/linter/lints/test"
|
||||
)
|
||||
|
||||
func TestCrlIsNotDelta(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
want lint.LintStatus
|
||||
wantSubStr string
|
||||
}{
|
||||
{
|
||||
name: "good",
|
||||
want: lint.Pass,
|
||||
},
|
||||
{
|
||||
name: "delta",
|
||||
want: lint.Notice,
|
||||
wantSubStr: "Delta",
|
||||
},
|
||||
{
|
||||
name: "freshest",
|
||||
want: lint.Notice,
|
||||
wantSubStr: "Freshest",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
l := NewCrlIsNotDelta()
|
||||
c := test.LoadPEMCRL(t, fmt.Sprintf("testdata/crl_%s.pem", tc.name))
|
||||
r := l.Execute(c)
|
||||
|
||||
if r.Status != tc.want {
|
||||
t.Errorf("expected %q, got %q", tc.want, r.Status)
|
||||
}
|
||||
if !strings.Contains(r.Details, tc.wantSubStr) {
|
||||
t.Errorf("expected %q, got %q", tc.wantSubStr, r.Details)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
-----BEGIN X509 CRL-----
|
||||
MIIBmDCCAR8CAQEwCgYIKoZIzj0EAwMwSTELMAkGA1UEBhMCWFgxFTATBgNVBAoT
|
||||
DEJvdWxkZXIgVGVzdDEjMCEGA1UEAxMaKFRFU1QpIEVsZWdhbnQgRWxlcGhhbnQg
|
||||
RTEXDTIyMTAxMDIwMTIwN1oXDTIyMTAxOTIwMTIwNlowKTAnAggDrlHbURVaPBcN
|
||||
MjIxMDEwMTkxMjA3WjAMMAoGA1UdFQQDCgEBoHoweDAfBgNVHSMEGDAWgBQB2rt6
|
||||
yyUgjl551vmWQi8CQSkHvjARBgNVHRQECgIIFxzOPeSCumEwQgYDVR0cAQH/BDgw
|
||||
NqAxoC+GLWh0dHA6Ly9jLmJvdWxkZXIudGVzdC82NjI4Mzc1NjkxMzU4ODI4OC8w
|
||||
LmNybIEB/zAKBggqhkjOPQQDAwNnADBkAjAvDkIUnTYavJ6h8606MDyFh2uw/cF+
|
||||
OVnM4sE8nUdGy0XYg0hGfbR4MY+kRxRQayICMFeQPpcpIr0zgXpP6lUXU0rcLSva
|
||||
tuaeQSVr24nGjZ7Py0vc94w0n7idZ8wje5+/Mw==
|
||||
-----END X509 CRL-----
|
|
@ -1,659 +0,0 @@
|
|||
package crl
|
||||
|
||||
import (
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/zmap/zlint/v3"
|
||||
"github.com/zmap/zlint/v3/lint"
|
||||
"golang.org/x/crypto/cryptobyte"
|
||||
cryptobyte_asn1 "golang.org/x/crypto/cryptobyte/asn1"
|
||||
|
||||
"github.com/letsencrypt/boulder/crl/crl_x509"
|
||||
)
|
||||
|
||||
const (
|
||||
utcTimeFormat = "YYMMDDHHMMSSZ"
|
||||
generalizedTimeFormat = "YYYYMMDDHHMMSSZ"
|
||||
)
|
||||
|
||||
type crlLint func(*crl_x509.RevocationList) *lint.LintResult
|
||||
|
||||
// registry is the collection of all known CRL lints. It is populated by this
|
||||
// file's init(), and should not be touched by anything else on pain of races.
|
||||
var registry map[string]crlLint
|
||||
|
||||
func init() {
|
||||
// NOTE TO DEVS: you MUST add your new lint function to this list or it
|
||||
// WILL NOT be run.
|
||||
registry = map[string]crlLint{
|
||||
"hasIssuerName": hasIssuerName,
|
||||
"hasNextUpdate": hasNextUpdate,
|
||||
"noEmptyRevokedCertificatesList": noEmptyRevokedCertificatesList,
|
||||
"hasAKI": hasAKI,
|
||||
"hasNumber": hasNumber,
|
||||
"isNotDelta": isNotDelta,
|
||||
"checkIDP": checkIDP,
|
||||
"hasNoFreshest": hasNoFreshest,
|
||||
"hasNoAIA": hasNoAIA,
|
||||
"noZeroReasonCodes": noZeroReasonCodes,
|
||||
"hasNoCertIssuers": hasNoCertIssuers,
|
||||
"hasAcceptableValidity": hasAcceptableValidity,
|
||||
"noCriticalReasons": noCriticalReasons,
|
||||
"noCertificateHolds": noCertificateHolds,
|
||||
"hasMozReasonCodes": hasMozReasonCodes,
|
||||
"hasValidTimestamps": hasValidTimestamps,
|
||||
}
|
||||
}
|
||||
|
||||
// getExtWithOID is a helper for several lints in this file. It returns the
|
||||
// extension with the given OID if it exists, or nil otherwise.
|
||||
func getExtWithOID(exts []pkix.Extension, oid asn1.ObjectIdentifier) *pkix.Extension {
|
||||
for _, ext := range exts {
|
||||
if ext.Id.Equal(oid) {
|
||||
return &ext
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LintCRL examines the given lint CRL, runs it through all of our checks, and
|
||||
// returns a list of all failures
|
||||
func LintCRL(lintCRL *crl_x509.RevocationList) *zlint.ResultSet {
|
||||
rset := zlint.ResultSet{
|
||||
Version: 0,
|
||||
Timestamp: time.Now().UnixNano(),
|
||||
Results: make(map[string]*lint.LintResult),
|
||||
}
|
||||
|
||||
type namedResult struct {
|
||||
Name string
|
||||
Result *lint.LintResult
|
||||
}
|
||||
resChan := make(chan namedResult, len(registry))
|
||||
|
||||
for name, callable := range registry {
|
||||
go func(name string, callable crlLint) {
|
||||
resChan <- namedResult{name, callable(lintCRL)}
|
||||
}(name, callable)
|
||||
}
|
||||
|
||||
for i := 0; i < len(registry); i++ {
|
||||
res := <-resChan
|
||||
switch res.Result.Status {
|
||||
case lint.Notice:
|
||||
rset.NoticesPresent = true
|
||||
case lint.Warn:
|
||||
rset.WarningsPresent = true
|
||||
case lint.Error:
|
||||
rset.ErrorsPresent = true
|
||||
case lint.Fatal:
|
||||
rset.FatalsPresent = true
|
||||
}
|
||||
rset.Results[res.Name] = res.Result
|
||||
}
|
||||
|
||||
return &rset
|
||||
}
|
||||
|
||||
// hasIssuerName checks RFC 5280, Section 5.1.2.3:
|
||||
// The issuer field MUST contain a non-empty X.500 distinguished name (DN).
|
||||
// This lint does not enforce that the issuer field complies with the rest of
|
||||
// the encoding rules of a certificate issuer name, because it (perhaps wrongly)
|
||||
// assumes that those were checked when the issuer was itself issued, and on all
|
||||
// certificates issued by this CRL issuer. Also because there are just a lot of
|
||||
// things to check there, and zlint doesn't expose a public helper for it.
|
||||
func hasIssuerName(crl *crl_x509.RevocationList) *lint.LintResult {
|
||||
if len(crl.Issuer.Names) == 0 {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "CRLs MUST have a non-empty issuer field",
|
||||
}
|
||||
}
|
||||
return &lint.LintResult{Status: lint.Pass}
|
||||
}
|
||||
|
||||
// TODO(#6222): Write a lint which checks RFC 5280, Section 5.1.2.4 and 5.1.2.5:
|
||||
// CRL issuers conforming to this profile MUST encode thisUpdate and nextUpdate
|
||||
// as UTCTime for dates through the year 2049. UTCTime and GeneralizedTime
|
||||
// values MUST be expressed in Greenwich Mean Time (Zulu) and MUST include
|
||||
// seconds, even where the number of seconds is zero.
|
||||
|
||||
// hasNextUpdate checks RFC 5280, Section 5.1.2.5:
|
||||
// Conforming CRL issuers MUST include the nextUpdate field in all CRLs.
|
||||
func hasNextUpdate(crl *crl_x509.RevocationList) *lint.LintResult {
|
||||
if crl.NextUpdate.IsZero() {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "Conforming CRL issuers MUST include the nextUpdate field in all CRLs",
|
||||
}
|
||||
}
|
||||
return &lint.LintResult{Status: lint.Pass}
|
||||
}
|
||||
|
||||
// noEmptyRevokedCertificatesList checks RFC 5280, Section 5.1.2.6:
|
||||
// When there are no revoked certificates, the revoked certificates list MUST be
|
||||
// absent.
|
||||
func noEmptyRevokedCertificatesList(crl *crl_x509.RevocationList) *lint.LintResult {
|
||||
if crl.RevokedCertificates != nil && len(crl.RevokedCertificates) == 0 {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "If the revokedCertificates list is empty, it must not be present",
|
||||
}
|
||||
}
|
||||
return &lint.LintResult{Status: lint.Pass}
|
||||
}
|
||||
|
||||
// hasAKI checks RFC 5280, Section 5.2.1:
|
||||
// Conforming CRL issuers MUST use the key identifier method, and MUST include
|
||||
// this extension in all CRLs issued.
|
||||
func hasAKI(crl *crl_x509.RevocationList) *lint.LintResult {
|
||||
if len(crl.AuthorityKeyId) == 0 {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "CRLs MUST include the authority key identifier extension",
|
||||
}
|
||||
}
|
||||
aki := cryptobyte.String(crl.AuthorityKeyId)
|
||||
var akiBody cryptobyte.String
|
||||
if !aki.ReadASN1(&akiBody, cryptobyte_asn1.SEQUENCE) {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "CRL has a malformed authority key identifier extension",
|
||||
}
|
||||
}
|
||||
if !akiBody.PeekASN1Tag(cryptobyte_asn1.Tag(0).ContextSpecific()) {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "CRLs MUST use the key identifier method in the authority key identifier extension",
|
||||
}
|
||||
}
|
||||
return &lint.LintResult{Status: lint.Pass}
|
||||
}
|
||||
|
||||
// hasNumber checks RFC 5280, Section 5.2.3:
|
||||
// CRL issuers conforming to this profile MUST include this extension in all
|
||||
// CRLs and MUST mark this extension as non-critical. Conforming CRL issuers
|
||||
// MUST NOT use CRLNumber values longer than 20 octets.
|
||||
func hasNumber(crl *crl_x509.RevocationList) *lint.LintResult {
|
||||
if crl.Number == nil {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "CRLs MUST include the CRL number extension",
|
||||
}
|
||||
}
|
||||
|
||||
crlNumberOID := asn1.ObjectIdentifier{2, 5, 29, 20} // id-ce-cRLNumber
|
||||
ext := getExtWithOID(crl.Extensions, crlNumberOID)
|
||||
if ext != nil && ext.Critical {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "CRL Number MUST NOT be marked critical",
|
||||
}
|
||||
}
|
||||
|
||||
numBytes := crl.Number.Bytes()
|
||||
if len(numBytes) > 20 || (len(numBytes) == 20 && numBytes[0]&0x80 != 0) {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "CRL Number MUST NOT be longer than 20 octets",
|
||||
}
|
||||
}
|
||||
return &lint.LintResult{Status: lint.Pass}
|
||||
}
|
||||
|
||||
// isNotDelta checks that the CRL is not a Delta CRL. (RFC 5280, Section 5.2.4).
|
||||
// There's no requirement against this, but Delta CRLs come with extra
|
||||
// requirements we don't want to deal with.
|
||||
func isNotDelta(crl *crl_x509.RevocationList) *lint.LintResult {
|
||||
deltaCRLIndicatorOID := asn1.ObjectIdentifier{2, 5, 29, 27} // id-ce-deltaCRLIndicator
|
||||
if getExtWithOID(crl.Extensions, deltaCRLIndicatorOID) != nil {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Notice,
|
||||
Details: "CRL is a Delta CRL",
|
||||
}
|
||||
}
|
||||
return &lint.LintResult{Status: lint.Pass}
|
||||
}
|
||||
|
||||
// checkIDP checks that the CRL does have an Issuing Distribution Point, that it
|
||||
// is critical, that it contains a single http distributionPointName, that it
|
||||
// asserts the onlyContainsUserCerts boolean, and that it does not contain any
|
||||
// of the other fields. (RFC 5280, Section 5.2.5).
|
||||
func checkIDP(crl *crl_x509.RevocationList) *lint.LintResult {
|
||||
idpOID := asn1.ObjectIdentifier{2, 5, 29, 28} // id-ce-issuingDistributionPoint
|
||||
idpe := getExtWithOID(crl.Extensions, idpOID)
|
||||
if idpe == nil {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Warn,
|
||||
Details: "CRL missing IDP",
|
||||
}
|
||||
}
|
||||
if !idpe.Critical {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "IDP MUST be critical",
|
||||
}
|
||||
}
|
||||
|
||||
// Step inside the outer issuingDistributionPoint sequence to get access to
|
||||
// its constituent fields, DistributionPoint and OnlyContainsUserCerts.
|
||||
idpv := cryptobyte.String(idpe.Value)
|
||||
if !idpv.ReadASN1(&idpv, cryptobyte_asn1.SEQUENCE) {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Warn,
|
||||
Details: "Failed to read issuingDistributionPoint",
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that the DistributionPoint is a reasonable URI. To get to the URI,
|
||||
// we have to step inside the DistributionPointName, then step inside that's
|
||||
// FullName, and finally read the singular SEQUENCE OF GeneralName element.
|
||||
if !idpv.PeekASN1Tag(cryptobyte_asn1.Tag(0).ContextSpecific().Constructed()) {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Warn,
|
||||
Details: "IDP should contain distributionPoint",
|
||||
}
|
||||
}
|
||||
|
||||
var dpName cryptobyte.String
|
||||
if !idpv.ReadASN1(&dpName, cryptobyte_asn1.Tag(0).ContextSpecific().Constructed()) {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Warn,
|
||||
Details: "Failed to read IDP distributionPoint",
|
||||
}
|
||||
}
|
||||
|
||||
if !dpName.ReadASN1(&dpName, cryptobyte_asn1.Tag(0).ContextSpecific().Constructed()) {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Warn,
|
||||
Details: "Failed to read IDP distributionPoint fullName",
|
||||
}
|
||||
}
|
||||
|
||||
uriBytes := make([]byte, 0)
|
||||
if !dpName.ReadASN1Bytes(&uriBytes, cryptobyte_asn1.Tag(6).ContextSpecific()) {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Warn,
|
||||
Details: "Failed to read IDP URI",
|
||||
}
|
||||
}
|
||||
|
||||
uri, err := url.Parse(string(uriBytes))
|
||||
if err != nil {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "Failed to parse IDP URI",
|
||||
}
|
||||
}
|
||||
|
||||
if uri.Scheme != "http" {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "IDP URI MUST use http scheme",
|
||||
}
|
||||
}
|
||||
|
||||
if !dpName.Empty() {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Warn,
|
||||
Details: "IDP should contain only one distributionPoint",
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that OnlyContainsUserCerts is True. We have to read this boolean as
|
||||
// a byte and ensure its value is 0xFF because cryptobyte.ReadASN1Boolean
|
||||
// can't handle custom encoding rules like this field's [1] tag.
|
||||
if !idpv.PeekASN1Tag(cryptobyte_asn1.Tag(1).ContextSpecific()) {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Warn,
|
||||
Details: "IDP should contain onlyContainsUserCerts",
|
||||
}
|
||||
}
|
||||
|
||||
onlyContainsUserCerts := make([]byte, 0)
|
||||
if !idpv.ReadASN1Bytes(&onlyContainsUserCerts, cryptobyte_asn1.Tag(1).ContextSpecific()) {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "Failed to read IDP onlyContainsUserCerts",
|
||||
}
|
||||
}
|
||||
|
||||
if len(onlyContainsUserCerts) != 1 || onlyContainsUserCerts[0] != 0xFF {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "IDP should set onlyContainsUserCerts: TRUE",
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that no other fields are set.
|
||||
if !idpv.Empty() {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Warn,
|
||||
Details: "IDP should not contain fields other than distributionPoint and onlyContainsUserCerts",
|
||||
}
|
||||
}
|
||||
|
||||
return &lint.LintResult{Status: lint.Pass}
|
||||
}
|
||||
|
||||
// hasNoFreshest checks that the CRL is does not have a Freshest CRL extension
|
||||
// (RFC 5280, Section 5.2.6). There's no requirement against this, but Freshest
|
||||
// CRL extensions (and the Delta CRLs they imply) come with extra requirements
|
||||
// we don't want to deal with.
|
||||
func hasNoFreshest(crl *crl_x509.RevocationList) *lint.LintResult {
|
||||
freshestOID := asn1.ObjectIdentifier{2, 5, 29, 46} // id-ce-freshestCRL
|
||||
if getExtWithOID(crl.Extensions, freshestOID) != nil {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Notice,
|
||||
Details: "CRL has a Freshest CRL url",
|
||||
}
|
||||
}
|
||||
return &lint.LintResult{Status: lint.Pass}
|
||||
}
|
||||
|
||||
// hasNoAIA checks that the CRL is does not have an Authority Information Access
|
||||
// extension (RFC 5280, Section 5.2.7). There's no requirement against this, but
|
||||
// AIAs come with extra requirements we don't want to deal with.
|
||||
func hasNoAIA(crl *crl_x509.RevocationList) *lint.LintResult {
|
||||
aiaOID := asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 1} // id-pe-authorityInfoAccess
|
||||
if getExtWithOID(crl.Extensions, aiaOID) != nil {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Notice,
|
||||
Details: "CRL has an Authority Information Access url",
|
||||
}
|
||||
}
|
||||
return &lint.LintResult{Status: lint.Pass}
|
||||
}
|
||||
|
||||
// hasNoCertIssuers checks that the CRL does not have any entries with the
|
||||
// Certificate Issuer extension (RFC 5280, Section 5.3.3). There is no
|
||||
// requirement against this, but the presence of this extension would mean that
|
||||
// the CRL includes certificates issued by an issuer other than the one signing
|
||||
// the CRL itself, which we don't want to do.
|
||||
func hasNoCertIssuers(crl *crl_x509.RevocationList) *lint.LintResult {
|
||||
certIssuerOID := asn1.ObjectIdentifier{2, 5, 29, 29} // id-ce-certificateIssuer
|
||||
for _, entry := range crl.RevokedCertificates {
|
||||
if getExtWithOID(entry.Extensions, certIssuerOID) != nil {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Notice,
|
||||
Details: "CRL has an entry with a Certificate Issuer extension",
|
||||
}
|
||||
}
|
||||
}
|
||||
return &lint.LintResult{Status: lint.Pass}
|
||||
}
|
||||
|
||||
// hasAcceptableValidity checks Baseline Requirements, Section 4.9.7:
|
||||
// The value of the nextUpdate field MUST NOT be more than ten days beyond the
|
||||
// value of the thisUpdate field.
|
||||
func hasAcceptableValidity(crl *crl_x509.RevocationList) *lint.LintResult {
|
||||
validity := crl.NextUpdate.Sub(crl.ThisUpdate)
|
||||
if validity <= 0 {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "CRL has NextUpdate at or before ThisUpdate",
|
||||
}
|
||||
} else if validity > 10*24*time.Hour {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "CRL has validity period greater than ten days",
|
||||
}
|
||||
}
|
||||
return &lint.LintResult{Status: lint.Pass}
|
||||
}
|
||||
|
||||
// noZeroReasonCodes checks Baseline Requirements, Section 7.2.2.1:
|
||||
// The CRLReason indicated MUST NOT be unspecified (0). If the reason for
|
||||
// revocation is unspecified, CAs MUST omit reasonCode entry extension, if
|
||||
// allowed by the previous requirements.
|
||||
// By extension, it therefore also checks RFC 5280, Section 5.3.1:
|
||||
// The reason code CRL entry extension SHOULD be absent instead of using the
|
||||
// unspecified (0) reasonCode value.
|
||||
func noZeroReasonCodes(crl *crl_x509.RevocationList) *lint.LintResult {
|
||||
for _, entry := range crl.RevokedCertificates {
|
||||
if entry.ReasonCode != nil && *entry.ReasonCode == 0 {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "CRL entries MUST NOT contain the unspecified (0) reason code",
|
||||
}
|
||||
}
|
||||
}
|
||||
return &lint.LintResult{Status: lint.Pass}
|
||||
}
|
||||
|
||||
// noCriticalReasons checks Baseline Requirements, Section 7.2.2.1:
|
||||
// If present, [the reasonCode] extension MUST NOT be marked critical.
|
||||
func noCriticalReasons(crl *crl_x509.RevocationList) *lint.LintResult {
|
||||
reasonCodeOID := asn1.ObjectIdentifier{2, 5, 29, 21} // id-ce-reasonCode
|
||||
for _, rc := range crl.RevokedCertificates {
|
||||
for _, ext := range rc.Extensions {
|
||||
if ext.Id.Equal(reasonCodeOID) && ext.Critical {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "CRL entry reasonCodes MUST NOT be critical",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return &lint.LintResult{Status: lint.Pass}
|
||||
}
|
||||
|
||||
// noCertificateHolds checks Baseline Requirements, Section 7.2.2.1:
|
||||
// The CRLReason MUST NOT be certificateHold (6).
|
||||
func noCertificateHolds(crl *crl_x509.RevocationList) *lint.LintResult {
|
||||
for _, entry := range crl.RevokedCertificates {
|
||||
if entry.ReasonCode != nil && *entry.ReasonCode == 6 {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "CRL entries MUST NOT use the certificateHold (6) reason code",
|
||||
}
|
||||
}
|
||||
}
|
||||
return &lint.LintResult{Status: lint.Pass}
|
||||
}
|
||||
|
||||
// hasMozReasonCodes checks MRSP v2.8 Section 6.1.1:
|
||||
// When the CRLReason code is not one of the following, then the reasonCode extension MUST NOT be provided:
|
||||
// - keyCompromise (RFC 5280 CRLReason #1);
|
||||
// - privilegeWithdrawn (RFC 5280 CRLReason #9);
|
||||
// - cessationOfOperation (RFC 5280 CRLReason #5);
|
||||
// - affiliationChanged (RFC 5280 CRLReason #3); or
|
||||
// - superseded (RFC 5280 CRLReason #4).
|
||||
func hasMozReasonCodes(crl *crl_x509.RevocationList) *lint.LintResult {
|
||||
for _, rc := range crl.RevokedCertificates {
|
||||
if rc.ReasonCode == nil {
|
||||
continue
|
||||
}
|
||||
switch *rc.ReasonCode {
|
||||
case 1: // keyCompromise
|
||||
case 3: // affiliationChanged
|
||||
case 4: // superseded
|
||||
case 5: // cessationOfOperation
|
||||
case 9: // privilegeWithdrawn
|
||||
continue
|
||||
default:
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "CRLs MUST NOT include reasonCodes other than 1, 3, 4, 5, and 9",
|
||||
}
|
||||
}
|
||||
}
|
||||
return &lint.LintResult{Status: lint.Pass}
|
||||
}
|
||||
|
||||
// hasValidTimestamps validates encoding of all CRL timestamp values as
|
||||
// specified in section 4.1.2.5 of RFC5280. Timestamp values MUST be encoded as
|
||||
// either UTCTime or a GeneralizedTime.
|
||||
//
|
||||
// UTCTime values MUST be expressed in Greenwich Mean Time (Zulu) and MUST
|
||||
// include seconds (i.e., times are YYMMDDHHMMSSZ), even where the number of
|
||||
// seconds is zero. See:
|
||||
// https://www.rfc-editor.org/rfc/rfc5280.html#section-4.1.2.5.1
|
||||
//
|
||||
// GeneralizedTime values MUST be expressed in Greenwich Mean Time (Zulu) and
|
||||
// MUST include seconds (i.e., times are YYYYMMDDHHMMSSZ), even where the number
|
||||
// of seconds is zero. GeneralizedTime values MUST NOT include fractional
|
||||
// seconds. See: https://www.rfc-editor.org/rfc/rfc5280.html#section-4.1.2.5.2
|
||||
//
|
||||
// Conforming applications MUST encode thisUpdate, nextUpdate, and cerficate
|
||||
// validity timestamps prior to 2050 as UTCTime and GeneralizedTime there-after.
|
||||
// See:
|
||||
// - https://www.rfc-editor.org/rfc/rfc5280.html#section-5.1.2.4
|
||||
// - https://www.rfc-editor.org/rfc/rfc5280.html#section-5.1.2.5
|
||||
// - https://www.rfc-editor.org/rfc/rfc5280.html#section-5.1.2.6
|
||||
func hasValidTimestamps(crl *crl_x509.RevocationList) *lint.LintResult {
|
||||
input := cryptobyte.String(crl.RawTBSRevocationList)
|
||||
lintFail := lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "Failed to re-parse tbsCertList during linting",
|
||||
}
|
||||
|
||||
// Read tbsCertList.
|
||||
var tbs cryptobyte.String
|
||||
if !input.ReadASN1(&tbs, cryptobyte_asn1.SEQUENCE) {
|
||||
return &lintFail
|
||||
}
|
||||
|
||||
// Skip (optional) version.
|
||||
if !tbs.SkipOptionalASN1(cryptobyte_asn1.INTEGER) {
|
||||
return &lintFail
|
||||
}
|
||||
|
||||
// Skip signature.
|
||||
if !tbs.SkipASN1(cryptobyte_asn1.SEQUENCE) {
|
||||
return &lintFail
|
||||
}
|
||||
|
||||
// Skip issuer.
|
||||
if !tbs.SkipASN1(cryptobyte_asn1.SEQUENCE) {
|
||||
return &lintFail
|
||||
}
|
||||
|
||||
// Read thisUpdate.
|
||||
var thisUpdate cryptobyte.String
|
||||
var thisUpdateTag cryptobyte_asn1.Tag
|
||||
if !tbs.ReadAnyASN1Element(&thisUpdate, &thisUpdateTag) {
|
||||
return &lintFail
|
||||
}
|
||||
|
||||
// Lint thisUpdate.
|
||||
err := lintTimestamp(&thisUpdate, thisUpdateTag)
|
||||
if err != nil {
|
||||
return &lint.LintResult{Status: lint.Error, Details: err.Error()}
|
||||
}
|
||||
|
||||
// Peek (optional) nextUpdate.
|
||||
if tbs.PeekASN1Tag(cryptobyte_asn1.UTCTime) || tbs.PeekASN1Tag(cryptobyte_asn1.GeneralizedTime) {
|
||||
// Read nextUpdate.
|
||||
var nextUpdate cryptobyte.String
|
||||
var nextUpdateTag cryptobyte_asn1.Tag
|
||||
if !tbs.ReadAnyASN1Element(&nextUpdate, &nextUpdateTag) {
|
||||
return &lintFail
|
||||
}
|
||||
|
||||
// Lint nextUpdate.
|
||||
err = lintTimestamp(&nextUpdate, nextUpdateTag)
|
||||
if err != nil {
|
||||
return &lint.LintResult{Status: lint.Error, Details: err.Error()}
|
||||
}
|
||||
}
|
||||
|
||||
// Peek (optional) revokedCertificates.
|
||||
if tbs.PeekASN1Tag(cryptobyte_asn1.SEQUENCE) {
|
||||
// Read sequence of revokedCertificate.
|
||||
var revokedSeq cryptobyte.String
|
||||
if !tbs.ReadASN1(&revokedSeq, cryptobyte_asn1.SEQUENCE) {
|
||||
return &lintFail
|
||||
}
|
||||
|
||||
// Iterate over each revokedCertificate sequence.
|
||||
for !revokedSeq.Empty() {
|
||||
// Read revokedCertificate.
|
||||
var certSeq cryptobyte.String
|
||||
if !revokedSeq.ReadASN1Element(&certSeq, cryptobyte_asn1.SEQUENCE) {
|
||||
return &lintFail
|
||||
}
|
||||
|
||||
if !certSeq.ReadASN1(&certSeq, cryptobyte_asn1.SEQUENCE) {
|
||||
return &lintFail
|
||||
}
|
||||
|
||||
// Skip userCertificate (serial number).
|
||||
if !certSeq.SkipASN1(cryptobyte_asn1.INTEGER) {
|
||||
return &lintFail
|
||||
}
|
||||
|
||||
// Read revocationDate.
|
||||
var revocationDate cryptobyte.String
|
||||
var revocationDateTag cryptobyte_asn1.Tag
|
||||
if !certSeq.ReadAnyASN1Element(&revocationDate, &revocationDateTag) {
|
||||
return &lintFail
|
||||
}
|
||||
|
||||
// Lint revocationDate.
|
||||
err = lintTimestamp(&revocationDate, revocationDateTag)
|
||||
if err != nil {
|
||||
return &lint.LintResult{Status: lint.Error, Details: err.Error()}
|
||||
}
|
||||
}
|
||||
}
|
||||
return &lint.LintResult{Status: lint.Pass}
|
||||
}
|
||||
|
||||
func lintTimestamp(der *cryptobyte.String, tag cryptobyte_asn1.Tag) error {
|
||||
// Preserve the original timestamp for length checking.
|
||||
derBytes := *der
|
||||
var tsBytes cryptobyte.String
|
||||
if !derBytes.ReadASN1(&tsBytes, tag) {
|
||||
return errors.New("failed to read timestamp")
|
||||
}
|
||||
tsLen := len(string(tsBytes))
|
||||
|
||||
var parsedTime time.Time
|
||||
switch tag {
|
||||
case cryptobyte_asn1.UTCTime:
|
||||
// Verify that the timestamp is properly formatted.
|
||||
if tsLen != len(utcTimeFormat) {
|
||||
return fmt.Errorf("timestamps encoded using UTCTime MUST be specified in the format %q", utcTimeFormat)
|
||||
}
|
||||
|
||||
if !der.ReadASN1UTCTime(&parsedTime) {
|
||||
return errors.New("failed to read timestamp encoded using UTCTime")
|
||||
}
|
||||
|
||||
// Verify that the timestamp is prior to the year 2050. This should
|
||||
// really never happen.
|
||||
if parsedTime.Year() > 2049 {
|
||||
return errors.New("ReadASN1UTCTime returned a UTCTime after 2049")
|
||||
}
|
||||
case cryptobyte_asn1.GeneralizedTime:
|
||||
// Verify that the timestamp is properly formatted.
|
||||
if tsLen != len(generalizedTimeFormat) {
|
||||
return fmt.Errorf(
|
||||
"timestamps encoded using GeneralizedTime MUST be specified in the format %q", generalizedTimeFormat,
|
||||
)
|
||||
}
|
||||
|
||||
if !der.ReadASN1GeneralizedTime(&parsedTime) {
|
||||
return fmt.Errorf("failed to read timestamp encoded using GeneralizedTime")
|
||||
}
|
||||
|
||||
// Verify that the timestamp occurred after the year 2049.
|
||||
if parsedTime.Year() < 2050 {
|
||||
return errors.New("timestamps prior to 2050 MUST be encoded using UTCTime")
|
||||
}
|
||||
default:
|
||||
return errors.New("unsupported time format")
|
||||
}
|
||||
|
||||
// Verify that the location is UTC.
|
||||
if parsedTime.Location() != time.UTC {
|
||||
return errors.New("time must be in UTC")
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,311 +0,0 @@
|
|||
package crl
|
||||
|
||||
import (
|
||||
"encoding/pem"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/letsencrypt/boulder/crl/crl_x509"
|
||||
"github.com/letsencrypt/boulder/test"
|
||||
"github.com/zmap/zlint/v3/lint"
|
||||
)
|
||||
|
||||
func loadPEMCRL(t *testing.T, filename string) *crl_x509.RevocationList {
|
||||
t.Helper()
|
||||
file, err := os.ReadFile(filename)
|
||||
test.AssertNotError(t, err, "reading CRL file")
|
||||
block, rest := pem.Decode(file)
|
||||
test.AssertEquals(t, block.Type, "X509 CRL")
|
||||
test.AssertEquals(t, len(rest), 0)
|
||||
crl, err := crl_x509.ParseRevocationList(block.Bytes)
|
||||
test.AssertNotError(t, err, "parsing CRL bytes")
|
||||
return crl
|
||||
}
|
||||
|
||||
func TestHasIssuerName(t *testing.T) {
|
||||
crl := loadPEMCRL(t, "testdata/good.pem")
|
||||
res := hasIssuerName(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Pass)
|
||||
|
||||
crl = loadPEMCRL(t, "testdata/no_issuer_name.pem")
|
||||
res = hasIssuerName(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Error)
|
||||
test.AssertContains(t, res.Details, "MUST have a non-empty issuer")
|
||||
}
|
||||
|
||||
func TestHasNextUpdate(t *testing.T) {
|
||||
crl := loadPEMCRL(t, "testdata/good.pem")
|
||||
res := hasNextUpdate(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Pass)
|
||||
|
||||
crl = loadPEMCRL(t, "testdata/no_next_update.pem")
|
||||
res = hasNextUpdate(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Error)
|
||||
test.AssertContains(t, res.Details, "MUST include the nextUpdate")
|
||||
}
|
||||
|
||||
func TestNoEmptyRevokedCertificatesList(t *testing.T) {
|
||||
crl := loadPEMCRL(t, "testdata/good.pem")
|
||||
res := noEmptyRevokedCertificatesList(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Pass)
|
||||
|
||||
crl = loadPEMCRL(t, "testdata/none_revoked.pem")
|
||||
res = noEmptyRevokedCertificatesList(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Pass)
|
||||
|
||||
crl = loadPEMCRL(t, "testdata/empty_revoked.pem")
|
||||
res = noEmptyRevokedCertificatesList(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Error)
|
||||
test.AssertContains(t, res.Details, "must not be present")
|
||||
}
|
||||
|
||||
func TestHasAKI(t *testing.T) {
|
||||
crl := loadPEMCRL(t, "testdata/good.pem")
|
||||
res := hasAKI(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Pass)
|
||||
|
||||
crl = loadPEMCRL(t, "testdata/no_aki.pem")
|
||||
res = hasAKI(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Error)
|
||||
test.AssertContains(t, res.Details, "MUST include the authority key identifier")
|
||||
|
||||
crl = loadPEMCRL(t, "testdata/aki_name_and_serial.pem")
|
||||
res = hasAKI(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Error)
|
||||
test.AssertContains(t, res.Details, "MUST use the key identifier method")
|
||||
}
|
||||
|
||||
func TestHashNumber(t *testing.T) {
|
||||
crl := loadPEMCRL(t, "testdata/good.pem")
|
||||
res := hasNumber(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Pass)
|
||||
|
||||
crl = loadPEMCRL(t, "testdata/no_number.pem")
|
||||
res = hasNumber(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Error)
|
||||
test.AssertContains(t, res.Details, "MUST include the CRL number")
|
||||
|
||||
crl = loadPEMCRL(t, "testdata/critical_number.pem")
|
||||
res = hasNumber(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Error)
|
||||
test.AssertContains(t, res.Details, "MUST NOT be marked critical")
|
||||
|
||||
crl = loadPEMCRL(t, "testdata/long_number.pem")
|
||||
res = hasNumber(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Error)
|
||||
test.AssertContains(t, res.Details, "MUST NOT be longer than 20 octets")
|
||||
}
|
||||
|
||||
func TestIsNotDelta(t *testing.T) {
|
||||
crl := loadPEMCRL(t, "testdata/good.pem")
|
||||
res := isNotDelta(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Pass)
|
||||
|
||||
crl = loadPEMCRL(t, "testdata/delta.pem")
|
||||
res = isNotDelta(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Notice)
|
||||
test.AssertContains(t, res.Details, "Delta")
|
||||
}
|
||||
|
||||
func TestCheckIDP(t *testing.T) {
|
||||
crl := loadPEMCRL(t, "testdata/good.pem")
|
||||
res := checkIDP(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Pass)
|
||||
|
||||
crl = loadPEMCRL(t, "testdata/no_idp.pem")
|
||||
res = checkIDP(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Warn)
|
||||
test.AssertContains(t, res.Details, "missing IDP")
|
||||
|
||||
crl = loadPEMCRL(t, "testdata/idp_no_uri.pem")
|
||||
res = checkIDP(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Warn)
|
||||
test.AssertContains(t, res.Details, "should contain distributionPoint")
|
||||
|
||||
crl = loadPEMCRL(t, "testdata/idp_two_uris.pem")
|
||||
res = checkIDP(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Warn)
|
||||
test.AssertContains(t, res.Details, "only one distributionPoint")
|
||||
|
||||
crl = loadPEMCRL(t, "testdata/idp_no_usercerts.pem")
|
||||
res = checkIDP(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Warn)
|
||||
test.AssertContains(t, res.Details, "should contain onlyContainsUserCerts")
|
||||
|
||||
crl = loadPEMCRL(t, "testdata/idp_some_reasons.pem")
|
||||
res = checkIDP(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Warn)
|
||||
test.AssertContains(t, res.Details, "should not contain fields other than")
|
||||
}
|
||||
|
||||
func TestHasNoFreshest(t *testing.T) {
|
||||
crl := loadPEMCRL(t, "testdata/good.pem")
|
||||
res := hasNoFreshest(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Pass)
|
||||
|
||||
crl = loadPEMCRL(t, "testdata/freshest.pem")
|
||||
res = hasNoFreshest(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Notice)
|
||||
test.AssertContains(t, res.Details, "Freshest")
|
||||
}
|
||||
|
||||
func TestHasNoAIA(t *testing.T) {
|
||||
crl := loadPEMCRL(t, "testdata/good.pem")
|
||||
res := hasNoAIA(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Pass)
|
||||
|
||||
crl = loadPEMCRL(t, "testdata/aia.pem")
|
||||
res = hasNoAIA(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Notice)
|
||||
test.AssertContains(t, res.Details, "Authority Information Access")
|
||||
}
|
||||
|
||||
func TestHasNoCertIssuers(t *testing.T) {
|
||||
crl := loadPEMCRL(t, "testdata/good.pem")
|
||||
res := hasNoCertIssuers(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Pass)
|
||||
|
||||
crl = loadPEMCRL(t, "testdata/cert_issuer.pem")
|
||||
res = hasNoCertIssuers(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Notice)
|
||||
test.AssertContains(t, res.Details, "Certificate Issuer")
|
||||
}
|
||||
|
||||
func TestHasAcceptableValidity(t *testing.T) {
|
||||
crl := loadPEMCRL(t, "testdata/good.pem")
|
||||
res := hasAcceptableValidity(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Pass)
|
||||
|
||||
crl = loadPEMCRL(t, "testdata/negative_validity.pem")
|
||||
res = hasAcceptableValidity(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Error)
|
||||
test.AssertContains(t, res.Details, "at or before")
|
||||
|
||||
crl = loadPEMCRL(t, "testdata/long_validity.pem")
|
||||
res = hasAcceptableValidity(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Error)
|
||||
test.AssertContains(t, res.Details, "greater than ten days")
|
||||
}
|
||||
|
||||
func TestNoZeroReasonCodes(t *testing.T) {
|
||||
crl := loadPEMCRL(t, "testdata/good.pem")
|
||||
res := noZeroReasonCodes(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Pass)
|
||||
|
||||
crl = loadPEMCRL(t, "testdata/reason_0.pem")
|
||||
res = noZeroReasonCodes(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Error)
|
||||
test.AssertContains(t, res.Details, "MUST NOT contain the unspecified")
|
||||
}
|
||||
|
||||
func TestNoCriticalReasons(t *testing.T) {
|
||||
crl := loadPEMCRL(t, "testdata/good.pem")
|
||||
res := noCriticalReasons(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Pass)
|
||||
|
||||
crl = loadPEMCRL(t, "testdata/critical_reason.pem")
|
||||
res = noCriticalReasons(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Error)
|
||||
test.AssertContains(t, res.Details, "reasonCodes MUST NOT be critical")
|
||||
}
|
||||
|
||||
func TestNoCertificateHolds(t *testing.T) {
|
||||
crl := loadPEMCRL(t, "testdata/good.pem")
|
||||
res := noCertificateHolds(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Pass)
|
||||
|
||||
crl = loadPEMCRL(t, "testdata/reason_6.pem")
|
||||
res = noCertificateHolds(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Error)
|
||||
test.AssertContains(t, res.Details, "MUST NOT use the certificateHold")
|
||||
}
|
||||
|
||||
func TestHasMozReasonCodes(t *testing.T) {
|
||||
// good.pem contains a revocation entry with no reason code extension.
|
||||
crl := loadPEMCRL(t, "testdata/good.pem")
|
||||
res := hasMozReasonCodes(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Pass)
|
||||
|
||||
crl = loadPEMCRL(t, "testdata/reason_0.pem")
|
||||
res = hasMozReasonCodes(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Error)
|
||||
test.AssertContains(t, res.Details, "MUST NOT include reasonCodes other than")
|
||||
|
||||
crl = loadPEMCRL(t, "testdata/reason_1.pem")
|
||||
res = hasMozReasonCodes(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Pass)
|
||||
|
||||
crl = loadPEMCRL(t, "testdata/reason_2.pem")
|
||||
res = hasMozReasonCodes(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Error)
|
||||
test.AssertContains(t, res.Details, "MUST NOT include reasonCodes other than")
|
||||
|
||||
crl = loadPEMCRL(t, "testdata/reason_3.pem")
|
||||
res = hasMozReasonCodes(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Pass)
|
||||
|
||||
crl = loadPEMCRL(t, "testdata/reason_4.pem")
|
||||
res = hasMozReasonCodes(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Pass)
|
||||
|
||||
crl = loadPEMCRL(t, "testdata/reason_5.pem")
|
||||
res = hasMozReasonCodes(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Pass)
|
||||
|
||||
crl = loadPEMCRL(t, "testdata/reason_6.pem")
|
||||
res = hasMozReasonCodes(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Error)
|
||||
test.AssertContains(t, res.Details, "MUST NOT include reasonCodes other than")
|
||||
|
||||
crl = loadPEMCRL(t, "testdata/reason_8.pem")
|
||||
res = hasMozReasonCodes(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Error)
|
||||
test.AssertContains(t, res.Details, "MUST NOT include reasonCodes other than")
|
||||
|
||||
crl = loadPEMCRL(t, "testdata/reason_9.pem")
|
||||
res = hasMozReasonCodes(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Pass)
|
||||
|
||||
crl = loadPEMCRL(t, "testdata/reason_10.pem")
|
||||
res = hasMozReasonCodes(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Error)
|
||||
test.AssertContains(t, res.Details, "MUST NOT include reasonCodes other than")
|
||||
}
|
||||
|
||||
func TestHasValidTimestamps(t *testing.T) {
|
||||
crl := loadPEMCRL(t, "testdata/good.pem")
|
||||
res := hasValidTimestamps(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Pass)
|
||||
|
||||
// Check that 'thisUpdate' of 'UTCTIME 500706164338Z' is considered valid.
|
||||
crl = loadPEMCRL(t, "testdata/good_utctime_1950.pem")
|
||||
res = hasValidTimestamps(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Pass)
|
||||
|
||||
// Check that 'thisUpdate' of 'GENERALIZEDTIME 20500706164338Z' is
|
||||
// considered valid.
|
||||
crl = loadPEMCRL(t, "testdata/good_gentime_2050.pem")
|
||||
res = hasValidTimestamps(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Pass)
|
||||
|
||||
// Check that 'thisUpdate' of 'GENERALIZEDTIME 20490706164338Z' (before
|
||||
// 2050) is considered invalid.
|
||||
crl = loadPEMCRL(t, "testdata/gentime_2049.pem")
|
||||
res = hasValidTimestamps(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Error)
|
||||
test.AssertContains(t, res.Details, "timestamps prior to 2050 MUST be encoded using UTCTime")
|
||||
|
||||
// Check that 'nextUpdate' of 'UTCTIME 2207061643Z' (missing seconds) is
|
||||
// considered invalid.
|
||||
crl = loadPEMCRL(t, "testdata/utctime_no_seconds.pem")
|
||||
res = hasValidTimestamps(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Error)
|
||||
test.AssertContains(t, res.Details, "timestamps encoded using UTCTime MUST be specified in the format \"YYMMDDHHMMSSZ\"")
|
||||
|
||||
// Check that 'revocationDate' of 'GENERALIZEDTIME 20490706154338Z' (before
|
||||
// 2050) is considered invalid.
|
||||
crl = loadPEMCRL(t, "testdata/gentime_revoked_2049.pem")
|
||||
res = hasValidTimestamps(crl)
|
||||
test.AssertEquals(t, res.Status, lint.Error)
|
||||
test.AssertContains(t, res.Details, "timestamps prior to 2050 MUST be encoded using UTCTime")
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
-----BEGIN X509 CRL-----
|
||||
MIIBYTCB6QIBATAKBggqhkjOPQQDAzBJMQswCQYDVQQGEwJYWDEVMBMGA1UEChMM
|
||||
Qm91bGRlciBUZXN0MSMwIQYDVQQDExooVEVTVCkgRWxlZ2FudCBFbGVwaGFudCBF
|
||||
MRcNMjIwNzA2MTY0MzM4WhcNMjIwNzE1MTY0MzM4WjApMCcCCAOuUdtRFVo8Fw0y
|
||||
MjA3MDYxNTQzMzhaMAwwCgYDVR0VBAMKAQGgRDBCMB8GA1UdIwQYMBaAFAHau3rL
|
||||
JSCOXnnW+ZZCLwJBKQe+MBEGA1UdFAQKAggW/0sm37IYDzAMBgNVHRwEBTADgQH/
|
||||
MAoGCCqGSM49BAMDA2cAMGQCMFayE0WLrRoxaXzYbdPAi7AEEr53OIulDND4vPlN
|
||||
0/A0RyJiIrgfXEPqsVCweqSoQQIwW7hgsE6Ke7wnxjuxc+jdK7iEyJxbbegQ0eYs
|
||||
1lDH112u5l4UkOooPYThzlkcUdNC
|
||||
-----END X509 CRL-----
|
|
@ -1,9 +0,0 @@
|
|||
-----BEGIN X509 CRL-----
|
||||
MIIBRDCBzAIBATAKBggqhkjOPQQDAzBJMQswCQYDVQQGEwJYWDEVMBMGA1UEChMM
|
||||
Qm91bGRlciBUZXN0MSMwIQYDVQQDExooVEVTVCkgRWxlZ2FudCBFbGVwaGFudCBF
|
||||
MRcNMjIwNzA2MTY0MzM4WjApMCcCCAOuUdtRFVo8Fw0yMjA3MDYxNTQzMzhaMAww
|
||||
CgYDVR0VBAMKAQGgNjA0MB8GA1UdIwQYMBaAFAHau3rLJSCOXnnW+ZZCLwJBKQe+
|
||||
MBEGA1UdFAQKAggW/0sm37IYDzAKBggqhkjOPQQDAwNnADBkAjBWshNFi60aMWl8
|
||||
2G3TwIuwBBK+dziLpQzQ+Lz5TdPwNEciYiK4H1xD6rFQsHqkqEECMFu4YLBOinu8
|
||||
J8Y7sXPo3Su4hMicW23oENHmLNZQx9ddruZeFJDqKD2E4c5ZHFHTQg==
|
||||
-----END X509 CRL-----
|
|
@ -1,10 +0,0 @@
|
|||
-----BEGIN X509 CRL-----
|
||||
MIIBUzCB2wIBATAKBggqhkjOPQQDAzBJMQswCQYDVQQGEwJYWDEVMBMGA1UEChMM
|
||||
Qm91bGRlciBUZXN0MSMwIQYDVQQDExooVEVTVCkgRWxlZ2FudCBFbGVwaGFudCBF
|
||||
MRcNMjIwNzA2MTY0MzM4WhcNMjIwNzE1MTY0MzM4WjApMCcCCAOuUdtRFVo8Fw0y
|
||||
MjA3MDYxNTQzMzhaMAwwCgYDVR0VBAMKAQGgNjA0MB8GA1UdIwQYMBaAFAHau3rL
|
||||
JSCOXnnW+ZZCLwJBKQe+MBEGA1UdFAQKAggW/0sm37IYDzAKBggqhkjOPQQDAwNn
|
||||
ADBkAjBWshNFi60aMWl82G3TwIuwBBK+dziLpQzQ+Lz5TdPwNEciYiK4H1xD6rFQ
|
||||
sHqkqEECMFu4YLBOinu8J8Y7sXPo3Su4hMicW23oENHmLNZQx9ddruZeFJDqKD2E
|
||||
4c5ZHFHTQg==
|
||||
-----END X509 CRL-----
|
|
@ -0,0 +1,62 @@
|
|||
package rfc
|
||||
|
||||
import (
|
||||
"github.com/zmap/zcrypto/x509"
|
||||
"github.com/zmap/zlint/v3/lint"
|
||||
"github.com/zmap/zlint/v3/util"
|
||||
"golang.org/x/crypto/cryptobyte"
|
||||
cryptobyte_asn1 "golang.org/x/crypto/cryptobyte/asn1"
|
||||
)
|
||||
|
||||
type crlHasAKI struct{}
|
||||
|
||||
/************************************************
|
||||
RFC 5280: 5.2.1
|
||||
Conforming CRL issuers MUST use the key identifier method, and MUST include this
|
||||
extension in all CRLs issued.
|
||||
************************************************/
|
||||
|
||||
func init() {
|
||||
lint.RegisterRevocationListLint(&lint.RevocationListLint{
|
||||
LintMetadata: lint.LintMetadata{
|
||||
Name: "e_crl_has_aki",
|
||||
Description: "Conforming",
|
||||
Citation: "RFC 5280: 5.2.1",
|
||||
Source: lint.RFC5280,
|
||||
EffectiveDate: util.RFC5280Date,
|
||||
},
|
||||
Lint: NewCrlHasAKI,
|
||||
})
|
||||
}
|
||||
|
||||
func NewCrlHasAKI() lint.RevocationListLintInterface {
|
||||
return &crlHasAKI{}
|
||||
}
|
||||
|
||||
func (l *crlHasAKI) CheckApplies(c *x509.RevocationList) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (l *crlHasAKI) Execute(c *x509.RevocationList) *lint.LintResult {
|
||||
if len(c.AuthorityKeyId) == 0 {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "CRLs MUST include the authority key identifier extension",
|
||||
}
|
||||
}
|
||||
aki := cryptobyte.String(c.AuthorityKeyId)
|
||||
var akiBody cryptobyte.String
|
||||
if !aki.ReadASN1(&akiBody, cryptobyte_asn1.SEQUENCE) {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "CRL has a malformed authority key identifier extension",
|
||||
}
|
||||
}
|
||||
if !akiBody.PeekASN1Tag(cryptobyte_asn1.Tag(0).ContextSpecific()) {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "CRLs MUST use the key identifier method in the authority key identifier extension",
|
||||
}
|
||||
}
|
||||
return &lint.LintResult{Status: lint.Pass}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package rfc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/zmap/zlint/v3/lint"
|
||||
|
||||
"github.com/letsencrypt/boulder/linter/lints/test"
|
||||
)
|
||||
|
||||
func TestCrlHasAKI(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
want lint.LintStatus
|
||||
wantSubStr string
|
||||
}{
|
||||
{
|
||||
name: "good",
|
||||
want: lint.Pass,
|
||||
},
|
||||
{
|
||||
name: "no_aki",
|
||||
want: lint.Error,
|
||||
wantSubStr: "MUST include the authority key identifier",
|
||||
},
|
||||
{
|
||||
name: "aki_name_and_serial",
|
||||
want: lint.Error,
|
||||
wantSubStr: "MUST use the key identifier method",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
l := NewCrlHasAKI()
|
||||
c := test.LoadPEMCRL(t, fmt.Sprintf("testdata/crl_%s.pem", tc.name))
|
||||
r := l.Execute(c)
|
||||
|
||||
if r.Status != tc.want {
|
||||
t.Errorf("expected %q, got %q", tc.want, r.Status)
|
||||
}
|
||||
if !strings.Contains(r.Details, tc.wantSubStr) {
|
||||
t.Errorf("expected %q, got %q", tc.wantSubStr, r.Details)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package rfc
|
||||
|
||||
import (
|
||||
"github.com/zmap/zcrypto/x509"
|
||||
"github.com/zmap/zlint/v3/lint"
|
||||
"github.com/zmap/zlint/v3/util"
|
||||
)
|
||||
|
||||
type crlHasIssuerName struct{}
|
||||
|
||||
/************************************************
|
||||
RFC 5280: 5.1.2.3
|
||||
The issuer field MUST contain a non-empty X.500 distinguished name (DN).
|
||||
|
||||
This lint does not enforce that the issuer field complies with the rest of
|
||||
the encoding rules of a certificate issuer name, because it (perhaps wrongly)
|
||||
assumes that those were checked when the issuer was itself issued, and on all
|
||||
certificates issued by this CRL issuer.
|
||||
************************************************/
|
||||
|
||||
func init() {
|
||||
lint.RegisterRevocationListLint(&lint.RevocationListLint{
|
||||
LintMetadata: lint.LintMetadata{
|
||||
Name: "e_crl_has_issuer_name",
|
||||
Description: "The CRL Issuer field MUST contain a non-empty X.500 distinguished name",
|
||||
Citation: "RFC 5280: 5.1.2.3",
|
||||
Source: lint.RFC5280,
|
||||
EffectiveDate: util.RFC5280Date,
|
||||
},
|
||||
Lint: NewCrlHasIssuerName,
|
||||
})
|
||||
}
|
||||
|
||||
func NewCrlHasIssuerName() lint.RevocationListLintInterface {
|
||||
return &crlHasIssuerName{}
|
||||
}
|
||||
|
||||
func (l *crlHasIssuerName) CheckApplies(c *x509.RevocationList) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (l *crlHasIssuerName) Execute(c *x509.RevocationList) *lint.LintResult {
|
||||
if len(c.Issuer.Names) == 0 {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "The CRL Issuer field MUST contain a non-empty X.500 distinguished name",
|
||||
}
|
||||
}
|
||||
return &lint.LintResult{Status: lint.Pass}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package rfc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/zmap/zlint/v3/lint"
|
||||
|
||||
"github.com/letsencrypt/boulder/linter/lints/test"
|
||||
)
|
||||
|
||||
func TestCrlHasIssuerName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
want lint.LintStatus
|
||||
wantSubStr string
|
||||
}{
|
||||
{
|
||||
name: "good",
|
||||
want: lint.Pass,
|
||||
},
|
||||
{
|
||||
name: "no_issuer_name",
|
||||
want: lint.Error,
|
||||
wantSubStr: "MUST contain a non-empty X.500 distinguished name",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
l := NewCrlHasIssuerName()
|
||||
c := test.LoadPEMCRL(t, fmt.Sprintf("testdata/crl_%s.pem", tc.name))
|
||||
r := l.Execute(c)
|
||||
|
||||
if r.Status != tc.want {
|
||||
t.Errorf("expected %q, got %q", tc.want, r.Status)
|
||||
}
|
||||
if !strings.Contains(r.Details, tc.wantSubStr) {
|
||||
t.Errorf("expected %q, got %q", tc.wantSubStr, r.Details)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package rfc
|
||||
|
||||
import (
|
||||
"github.com/zmap/zcrypto/encoding/asn1"
|
||||
"github.com/zmap/zcrypto/x509"
|
||||
"github.com/zmap/zlint/v3/lint"
|
||||
"github.com/zmap/zlint/v3/util"
|
||||
|
||||
"github.com/letsencrypt/boulder/linter/lints"
|
||||
)
|
||||
|
||||
type crlHasNumber struct{}
|
||||
|
||||
/************************************************
|
||||
RFC 5280: 5.2.3
|
||||
CRL issuers conforming to this profile MUST include this extension in all CRLs
|
||||
and MUST mark this extension as non-critical. Conforming CRL issuers MUST NOT
|
||||
use CRLNumber values longer than 20 octets.
|
||||
************************************************/
|
||||
|
||||
func init() {
|
||||
lint.RegisterRevocationListLint(&lint.RevocationListLint{
|
||||
LintMetadata: lint.LintMetadata{
|
||||
Name: "e_crl_has_number",
|
||||
Description: "CRLs must have a well-formed CRL Number extension",
|
||||
Citation: "RFC 5280: 5.2.3",
|
||||
Source: lint.RFC5280,
|
||||
EffectiveDate: util.RFC5280Date,
|
||||
},
|
||||
Lint: NewCrlHasNumber,
|
||||
})
|
||||
}
|
||||
|
||||
func NewCrlHasNumber() lint.RevocationListLintInterface {
|
||||
return &crlHasNumber{}
|
||||
}
|
||||
|
||||
func (l *crlHasNumber) CheckApplies(c *x509.RevocationList) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (l *crlHasNumber) Execute(c *x509.RevocationList) *lint.LintResult {
|
||||
if c.Number == nil {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "CRLs MUST include the CRL number extension",
|
||||
}
|
||||
}
|
||||
|
||||
crlNumberOID := asn1.ObjectIdentifier{2, 5, 29, 20} // id-ce-cRLNumber
|
||||
ext := lints.GetExtWithOID(c.Extensions, crlNumberOID)
|
||||
if ext != nil && ext.Critical {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "CRL Number MUST NOT be marked critical",
|
||||
}
|
||||
}
|
||||
|
||||
numBytes := c.Number.Bytes()
|
||||
if len(numBytes) > 20 || (len(numBytes) == 20 && numBytes[0]&0x80 != 0) {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "CRL Number MUST NOT be longer than 20 octets",
|
||||
}
|
||||
}
|
||||
return &lint.LintResult{Status: lint.Pass}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package rfc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/zmap/zlint/v3/lint"
|
||||
|
||||
"github.com/letsencrypt/boulder/linter/lints/test"
|
||||
)
|
||||
|
||||
func TestCrlHasNumber(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
want lint.LintStatus
|
||||
wantSubStr string
|
||||
}{
|
||||
{
|
||||
name: "good",
|
||||
want: lint.Pass,
|
||||
},
|
||||
{
|
||||
name: "no_number",
|
||||
want: lint.Error,
|
||||
wantSubStr: "MUST include the CRL number",
|
||||
},
|
||||
{
|
||||
name: "critical_number",
|
||||
want: lint.Error,
|
||||
wantSubStr: "MUST NOT be marked critical",
|
||||
},
|
||||
{
|
||||
name: "long_number",
|
||||
want: lint.Error,
|
||||
wantSubStr: "MUST NOT be longer than 20 octets",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
l := NewCrlHasNumber()
|
||||
c := test.LoadPEMCRL(t, fmt.Sprintf("testdata/crl_%s.pem", tc.name))
|
||||
r := l.Execute(c)
|
||||
|
||||
if r.Status != tc.want {
|
||||
t.Errorf("expected %q, got %q", tc.want, r.Status)
|
||||
}
|
||||
if !strings.Contains(r.Details, tc.wantSubStr) {
|
||||
t.Errorf("expected %q, got %q", tc.wantSubStr, r.Details)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,230 @@
|
|||
package rfc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/zmap/zcrypto/x509"
|
||||
"github.com/zmap/zlint/v3/lint"
|
||||
"github.com/zmap/zlint/v3/util"
|
||||
"golang.org/x/crypto/cryptobyte"
|
||||
cryptobyte_asn1 "golang.org/x/crypto/cryptobyte/asn1"
|
||||
)
|
||||
|
||||
const (
|
||||
utcTimeFormat = "YYMMDDHHMMSSZ"
|
||||
generalizedTimeFormat = "YYYYMMDDHHMMSSZ"
|
||||
)
|
||||
|
||||
type crlHasValidTimestamps struct{}
|
||||
|
||||
/************************************************
|
||||
RFC 5280: 5.1.2.4
|
||||
CRL issuers conforming to this profile MUST encode thisUpdate as UTCTime for
|
||||
dates through the year 2049. CRL issuers conforming to this profile MUST encode
|
||||
thisUpdate as GeneralizedTime for dates in the year 2050 or later. Conforming
|
||||
applications MUST be able to process dates that are encoded in either UTCTime or
|
||||
GeneralizedTime.
|
||||
|
||||
Where encoded as UTCTime, thisUpdate MUST be specified and interpreted as
|
||||
defined in Section 4.1.2.5.1. Where encoded as GeneralizedTime, thisUpdate MUST
|
||||
be specified and interpreted as defined in Section 4.1.2.5.2.
|
||||
|
||||
RFC 5280: 5.1.2.5
|
||||
CRL issuers conforming to this profile MUST encode nextUpdate as UTCTime for
|
||||
dates through the year 2049. CRL issuers conforming to this profile MUST encode
|
||||
nextUpdate as GeneralizedTime for dates in the year 2050 or later. Conforming
|
||||
applications MUST be able to process dates that are encoded in either UTCTime or
|
||||
GeneralizedTime.
|
||||
|
||||
Where encoded as UTCTime, nextUpdate MUST be specified and interpreted as
|
||||
defined in Section 4.1.2.5.1. Where encoded as GeneralizedTime, nextUpdate MUST
|
||||
be specified and interpreted as defined in Section 4.1.2.5.2.
|
||||
|
||||
RFC 5280: 5.1.2.6
|
||||
The time for revocationDate MUST be expressed as described in Section 5.1.2.4.
|
||||
|
||||
RFC 5280: 4.1.2.5.1
|
||||
UTCTime values MUST be expressed in Greenwich Mean Time (Zulu) and MUST include
|
||||
seconds (i.e., times are YYMMDDHHMMSSZ), even where the number of seconds is
|
||||
zero.
|
||||
|
||||
RFC 5280: 4.1.2.5.2
|
||||
GeneralizedTime values MUST be expressed in Greenwich Mean Time (Zulu) and MUST
|
||||
include seconds (i.e., times are YYYYMMDDHHMMSSZ), even where the number of
|
||||
seconds is zero. GeneralizedTime values MUST NOT include fractional seconds.
|
||||
************************************************/
|
||||
|
||||
func init() {
|
||||
lint.RegisterRevocationListLint(&lint.RevocationListLint{
|
||||
LintMetadata: lint.LintMetadata{
|
||||
Name: "e_crl_has_valid_timestamps",
|
||||
Description: "CRL thisUpdate, nextUpdate, and revocationDates must be properly encoded",
|
||||
Citation: "RFC 5280: 5.1.2.4, 5.1.2.5, and 5.1.2.6",
|
||||
Source: lint.RFC5280,
|
||||
EffectiveDate: util.RFC5280Date,
|
||||
},
|
||||
Lint: NewCrlHasValidTimestamps,
|
||||
})
|
||||
}
|
||||
|
||||
func NewCrlHasValidTimestamps() lint.RevocationListLintInterface {
|
||||
return &crlHasValidTimestamps{}
|
||||
}
|
||||
|
||||
func (l *crlHasValidTimestamps) CheckApplies(c *x509.RevocationList) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (l *crlHasValidTimestamps) Execute(c *x509.RevocationList) *lint.LintResult {
|
||||
input := cryptobyte.String(c.RawTBSRevocationList)
|
||||
lintFail := lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "Failed to re-parse tbsCertList during linting",
|
||||
}
|
||||
|
||||
// Read tbsCertList.
|
||||
var tbs cryptobyte.String
|
||||
if !input.ReadASN1(&tbs, cryptobyte_asn1.SEQUENCE) {
|
||||
return &lintFail
|
||||
}
|
||||
|
||||
// Skip (optional) version.
|
||||
if !tbs.SkipOptionalASN1(cryptobyte_asn1.INTEGER) {
|
||||
return &lintFail
|
||||
}
|
||||
|
||||
// Skip signature.
|
||||
if !tbs.SkipASN1(cryptobyte_asn1.SEQUENCE) {
|
||||
return &lintFail
|
||||
}
|
||||
|
||||
// Skip issuer.
|
||||
if !tbs.SkipASN1(cryptobyte_asn1.SEQUENCE) {
|
||||
return &lintFail
|
||||
}
|
||||
|
||||
// Read thisUpdate.
|
||||
var thisUpdate cryptobyte.String
|
||||
var thisUpdateTag cryptobyte_asn1.Tag
|
||||
if !tbs.ReadAnyASN1Element(&thisUpdate, &thisUpdateTag) {
|
||||
return &lintFail
|
||||
}
|
||||
|
||||
// Lint thisUpdate.
|
||||
err := lintTimestamp(&thisUpdate, thisUpdateTag)
|
||||
if err != nil {
|
||||
return &lint.LintResult{Status: lint.Error, Details: err.Error()}
|
||||
}
|
||||
|
||||
// Peek (optional) nextUpdate.
|
||||
if tbs.PeekASN1Tag(cryptobyte_asn1.UTCTime) || tbs.PeekASN1Tag(cryptobyte_asn1.GeneralizedTime) {
|
||||
// Read nextUpdate.
|
||||
var nextUpdate cryptobyte.String
|
||||
var nextUpdateTag cryptobyte_asn1.Tag
|
||||
if !tbs.ReadAnyASN1Element(&nextUpdate, &nextUpdateTag) {
|
||||
return &lintFail
|
||||
}
|
||||
|
||||
// Lint nextUpdate.
|
||||
err = lintTimestamp(&nextUpdate, nextUpdateTag)
|
||||
if err != nil {
|
||||
return &lint.LintResult{Status: lint.Error, Details: err.Error()}
|
||||
}
|
||||
}
|
||||
|
||||
// Peek (optional) revokedCertificates.
|
||||
if tbs.PeekASN1Tag(cryptobyte_asn1.SEQUENCE) {
|
||||
// Read sequence of revokedCertificate.
|
||||
var revokedSeq cryptobyte.String
|
||||
if !tbs.ReadASN1(&revokedSeq, cryptobyte_asn1.SEQUENCE) {
|
||||
return &lintFail
|
||||
}
|
||||
|
||||
// Iterate over each revokedCertificate sequence.
|
||||
for !revokedSeq.Empty() {
|
||||
// Read revokedCertificate.
|
||||
var certSeq cryptobyte.String
|
||||
if !revokedSeq.ReadASN1Element(&certSeq, cryptobyte_asn1.SEQUENCE) {
|
||||
return &lintFail
|
||||
}
|
||||
|
||||
if !certSeq.ReadASN1(&certSeq, cryptobyte_asn1.SEQUENCE) {
|
||||
return &lintFail
|
||||
}
|
||||
|
||||
// Skip userCertificate (serial number).
|
||||
if !certSeq.SkipASN1(cryptobyte_asn1.INTEGER) {
|
||||
return &lintFail
|
||||
}
|
||||
|
||||
// Read revocationDate.
|
||||
var revocationDate cryptobyte.String
|
||||
var revocationDateTag cryptobyte_asn1.Tag
|
||||
if !certSeq.ReadAnyASN1Element(&revocationDate, &revocationDateTag) {
|
||||
return &lintFail
|
||||
}
|
||||
|
||||
// Lint revocationDate.
|
||||
err = lintTimestamp(&revocationDate, revocationDateTag)
|
||||
if err != nil {
|
||||
return &lint.LintResult{Status: lint.Error, Details: err.Error()}
|
||||
}
|
||||
}
|
||||
}
|
||||
return &lint.LintResult{Status: lint.Pass}
|
||||
}
|
||||
|
||||
func lintTimestamp(der *cryptobyte.String, tag cryptobyte_asn1.Tag) error {
|
||||
// Preserve the original timestamp for length checking.
|
||||
derBytes := *der
|
||||
var tsBytes cryptobyte.String
|
||||
if !derBytes.ReadASN1(&tsBytes, tag) {
|
||||
return errors.New("failed to read timestamp")
|
||||
}
|
||||
tsLen := len(string(tsBytes))
|
||||
|
||||
var parsedTime time.Time
|
||||
switch tag {
|
||||
case cryptobyte_asn1.UTCTime:
|
||||
// Verify that the timestamp is properly formatted.
|
||||
if tsLen != len(utcTimeFormat) {
|
||||
return fmt.Errorf("timestamps encoded using UTCTime MUST be specified in the format %q", utcTimeFormat)
|
||||
}
|
||||
|
||||
if !der.ReadASN1UTCTime(&parsedTime) {
|
||||
return errors.New("failed to read timestamp encoded using UTCTime")
|
||||
}
|
||||
|
||||
// Verify that the timestamp is prior to the year 2050. This should
|
||||
// really never happen.
|
||||
if parsedTime.Year() > 2049 {
|
||||
return errors.New("ReadASN1UTCTime returned a UTCTime after 2049")
|
||||
}
|
||||
case cryptobyte_asn1.GeneralizedTime:
|
||||
// Verify that the timestamp is properly formatted.
|
||||
if tsLen != len(generalizedTimeFormat) {
|
||||
return fmt.Errorf(
|
||||
"timestamps encoded using GeneralizedTime MUST be specified in the format %q", generalizedTimeFormat,
|
||||
)
|
||||
}
|
||||
|
||||
if !der.ReadASN1GeneralizedTime(&parsedTime) {
|
||||
return fmt.Errorf("failed to read timestamp encoded using GeneralizedTime")
|
||||
}
|
||||
|
||||
// Verify that the timestamp occurred after the year 2049.
|
||||
if parsedTime.Year() < 2050 {
|
||||
return errors.New("timestamps prior to 2050 MUST be encoded using UTCTime")
|
||||
}
|
||||
default:
|
||||
return errors.New("unsupported time format")
|
||||
}
|
||||
|
||||
// Verify that the location is UTC.
|
||||
if parsedTime.Location() != time.UTC {
|
||||
return errors.New("time must be in UTC")
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
package rfc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/zmap/zlint/v3/lint"
|
||||
|
||||
"github.com/letsencrypt/boulder/linter/lints/test"
|
||||
)
|
||||
|
||||
func TestCrlHasValidTimestamps(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
want lint.LintStatus
|
||||
wantSubStr string
|
||||
}{
|
||||
{
|
||||
name: "good",
|
||||
want: lint.Pass,
|
||||
},
|
||||
{
|
||||
name: "good_utctime_1950",
|
||||
want: lint.Pass,
|
||||
},
|
||||
{
|
||||
name: "good_gentime_2050",
|
||||
want: lint.Pass,
|
||||
},
|
||||
{
|
||||
name: "gentime_2049",
|
||||
want: lint.Error,
|
||||
wantSubStr: "timestamps prior to 2050 MUST be encoded using UTCTime",
|
||||
},
|
||||
{
|
||||
name: "utctime_no_seconds",
|
||||
want: lint.Error,
|
||||
wantSubStr: "timestamps encoded using UTCTime MUST be specified in the format \"YYMMDDHHMMSSZ\"",
|
||||
},
|
||||
{
|
||||
name: "gentime_revoked_2049",
|
||||
want: lint.Error,
|
||||
wantSubStr: "timestamps prior to 2050 MUST be encoded using UTCTime",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
l := NewCrlHasValidTimestamps()
|
||||
c := test.LoadPEMCRL(t, fmt.Sprintf("testdata/crl_%s.pem", tc.name))
|
||||
r := l.Execute(c)
|
||||
|
||||
if r.Status != tc.want {
|
||||
t.Errorf("expected %q, got %q", tc.want, r.Status)
|
||||
}
|
||||
if !strings.Contains(r.Details, tc.wantSubStr) {
|
||||
t.Errorf("expected %q, got %q", tc.wantSubStr, r.Details)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package rfc
|
||||
|
||||
import (
|
||||
"github.com/zmap/zcrypto/x509"
|
||||
"github.com/zmap/zlint/v3/lint"
|
||||
"github.com/zmap/zlint/v3/util"
|
||||
)
|
||||
|
||||
type crlNoEmptyRevokedCertsList struct{}
|
||||
|
||||
/************************************************
|
||||
RFC 5280: 5.1.2.6
|
||||
When there are no revoked certificates, the revoked certificates list MUST be
|
||||
absent.
|
||||
************************************************/
|
||||
|
||||
func init() {
|
||||
lint.RegisterRevocationListLint(&lint.RevocationListLint{
|
||||
LintMetadata: lint.LintMetadata{
|
||||
Name: "e_crl_no_empty_revoked_certificates_list",
|
||||
Description: "When there are no revoked certificates, the revoked certificates list MUST be absent.",
|
||||
Citation: "RFC 5280: 5.1.2.6",
|
||||
Source: lint.RFC5280,
|
||||
EffectiveDate: util.RFC5280Date,
|
||||
},
|
||||
Lint: NewCrlNoEmptyRevokedCertsList,
|
||||
})
|
||||
}
|
||||
|
||||
func NewCrlNoEmptyRevokedCertsList() lint.RevocationListLintInterface {
|
||||
return &crlNoEmptyRevokedCertsList{}
|
||||
}
|
||||
|
||||
func (l *crlNoEmptyRevokedCertsList) CheckApplies(c *x509.RevocationList) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (l *crlNoEmptyRevokedCertsList) Execute(c *x509.RevocationList) *lint.LintResult {
|
||||
// TODO(#6741): Rewrite this lint because upstream does not make this distinction.
|
||||
if c.RevokedCertificates != nil && len(c.RevokedCertificates) == 0 {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: "If the revokedCertificates list is empty, it must not be present",
|
||||
}
|
||||
}
|
||||
return &lint.LintResult{Status: lint.Pass}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package rfc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/zmap/zlint/v3/lint"
|
||||
|
||||
"github.com/letsencrypt/boulder/linter/lints/test"
|
||||
)
|
||||
|
||||
func TestCrlNoEmptyRevokedCertsList(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
want lint.LintStatus
|
||||
wantSubStr string
|
||||
}{
|
||||
{
|
||||
name: "good",
|
||||
want: lint.Pass,
|
||||
},
|
||||
{
|
||||
name: "none_revoked",
|
||||
want: lint.Pass,
|
||||
},
|
||||
{
|
||||
name: "empty_revoked",
|
||||
want: lint.Error,
|
||||
wantSubStr: "must not be present",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
l := NewCrlNoEmptyRevokedCertsList()
|
||||
c := test.LoadPEMCRL(t, fmt.Sprintf("testdata/crl_%s.pem", tc.name))
|
||||
r := l.Execute(c)
|
||||
|
||||
if r.Status != tc.want {
|
||||
t.Errorf("expected %q, got %q", tc.want, r.Status)
|
||||
}
|
||||
if !strings.Contains(r.Details, tc.wantSubStr) {
|
||||
t.Errorf("expected %q, got %q", tc.wantSubStr, r.Details)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
-----BEGIN X509 CRL-----
|
||||
MIIBmDCCAR8CAQEwCgYIKoZIzj0EAwMwSTELMAkGA1UEBhMCWFgxFTATBgNVBAoT
|
||||
DEJvdWxkZXIgVGVzdDEjMCEGA1UEAxMaKFRFU1QpIEVsZWdhbnQgRWxlcGhhbnQg
|
||||
RTEXDTIyMTAxMDIwMTIwN1oXDTIyMTAxOTIwMTIwNlowKTAnAggDrlHbURVaPBcN
|
||||
MjIxMDEwMTkxMjA3WjAMMAoGA1UdFQQDCgEBoHoweDAfBgNVHSMEGDAWgBQB2rt6
|
||||
yyUgjl551vmWQi8CQSkHvjARBgNVHRQECgIIFxzOPeSCumEwQgYDVR0cAQH/BDgw
|
||||
NqAxoC+GLWh0dHA6Ly9jLmJvdWxkZXIudGVzdC82NjI4Mzc1NjkxMzU4ODI4OC8w
|
||||
LmNybIEB/zAKBggqhkjOPQQDAwNnADBkAjAvDkIUnTYavJ6h8606MDyFh2uw/cF+
|
||||
OVnM4sE8nUdGy0XYg0hGfbR4MY+kRxRQayICMFeQPpcpIr0zgXpP6lUXU0rcLSva
|
||||
tuaeQSVr24nGjZ7Py0vc94w0n7idZ8wje5+/Mw==
|
||||
-----END X509 CRL-----
|
|
@ -0,0 +1,23 @@
|
|||
package test
|
||||
|
||||
import (
|
||||
"encoding/pem"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/zmap/zcrypto/x509"
|
||||
|
||||
"github.com/letsencrypt/boulder/test"
|
||||
)
|
||||
|
||||
func LoadPEMCRL(t *testing.T, filename string) *x509.RevocationList {
|
||||
t.Helper()
|
||||
file, err := os.ReadFile(filename)
|
||||
test.AssertNotError(t, err, "reading CRL file")
|
||||
block, rest := pem.Decode(file)
|
||||
test.AssertEquals(t, block.Type, "X509 CRL")
|
||||
test.AssertEquals(t, len(rest), 0)
|
||||
crl, err := x509.ParseRevocationList(block.Bytes)
|
||||
test.AssertNotError(t, err, "parsing CRL bytes")
|
||||
return crl
|
||||
}
|
Loading…
Reference in New Issue