Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e639df78ec | |||
| 5dbb46c3ee | |||
| 30b5a6699a | |||
| d5cee37433 | |||
| e1cb7efbf1 |
@@ -1,5 +1,11 @@
|
||||
CHANGELOG
|
||||
|
||||
v1.19.0 - 2026-02-12
|
||||
|
||||
Added:
|
||||
- certlib/dump: DisplayCSR function for dumping Certificate Signing Request information.
|
||||
- certlib: MatchKeysCSR function for testing if a CSR's public key matches a private key.
|
||||
|
||||
v1.18.0 - 2025-11-21
|
||||
|
||||
Changed:
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -88,12 +89,17 @@ func (cs CertificateRequest) Request(priv crypto.PrivateKey) (*x509.CertificateR
|
||||
}
|
||||
}
|
||||
|
||||
dnsNames := cs.Subject.DNSNames
|
||||
if isFQDN(cs.Subject.CommonName) && !slices.Contains(dnsNames, cs.Subject.CommonName) {
|
||||
dnsNames = append(dnsNames, cs.Subject.CommonName)
|
||||
}
|
||||
|
||||
req := &x509.CertificateRequest{
|
||||
PublicKeyAlgorithm: 0,
|
||||
PublicKey: getPublic(priv),
|
||||
Subject: subject,
|
||||
EmailAddresses: cs.Subject.Email,
|
||||
DNSNames: cs.Subject.DNSNames,
|
||||
DNSNames: dnsNames,
|
||||
IPAddresses: ipAddresses,
|
||||
}
|
||||
|
||||
@@ -130,6 +136,8 @@ type Profile struct {
|
||||
KeyUse []string `yaml:"key_uses"`
|
||||
ExtKeyUsages []string `yaml:"ext_key_usages"`
|
||||
Expiry string `yaml:"expiry"`
|
||||
OCSPServer []string `yaml:"ocsp_server,omitempty"`
|
||||
IssuingCertificateURL []string `yaml:"issuing_certificate_url,omitempty"`
|
||||
}
|
||||
|
||||
func (p Profile) templateFromRequest(req *x509.CertificateRequest) (*x509.Certificate, error) {
|
||||
@@ -175,6 +183,13 @@ func (p Profile) templateFromRequest(req *x509.CertificateRequest) (*x509.Certif
|
||||
certTemplate.ExtKeyUsage = append(certTemplate.ExtKeyUsage, eku)
|
||||
}
|
||||
|
||||
if len(p.OCSPServer) > 0 {
|
||||
certTemplate.OCSPServer = p.OCSPServer
|
||||
}
|
||||
if len(p.IssuingCertificateURL) > 0 {
|
||||
certTemplate.IssuingCertificateURL = p.IssuingCertificateURL
|
||||
}
|
||||
|
||||
return certTemplate, nil
|
||||
}
|
||||
|
||||
@@ -210,6 +225,32 @@ func (p Profile) SelfSign(req *x509.CertificateRequest, priv crypto.PrivateKey)
|
||||
return p.SignRequest(certTemplate, req, priv)
|
||||
}
|
||||
|
||||
// isFQDN returns true if s looks like a fully-qualified domain name.
|
||||
func isFQDN(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
// Must contain at least one dot and no spaces.
|
||||
if !strings.Contains(s, ".") || strings.ContainsAny(s, " \t") {
|
||||
return false
|
||||
}
|
||||
// Each label must be non-empty and consist of letters, digits, or hyphens.
|
||||
for label := range strings.SplitSeq(strings.TrimSuffix(s, "."), ".") {
|
||||
if label == "" {
|
||||
return false
|
||||
}
|
||||
for _, c := range label {
|
||||
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if label[0] == '-' || label[len(label)-1] == '-' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func SerialNumber() (*big.Int, error) {
|
||||
serialNumberBytes := make([]byte, 20)
|
||||
_, err := rand.Read(serialNumberBytes)
|
||||
|
||||
188
certlib/certgen/config_test.go
Normal file
188
certlib/certgen/config_test.go
Normal file
@@ -0,0 +1,188 @@
|
||||
package certgen
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsFQDN(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
want bool
|
||||
}{
|
||||
{"example.com", true},
|
||||
{"sub.example.com", true},
|
||||
{"example.com.", true}, // trailing dot
|
||||
{"localhost", false}, // no dot
|
||||
{"", false},
|
||||
{"foo bar.com", false}, // space
|
||||
{"-bad.com", false}, // leading hyphen
|
||||
{"bad-.com", false}, // trailing hyphen
|
||||
{"a..b.com", false}, // empty label
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := isFQDN(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("isFQDN(%q) = %v, want %v", tt.input, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestAddsFQDNAsDNSSAN(t *testing.T) {
|
||||
creq := &CertificateRequest{
|
||||
KeySpec: KeySpec{Algorithm: "ecdsa", Size: 256},
|
||||
Subject: Subject{
|
||||
CommonName: "example.com",
|
||||
Organization: "Test Org",
|
||||
},
|
||||
Profile: Profile{
|
||||
Expiry: "1h",
|
||||
},
|
||||
}
|
||||
|
||||
_, req, err := creq.Generate()
|
||||
if err != nil {
|
||||
t.Fatalf("Generate() error: %v", err)
|
||||
}
|
||||
|
||||
if !slices.Contains(req.DNSNames, "example.com") {
|
||||
t.Errorf("expected DNS SAN to contain %q, got %v", "example.com", req.DNSNames)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestFQDNNotDuplicated(t *testing.T) {
|
||||
creq := &CertificateRequest{
|
||||
KeySpec: KeySpec{Algorithm: "ecdsa", Size: 256},
|
||||
Subject: Subject{
|
||||
CommonName: "example.com",
|
||||
Organization: "Test Org",
|
||||
DNSNames: []string{"example.com", "www.example.com"},
|
||||
},
|
||||
Profile: Profile{
|
||||
Expiry: "1h",
|
||||
},
|
||||
}
|
||||
|
||||
_, req, err := creq.Generate()
|
||||
if err != nil {
|
||||
t.Fatalf("Generate() error: %v", err)
|
||||
}
|
||||
|
||||
count := 0
|
||||
for _, name := range req.DNSNames {
|
||||
if name == "example.com" {
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
if count != 1 {
|
||||
t.Errorf("expected exactly 1 occurrence of %q in DNS SANs, got %d: %v", "example.com", count, req.DNSNames)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileAIAFieldsInCertificate(t *testing.T) {
|
||||
caKey := KeySpec{Algorithm: "ecdsa", Size: 256}
|
||||
_, caPriv, err := caKey.Generate()
|
||||
if err != nil {
|
||||
t.Fatalf("generate CA key: %v", err)
|
||||
}
|
||||
|
||||
caProfile := Profile{
|
||||
IsCA: true,
|
||||
PathLen: 1,
|
||||
KeyUse: []string{"cert sign", "crl sign"},
|
||||
Expiry: "8760h",
|
||||
}
|
||||
|
||||
caReq := &CertificateRequest{
|
||||
KeySpec: caKey,
|
||||
Subject: Subject{CommonName: "Test CA", Organization: "Test"},
|
||||
Profile: caProfile,
|
||||
}
|
||||
caCSR, err := caReq.Request(caPriv)
|
||||
if err != nil {
|
||||
t.Fatalf("generate CA CSR: %v", err)
|
||||
}
|
||||
caCert, err := caProfile.SelfSign(caCSR, caPriv)
|
||||
if err != nil {
|
||||
t.Fatalf("self-sign CA: %v", err)
|
||||
}
|
||||
|
||||
leafProfile := Profile{
|
||||
KeyUse: []string{"digital signature"},
|
||||
ExtKeyUsages: []string{"server auth"},
|
||||
Expiry: "24h",
|
||||
OCSPServer: []string{"https://ocsp.example.com"},
|
||||
IssuingCertificateURL: []string{"https://pki.example.com/ca.pem"},
|
||||
}
|
||||
|
||||
leafReq := &CertificateRequest{
|
||||
KeySpec: KeySpec{Algorithm: "ecdsa", Size: 256},
|
||||
Subject: Subject{CommonName: "leaf.example.com", Organization: "Test"},
|
||||
Profile: leafProfile,
|
||||
}
|
||||
_, leafCSR, err := leafReq.Generate()
|
||||
if err != nil {
|
||||
t.Fatalf("generate leaf CSR: %v", err)
|
||||
}
|
||||
|
||||
leafCert, err := leafProfile.SignRequest(caCert, leafCSR, caPriv)
|
||||
if err != nil {
|
||||
t.Fatalf("sign leaf: %v", err)
|
||||
}
|
||||
|
||||
if len(leafCert.OCSPServer) != 1 || leafCert.OCSPServer[0] != "https://ocsp.example.com" {
|
||||
t.Errorf("OCSPServer = %v, want [https://ocsp.example.com]", leafCert.OCSPServer)
|
||||
}
|
||||
if len(leafCert.IssuingCertificateURL) != 1 || leafCert.IssuingCertificateURL[0] != "https://pki.example.com/ca.pem" {
|
||||
t.Errorf("IssuingCertificateURL = %v, want [https://pki.example.com/ca.pem]", leafCert.IssuingCertificateURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileWithoutAIAOmitsExtension(t *testing.T) {
|
||||
profile := Profile{
|
||||
KeyUse: []string{"digital signature"},
|
||||
ExtKeyUsages: []string{"server auth"},
|
||||
Expiry: "24h",
|
||||
}
|
||||
|
||||
creq := &CertificateRequest{
|
||||
KeySpec: KeySpec{Algorithm: "ecdsa", Size: 256},
|
||||
Subject: Subject{CommonName: "noaia.example.com", Organization: "Test"},
|
||||
Profile: profile,
|
||||
}
|
||||
cert, _, err := GenerateSelfSigned(creq)
|
||||
if err != nil {
|
||||
t.Fatalf("generate: %v", err)
|
||||
}
|
||||
|
||||
if len(cert.OCSPServer) != 0 {
|
||||
t.Errorf("OCSPServer = %v, want empty", cert.OCSPServer)
|
||||
}
|
||||
if len(cert.IssuingCertificateURL) != 0 {
|
||||
t.Errorf("IssuingCertificateURL = %v, want empty", cert.IssuingCertificateURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestNonFQDNCommonNameNotAdded(t *testing.T) {
|
||||
creq := &CertificateRequest{
|
||||
KeySpec: KeySpec{Algorithm: "ecdsa", Size: 256},
|
||||
Subject: Subject{
|
||||
CommonName: "localhost",
|
||||
Organization: "Test Org",
|
||||
},
|
||||
Profile: Profile{
|
||||
Expiry: "1h",
|
||||
},
|
||||
}
|
||||
|
||||
_, req, err := creq.Generate()
|
||||
if err != nil {
|
||||
t.Fatalf("Generate() error: %v", err)
|
||||
}
|
||||
|
||||
if slices.Contains(req.DNSNames, "localhost") {
|
||||
t.Errorf("expected DNS SANs to not contain %q, got %v", "localhost", req.DNSNames)
|
||||
}
|
||||
}
|
||||
213
certlib/certgen/testca.go
Normal file
213
certlib/certgen/testca.go
Normal file
@@ -0,0 +1,213 @@
|
||||
package certgen
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestCA is an in-memory certificate authority for use in tests. It
|
||||
// provides a root CA certificate and the ability to issue leaf
|
||||
// certificates for TLS testing with full verification enabled.
|
||||
type TestCA struct {
|
||||
cert *x509.Certificate
|
||||
key *ecdsa.PrivateKey
|
||||
}
|
||||
|
||||
// NewTestCA creates a new TestCA with a self-signed P-256 root
|
||||
// certificate. The CA is valid for 1 hour.
|
||||
func NewTestCA() (*TestCA, error) {
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("certgen: generating CA key: %w", err)
|
||||
}
|
||||
|
||||
serial, err := SerialNumber()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("certgen: generating serial: %w", err)
|
||||
}
|
||||
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: serial,
|
||||
Subject: pkix.Name{
|
||||
CommonName: "Test CA",
|
||||
Organization: []string{"Test"},
|
||||
},
|
||||
NotBefore: time.Now().Add(-1 * time.Minute),
|
||||
NotAfter: time.Now().Add(1 * time.Hour),
|
||||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
MaxPathLen: 1,
|
||||
}
|
||||
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("certgen: creating CA certificate: %w", err)
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(certDER)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("certgen: parsing CA certificate: %w", err)
|
||||
}
|
||||
|
||||
return &TestCA{cert: cert, key: key}, nil
|
||||
}
|
||||
|
||||
// Certificate returns the root CA certificate.
|
||||
func (ca *TestCA) Certificate() *x509.Certificate {
|
||||
return ca.cert
|
||||
}
|
||||
|
||||
// CertificatePEM returns the root CA certificate as a PEM-encoded
|
||||
// byte slice.
|
||||
func (ca *TestCA) CertificatePEM() []byte {
|
||||
return pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: ca.cert.Raw,
|
||||
})
|
||||
}
|
||||
|
||||
// CertPool returns a certificate pool containing the root CA
|
||||
// certificate, suitable for use as a TLS root CA pool.
|
||||
func (ca *TestCA) CertPool() *x509.CertPool {
|
||||
pool := x509.NewCertPool()
|
||||
pool.AddCert(ca.cert)
|
||||
return pool
|
||||
}
|
||||
|
||||
// Issue creates a new leaf certificate signed by the CA for the given
|
||||
// DNS names and IP addresses. It returns the leaf private key and
|
||||
// certificate. The leaf certificate is valid for 1 hour with key
|
||||
// usage appropriate for a TLS server.
|
||||
func (ca *TestCA) Issue(dnsNames []string, ips []net.IP) (crypto.Signer, *x509.Certificate, error) {
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("certgen: generating leaf key: %w", err)
|
||||
}
|
||||
|
||||
serial, err := SerialNumber()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("certgen: generating serial: %w", err)
|
||||
}
|
||||
|
||||
cn := "localhost"
|
||||
if len(dnsNames) > 0 {
|
||||
cn = dnsNames[0]
|
||||
}
|
||||
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: serial,
|
||||
Subject: pkix.Name{
|
||||
CommonName: cn,
|
||||
Organization: []string{"Test"},
|
||||
},
|
||||
NotBefore: time.Now().Add(-1 * time.Minute),
|
||||
NotAfter: time.Now().Add(1 * time.Hour),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
||||
BasicConstraintsValid: true,
|
||||
DNSNames: dnsNames,
|
||||
IPAddresses: ips,
|
||||
}
|
||||
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, template, ca.cert, &key.PublicKey, ca.key)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("certgen: creating leaf certificate: %w", err)
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(certDER)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("certgen: parsing leaf certificate: %w", err)
|
||||
}
|
||||
|
||||
return key, cert, nil
|
||||
}
|
||||
|
||||
// IssueServer is a convenience wrapper around Issue for the common
|
||||
// case of a server certificate for localhost (both DNS and IP).
|
||||
func (ca *TestCA) IssueServer() (crypto.Signer, *x509.Certificate, error) {
|
||||
return ca.Issue(
|
||||
[]string{"localhost"},
|
||||
[]net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback},
|
||||
)
|
||||
}
|
||||
|
||||
// TLSConfig returns a tls.Config (by value) configured with the CA's
|
||||
// root pool for verification. The caller can set additional fields
|
||||
// (e.g., Certificates) or modify the returned config safely.
|
||||
func (ca *TestCA) TLSConfig() tls.Config {
|
||||
return tls.Config{
|
||||
RootCAs: ca.CertPool(),
|
||||
MinVersion: tls.VersionTLS13,
|
||||
}
|
||||
}
|
||||
|
||||
// ServerTLSConfig returns a tls.Config (by value) for a TLS server
|
||||
// using the given leaf key and certificate, with client verification
|
||||
// against the CA root pool. Pass key and cert from Issue or
|
||||
// IssueServer.
|
||||
func (ca *TestCA) ServerTLSConfig(key crypto.Signer, cert *x509.Certificate) tls.Config {
|
||||
return tls.Config{
|
||||
Certificates: []tls.Certificate{
|
||||
{
|
||||
Certificate: [][]byte{cert.Raw},
|
||||
PrivateKey: key,
|
||||
Leaf: cert,
|
||||
},
|
||||
},
|
||||
ClientCAs: ca.CertPool(),
|
||||
MinVersion: tls.VersionTLS13,
|
||||
}
|
||||
}
|
||||
|
||||
// TLSKeyPair returns a tls.Certificate from the given key and
|
||||
// certificate, suitable for use in a tls.Config.Certificates slice.
|
||||
func TLSKeyPair(key crypto.Signer, cert *x509.Certificate) tls.Certificate {
|
||||
return tls.Certificate{
|
||||
Certificate: [][]byte{cert.Raw},
|
||||
PrivateKey: key,
|
||||
Leaf: cert,
|
||||
}
|
||||
}
|
||||
|
||||
// MustTestCA calls NewTestCA and panics on error. Intended for use
|
||||
// in TestMain or test helpers where error handling is impractical.
|
||||
func MustTestCA() *TestCA {
|
||||
ca, err := NewTestCA()
|
||||
if err != nil {
|
||||
panic("certgen: " + err.Error())
|
||||
}
|
||||
return ca
|
||||
}
|
||||
|
||||
// CertificatePEM returns a PEM-encoded byte slice for the given
|
||||
// certificate.
|
||||
func CertificatePEM(cert *x509.Certificate) []byte {
|
||||
return pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: cert.Raw,
|
||||
})
|
||||
}
|
||||
|
||||
// PrivateKeyPEM returns a PEM-encoded PKCS#8 byte slice for the
|
||||
// given private key.
|
||||
func PrivateKeyPEM(key crypto.Signer) ([]byte, error) {
|
||||
der, err := x509.MarshalPKCS8PrivateKey(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("certgen: marshaling private key: %w", err)
|
||||
}
|
||||
return pem.EncodeToMemory(&pem.Block{
|
||||
Type: "PRIVATE KEY",
|
||||
Bytes: der,
|
||||
}), nil
|
||||
}
|
||||
|
||||
223
certlib/certgen/testca_test.go
Normal file
223
certlib/certgen/testca_test.go
Normal file
@@ -0,0 +1,223 @@
|
||||
package certgen
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewTestCA(t *testing.T) {
|
||||
ca, err := NewTestCA()
|
||||
if err != nil {
|
||||
t.Fatalf("NewTestCA: %v", err)
|
||||
}
|
||||
|
||||
cert := ca.Certificate()
|
||||
if !cert.IsCA {
|
||||
t.Fatal("expected CA certificate")
|
||||
}
|
||||
if cert.Subject.CommonName != "Test CA" {
|
||||
t.Fatalf("got CN %q, want %q", cert.Subject.CommonName, "Test CA")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCertificatePEMRoundtrip(t *testing.T) {
|
||||
ca, err := NewTestCA()
|
||||
if err != nil {
|
||||
t.Fatalf("NewTestCA: %v", err)
|
||||
}
|
||||
|
||||
pemBytes := ca.CertificatePEM()
|
||||
block, _ := pem.Decode(pemBytes)
|
||||
if block == nil {
|
||||
t.Fatal("failed to decode PEM")
|
||||
}
|
||||
if block.Type != "CERTIFICATE" {
|
||||
t.Fatalf("got PEM type %q, want CERTIFICATE", block.Type)
|
||||
}
|
||||
|
||||
parsed, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
t.Fatalf("parse certificate: %v", err)
|
||||
}
|
||||
if !parsed.Equal(ca.Certificate()) {
|
||||
t.Fatal("parsed certificate does not match original")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCertPool(t *testing.T) {
|
||||
ca, err := NewTestCA()
|
||||
if err != nil {
|
||||
t.Fatalf("NewTestCA: %v", err)
|
||||
}
|
||||
|
||||
pool := ca.CertPool()
|
||||
|
||||
// Verify the CA cert validates against its own pool.
|
||||
chains, err := ca.Certificate().Verify(x509.VerifyOptions{
|
||||
Roots: pool,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("verify CA cert against its own pool: %v", err)
|
||||
}
|
||||
if len(chains) == 0 {
|
||||
t.Fatal("expected at least one chain")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssue(t *testing.T) {
|
||||
ca, err := NewTestCA()
|
||||
if err != nil {
|
||||
t.Fatalf("NewTestCA: %v", err)
|
||||
}
|
||||
|
||||
dnsNames := []string{"example.test", "www.example.test"}
|
||||
ips := []net.IP{net.IPv4(10, 0, 0, 1)}
|
||||
|
||||
key, cert, err := ca.Issue(dnsNames, ips)
|
||||
if err != nil {
|
||||
t.Fatalf("Issue: %v", err)
|
||||
}
|
||||
if key == nil {
|
||||
t.Fatal("expected non-nil key")
|
||||
}
|
||||
if cert.IsCA {
|
||||
t.Fatal("leaf cert should not be CA")
|
||||
}
|
||||
if cert.Subject.CommonName != "example.test" {
|
||||
t.Fatalf("got CN %q, want %q", cert.Subject.CommonName, "example.test")
|
||||
}
|
||||
|
||||
// Verify the leaf cert chains to the CA.
|
||||
_, err = cert.Verify(x509.VerifyOptions{
|
||||
Roots: ca.CertPool(),
|
||||
DNSName: "example.test",
|
||||
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("verify leaf cert: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueServerTLS(t *testing.T) {
|
||||
ca, err := NewTestCA()
|
||||
if err != nil {
|
||||
t.Fatalf("NewTestCA: %v", err)
|
||||
}
|
||||
|
||||
key, cert, err := ca.IssueServer()
|
||||
if err != nil {
|
||||
t.Fatalf("IssueServer: %v", err)
|
||||
}
|
||||
|
||||
// Start a TLS server with the issued cert.
|
||||
serverCfg := ca.ServerTLSConfig(key, cert)
|
||||
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
srv.TLS = &serverCfg
|
||||
srv.StartTLS()
|
||||
defer srv.Close()
|
||||
|
||||
// Create a client that verifies the server cert against the CA.
|
||||
clientCfg := ca.TLSConfig()
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &clientCfg,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := client.Get(srv.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("GET: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("got status %d, want 200", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTLSConfigReturnsByValue(t *testing.T) {
|
||||
ca, err := NewTestCA()
|
||||
if err != nil {
|
||||
t.Fatalf("NewTestCA: %v", err)
|
||||
}
|
||||
|
||||
cfg1 := ca.TLSConfig()
|
||||
cfg2 := ca.TLSConfig()
|
||||
|
||||
// Modifying one should not affect the other.
|
||||
cfg1.ServerName = "modified"
|
||||
if cfg2.ServerName == "modified" {
|
||||
t.Fatal("TLSConfig should return independent values")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTLSConfigEnforcesTLS13(t *testing.T) {
|
||||
ca, err := NewTestCA()
|
||||
if err != nil {
|
||||
t.Fatalf("NewTestCA: %v", err)
|
||||
}
|
||||
|
||||
cfg := ca.TLSConfig()
|
||||
if cfg.MinVersion != tls.VersionTLS13 {
|
||||
t.Fatalf("got MinVersion %d, want TLS 1.3 (%d)", cfg.MinVersion, tls.VersionTLS13)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMustTestCA(t *testing.T) {
|
||||
// Should not panic.
|
||||
ca := MustTestCA()
|
||||
if ca.Certificate() == nil {
|
||||
t.Fatal("expected non-nil certificate")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrivateKeyPEM(t *testing.T) {
|
||||
ca, err := NewTestCA()
|
||||
if err != nil {
|
||||
t.Fatalf("NewTestCA: %v", err)
|
||||
}
|
||||
|
||||
key, _, err := ca.IssueServer()
|
||||
if err != nil {
|
||||
t.Fatalf("IssueServer: %v", err)
|
||||
}
|
||||
|
||||
pemBytes, err := PrivateKeyPEM(key)
|
||||
if err != nil {
|
||||
t.Fatalf("PrivateKeyPEM: %v", err)
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(pemBytes)
|
||||
if block == nil {
|
||||
t.Fatal("failed to decode PEM")
|
||||
}
|
||||
if block.Type != "PRIVATE KEY" {
|
||||
t.Fatalf("got PEM type %q, want PRIVATE KEY", block.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUntrustedCAFails(t *testing.T) {
|
||||
ca1 := MustTestCA()
|
||||
ca2 := MustTestCA()
|
||||
|
||||
// Issue a cert from ca1, try to verify against ca2's pool.
|
||||
_, cert, err := ca1.IssueServer()
|
||||
if err != nil {
|
||||
t.Fatalf("IssueServer: %v", err)
|
||||
}
|
||||
|
||||
_, err = cert.Verify(x509.VerifyOptions{
|
||||
Roots: ca2.CertPool(),
|
||||
DNSName: "localhost",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected verification to fail with wrong CA")
|
||||
}
|
||||
}
|
||||
@@ -165,6 +165,28 @@ func certPublic(cert *x509.Certificate) string {
|
||||
}
|
||||
}
|
||||
|
||||
func csrPublic(csr *x509.CertificateRequest) string {
|
||||
switch pub := csr.PublicKey.(type) {
|
||||
case *rsa.PublicKey:
|
||||
return fmt.Sprintf("RSA-%d", pub.N.BitLen())
|
||||
case *ecdsa.PublicKey:
|
||||
switch pub.Curve {
|
||||
case elliptic.P256():
|
||||
return "ECDSA-prime256v1"
|
||||
case elliptic.P384():
|
||||
return "ECDSA-secp384r1"
|
||||
case elliptic.P521():
|
||||
return "ECDSA-secp521r1"
|
||||
default:
|
||||
return "ECDSA (unknown curve)"
|
||||
}
|
||||
case *dsa.PublicKey:
|
||||
return "DSA"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func DisplayName(name pkix.Name) string {
|
||||
var ns []string
|
||||
|
||||
@@ -333,3 +355,36 @@ func DisplayCert(w io.Writer, cert *x509.Certificate, showHash bool) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func DisplayCSR(w io.Writer, csr *x509.CertificateRequest, showHash bool) {
|
||||
fmt.Fprintln(w, "CERTIFICATE REQUEST")
|
||||
if showHash {
|
||||
fmt.Fprintln(w, wrap(fmt.Sprintf("SHA256: %x", sha256.Sum256(csr.Raw)), 0))
|
||||
}
|
||||
|
||||
fmt.Fprintln(w, wrap("Subject: "+DisplayName(csr.Subject), 0))
|
||||
fmt.Fprintf(w, "\tSignature algorithm: %s / %s\n", sigAlgoPK(csr.SignatureAlgorithm),
|
||||
sigAlgoHash(csr.SignatureAlgorithm))
|
||||
fmt.Fprintln(w, "Details:")
|
||||
wrapPrint("Public key: "+csrPublic(csr), 1)
|
||||
|
||||
validNames := make([]string, 0, len(csr.DNSNames)+len(csr.EmailAddresses)+len(csr.IPAddresses)+len(csr.URIs))
|
||||
for i := range csr.DNSNames {
|
||||
validNames = append(validNames, "dns:"+csr.DNSNames[i])
|
||||
}
|
||||
|
||||
for i := range csr.EmailAddresses {
|
||||
validNames = append(validNames, "email:"+csr.EmailAddresses[i])
|
||||
}
|
||||
|
||||
for i := range csr.IPAddresses {
|
||||
validNames = append(validNames, "ip:"+csr.IPAddresses[i].String())
|
||||
}
|
||||
|
||||
for i := range csr.URIs {
|
||||
validNames = append(validNames, "uri:"+csr.URIs[i].String())
|
||||
}
|
||||
|
||||
sans := fmt.Sprintf("SANs (%d): %s\n", len(validNames), strings.Join(validNames, ", "))
|
||||
wrapPrint(sans, 1)
|
||||
}
|
||||
|
||||
@@ -133,3 +133,48 @@ func MatchKeys(cert *x509.Certificate, priv crypto.Signer) (bool, string) {
|
||||
return false, fmt.Sprintf("unrecognised private key type: %T", priv.Public())
|
||||
}
|
||||
}
|
||||
|
||||
// MatchKeysCSR determines whether the CSR's public key matches the given private key.
|
||||
// It returns true if they match; otherwise, it returns false and a human-friendly reason.
|
||||
func MatchKeysCSR(csr *x509.CertificateRequest, priv crypto.Signer) (bool, string) {
|
||||
switch keyPub := priv.Public().(type) {
|
||||
case *rsa.PublicKey:
|
||||
switch csrPub := csr.PublicKey.(type) {
|
||||
case *rsa.PublicKey:
|
||||
if matchRSA(csrPub, keyPub) {
|
||||
return true, ""
|
||||
}
|
||||
return false, "public keys don't match"
|
||||
case *ecdsa.PublicKey:
|
||||
return false, "RSA private key, EC public key"
|
||||
default:
|
||||
return false, fmt.Sprintf("unsupported CSR public key type: %T", csr.PublicKey)
|
||||
}
|
||||
case *ecdsa.PublicKey:
|
||||
switch csrPub := csr.PublicKey.(type) {
|
||||
case *ecdsa.PublicKey:
|
||||
if matchECDSA(csrPub, keyPub) {
|
||||
return true, ""
|
||||
}
|
||||
// Determine a more precise reason
|
||||
kc := getECCurve(keyPub)
|
||||
cc := getECCurve(csrPub)
|
||||
if kc == curveInvalid {
|
||||
return false, "invalid private key curve"
|
||||
}
|
||||
if cc == curveRSA {
|
||||
return false, "private key is EC, CSR is RSA"
|
||||
}
|
||||
if kc != cc {
|
||||
return false, "EC curves don't match"
|
||||
}
|
||||
return false, "public keys don't match"
|
||||
case *rsa.PublicKey:
|
||||
return false, "private key is EC, CSR is RSA"
|
||||
default:
|
||||
return false, fmt.Sprintf("unsupported CSR public key type: %T", csr.PublicKey)
|
||||
}
|
||||
default:
|
||||
return false, fmt.Sprintf("unrecognised private key type: %T", priv.Public())
|
||||
}
|
||||
}
|
||||
|
||||
46
cmd/ccfind/main.go
Normal file
46
cmd/ccfind/main.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package main
|
||||
|
||||
// Prompt:
|
||||
// The current main.go should accept a list of paths to search. In each
|
||||
// of those paths, without recursing, it should find all files ending in
|
||||
// C/C++ source extensions and print them one per line.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var extensions = []string{
|
||||
".c", ".cpp", ".cc", ".cxx",
|
||||
".h", ".hpp", ".hh", ".hxx",
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Fprintf(os.Stderr, "Usage: %s <path> [path...]\n", os.Args[0])
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
for _, path := range os.Args[1:] {
|
||||
entries, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error reading %s: %v\n", path, err)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
name := entry.Name()
|
||||
ext := filepath.Ext(name)
|
||||
if slices.Contains(extensions, strings.ToLower(ext)) {
|
||||
fmt.Println(filepath.Join(path, name))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user