Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Code Signing lints for EKU, Key Usage, RSA Key Size and CRLDistributionPoints #865

Merged
merged 15 commits into from
Jul 21, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions v3/lint/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,9 @@ func (l *CertificateLint) Execute(cert *x509.Certificate, config Configuration)
if l.Source == CABFSMIMEBaselineRequirements && !util.IsEmailProtectionCert(cert) {
return &LintResult{Status: NA}
}
if l.Source == CABFCSBaselineRequirements && !util.IsCodeSigning(cert.PolicyIdentifiers) {
return &LintResult{Status: NA}
}
lint := l.Lint()
err := config.MaybeConfigure(lint, l.Name)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions v3/lint/source.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const (
RFC6962 LintSource = "RFC6962"
RFC8813 LintSource = "RFC8813"
CABFBaselineRequirements LintSource = "CABF_BR"
CABFCSBaselineRequirements LintSource = "CABF_CS_BR"
CABFSMIMEBaselineRequirements LintSource = "CABF_SMIME_BR"
CABFEVGuidelines LintSource = "CABF_EV"
MozillaRootStorePolicy LintSource = "Mozilla"
Expand Down
62 changes: 62 additions & 0 deletions v3/lints/cabf_cs_br/lint_cs_crl_distribution_points.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package cabf_cs_br

import (
"strings"

"github.com/zmap/zcrypto/x509"
"github.com/zmap/zlint/v3/lint"
"github.com/zmap/zlint/v3/util"
)

/*7.1.2.3 b. cRLDistributionPoints
This extension MUST be present. It MUST NOT be marked critical, and it MUST contain the
HTTP URL of the CA’s CRL service*/

func init() {
lint.RegisterCertificateLint(&lint.CertificateLint{
LintMetadata: lint.LintMetadata{
Name: "e_cs_crl_distribution_points",
Description: "This extension MUST be present. It MUST NOT be marked critical. It MUST contain the HTTP URL of the CA's CRL service",
Citation: "CABF CS BRs 7.1.2.3.b",
Source: lint.CABFCSBaselineRequirements,
EffectiveDate: util.CABF_CS_BRs_1_2_Date,
},
Lint: NewCrlDistributionPoints,
})
}

type crlDistributionPoints struct{}

func NewCrlDistributionPoints() lint.LintInterface {
return &crlDistributionPoints{}
}

func (l *crlDistributionPoints) CheckApplies(c *x509.Certificate) bool {
return util.IsCodeSigning(c.PolicyIdentifiers) && util.IsSubscriberCert(c)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the preamble to the document...

The scope of these Requirements includes all "Code Signing Certificates", as defined below, and associated Timestamp Authorities, and all Certification Authorities technically capable of issuing Code Signing Certificates, including any Root CA that is publicly trusted for code signing and all other CAs that might serve to complete the validation path to such Root CA.

...I believe that we can safely add the following assumption to base.go.

	if l.Source == CABFCSBaselineRequirements && !util.IsCodeSigning(cert) {
		return &LintResult{Status: NA}
	}

Additionally, I am somewhat indecisive about the IsSubscriber clause that we have attached to these specific lints. It's unfortunate, but the document seems to have a mix of explicitly stating when a set of requirements is target specifically subscriber certs as well as implicit. That is, lints such as these seem like they would naturally apply to subscriber certs, but it's not explicitly spelled out like I am seeing in some other sections...unless I am simply not seeing it.

Copy link
Contributor Author

@digirenpeter digirenpeter Jul 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the CABF CS BR 7.1.2, these lints are targeted at the Subscriber Certificate profile, there is some overlap with the Sub CA profile, not so much with the root CA profile.

e_cs_crl_distribution_points
Applies to: Both Subscriber and Subordinate CA certificates. (7.1.2.2b and 7.1.2.3b)
Root CA: Not explicitly required.

e_cs_eku_required
Applies to: Both Subscriber and Subordinate CA certificates.
Root CA: EKUs MUST NOT be present. (7.1.2.1d)
Subordinate CA: Also prohibits id-kp-emailProtection. (7.1.2.2f)

e_cs_key_usage_required
Applies to: Specifically to Subscriber certificates.
Subordinate CA and Root CA: Both require cRLSign and keyCertSign, whereas Code Signing Certificates prohibits both. 

e_cs_rsa_key_size
Subscriber Certificates: RSA key size MUST be at least 3072 bits.
Root and Subordinate CA Certificates: RSA key size MUST be at least 4096 bits.

So we could remove the IsSubscriber check, I would need to update the lints to accommodate the other profiles. But we should keep the IsSubscriber for the key_usage check, since it's basically opposite to the CAs.

digirenpeter marked this conversation as resolved.
Show resolved Hide resolved
}

func (l *crlDistributionPoints) Execute(c *x509.Certificate) *lint.LintResult {
cdp := util.GetExtFromCert(c, util.CrlDistOID)
if cdp == nil {
return &lint.LintResult{
Status: lint.Error,
Details: "The cRLDistributionPoints extension MUST be present."}
}

if cdp.Critical {
return &lint.LintResult{
Status: lint.Error,
Details: "The cRLDistributionPoints MUST NOT be marked critical."}
}

// MUST contain the HTTP URL of the CA’s CRL service
for _, uri := range c.CRLDistributionPoints {
if !strings.HasPrefix(uri, "http://") {
return &lint.LintResult{Status: lint.Error, Details: "cRLDistributionPoints MUST contain the HTTP URL of the CA's CRL service"}
}
}

return &lint.LintResult{
Status: lint.Pass,
}
}
40 changes: 40 additions & 0 deletions v3/lints/cabf_cs_br/lint_cs_crl_distribution_points_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package cabf_cs_br

import (
"testing"

"github.com/zmap/zlint/v3/lint"
"github.com/zmap/zlint/v3/test"
)

func TestCsCrlDistributionPoints(t *testing.T) {
testCases := []struct {
Name string
InputFilename string
ExpectedResult lint.LintStatus
}{
{
Name: "pass - code signing certificate with CRLDistributionPoints",
InputFilename: "code_signing/validCodeSigningCertificate.pem",
ExpectedResult: lint.Pass,
},
{
Name: "fail - code signing certificate without CRLDistributionPoints",
InputFilename: "code_signing/noCrldpIncluded.pem",
ExpectedResult: lint.Error,
},
{
Name: "fail - code signing certificate with CRLDistributionPoints without http",
InputFilename: "code_signing/crlDpNoHttp.pem",
ExpectedResult: lint.Error,
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
result := test.TestLint("e_cs_crl_distribution_points", tc.InputFilename)
if result.Status != tc.ExpectedResult {
t.Errorf("expected result %v was %v - details: %v", tc.ExpectedResult, result.Status, result.Details)
}
})
}
}
83 changes: 83 additions & 0 deletions v3/lints/cabf_cs_br/lint_cs_eku_required.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package cabf_cs_br

