// Copyright 2026 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package x509 import ( "crypto/internal/cryptotest" "crypto/internal/cryptotest/x509limbo" "encoding/json" "encoding/pem" "flag" "fmt" "internal/testenv" "os" "path/filepath" "slices" "strings" "testing" "time" ) var limboCases = flag.String("limbo_cases", "", "comma-separated limbo case ids to run; if empty, all cases run") // Instances where we do **not** produce an error, but the test corpus says // we should have. The map value justifies each allow. var allowedUnexpectedVerifications = map[string]string{ // These are instances where we should consider updating the implementation. "rfc5280::san::noncritical-with-empty-subject": "TODO(#79741)", "webpki::san::san-critical-with-nonempty-subject": "TODO(#79741)", "rfc5280::nc::not-allowed-in-ee-noncritical": "TODO(#79742)", "rfc5280::nc::not-allowed-in-ee-critical": "TODO(#79742)", "rfc5280::eku::ee-eku-empty": "TODO(#79743)", "rfc5280::ca-empty-subject": "TODO(#79744)", // Underscores and other invalid characters are presently allowed after // tightening up the validation caused issues with real world certificates. "rfc5280::san::underscore-dns": "TODO(#75835)", // Go does not apply CABF key-strength policies. "webpki::forbidden-dsa-leaf": "Go doesn't enforce CABF key strength policies", "webpki::forbidden-weak-rsa-key-in-root": "Go doesn't enforce CABF key strength policies", "webpki::forbidden-weak-rsa-in-leaf": "Go doesn't enforce CABF key strength policies", "webpki::forbidden-rsa-not-divisable-by-8-in-root": "Go doesn't enforce CABF key strength policies", "webpki::forbidden-rsa-key-not-divisable-by-8-in-leaf": "Go doesn't enforce CABF key strength policies", // We don't want to take a public suffix data dependency, other heuristics // are incomplete and will interact badly with private PKIs. "webpki::san::public-suffix-wildcard-san": "Go doesn't include the PSL in its stdlib", // Trust anchors are implicitly considered issuers regardless of basic // constraints extension. "rfc5280::root-non-critical-basic-constraints": "Go only considers BC on intermediates", // Similarly, KeyUsage status flags are ignored by design. See Certificate.isValid // comment in body of implementation. "rfc5280::root-inconsistent-ca-extensions": "Go ignores KU, only considers BC on intermediates", "rfc5280::leaf-ku-keycertsign": "Go ignores KU, only considers BC on intermediates", // Enforcing ee-basicconstraints-ca/ca-as-leaf may additionally break the // somewhat common practice of using a self-signed issuer as the sole leaf // certificate in a chain. "webpki::ee-basicconstraints-ca": "Go ignores KU", "webpki::ca-as-leaf": "Go ignores KU", // Certificate.Verify documents that we allow a leading period for DNS // name constraints, similar to emails/URIs. "rfc5280::nc::invalid-dnsname-leading-period": "Go accepts leading period", // AKI is not load-bearing for validation. We only use it as a // parent-ordering hint in CertPool.findPotentialParents. "rfc5280::aki::cross-signed-root-missing-aki": "Go only uses AKI for ordering hint, not a verification requirement", "rfc5280::aki::leaf-missing-aki": "Go only uses AKI for ordering hint, not a verification requirement", "webpki::aki::root-with-aki-missing-keyidentifier": "Go does not enforce CABF requirement that root AKI contain a keyIdentifier field", "webpki::aki::root-with-aki-authoritycertissuer": "Go does not enforce CABF prohibition on authorityCertIssuer in root AKI", "webpki::aki::root-with-aki-authoritycertserialnumber": "Go does not enforce CABF prohibition on authorityCertSerialNumber in root AKI", "webpki::aki::root-with-aki-all-fields": "Go does not enforce CABF restrictions on AKI field composition in roots", "webpki::aki::root-with-aki-ski-mismatch": "Go does not enforce CABF requirement that a self-signed root's AKI keyIdentifier match its SKI", // Enforcing criticality is of dubious value in these cases and likely bumps // into incorrect real world certificates. Additionally, no other verifiers // tested by x509-limbo upstream treat these as a failure condition. "webpki::eku::ee-critical-eku": "Go doesn't reject this extension when marked critical", "rfc5280::nc::permitted-dns-match-noncritical": "Go doesn't require this extension to be critical", "rfc5280::pc::ica-noncritical-pc": "Go doesn't require this extension to be critical", // Serial parsing enforces no negatives, but doesn't enforce max length or // non-zero. Important roots have a serial of zero, and enforcing serial // length broke enough private PKIs that the enforcement change was reverted. "rfc5280::serial::too-long": "Causes significant breakage of real-world private PKIs", "rfc5280::serial::zero": "RFC 5280 says certificate users SHOULD gracefully handle zero", // These are skipped based on CT analysis of affected certificates. // See https://github.com/golang/go/issues/65085#issuecomment-1932886623 "rfc5280::ski::root-missing-ski": "would break various trusted Verisign roots", "rfc5280::ski::intermediate-missing-ski": "would break various trusted intermediates", "rfc5280::aki::intermediate-missing-aki": "would break real world certificates", // Go enforces EKU as an application-level capability filter, not according // to CABF webpki policy where (for e.g.) anyExtendedKeyUsage is forbidden // on leaves. "webpki::eku::ee-anyeku": "Go treats anyExtendedKeyUsage as overriding any other key usage.", "webpki::eku::ee-without-eku": "Go skips certs with no EKU when checking chain usage.", "webpki::eku::root-has-eku": "Go allows a root to have an EKU as a downward constraint", // Our implementation handles these degenerate name constraint tests // without error. They are described as standards compliant but are // marked expected-reject upstream because quadratic implementations // hit a fixed DoS prevention limit. nc-dos-3 is not listed: it matches // the expected failure result, but due to the use of a subject CN // without SAN, not because of quadratic NC checking. "pathological::nc-dos-1": "standards compliant; upstream rejects due to quadratic DoS limit", "pathological::nc-dos-2": "standards compliant; upstream rejects due to quadratic DoS limit", // These webpki::cn::* cases test CABF BR 7.1.4.3 constraints on the // CN field. Go's x509 package intentionally ignores the legacy Common Name // (CN) field for hostname matching (see Certificate.VerifyHostname), so // verification succeeds via the well-formed SAN even when the CN is // non-conformant. "webpki::cn::case-mismatch": "Go ignores legacy CN", "webpki::cn::ipv4-hex-mismatch": "Go ignores legacy CN", "webpki::cn::ipv4-leading-zeros-mismatch": "Go ignores legacy CN", "webpki::cn::ipv6-non-rfc5952-mismatch": "Go ignores legacy CN", "webpki::cn::ipv6-uncompressed-mismatch": "Go ignores legacy CN", "webpki::cn::ipv6-uppercase-mismatch": "Go ignores legacy CN", "webpki::cn::not-in-san": "Go ignores legacy CN", "webpki::cn::punycode-not-in-san": "Go ignores legacy CN", "webpki::cn::utf8-vs-punycode-mismatch": "Go ignores legacy CN", } // Instances where we produce an error, but the test corpus says we // shouldn't have. The map value justifies each allow. var allowedUnexpectedFailures = map[string]string{ // This looks like a small oversight in our implementation, and should be // fixed. "rfc5280::nc::permitted-self-issued": "TODO(#79746)", // The spec-conformant behavior weakens the security value of pathlen, and // has limited real-world impact on webpki certificates. Other // implementations like mozilla::pkix have reached a similar conclusion. // See https://bugzilla.mozilla.org/show_bug.cgi?id=926265 and // https://github.com/golang/go/issues/79745#issuecomment-4578179884 "pathlen::self-issued-certs-pathlen": "Go prefers a stricter pathen implementation", // Limbo argues there are no OtherName GeneralName's in the chain being // validated, and so it should pass. We take a more conservative stance // backed by 5280 ยง4.2 that we have a critical extension we can't process, // and don't make a determination based on usage in verification. "rfc5280::nc::nc-forbids-othername-noop": "Go rejects critical NC with GeneralName types it doesn't implement", // Per the test's description there is "no clear 'winning' interpretation" // between second-granularity checks vs instantaneous. Changing our // behavior in this case seems low-priority. "rfc5280::validity::notafter-fractional": "Go uses instantaneous time comparisons", } var extKeyUsagesMap = map[x509limbo.KnownEKUs]ExtKeyUsage{ x509limbo.KnownEKUsAnyExtendedKeyUsage: ExtKeyUsageAny, x509limbo.KnownEKUsClientAuth: ExtKeyUsageClientAuth, x509limbo.KnownEKUsCodeSigning: ExtKeyUsageCodeSigning, x509limbo.KnownEKUsEmailProtection: ExtKeyUsageEmailProtection, x509limbo.KnownEKUsOCSPSigning: ExtKeyUsageOCSPSigning, x509limbo.KnownEKUsServerAuth: ExtKeyUsageServerAuth, x509limbo.KnownEKUsTimeStamping: ExtKeyUsageTimeStamping, } // Tests the x509 package using the test vectors from https://x509-limbo.com/ func TestX509Limbo(t *testing.T) { testenv.SkipIfShortAndSlow(t) limboDir := cryptotest.FetchModule(t, x509limbo.X509LimboModule, x509limbo.X509LimboVersion) limboJson, err := os.ReadFile(filepath.Join(limboDir, "limbo.json")) if err != nil { t.Fatalf("error reading limbo.json: %v", err) } var limbo x509limbo.Limbo if err := json.Unmarshal(limboJson, &limbo); err != nil { t.Fatalf("failed to unmarshal limbo.json: %v", err) } for _, tc := range limbo.Testcases { t.Run(tc.Id, func(t *testing.T) { t.Parallel() if *limboCases != "" && !slices.Contains(strings.Split(*limboCases, ","), tc.Id) { t.Skip("filtered out by -limbo_cases") } if slices.Contains(tc.Features, x509limbo.FeatureHasCrl) { t.Skipf("CRL revocation checking not supported") } if slices.Contains(tc.Features, x509limbo.FeatureMaxChainDepth) { t.Skipf("customizable max chain depth not supported") } if slices.Contains(tc.Features, x509limbo.FeatureNameConstraintDn) { t.Skipf("name constraints for DirectoryNames are not supported") } if len(tc.SignatureAlgorithms) != 0 { // Note: there are no limbo.json test cases that specify signature // algorithms at this time, so this skip is largely a no-op. t.Skipf("signature algorithms are not customizable through the x509 interface") } if len(tc.KeyUsage) != 0 && !slices.Contains(tc.KeyUsage, x509limbo.KeyUsageDigitalSignature) { // Note: there are no limbo.json test cases that specify key usages other // than digitalSignature at this time, so this skip is largely a no-op. t.Skipf("key usage checks other than Digital Signature are not supported") } // In the server validation context we may be given a single expected // peer name to use for our verify options. var verifyDnsName string if tc.ExpectedPeerName != nil && tc.ValidationKind == x509limbo.ValidationKindSERVER { switch tc.ExpectedPeerName.Kind { case x509limbo.PeerKindDNS: verifyDnsName = tc.ExpectedPeerName.Value case x509limbo.PeerKindIP: verifyDnsName = fmt.Sprintf("[%s]", tc.ExpectedPeerName.Value) default: t.Skipf("unsupported peer name kind: %v", tc.ExpectedPeerName.Kind) } } roots, intermediates := NewCertPool(), NewCertPool() for _, rootPem := range tc.TrustedCerts { roots.AppendCertsFromPEM([]byte(rootPem)) } for _, intermediatePem := range tc.UntrustedIntermediates { intermediates.AppendCertsFromPEM([]byte(intermediatePem)) } block, rest := pem.Decode([]byte(tc.PeerCertificate)) if block == nil { t.Fatalf("unable to PEM decode peer certificate") } else if block.Type != "CERTIFICATE" { t.Fatalf("unexpected data, expected cert: %+#v", *block) } else if len(rest) > 0 { t.Fatalf("peer certificate has %d trailing bytes", len(rest)) } peer, parseErr := ParseCertificate(block.Bytes) if parseErr != nil { if tc.ExpectedResult == x509limbo.ExpectedResultFAILURE { // The test expects failure and we detect an error at parse // time instead of verification time. Considered a pass. return } printChainDetails(t, tc, parseErr) t.Errorf("expected success, parsing peer certificate failed: %v", parseErr) return } validationTime := time.Now() if tc.ValidationTime != nil { vtStr, ok := tc.ValidationTime.(string) if !ok { t.Fatalf("validation time is not a string: %T %v", tc.ValidationTime, tc.ValidationTime) } parsed, err := time.Parse(time.RFC3339, vtStr) if err != nil { t.Fatalf("invalid validation time %q: %v", vtStr, err) } validationTime = parsed } var ekus []ExtKeyUsage for _, elem := range tc.ExtendedKeyUsage { eku, ok := extKeyUsagesMap[elem] if !ok { t.Skipf("unsupported extended key usage: %v", elem) } ekus = append(ekus, eku) } _, err := peer.Verify(VerifyOptions{ DNSName: verifyDnsName, Intermediates: intermediates, Roots: roots, CurrentTime: validationTime, KeyUsages: ekus, }) if err == nil && tc.ExpectedResult == x509limbo.ExpectedResultFAILURE { if _, allowed := allowedUnexpectedVerifications[tc.Id]; !allowed { printChainDetails(t, tc, nil) t.Errorf("expected failure, built chain without error") } } else if err != nil && tc.ExpectedResult == x509limbo.ExpectedResultSUCCESS { if _, allowed := allowedUnexpectedFailures[tc.Id]; !allowed { printChainDetails(t, tc, err) t.Errorf("expected success, built chain with error: %v", err) } } // In the client validation context we may be given multiple expected // peer names so we check these explicitly after path building. // The DNSName in our VerifyOpts will have been empty. if tc.ValidationKind == x509limbo.ValidationKindCLIENT { for _, name := range tc.ExpectedPeerNames { if name.Kind != x509limbo.PeerKindIP && name.Kind != x509limbo.PeerKindDNS { // We don't support verifying RFC8222 peer names. t.Skipf("unsupported peer name kind: %v", name.Kind) } err = peer.VerifyHostname(name.Value) // We don't check allowedUnexpectedVerifications or allowedUnexpectedFailures // here because there aren't any that apply to ValidationKindCLIENT // at this time. if err == nil && tc.ExpectedResult == x509limbo.ExpectedResultFAILURE { printChainDetails(t, tc, nil) t.Errorf("expected failure, built chain without error") } else if err != nil && tc.ExpectedResult == x509limbo.ExpectedResultSUCCESS { printChainDetails(t, tc, err) t.Errorf("expected success, built chain with error: %v", err) } } } }) } } func printChainDetails(t *testing.T, tc x509limbo.Testcase, actualResult error) { t.Log("----") t.Logf("testcase: %q expected result: %v actual result: %v", tc.Id, tc.ExpectedResult, actualResult) t.Log("trust anchor PEM:") for _, root := range tc.TrustedCerts { t.Log(root) } t.Log("intermediates PEM:") for _, intermediate := range tc.UntrustedIntermediates { t.Log(intermediate) } t.Log("end entity PEM:") t.Log(tc.PeerCertificate) t.Log("----") }