]> Cypherpunks.ru repositories - gostls13.git/commitdiff
crypto/x509: use synthetic root for platform testing
authorRoland Shoemaker <roland@golang.org>
Tue, 25 Apr 2023 20:15:04 +0000 (13:15 -0700)
committerGopher Robot <gobot@golang.org>
Wed, 14 Jun 2023 18:53:13 +0000 (18:53 +0000)
Rather than using the external network and real-world chains for testing
the integrations with platform verifiers, use a synthetic test root.

This changes adds a constrained root and key pair to the tree, and adds
a test suite that verifies certificates issued from that root. These
tests are only executed if the root is detected in the trust store. For
reference, the script used to generate the root and key is attached to
the bottom of this commit message.

This change leaves the existing windows/darwin TestPlatformVerifier
tests in place, since the trybots do not currently have the test root in
place, and as such cannot run the suite. Once the builder images have
the root integrated, we can remove the old flaky tests, and the trybots
will begin running the new suite automatically.

Updates #52108

-- gen.go --
package main

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"flag"
"log"
"math/big"
"net"
"os"
"time"
)

func writePEM(pemType string, der []byte, path string) error {
enc := pem.EncodeToMemory(&pem.Block{
Type:  pemType,
Bytes: der,
})
return os.WriteFile(path, enc, 0666)
}

func main() {
certPath := flag.String("cert-path", "platform_root_cert.pem", "Path to write certificate PEM")
keyPath := flag.String("key-path", "platform_root_key.pem", "Path to write key PEM")
flag.Parse()

key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
log.Fatalf("ecdsa.GenerateKey failed: %s", err)
}

now := time.Now()
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(9009),
Subject: pkix.Name{
CommonName: "Go platform verifier testing root",
},
NotBefore:                   now.Add(-time.Hour),
NotAfter:                    now.Add(time.Hour * 24 * 365 * 5),
IsCA:                        true,
BasicConstraintsValid:       true,
PermittedDNSDomainsCritical: true,
// PermittedDNSDomains restricts the names in certificates issued from this root to *.testing.golang.invalid.
// The .invalid TLD is, per RFC 2606, reserved for testing, and as such anything issued for this certificate
// should never be valid in the real world.
PermittedDNSDomains: []string{"testing.golang.invalid"},
// ExcludedIPRanges prevents any certificate issued from this root that contains an IP address in both the full
// IPv4 and IPv6 ranges from being considered valid.
ExcludedIPRanges: []*net.IPNet{{IP: make([]byte, 4), Mask: make([]byte, 4)}, {IP: make([]byte, 16), Mask: make([]byte, 16)}},
KeyUsage:         x509.KeyUsageCertSign,
ExtKeyUsage:      []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}

certDER, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key)
if err != nil {
log.Fatalf("x509.CreateCertificate failed: %s", err)
}

keyDER, err := x509.MarshalECPrivateKey(key)
if err != nil {
log.Fatalf("x509.MarshalECPrivateKey failed: %s", err)
}

if err := writePEM("CERTIFICATE", certDER, *certPath); err != nil {
log.Fatalf("failed to write certificate PEM: %s", err)
}
if err := writePEM("EC PRIVATE KEY", keyDER, *keyPath); err != nil {
log.Fatalf("failed to write key PEM: %s", err)
}
}

Change-Id: If7c4a9f18466662a60fea5443e603232a9260026
Reviewed-on: https://go-review.googlesource.com/c/go/+/488855
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Auto-Submit: Roland Shoemaker <roland@golang.org>
Reviewed-by: Bryan Mills <bcmills@google.com>
Run-TryBot: Roland Shoemaker <roland@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>

src/crypto/x509/platform_root_cert.pem [new file with mode: 0644]
src/crypto/x509/platform_root_key.pem [new file with mode: 0644]
src/crypto/x509/platform_test.go [new file with mode: 0644]

diff --git a/src/crypto/x509/platform_root_cert.pem b/src/crypto/x509/platform_root_cert.pem
new file mode 100644 (file)
index 0000000..bef31f4
--- /dev/null
@@ -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 (file)
index 0000000..c0b6eeb
--- /dev/null
@@ -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 (file)
index 0000000..c35f0b4
--- /dev/null
@@ -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)
+                       }
+               })
+       }
+}