-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Janos <[email protected]>
- Loading branch information
Janos
committed
Aug 12, 2024
1 parent
92a2653
commit 6b1fd02
Showing
5 changed files
with
507 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package tofutestutils | ||
|
||
import ( | ||
"testing" | ||
"time" | ||
|
||
"github.com/opentofu/tofutestutils/testca" | ||
) | ||
|
||
// CA returns a certificate authority configured for the provided test. This implementation will configure the CA to use | ||
// a pseudorandom source. You can call testca.New() for more configuration options. | ||
func CA(t *testing.T) testca.CertificateAuthority { | ||
return testca.New(t, RandomSource(), time.Now) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
# Certificate authority | ||
|
||
This folder contains a basic x509 certificate authority implementation for testing purposes. You can use it whenever you need a certificate for servers or clients. | ||
|
||
```go | ||
package your_test | ||
|
||
import ( | ||
"crypto/tls" | ||
"io" | ||
"net" | ||
"strconv" | ||
"testing" | ||
"time" | ||
|
||
"github.com/opentofu/tofutestutils" | ||
"github.com/opentofu/tofutestutils/testca" | ||
"github.com/opentofu/tofutestutils/testrandom" | ||
) | ||
|
||
func TestMySocket(t *testing.T) { | ||
// Configure a desired randomness and time source. You can use this to create deterministic behavior. | ||
currentTimeSource := time.Now | ||
ca := testca.New(t, testrandom.DeterministicSource(t), currentTimeSource) | ||
|
||
// Server side: | ||
tlsListener := tofutestutils.Must2(tls.Listen("tcp", "127.0.0.1:0", ca.CreateLocalhostServerCert().GetServerTLSConfig())) | ||
go func() { | ||
conn, serverErr := tlsListener.Accept() | ||
if serverErr != nil { | ||
return | ||
} | ||
defer func() { | ||
_ = conn.Close() | ||
}() | ||
_, _ = conn.Write([]byte("Hello world!")) | ||
}() | ||
|
||
// Client side: | ||
port := tlsListener.Addr().(*net.TCPAddr).Port | ||
client := tofutestutils.Must2(tls.Dial("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(port)), ca.GetClientTLSConfig())) | ||
defer func() { | ||
_ = client.Close() | ||
}() | ||
|
||
t.Logf("%s", tofutestutils.Must2(io.ReadAll(client))) | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,262 @@ | ||
// Copyright (c) The OpenTofu Authors | ||
// SPDX-License-Identifier: MPL-2.0 | ||
// Copyright (c) 2023 HashiCorp, Inc. | ||
// SPDX-License-Identifier: MPL-2.0 | ||
|
||
package testca | ||
|
||
import ( | ||
"bytes" | ||
"crypto" | ||
"crypto/rsa" | ||
"crypto/tls" | ||
"crypto/x509" | ||
"crypto/x509/pkix" | ||
"encoding/pem" | ||
"io" | ||
"math/big" | ||
"net" | ||
"sync" | ||
"testing" | ||
"time" | ||
) | ||
|
||
const caKeySize = 2048 | ||
const expirationYears = 10 | ||
|
||
// New creates an x509 CA certificate that can produce certificates for testing purposes. Pass a desired randomSource | ||
// to create a deterministic source of certificates alongside a deterministic timeSource. | ||
func New(t *testing.T, randomSource io.Reader, timeSource func() time.Time) CertificateAuthority { | ||
// We use a non-deterministic cheap randomness source because the certificate won't be reproducible anyway due to | ||
// the NotBefore / NotAfter being different every time. We don't use crypto/rand.Rand because it can get blocked | ||
// if not enough entropy is available and it doesn't matter for the test use case. | ||
|
||
now := timeSource() | ||
|
||
caCert := &x509.Certificate{ | ||
SerialNumber: big.NewInt(1), | ||
Subject: pkix.Name{ | ||
Organization: []string{"OpenTofu a Series of LF Projects, LLC"}, | ||
Country: []string{"US"}, | ||
}, | ||
NotBefore: now, | ||
NotAfter: now.AddDate(expirationYears, 0, 0), | ||
IsCA: true, | ||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, | ||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, | ||
BasicConstraintsValid: true, | ||
} | ||
|
||
caPrivateKey, err := rsa.GenerateKey(randomSource, caKeySize) | ||
if err != nil { | ||
t.Skipf("Failed to create private key: %v", err) | ||
} | ||
caCertData, err := x509.CreateCertificate(randomSource, caCert, caCert, &caPrivateKey.PublicKey, caPrivateKey) | ||
if err != nil { | ||
t.Skipf("Failed to create CA certificate: %v", err) | ||
} | ||
caPEM := new(bytes.Buffer) | ||
if err := pem.Encode(caPEM, &pem.Block{ | ||
Type: "CERTIFICATE", | ||
Bytes: caCertData, | ||
}); err != nil { | ||
t.Skipf("Failed to encode CA cert: %v", err) | ||
} | ||
return &ca{ | ||
t: t, | ||
random: randomSource, | ||
caCert: caCert, | ||
caCertPEM: caPEM.Bytes(), | ||
privateKey: caPrivateKey, | ||
serial: big.NewInt(0), | ||
lock: &sync.Mutex{}, | ||
timeSource: timeSource, | ||
} | ||
} | ||
|
||
// CertConfig is the configuration structure for creating specialized certificates using | ||
// CertificateAuthority.CreateConfiguredServerCert. | ||
type CertConfig struct { | ||
// IPAddresses contains a list of IP addresses that should be added to the SubjectAltName field of the certificate. | ||
IPAddresses []string | ||
// Hosts contains a list of host names that should be added to the SubjectAltName field of the certificate. | ||
Hosts []string | ||
// Subject is the subject (CN, etc) setting for the certificate. Most commonly, you will want the CN field to match | ||
// one of hour host names. | ||
Subject pkix.Name | ||
// ExtKeyUsage describes the extended key usage. Typically, this should be: | ||
// | ||
// []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth} | ||
ExtKeyUsage []x509.ExtKeyUsage | ||
} | ||
|
||
// KeyPair contains a certificate and private key in PEM format. | ||
type KeyPair struct { | ||
// Certificate contains an x509 certificate in PEM format. | ||
Certificate []byte | ||
// PrivateKey contains an RSA or other private key in PEM format. | ||
PrivateKey []byte | ||
} | ||
|
||
// GetPrivateKey returns a crypto.Signer for the private key. | ||
func (k KeyPair) GetPrivateKey() crypto.PrivateKey { | ||
block, _ := pem.Decode(k.PrivateKey) | ||
key, err := x509.ParsePKCS1PrivateKey(block.Bytes) | ||
if err != nil { | ||
panic(err) | ||
} | ||
return key | ||
} | ||
|
||
// GetTLSCertificate returns the tls.Certificate based on this key pair. | ||
func (k KeyPair) GetTLSCertificate() tls.Certificate { | ||
cert, err := tls.X509KeyPair(k.Certificate, k.PrivateKey) | ||
if err != nil { | ||
panic(err) | ||
} | ||
return cert | ||
} | ||
|
||
// GetServerTLSConfig returns a tls.Config suitable for a TLS server with this key pair. | ||
func (k KeyPair) GetServerTLSConfig() *tls.Config { | ||
return &tls.Config{ | ||
Certificates: []tls.Certificate{ | ||
k.GetTLSCertificate(), | ||
}, | ||
MinVersion: tls.VersionTLS12, | ||
} | ||
} | ||
|
||
// CertificateAuthority provides simple access to x509 CA functions for testing purposes only. | ||
type CertificateAuthority interface { | ||
// GetPEMCACert returns the CA certificate in PEM format. | ||
GetPEMCACert() []byte | ||
// GetCertPool returns an x509.CertPool configured for this CA. | ||
GetCertPool() *x509.CertPool | ||
// GetClientTLSConfig returns a *tls.Config with a valid cert pool configured for this CA. | ||
GetClientTLSConfig() *tls.Config | ||
// CreateLocalhostServerCert creates a server certificate pre-configured for "localhost", which is sufficient for | ||
// most test cases. | ||
CreateLocalhostServerCert() KeyPair | ||
// CreateLocalhostClientCert creates a client certificate pre-configured for "localhost", which is sufficient for | ||
// most test cases. | ||
CreateLocalhostClientCert() KeyPair | ||
// CreateConfiguredCert creates a certificate with a specialized configuration. | ||
CreateConfiguredCert(config CertConfig) KeyPair | ||
} | ||
|
||
type ca struct { | ||
caCert *x509.Certificate | ||
caCertPEM []byte | ||
privateKey *rsa.PrivateKey | ||
serial *big.Int | ||
lock *sync.Mutex | ||
t *testing.T | ||
random io.Reader | ||
timeSource func() time.Time | ||
} | ||
|
||
func (c *ca) GetClientTLSConfig() *tls.Config { | ||
certPool := c.GetCertPool() | ||
|
||
return &tls.Config{ | ||
RootCAs: certPool, | ||
MinVersion: tls.VersionTLS12, | ||
} | ||
} | ||
|
||
func (c *ca) GetCertPool() *x509.CertPool { | ||
certPool := x509.NewCertPool() | ||
certPool.AppendCertsFromPEM(c.caCertPEM) | ||
return certPool | ||
} | ||
|
||
func (c *ca) GetPEMCACert() []byte { | ||
return c.caCertPEM | ||
} | ||
|
||
func (c *ca) CreateConfiguredCert(config CertConfig) KeyPair { | ||
c.lock.Lock() | ||
defer c.lock.Unlock() | ||
c.serial.Add(c.serial, big.NewInt(1)) | ||
|
||
ipAddresses := make([]net.IP, len(config.IPAddresses)) | ||
for i, ip := range config.IPAddresses { | ||
ipAddresses[i] = net.ParseIP(ip) | ||
} | ||
|
||
now := c.timeSource() | ||
|
||
cert := &x509.Certificate{ | ||
SerialNumber: c.serial, | ||
Subject: config.Subject, | ||
NotBefore: now, | ||
NotAfter: now.AddDate(0, 0, 1), | ||
SubjectKeyId: []byte{1}, | ||
ExtKeyUsage: config.ExtKeyUsage, | ||
KeyUsage: x509.KeyUsageDigitalSignature, | ||
DNSNames: config.Hosts, | ||
IPAddresses: ipAddresses, | ||
} | ||
certPrivKey, err := rsa.GenerateKey(c.random, caKeySize) | ||
if err != nil { | ||
c.t.Skipf("Failed to generate private key: %v", err) | ||
} | ||
certBytes, err := x509.CreateCertificate( | ||
c.random, | ||
cert, | ||
c.caCert, | ||
&certPrivKey.PublicKey, | ||
c.privateKey, | ||
) | ||
if err != nil { | ||
c.t.Skipf("Failed to create certificate: %v", err) | ||
} | ||
certPrivKeyPEM := new(bytes.Buffer) | ||
if err := pem.Encode(certPrivKeyPEM, &pem.Block{ | ||
Type: "RSA PRIVATE KEY", | ||
Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey), | ||
}); err != nil { | ||
c.t.Skipf("Failed to encode private key: %v", err) | ||
} | ||
certPEM := new(bytes.Buffer) | ||
if err := pem.Encode(certPEM, | ||
&pem.Block{Type: "CERTIFICATE", Bytes: certBytes}, | ||
); err != nil { | ||
c.t.Skipf("Failed to encode certificate: %v", err) | ||
} | ||
return KeyPair{ | ||
Certificate: certPEM.Bytes(), | ||
PrivateKey: certPrivKeyPEM.Bytes(), | ||
} | ||
} | ||
|
||
func (c *ca) CreateLocalhostServerCert() KeyPair { | ||
return c.CreateConfiguredCert(CertConfig{ | ||
IPAddresses: []string{"127.0.0.1", "::1"}, | ||
Subject: pkix.Name{ | ||
Country: []string{"US"}, | ||
Organization: []string{"OpenTofu a Series of LF Projects, LLC"}, | ||
CommonName: "localhost", | ||
}, | ||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, | ||
Hosts: []string{ | ||
"localhost", | ||
}, | ||
}) | ||
} | ||
|
||
func (c *ca) CreateLocalhostClientCert() KeyPair { | ||
return c.CreateConfiguredCert(CertConfig{ | ||
IPAddresses: []string{"127.0.0.1", "::1"}, | ||
Subject: pkix.Name{ | ||
Country: []string{"US"}, | ||
Organization: []string{"OpenTofu a Series of LF Projects, LLC"}, | ||
CommonName: "localhost", | ||
}, | ||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, | ||
Hosts: []string{ | ||
"localhost", | ||
}, | ||
}) | ||
} |
Oops, something went wrong.