1// Copyright 2023 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package x509
6
7import (
8	"crypto/ecdsa"
9	"crypto/elliptic"
10	"crypto/rand"
11	"encoding/pem"
12	"math/big"
13	"os"
14	"runtime"
15	"strings"
16	"testing"
17	"time"
18)
19
20// In order to run this test suite locally, you need to insert the test root, at
21// the path below, into your trust store. This root is constrained such that it
22// should not be dangerous to local developers to trust, but care should be
23// taken when inserting it into the trust store not to give it increased
24// permissions.
25//
26// On macOS the certificate can be further constrained to only be valid for
27// 'SSL' in the certificate properties pane of the 'Keychain Access' program.
28//
29// On Windows the certificate can also be constrained to only server
30// authentication in the properties pane of the certificate in the
31// "Certificates" snap-in of mmc.exe.
32
33const (
34	rootCertPath = "platform_root_cert.pem"
35	rootKeyPath  = "platform_root_key.pem"
36)
37
38func TestPlatformVerifier(t *testing.T) {
39	if runtime.GOOS != "windows" && runtime.GOOS != "darwin" {
40		t.Skip("only tested on windows and darwin")
41	}
42
43	der, err := os.ReadFile(rootCertPath)
44	if err != nil {
45		t.Fatalf("failed to read test root: %s", err)
46	}
47	b, _ := pem.Decode(der)
48	testRoot, err := ParseCertificate(b.Bytes)
49	if err != nil {
50		t.Fatalf("failed to parse test root: %s", err)
51	}
52
53	der, err = os.ReadFile(rootKeyPath)
54	if err != nil {
55		t.Fatalf("failed to read test key: %s", err)
56	}
57	b, _ = pem.Decode(der)
58	testRootKey, err := ParseECPrivateKey(b.Bytes)
59	if err != nil {
60		t.Fatalf("failed to parse test key: %s", err)
61	}
62
63	if _, err := testRoot.Verify(VerifyOptions{}); err != nil {
64		t.Skipf("test root is not in trust store, skipping (err: %q)", err)
65	}
66
67	now := time.Now()
68
69	tests := []struct {
70		name       string
71		cert       *Certificate
72		selfSigned bool
73		dnsName    string
74		time       time.Time
75		eku        []ExtKeyUsage
76
77		expectedErr string
78		windowsErr  string
79		macosErr    string
80	}{
81		{
82			name: "valid",
83			cert: &Certificate{
84				SerialNumber: big.NewInt(1),
85				DNSNames:     []string{"valid.testing.golang.invalid"},
86				NotBefore:    now.Add(-time.Hour),
87				NotAfter:     now.Add(time.Hour),
88				ExtKeyUsage:  []ExtKeyUsage{ExtKeyUsageServerAuth},
89			},
90		},
91		{
92			name: "valid (with name)",
93			cert: &Certificate{
94				SerialNumber: big.NewInt(1),
95				DNSNames:     []string{"valid.testing.golang.invalid"},
96				NotBefore:    now.Add(-time.Hour),
97				NotAfter:     now.Add(time.Hour),
98				ExtKeyUsage:  []ExtKeyUsage{ExtKeyUsageServerAuth},
99			},
100			dnsName: "valid.testing.golang.invalid",
101		},
102		{
103			name: "valid (with time)",
104			cert: &Certificate{
105				SerialNumber: big.NewInt(1),
106				DNSNames:     []string{"valid.testing.golang.invalid"},
107				NotBefore:    now.Add(-time.Hour),
108				NotAfter:     now.Add(time.Hour),
109				ExtKeyUsage:  []ExtKeyUsage{ExtKeyUsageServerAuth},
110			},
111			time: now.Add(time.Minute * 30),
112		},
113		{
114			name: "valid (with eku)",
115			cert: &Certificate{
116				SerialNumber: big.NewInt(1),
117				DNSNames:     []string{"valid.testing.golang.invalid"},
118				NotBefore:    now.Add(-time.Hour),
119				NotAfter:     now.Add(time.Hour),
120				ExtKeyUsage:  []ExtKeyUsage{ExtKeyUsageServerAuth},
121			},
122			eku: []ExtKeyUsage{ExtKeyUsageServerAuth},
123		},
124		{
125			name: "wrong name",
126			cert: &Certificate{
127				SerialNumber: big.NewInt(1),
128				DNSNames:     []string{"valid.testing.golang.invalid"},
129				NotBefore:    now.Add(-time.Hour),
130				NotAfter:     now.Add(time.Hour),
131				ExtKeyUsage:  []ExtKeyUsage{ExtKeyUsageServerAuth},
132			},
133			dnsName:     "invalid.testing.golang.invalid",
134			expectedErr: "x509: certificate is valid for valid.testing.golang.invalid, not invalid.testing.golang.invalid",
135		},
136		{
137			name: "expired (future)",
138			cert: &Certificate{
139				SerialNumber: big.NewInt(1),
140				DNSNames:     []string{"valid.testing.golang.invalid"},
141				NotBefore:    now.Add(-time.Hour),
142				NotAfter:     now.Add(time.Hour),
143				ExtKeyUsage:  []ExtKeyUsage{ExtKeyUsageServerAuth},
144			},
145			time:        now.Add(time.Hour * 2),
146			expectedErr: "x509: certificate has expired or is not yet valid",
147		},
148		{
149			name: "expired (past)",
150			cert: &Certificate{
151				SerialNumber: big.NewInt(1),
152				DNSNames:     []string{"valid.testing.golang.invalid"},
153				NotBefore:    now.Add(-time.Hour),
154				NotAfter:     now.Add(time.Hour),
155				ExtKeyUsage:  []ExtKeyUsage{ExtKeyUsageServerAuth},
156			},
157			time:        now.Add(time.Hour * 2),
158			expectedErr: "x509: certificate has expired or is not yet valid",
159		},
160		{
161			name: "self-signed",
162			cert: &Certificate{
163				SerialNumber: big.NewInt(1),
164				DNSNames:     []string{"valid.testing.golang.invalid"},
165				NotBefore:    now.Add(-time.Hour),
166				NotAfter:     now.Add(time.Hour),
167				ExtKeyUsage:  []ExtKeyUsage{ExtKeyUsageServerAuth},
168			},
169			selfSigned: true,
170			macosErr:   "x509: “valid.testing.golang.invalid” certificate is not trusted",
171			windowsErr: "x509: certificate signed by unknown authority",
172		},
173		{
174			name: "non-specified KU",
175			cert: &Certificate{
176				SerialNumber: big.NewInt(1),
177				DNSNames:     []string{"valid.testing.golang.invalid"},
178				NotBefore:    now.Add(-time.Hour),
179				NotAfter:     now.Add(time.Hour),
180				ExtKeyUsage:  []ExtKeyUsage{ExtKeyUsageServerAuth},
181			},
182			eku:         []ExtKeyUsage{ExtKeyUsageEmailProtection},
183			expectedErr: "x509: certificate specifies an incompatible key usage",
184		},
185		{
186			name: "non-nested KU",
187			cert: &Certificate{
188				SerialNumber: big.NewInt(1),
189				DNSNames:     []string{"valid.testing.golang.invalid"},
190				NotBefore:    now.Add(-time.Hour),
191				NotAfter:     now.Add(time.Hour),
192				ExtKeyUsage:  []ExtKeyUsage{ExtKeyUsageEmailProtection},
193			},
194			macosErr:   "x509: “valid.testing.golang.invalid” certificate is not permitted for this usage",
195			windowsErr: "x509: certificate specifies an incompatible key usage",
196		},
197	}
198
199	leafKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
200	if err != nil {
201		t.Fatalf("ecdsa.GenerateKey failed: %s", err)
202	}
203
204	for _, tc := range tests {
205		tc := tc
206		t.Run(tc.name, func(t *testing.T) {
207			t.Parallel()
208			parent := testRoot
209			if tc.selfSigned {
210				parent = tc.cert
211			}
212			certDER, err := CreateCertificate(rand.Reader, tc.cert, parent, leafKey.Public(), testRootKey)
213			if err != nil {
214				t.Fatalf("CreateCertificate failed: %s", err)
215			}
216			cert, err := ParseCertificate(certDER)
217			if err != nil {
218				t.Fatalf("ParseCertificate failed: %s", err)
219			}
220
221			var opts VerifyOptions
222			if tc.dnsName != "" {
223				opts.DNSName = tc.dnsName
224			}
225			if !tc.time.IsZero() {
226				opts.CurrentTime = tc.time
227			}
228			if len(tc.eku) > 0 {
229				opts.KeyUsages = tc.eku
230			}
231
232			expectedErr := tc.expectedErr
233			if runtime.GOOS == "darwin" && tc.macosErr != "" {
234				expectedErr = tc.macosErr
235			} else if runtime.GOOS == "windows" && tc.windowsErr != "" {
236				expectedErr = tc.windowsErr
237			}
238
239			_, err = cert.Verify(opts)
240			if err != nil && expectedErr == "" {
241				t.Errorf("unexpected verification error: %s", err)
242			} else if err != nil && !strings.HasPrefix(err.Error(), expectedErr) {
243				t.Errorf("unexpected verification error: got %q, want %q", err.Error(), expectedErr)
244			} else if err == nil && expectedErr != "" {
245				t.Errorf("unexpected verification success: want %q", expectedErr)
246			}
247		})
248	}
249}
250