aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRoland Shoemaker <roland@golang.org>2023-04-25 13:15:04 -0700
committerGopher Robot <gobot@golang.org>2023-06-14 18:53:13 +0000
commit3aea422e2cb8b1ec2e0c2774be97fe96c7299838 (patch)
tree4626a1a487277aac2bd388f54ebc0b2872cc4e1c
parent0a48e5cbfabd679eecdec1efa731692cd6babf83 (diff)
downloadgo-3aea422e2cb8b1ec2e0c2774be97fe96c7299838.tar.gz
go-3aea422e2cb8b1ec2e0c2774be97fe96c7299838.zip
crypto/x509: use synthetic root for platform testing
Rather than using the external network and real-world chains for testing the integrations with platform verifiers, use a synthetic test root. This changes adds a constrained root and key pair to the tree, and adds a test suite that verifies certificates issued from that root. These tests are only executed if the root is detected in the trust store. For reference, the script used to generate the root and key is attached to the bottom of this commit message. This change leaves the existing windows/darwin TestPlatformVerifier tests in place, since the trybots do not currently have the test root in place, and as such cannot run the suite. Once the builder images have the root integrated, we can remove the old flaky tests, and the trybots will begin running the new suite automatically. Updates #52108 -- gen.go -- package main import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "flag" "log" "math/big" "net" "os" "time" ) func writePEM(pemType string, der []byte, path string) error { enc := pem.EncodeToMemory(&pem.Block{ Type: pemType, Bytes: der, }) return os.WriteFile(path, enc, 0666) } func main() { certPath := flag.String("cert-path", "platform_root_cert.pem", "Path to write certificate PEM") keyPath := flag.String("key-path", "platform_root_key.pem", "Path to write key PEM") flag.Parse() key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { log.Fatalf("ecdsa.GenerateKey failed: %s", err) } now := time.Now() tmpl := &x509.Certificate{ SerialNumber: big.NewInt(9009), Subject: pkix.Name{ CommonName: "Go platform verifier testing root", }, NotBefore: now.Add(-time.Hour), NotAfter: now.Add(time.Hour * 24 * 365 * 5), IsCA: true, BasicConstraintsValid: true, PermittedDNSDomainsCritical: true, // PermittedDNSDomains restricts the names in certificates issued from this root to *.testing.golang.invalid. // The .invalid TLD is, per RFC 2606, reserved for testing, and as such anything issued for this certificate // should never be valid in the real world. PermittedDNSDomains: []string{"testing.golang.invalid"}, // ExcludedIPRanges prevents any certificate issued from this root that contains an IP address in both the full // IPv4 and IPv6 ranges from being considered valid. ExcludedIPRanges: []*net.IPNet{{IP: make([]byte, 4), Mask: make([]byte, 4)}, {IP: make([]byte, 16), Mask: make([]byte, 16)}}, KeyUsage: x509.KeyUsageCertSign, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, } certDER, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key) if err != nil { log.Fatalf("x509.CreateCertificate failed: %s", err) } keyDER, err := x509.MarshalECPrivateKey(key) if err != nil { log.Fatalf("x509.MarshalECPrivateKey failed: %s", err) } if err := writePEM("CERTIFICATE", certDER, *certPath); err != nil { log.Fatalf("failed to write certificate PEM: %s", err) } if err := writePEM("EC PRIVATE KEY", keyDER, *keyPath); err != nil { log.Fatalf("failed to write key PEM: %s", err) } } Change-Id: If7c4a9f18466662a60fea5443e603232a9260026 Reviewed-on: https://go-review.googlesource.com/c/go/+/488855 Reviewed-by: Filippo Valsorda <filippo@golang.org> Auto-Submit: Roland Shoemaker <roland@golang.org> Reviewed-by: Bryan Mills <bcmills@google.com> Run-TryBot: Roland Shoemaker <roland@golang.org> TryBot-Result: Gopher Robot <gobot@golang.org>
-rw-r--r--src/crypto/x509/platform_root_cert.pem13
-rw-r--r--src/crypto/x509/platform_root_key.pem5
-rw-r--r--src/crypto/x509/platform_test.go251
3 files changed, 269 insertions, 0 deletions
diff --git a/src/crypto/x509/platform_root_cert.pem b/src/crypto/x509/platform_root_cert.pem
new file mode 100644
index 0000000000..bef31f4c4e
--- /dev/null
+++ b/src/crypto/x509/platform_root_cert.pem
@@ -0,0 +1,13 @@
+-----BEGIN CERTIFICATE-----
+MIIB/DCCAaOgAwIBAgICIzEwCgYIKoZIzj0EAwIwLDEqMCgGA1UEAxMhR28gcGxh
+dGZvcm0gdmVyaWZpZXIgdGVzdGluZyByb290MB4XDTIzMDUyNjE3NDQwMVoXDTI4
+MDUyNDE4NDQwMVowLDEqMCgGA1UEAxMhR28gcGxhdGZvcm0gdmVyaWZpZXIgdGVz
+dGluZyByb290MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5dNQY4FY29i2g3xx
+7FyH4XiZz0C0AM4uyPUsXCZNb7CsctHDLhLtzABWSfFz76j+oVhq+qKrwIHsLX+7
+f6YTQqOBtDCBsTAOBgNVHQ8BAf8EBAMCAgQwEwYDVR0lBAwwCgYIKwYBBQUHAwEw
+DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUEJInRbtQR6xTUSwvtdAe9A4XHwQw
+WgYDVR0eAQH/BFAwTqAaMBiCFnRlc3RpbmcuZ29sYW5nLmludmFsaWShMDAKhwgA
+AAAAAAAAADAihyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAKBggq
+hkjOPQQDAgNHADBEAiBgzgLyQm4rK1AuIcElH3MdRqlteq3nzZCxKOI4xHXYjQIg
+BCSzaCb1+/AK+mhRubrdebFYlUdveTH98wAfKQHaw64=
+-----END CERTIFICATE-----
diff --git a/src/crypto/x509/platform_root_key.pem b/src/crypto/x509/platform_root_key.pem
new file mode 100644
index 0000000000..c0b6eeba8b
--- /dev/null
+++ b/src/crypto/x509/platform_root_key.pem
@@ -0,0 +1,5 @@
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIHhv8LVzb9gqJzAY0P442+FW0oqbfBrLnfqxyyAujOFSoAoGCCqGSM49
+AwEHoUQDQgAE5dNQY4FY29i2g3xx7FyH4XiZz0C0AM4uyPUsXCZNb7CsctHDLhLt
+zABWSfFz76j+oVhq+qKrwIHsLX+7f6YTQg==
+-----END EC PRIVATE KEY-----
diff --git a/src/crypto/x509/platform_test.go b/src/crypto/x509/platform_test.go
new file mode 100644
index 0000000000..c35f0b448e
--- /dev/null
+++ b/src/crypto/x509/platform_test.go
@@ -0,0 +1,251 @@
+// Copyright 2023 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
+
+//go:generate go run gen_testing_root.go
+
+import (
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "encoding/pem"
+ "math/big"
+ "os"
+ "runtime"
+ "strings"
+ "testing"
+ "time"
+)
+
+// In order to run this test suite locally, you need to insert the test root, at
+// the path below, into your trust store. This root is constrained such that it
+// should not be dangerous to local developers to trust, but care should be
+// taken when inserting it into the trust store not to give it increased
+// permissions.
+//
+// On macOS the certificate can be further constrained to only be valid for
+// 'SSL' in the certificate properties pane of the 'Keychain Access' program.
+//
+// On Windows the certificate can also be constrained to only server
+// authentication in the properties pane of the certificate in the
+// "Certificates" snap-in of mmc.exe.
+
+const (
+ rootCertPath = "platform_root_cert.pem"
+ rootKeyPath = "platform_root_key.pem"
+)
+
+func TestPlatformVerifier(t *testing.T) {
+ if runtime.GOOS != "windows" && runtime.GOOS != "darwin" {
+ t.Skip("only tested on windows and darwin")
+ }
+
+ der, err := os.ReadFile(rootCertPath)
+ if err != nil {
+ t.Fatalf("failed to read test root: %s", err)
+ }
+ b, _ := pem.Decode(der)
+ testRoot, err := ParseCertificate(b.Bytes)
+ if err != nil {
+ t.Fatalf("failed to parse test root: %s", err)
+ }
+
+ der, err = os.ReadFile(rootKeyPath)
+ if err != nil {
+ t.Fatalf("failed to read test key: %s", err)
+ }
+ b, _ = pem.Decode(der)
+ testRootKey, err := ParseECPrivateKey(b.Bytes)
+ if err != nil {
+ t.Fatalf("failed to parse test key: %s", err)
+ }
+
+ if _, err := testRoot.Verify(VerifyOptions{}); err != nil {
+ t.Skipf("test root is not in trust store, skipping (err: %q)", err)
+ }
+
+ now := time.Now()
+
+ tests := []struct {
+ name string
+ cert *Certificate
+ selfSigned bool
+ dnsName string
+ time time.Time
+ eku []ExtKeyUsage
+
+ expectedErr string
+ windowsErr string
+ macosErr string
+ }{
+ {
+ name: "valid",
+ cert: &Certificate{
+ SerialNumber: big.NewInt(1),
+ DNSNames: []string{"valid.testing.golang.invalid"},
+ NotBefore: now.Add(-time.Hour),
+ NotAfter: now.Add(time.Hour),
+ ExtKeyUsage: []ExtKeyUsage{ExtKeyUsageServerAuth},
+ },
+ },
+ {
+ name: "valid (with name)",
+ cert: &Certificate{
+ SerialNumber: big.NewInt(1),
+ DNSNames: []string{"valid.testing.golang.invalid"},
+ NotBefore: now.Add(-time.Hour),
+ NotAfter: now.Add(time.Hour),
+ ExtKeyUsage: []ExtKeyUsage{ExtKeyUsageServerAuth},
+ },
+ dnsName: "valid.testing.golang.invalid",
+ },
+ {
+ name: "valid (with time)",
+ cert: &Certificate{
+ SerialNumber: big.NewInt(1),
+ DNSNames: []string{"valid.testing.golang.invalid"},
+ NotBefore: now.Add(-time.Hour),
+ NotAfter: now.Add(time.Hour),
+ ExtKeyUsage: []ExtKeyUsage{ExtKeyUsageServerAuth},
+ },
+ time: now.Add(time.Minute * 30),
+ },
+ {
+ name: "valid (with eku)",
+ cert: &Certificate{
+ SerialNumber: big.NewInt(1),
+ DNSNames: []string{"valid.testing.golang.invalid"},
+ NotBefore: now.Add(-time.Hour),
+ NotAfter: now.Add(time.Hour),
+ ExtKeyUsage: []ExtKeyUsage{ExtKeyUsageServerAuth},
+ },
+ eku: []ExtKeyUsage{ExtKeyUsageServerAuth},
+ },
+ {
+ name: "wrong name",
+ cert: &Certificate{
+ SerialNumber: big.NewInt(1),
+ DNSNames: []string{"valid.testing.golang.invalid"},
+ NotBefore: now.Add(-time.Hour),
+ NotAfter: now.Add(time.Hour),
+ ExtKeyUsage: []ExtKeyUsage{ExtKeyUsageServerAuth},
+ },
+ dnsName: "invalid.testing.golang.invalid",
+ expectedErr: "x509: certificate is valid for valid.testing.golang.invalid, not invalid.testing.golang.invalid",
+ },
+ {
+ name: "expired (future)",
+ cert: &Certificate{
+ SerialNumber: big.NewInt(1),
+ DNSNames: []string{"valid.testing.golang.invalid"},
+ NotBefore: now.Add(-time.Hour),
+ NotAfter: now.Add(time.Hour),
+ ExtKeyUsage: []ExtKeyUsage{ExtKeyUsageServerAuth},
+ },
+ time: now.Add(time.Hour * 2),
+ expectedErr: "x509: certificate has expired or is not yet valid",
+ },
+ {
+ name: "expired (past)",
+ cert: &Certificate{
+ SerialNumber: big.NewInt(1),
+ DNSNames: []string{"valid.testing.golang.invalid"},
+ NotBefore: now.Add(-time.Hour),
+ NotAfter: now.Add(time.Hour),
+ ExtKeyUsage: []ExtKeyUsage{ExtKeyUsageServerAuth},
+ },
+ time: now.Add(time.Hour * 2),
+ expectedErr: "x509: certificate has expired or is not yet valid",
+ },
+ {
+ name: "self-signed",
+ cert: &Certificate{
+ SerialNumber: big.NewInt(1),
+ DNSNames: []string{"valid.testing.golang.invalid"},
+ NotBefore: now.Add(-time.Hour),
+ NotAfter: now.Add(time.Hour),
+ ExtKeyUsage: []ExtKeyUsage{ExtKeyUsageServerAuth},
+ },
+ selfSigned: true,
+ macosErr: "x509: “valid.testing.golang.invalid” certificate is not trusted",
+ windowsErr: "x509: certificate signed by unknown authority",
+ },
+ {
+ name: "non-specified KU",
+ cert: &Certificate{
+ SerialNumber: big.NewInt(1),
+ DNSNames: []string{"valid.testing.golang.invalid"},
+ NotBefore: now.Add(-time.Hour),
+ NotAfter: now.Add(time.Hour),
+ ExtKeyUsage: []ExtKeyUsage{ExtKeyUsageServerAuth},
+ },
+ eku: []ExtKeyUsage{ExtKeyUsageEmailProtection},
+ expectedErr: "x509: certificate specifies an incompatible key usage",
+ },
+ {
+ name: "non-nested KU",
+ cert: &Certificate{
+ SerialNumber: big.NewInt(1),
+ DNSNames: []string{"valid.testing.golang.invalid"},
+ NotBefore: now.Add(-time.Hour),
+ NotAfter: now.Add(time.Hour),
+ ExtKeyUsage: []ExtKeyUsage{ExtKeyUsageEmailProtection},
+ },
+ macosErr: "x509: “valid.testing.golang.invalid” certificate is not permitted for this usage",
+ windowsErr: "x509: certificate specifies an incompatible key usage",
+ },
+ }
+
+ leafKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ t.Fatalf("ecdsa.GenerateKey failed: %s", err)
+ }
+
+ for _, tc := range tests {
+ tc := tc
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+ parent := testRoot
+ if tc.selfSigned {
+ parent = tc.cert
+ }
+ certDER, err := CreateCertificate(rand.Reader, tc.cert, parent, leafKey.Public(), testRootKey)
+ if err != nil {
+ t.Fatalf("CreateCertificate failed: %s", err)
+ }
+ cert, err := ParseCertificate(certDER)
+ if err != nil {
+ t.Fatalf("ParseCertificate failed: %s", err)
+ }
+
+ var opts VerifyOptions
+ if tc.dnsName != "" {
+ opts.DNSName = tc.dnsName
+ }
+ if !tc.time.IsZero() {
+ opts.CurrentTime = tc.time
+ }
+ if len(tc.eku) > 0 {
+ opts.KeyUsages = tc.eku
+ }
+
+ expectedErr := tc.expectedErr
+ if runtime.GOOS == "darwin" && tc.macosErr != "" {
+ expectedErr = tc.macosErr
+ } else if runtime.GOOS == "windows" && tc.windowsErr != "" {
+ expectedErr = tc.windowsErr
+ }
+
+ _, err = cert.Verify(opts)
+ if err != nil && expectedErr == "" {
+ t.Errorf("unexpected verification error: %s", err)
+ } else if err != nil && !strings.HasPrefix(err.Error(), expectedErr) {
+ t.Errorf("unexpected verification error: got %q, want %q", err.Error(), expectedErr)
+ } else if err == nil && expectedErr != "" {
+ t.Errorf("unexpected verification success: want %q", expectedErr)
+ }
+ })
+ }
+}