Source file src/crypto/x509/x509limbo_test.go

     1  // Copyright 2026 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package x509
     6  
     7  import (
     8  	"crypto/fips140"
     9  	"crypto/internal/cryptotest"
    10  	"crypto/internal/cryptotest/x509limbo"
    11  	"encoding/json"
    12  	"encoding/pem"
    13  	"flag"
    14  	"fmt"
    15  	"internal/testenv"
    16  	"os"
    17  	"path/filepath"
    18  	"slices"
    19  	"strings"
    20  	"testing"
    21  	"time"
    22  )
    23  
    24  var limboCases = flag.String("limbo_cases", "", "comma-separated limbo case ids to run; if empty, all cases run")
    25  
    26  // Instances where we do **not** produce an error, but the test corpus says
    27  // we should have. The map value justifies each allow.
    28  var allowedUnexpectedVerifications = map[string]string{
    29  	// These are instances where we should consider updating the implementation.
    30  	"rfc5280::san::noncritical-with-empty-subject":    "TODO(#79741)",
    31  	"webpki::san::san-critical-with-nonempty-subject": "TODO(#79741)",
    32  	"rfc5280::nc::not-allowed-in-ee-noncritical":      "TODO(#79742)",
    33  	"rfc5280::nc::not-allowed-in-ee-critical":         "TODO(#79742)",
    34  	"rfc5280::eku::ee-eku-empty":                      "TODO(#79743)",
    35  	"rfc5280::ca-empty-subject":                       "TODO(#79744)",
    36  
    37  	// Underscores and other invalid characters are presently allowed after
    38  	// tightening up the validation caused issues with real world certificates.
    39  	"rfc5280::san::underscore-dns": "TODO(#75835)",
    40  
    41  	// Go does not apply CABF key-strength policies.
    42  	"webpki::forbidden-dsa-leaf":                           "Go doesn't enforce CABF key strength policies",
    43  	"webpki::forbidden-weak-rsa-key-in-root":               "Go doesn't enforce CABF key strength policies",
    44  	"webpki::forbidden-weak-rsa-in-leaf":                   "Go doesn't enforce CABF key strength policies",
    45  	"webpki::forbidden-rsa-not-divisible-by-8-in-root":     "Go doesn't enforce CABF key strength policies",
    46  	"webpki::forbidden-rsa-key-not-divisible-by-8-in-leaf": "Go doesn't enforce CABF key strength policies",
    47  
    48  	// We don't want to take a public suffix data dependency, other heuristics
    49  	// are incomplete and will interact badly with private PKIs.
    50  	"webpki::san::public-suffix-wildcard-san":                   "Go doesn't include the PSL in its stdlib",
    51  	"webpki::san::public-suffix-multi-label-wildcard-san":       "Go doesn't include the PSL in its stdlib",
    52  	"webpki::san::public-suffix-private-namespace-wildcard-san": "Go doesn't include the PSL in its stdlib",
    53  
    54  	// Trust anchors are implicitly considered issuers regardless of basic
    55  	// constraints extension.
    56  	"rfc5280::root-non-critical-basic-constraints": "Go only considers BC on intermediates",
    57  	// Similarly, KeyUsage status flags are ignored by design. See Certificate.isValid
    58  	// comment in body of implementation.
    59  	"rfc5280::root-inconsistent-ca-extensions": "Go ignores KU, only considers BC on intermediates",
    60  	"rfc5280::leaf-ku-keycertsign":             "Go ignores KU, only considers BC on intermediates",
    61  	// RFC 9881 §5 forbids keyEncipherment/keyAgreement KU bits with ML-DSA
    62  	// keys, but Go ignores the KU extension by design (see above).
    63  	"rfc9881::ml-dsa-44-key-encipherment": "Go ignores KU",
    64  	"rfc9881::ml-dsa-44-key-agreement":    "Go ignores KU",
    65  
    66  	// Enforcing ee-basicconstraints-ca/ca-as-leaf may additionally break the
    67  	// somewhat common practice of using a self-signed issuer as the sole leaf
    68  	// certificate in a chain.
    69  	"webpki::ee-basicconstraints-ca": "Go ignores KU",
    70  	"webpki::ca-as-leaf":             "Go ignores KU",
    71  
    72  	// Certificate.Verify documents that we allow a leading period for DNS
    73  	// name constraints, similar to emails/URIs.
    74  	"rfc5280::nc::invalid-dnsname-leading-period": "Go accepts leading period",
    75  
    76  	// AKI is not load-bearing for validation. We only use it as a
    77  	// parent-ordering hint in CertPool.findPotentialParents.
    78  	"rfc5280::aki::cross-signed-root-missing-aki":          "Go only uses AKI for ordering hint, not a verification requirement",
    79  	"rfc5280::aki::leaf-missing-aki":                       "Go only uses AKI for ordering hint, not a verification requirement",
    80  	"webpki::aki::root-with-aki-missing-keyidentifier":     "Go does not enforce CABF requirement that root AKI contain a keyIdentifier field",
    81  	"webpki::aki::root-with-aki-authoritycertissuer":       "Go does not enforce CABF prohibition on authorityCertIssuer in root AKI",
    82  	"webpki::aki::root-with-aki-authoritycertserialnumber": "Go does not enforce CABF prohibition on authorityCertSerialNumber in root AKI",
    83  	"webpki::aki::root-with-aki-all-fields":                "Go does not enforce CABF restrictions on AKI field composition in roots",
    84  	"webpki::aki::root-with-aki-ski-mismatch":              "Go does not enforce CABF requirement that a self-signed root's AKI keyIdentifier match its SKI",
    85  
    86  	// Enforcing criticality is of dubious value in these cases and likely bumps
    87  	// into incorrect real world certificates. Additionally, no other verifiers
    88  	// tested by x509-limbo upstream treat these as a failure condition.
    89  	"webpki::eku::ee-critical-eku":                 "Go doesn't reject this extension when marked critical",
    90  	"rfc5280::nc::permitted-dns-match-noncritical": "Go doesn't require this extension to be critical",
    91  	"rfc5280::pc::ica-noncritical-pc":              "Go doesn't require this extension to be critical",
    92  
    93  	// Serial parsing enforces no negatives, but doesn't enforce max length or
    94  	// non-zero. Important roots have a serial of zero, and enforcing serial
    95  	// length broke enough private PKIs that the enforcement change was reverted.
    96  	"rfc5280::serial::too-long": "Causes significant breakage of real-world private PKIs",
    97  	"rfc5280::serial::zero":     "RFC 5280 says certificate users SHOULD gracefully handle zero",
    98  
    99  	// These are skipped based on CT analysis of affected certificates.
   100  	// See https://github.com/golang/go/issues/65085#issuecomment-1932886623
   101  	"rfc5280::ski::root-missing-ski":         "would break various trusted Verisign roots",
   102  	"rfc5280::ski::intermediate-missing-ski": "would break various trusted intermediates",
   103  	"rfc5280::aki::intermediate-missing-aki": "would break real world certificates",
   104  
   105  	// Go enforces EKU as an application-level capability filter, not according
   106  	// to CABF webpki policy where (for e.g.) anyExtendedKeyUsage is forbidden
   107  	// on leaves.
   108  	"webpki::eku::ee-anyeku":      "Go treats anyExtendedKeyUsage as overriding any other key usage.",
   109  	"webpki::eku::ee-without-eku": "Go skips certs with no EKU when checking chain usage.",
   110  	"webpki::eku::root-has-eku":   "Go allows a root to have an EKU as a downward constraint",
   111  
   112  	// Our implementation handles these degenerate name constraint tests
   113  	// without error. They are described as standards compliant but are
   114  	// marked expected-reject upstream because quadratic implementations
   115  	// hit a fixed DoS prevention limit. nc-dos-3 is not listed: it matches
   116  	// the expected failure result, but due to the use of a subject CN
   117  	// without SAN, not because of quadratic NC checking.
   118  	"pathological::nc-dos-1": "standards compliant; upstream rejects due to quadratic DoS limit",
   119  	"pathological::nc-dos-2": "standards compliant; upstream rejects due to quadratic DoS limit",
   120  
   121  	// These webpki::cn::* cases test CABF BR 7.1.4.3 constraints on the
   122  	// CN field. Go's x509 package intentionally ignores the legacy Common Name
   123  	// (CN) field for hostname matching (see Certificate.VerifyHostname), so
   124  	// verification succeeds via the well-formed SAN even when the CN is
   125  	// non-conformant.
   126  	"webpki::cn::case-mismatch":               "Go ignores legacy CN",
   127  	"webpki::cn::ipv4-hex-mismatch":           "Go ignores legacy CN",
   128  	"webpki::cn::ipv4-leading-zeros-mismatch": "Go ignores legacy CN",
   129  	"webpki::cn::ipv6-non-rfc5952-mismatch":   "Go ignores legacy CN",
   130  	"webpki::cn::ipv6-uncompressed-mismatch":  "Go ignores legacy CN",
   131  	"webpki::cn::ipv6-uppercase-mismatch":     "Go ignores legacy CN",
   132  	"webpki::cn::not-in-san":                  "Go ignores legacy CN",
   133  	"webpki::cn::punycode-not-in-san":         "Go ignores legacy CN",
   134  	"webpki::cn::utf8-vs-punycode-mismatch":   "Go ignores legacy CN",
   135  }
   136  
   137  // Instances where we produce an error, but the test corpus says we
   138  // shouldn't have. The map value justifies each allow.
   139  var allowedUnexpectedFailures = map[string]string{
   140  	// This looks like a small oversight in our implementation, and should be
   141  	// fixed.
   142  	"rfc5280::nc::permitted-self-issued": "TODO(#79746)",
   143  
   144  	// The spec-conformant behavior weakens the security value of pathlen, and
   145  	// has limited real-world impact on webpki certificates. Other
   146  	// implementations like mozilla::pkix have reached a similar conclusion.
   147  	// See https://bugzilla.mozilla.org/show_bug.cgi?id=926265 and
   148  	// https://github.com/golang/go/issues/79745#issuecomment-4578179884
   149  	"pathlen::self-issued-certs-pathlen": "Go prefers a stricter pathen implementation",
   150  
   151  	// Limbo argues there are no OtherName GeneralName's in the chain being
   152  	// validated, and so it should pass. We take a more conservative stance
   153  	// backed by 5280 §4.2 that we have a critical extension we can't process,
   154  	// and don't make a determination based on usage in verification.
   155  	"rfc5280::nc::nc-forbids-othername-noop": "Go rejects critical NC with GeneralName types it doesn't implement",
   156  
   157  	// Per the test's description there is "no clear 'winning' interpretation"
   158  	// between second-granularity checks vs instantaneous. Changing our
   159  	// behavior in this case seems low-priority.
   160  	"rfc5280::validity::notafter-fractional": "Go uses instantaneous time comparisons",
   161  }
   162  
   163  var extKeyUsagesMap = map[x509limbo.KnownEKUs]ExtKeyUsage{
   164  	x509limbo.KnownEKUsAnyExtendedKeyUsage: ExtKeyUsageAny,
   165  	x509limbo.KnownEKUsClientAuth:          ExtKeyUsageClientAuth,
   166  	x509limbo.KnownEKUsCodeSigning:         ExtKeyUsageCodeSigning,
   167  	x509limbo.KnownEKUsEmailProtection:     ExtKeyUsageEmailProtection,
   168  	x509limbo.KnownEKUsOCSPSigning:         ExtKeyUsageOCSPSigning,
   169  	x509limbo.KnownEKUsServerAuth:          ExtKeyUsageServerAuth,
   170  	x509limbo.KnownEKUsTimeStamping:        ExtKeyUsageTimeStamping,
   171  }
   172  
   173  // Tests the x509 package using the test vectors from https://x509-limbo.com/
   174  func TestX509Limbo(t *testing.T) {
   175  	testenv.SkipIfShortAndSlow(t)
   176  
   177  	limboDir := cryptotest.FetchModule(t, x509limbo.X509LimboModule, x509limbo.X509LimboVersion)
   178  
   179  	limboJson, err := os.ReadFile(filepath.Join(limboDir, "limbo.json"))
   180  	if err != nil {
   181  		t.Fatalf("error reading limbo.json: %v", err)
   182  	}
   183  
   184  	var limbo x509limbo.Limbo
   185  	if err := json.Unmarshal(limboJson, &limbo); err != nil {
   186  		t.Fatalf("failed to unmarshal limbo.json: %v", err)
   187  	}
   188  
   189  	for _, tc := range limbo.Testcases {
   190  		t.Run(tc.Id, func(t *testing.T) {
   191  			t.Parallel()
   192  
   193  			if *limboCases != "" && !slices.Contains(strings.Split(*limboCases, ","), tc.Id) {
   194  				t.Skip("filtered out by -limbo_cases")
   195  			}
   196  
   197  			if slices.Contains(tc.Features, x509limbo.FeatureHasCrl) {
   198  				t.Skipf("CRL revocation checking not supported")
   199  			}
   200  
   201  			if slices.Contains(tc.Features, x509limbo.FeatureMaxChainDepth) {
   202  				t.Skipf("customizable max chain depth not supported")
   203  			}
   204  
   205  			if slices.Contains(tc.Features, x509limbo.FeatureNameConstraintDn) {
   206  				t.Skipf("name constraints for DirectoryNames are not supported")
   207  			}
   208  
   209  			if slices.Contains(tc.Features, x509limbo.FeatureHasMldsa) && fips140.Version() == "v1.0.0" {
   210  				t.Skipf("ML-DSA is not available in FIPS 140-3 module v1.0.0")
   211  			}
   212  
   213  			if len(tc.SignatureAlgorithms) != 0 {
   214  				// Note: there are no limbo.json test cases that specify signature
   215  				// algorithms at this time, so this skip is largely a no-op.
   216  				t.Skipf("signature algorithms are not customizable through the x509 interface")
   217  			}
   218  
   219  			if len(tc.KeyUsage) != 0 &&
   220  				!slices.Contains(tc.KeyUsage, x509limbo.KeyUsageDigitalSignature) {
   221  				// Note: there are no limbo.json test cases that specify key usages other
   222  				// than digitalSignature at this time, so this skip is largely a no-op.
   223  				t.Skipf("key usage checks other than Digital Signature are not supported")
   224  			}
   225  
   226  			// In the server validation context we may be given a single expected
   227  			// peer name to use for our verify options.
   228  			var verifyDnsName string
   229  			if tc.ExpectedPeerName != nil && tc.ValidationKind == x509limbo.ValidationKindSERVER {
   230  				switch tc.ExpectedPeerName.Kind {
   231  				case x509limbo.PeerKindDNS:
   232  					verifyDnsName = tc.ExpectedPeerName.Value
   233  				case x509limbo.PeerKindIP:
   234  					verifyDnsName = fmt.Sprintf("[%s]", tc.ExpectedPeerName.Value)
   235  				default:
   236  					t.Skipf("unsupported peer name kind: %v", tc.ExpectedPeerName.Kind)
   237  				}
   238  			}
   239  
   240  			roots, intermediates := NewCertPool(), NewCertPool()
   241  			for _, rootPem := range tc.TrustedCerts {
   242  				roots.AppendCertsFromPEM([]byte(rootPem))
   243  			}
   244  			for _, intermediatePem := range tc.UntrustedIntermediates {
   245  				intermediates.AppendCertsFromPEM([]byte(intermediatePem))
   246  			}
   247  
   248  			block, rest := pem.Decode([]byte(tc.PeerCertificate))
   249  			if block == nil {
   250  				t.Fatalf("unable to PEM decode peer certificate")
   251  			} else if block.Type != "CERTIFICATE" {
   252  				t.Fatalf("unexpected data, expected cert: %+#v", *block)
   253  			} else if len(rest) > 0 {
   254  				t.Fatalf("peer certificate has %d trailing bytes", len(rest))
   255  			}
   256  
   257  			peer, parseErr := ParseCertificate(block.Bytes)
   258  			if parseErr != nil {
   259  				if tc.ExpectedResult == x509limbo.ExpectedResultFAILURE {
   260  					// The test expects failure and we detect an error at parse
   261  					// time instead of verification time. Considered a pass.
   262  					return
   263  				}
   264  				printChainDetails(t, tc, parseErr)
   265  				t.Errorf("expected success, parsing peer certificate failed: %v", parseErr)
   266  				return
   267  			}
   268  
   269  			validationTime := time.Now()
   270  			if tc.ValidationTime != nil {
   271  				vtStr, ok := tc.ValidationTime.(string)
   272  				if !ok {
   273  					t.Fatalf("validation time is not a string: %T %v", tc.ValidationTime, tc.ValidationTime)
   274  				}
   275  				parsed, err := time.Parse(time.RFC3339, vtStr)
   276  				if err != nil {
   277  					t.Fatalf("invalid validation time %q: %v", vtStr, err)
   278  				}
   279  				validationTime = parsed
   280  			}
   281  
   282  			var ekus []ExtKeyUsage
   283  			for _, elem := range tc.ExtendedKeyUsage {
   284  				eku, ok := extKeyUsagesMap[elem]
   285  				if !ok {
   286  					t.Skipf("unsupported extended key usage: %v", elem)
   287  				}
   288  				ekus = append(ekus, eku)
   289  			}
   290  
   291  			_, err := peer.Verify(VerifyOptions{
   292  				DNSName:       verifyDnsName,
   293  				Intermediates: intermediates,
   294  				Roots:         roots,
   295  				CurrentTime:   validationTime,
   296  				KeyUsages:     ekus,
   297  			})
   298  			if err == nil && tc.ExpectedResult == x509limbo.ExpectedResultFAILURE {
   299  				if _, allowed := allowedUnexpectedVerifications[tc.Id]; !allowed {
   300  					printChainDetails(t, tc, nil)
   301  					t.Errorf("expected failure, built chain without error")
   302  				}
   303  			} else if err != nil && tc.ExpectedResult == x509limbo.ExpectedResultSUCCESS {
   304  				if _, allowed := allowedUnexpectedFailures[tc.Id]; !allowed {
   305  					printChainDetails(t, tc, err)
   306  					t.Errorf("expected success, built chain with error: %v", err)
   307  				}
   308  			}
   309  
   310  			// In the client validation context we may be given multiple expected
   311  			// peer names so we check these explicitly after path building.
   312  			// The DNSName in our VerifyOpts will have been empty.
   313  			if tc.ValidationKind == x509limbo.ValidationKindCLIENT {
   314  				for _, name := range tc.ExpectedPeerNames {
   315  					if name.Kind != x509limbo.PeerKindIP && name.Kind != x509limbo.PeerKindDNS {
   316  						// We don't support verifying RFC8222 peer names.
   317  						t.Skipf("unsupported peer name kind: %v", name.Kind)
   318  					}
   319  					err = peer.VerifyHostname(name.Value)
   320  					// We don't check allowedUnexpectedVerifications or allowedUnexpectedFailures
   321  					// here because there aren't any that apply to ValidationKindCLIENT
   322  					// at this time.
   323  					if err == nil && tc.ExpectedResult == x509limbo.ExpectedResultFAILURE {
   324  						printChainDetails(t, tc, nil)
   325  						t.Errorf("expected failure, built chain without error")
   326  					} else if err != nil && tc.ExpectedResult == x509limbo.ExpectedResultSUCCESS {
   327  						printChainDetails(t, tc, err)
   328  						t.Errorf("expected success, built chain with error: %v", err)
   329  					}
   330  				}
   331  			}
   332  		})
   333  	}
   334  }
   335  
   336  func printChainDetails(t *testing.T, tc x509limbo.Testcase, actualResult error) {
   337  	t.Log("----")
   338  	t.Logf("testcase: %q expected result: %v actual result: %v", tc.Id, tc.ExpectedResult, actualResult)
   339  	t.Log("trust anchor PEM:")
   340  	for _, root := range tc.TrustedCerts {
   341  		t.Log(root)
   342  	}
   343  	t.Log("intermediates PEM:")
   344  	for _, intermediate := range tc.UntrustedIntermediates {
   345  		t.Log(intermediate)
   346  	}
   347  	t.Log("end entity PEM:")
   348  	t.Log(tc.PeerCertificate)
   349  	t.Log("----")
   350  }
   351  

View as plain text