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) + } + }) + } +}