--- /dev/null
+// 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)
+ }
+ })
+ }
+}