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