import (
"fmt"

"github.com/zmap/zcrypto/x509"

"github.com/zmap/zlint/v3/lint"
"github.com/zmap/zlint/v3/util"
)

/* 7.1.2.3 Code signing and Timestamp Certificate
f. extKeyUsage
If the Certificate is a Code Signing Certificate, then id-kp-codeSigning MUST be present
and the following EKUs MAY be present:
• Lifetime Signing OID (1.3.6.1.4.1.311.10.3.13)
• id-kp-emailProtection
• Document Signing (1.3.6.1.4.1.311.3.10.3.12)

If the Certificate is a Timestamp Certificate, then id-kp-timeStamping MUST be present
and MUST be marked critical.
Additionally, the following EKUs MUST NOT be present:
• anyExtendedKeyUsage
• id-kp-serverAuth

Other values SHOULD NOT be present. If any other value is present, the CA MUST have a
business agreement with a Platform vendor requiring that EKU in order to issue a
Platform‐specific code signing certificate with that EKU.
*/

func init() {
lint.RegisterCertificateLint(&lint.CertificateLint{
LintMetadata: lint.LintMetadata{
Name: "e_cs_eku_required",
Description: "If the Certificate is a Code Signing Certificate, then id-kp-codeSigning MUST be present. anyExtendedKeyUsage and id-kp-serverAuth MUST NOT be present.",
Citation: "CABF CS BRs 7.1.2.3.f",
Source: lint.CABFCSBaselineRequirements,
EffectiveDate: util.CABF_CS_BRs_1_2_Date,
},
Lint: NewCsEKURequired,
})
}

type csEKURequired struct{}

func NewCsEKURequired() lint.LintInterface {
return &csEKURequired{}
}

func (l *csEKURequired) CheckApplies(c *x509.Certificate) bool {
return util.IsCodeSigning(c.PolicyIdentifiers) && util.IsSubscriberCert(c)
digirenpeter marked this conversation as resolved.
Show resolved Hide resolved
}

func (l *csEKURequired) Execute(c *x509.Certificate) *lint.LintResult {
prohibitedEKUs := map[x509.ExtKeyUsage]struct{}{
x509.ExtKeyUsageAny: {},
x509.ExtKeyUsageServerAuth: {},
}

hasCodeSigningEKU := false

for _, eku := range c.ExtKeyUsage {
if eku == x509.ExtKeyUsageCodeSigning {
hasCodeSigningEKU = true
}

if _, isProhibited := prohibitedEKUs[eku]; isProhibited {
return &lint.LintResult{
Status: lint.Error,
Details: fmt.Sprintf("Code Signing certificate includes prohibited EKU: %v", eku),
}
}
}

if !hasCodeSigningEKU {
return &lint.LintResult{
Status: lint.Error,
Details: "Code Signing certificate missing required Code Signing EKU",
}
}

return &lint.LintResult{Status: lint.Pass}
}
40 changes: 40 additions & 0 deletions v3/lints/cabf_cs_br/lint_cs_eku_required_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package cabf_cs_br

import (
"testing"

"github.com/zmap/zlint/v3/lint"
"github.com/zmap/zlint/v3/test"
)

