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

View as plain text