| // 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 | |
| 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 { | |
| 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) | |
| } | |
| }) | |
| } | |
| } |