func TestCsEKUCheck(t *testing.T) {
testCases := []struct {
Name string
InputFilename string
ExpectedResult lint.LintStatus
}{
{
Name: "pass - valid code signing certificate with required EKU",
InputFilename: "code_signing/validCodeSigningCertificate.pem",
ExpectedResult: lint.Pass,
},
{
Name: "fail - code signing certificate without required EKU",
InputFilename: "code_signing/noRequiredCodeSigningEKU.pem",
ExpectedResult: lint.Error,
},
{
Name: "fail - code signing certificate with prohibited EKU",
InputFilename: "code_signing/containsProhibitedEKU.pem",
ExpectedResult: lint.Error,
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
result := test.TestLint("e_cs_eku_required", tc.InputFilename)
if result.Status != tc.ExpectedResult {
t.Errorf("expected result %v was %v - details: %v", tc.ExpectedResult, result.Status, result.Details)
}
})
}
}
79 changes: 79 additions & 0 deletions v3/lints/cabf_cs_br/lint_cs_key_usage_required.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package cabf_cs_br

import (
"github.com/zmap/zcrypto/x509"

"github.com/zmap/zlint/v3/lint"
"github.com/zmap/zlint/v3/util"
)

/* 7.1.2.3 Code signing and Timestamp Certificate
e. keyUsage
This extension MUST be present and MUST be marked critical.
The bit position for digitalSignature MUST be set. Bit positions for keyCertSign and
cRLSign MUST NOT be set. All other bit positions SHOULD NOT be set.
*/

func init() {
lint.RegisterCertificateLint(&lint.CertificateLint{
LintMetadata: lint.LintMetadata{
Name: "e_cs_key_usage_required",
Description: "This extension MUST be present and MUST be marked critical. The bit position for digitalSignature MUST be set. The bit positions for keyCertSign and cRLSign MUST NOT be set. All other bit positions SHOULD NOT be set.",
Citation: "CABF CS BRs 7.1.2.3e",
Source: lint.CABFCSBaselineRequirements,
EffectiveDate: util.CABF_CS_BRs_1_2_Date,
},
Lint: NewCsKeyUsageRequired,
})
}

type csKeyUsageRequired struct{}

func NewCsKeyUsageRequired() lint.LintInterface {
return &csKeyUsageRequired{}
}

func (l *csKeyUsageRequired) CheckApplies(c *x509.Certificate) bool {
return util.IsCodeSigning(c.PolicyIdentifiers) && util.IsSubscriberCert(c)
digirenpeter marked this conversation as resolved.
Show resolved Hide resolved
}

func (l *csKeyUsageRequired) Execute(c *x509.Certificate) *lint.LintResult {
ku := util.GetExtFromCert(c, util.KeyUsageOID)
if ku == nil {
return &lint.LintResult{
Status: lint.Error,
Details: "Key usage extension MUST be present.",
}
}

if !ku.Critical {
return &lint.LintResult{
Status: lint.Error,
Details: "Key usage extension MUST be marked critical",
}
}

if (c.KeyUsage & x509.KeyUsageDigitalSignature) == 0 {
return &lint.LintResult{
Status: lint.Error,
Details: "Code Signing certificate must have digitalSignature key usage",
}
}

// keyCertSign and cRLSign bits MUST NOT be set.
if (c.KeyUsage & (x509.KeyUsageCertSign | x509.KeyUsageCRLSign)) != 0 {
return &lint.LintResult{
Status: lint.Error,
Details: "keyCertSign and cRLSign key usages MUST NOT be set",
}
}

// All other bit positions SHOULD NOT be set.
if c.KeyUsage & ^x509.KeyUsageDigitalSignature != 0 {
return &lint.LintResult{
Status: lint.Warn,
Details: "Only digitalSignature key usage is recommended. Other key usages SHOULD NOT be set."}
}

return &lint.LintResult{Status: lint.Pass}
}
45 changes: 45 additions & 0 deletions v3/lints/cabf_cs_br/lint_cs_key_usage_required_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package cabf_cs_br

import (
"testing"

"github.com/zmap/zlint/v3/lint"
"github.com/zmap/zlint/v3/test"
)

func TestCsKeyUsageCheck(t *testing.T) {
testCases := []struct {
Name string
InputFilename string
ExpectedResult lint.LintStatus
}{
{
Name: "pass - valid code signing certificate with digital signature key usage",
InputFilename: "code_signing/validCodeSigningCertificate.pem",
ExpectedResult: lint.Pass,
},
{
Name: "fail - code signing certificate without required key usage",
InputFilename: "code_signing/noDigitalSignatureKeyUsage.pem",
ExpectedResult: lint.Error,
},
{
Name: "fail - code signing certificate with prohibited key usage",
InputFilename: "code_signing/containsProhibitedKeyUsage.pem",
ExpectedResult: lint.Error,
},
{
Name: "warn - code signing certificate with not recommended key usage",
InputFilename: "code_signing/containsNotRecommendedKeyUsage.pem",
ExpectedResult: lint.Warn,
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
result := test.TestLint("e_cs_key_usage_required", tc.InputFilename)
if result.Status != tc.ExpectedResult {
t.Errorf("expected result %v was %v - details: %v", tc.ExpectedResult, result.Status, result.Details)
}
})
}
}
Loading
Loading