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

View as plain text