Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ kubectl coco initdata dump | kubectl coco initdata validate
Validation checks:
- `version` is `0.1.0` and `algorithm` is one of `sha256`, `sha384`, `sha512`
- Required keys `aa.toml` and `cdh.toml` are present (`policy.rego` is optional)
- Embedded certificates pass rustls rules: CA certs must have `keyCertSign`; leaf certs must have a SubjectAltName and `extendedKeyUsage=serverAuth` and must not be self-signed
- Embedded certificates must be CA certificates (`CA:TRUE`, `keyCertSign`); rejected: leaf/non-CA certs, expired or not-yet-valid certs, SHA-1 or MD5 signatures, unknown critical extensions, RSA keys shorter than 1024 bits
- All `aa.toml` token config URLs are consistent with `cdh.toml` kbc URL (a warning is printed if any differ)

### Transform and Apply Manifests
Expand Down
98 changes: 38 additions & 60 deletions cmd/initdata/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ package initdata
import (
"bytes"
"compress/gzip"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"strconv"
"encoding/pem"
"fmt"
"io"
Expand Down Expand Up @@ -101,20 +103,46 @@ func checkExpiry(cert *x509.Certificate) error {
return nil
}

// validateCACert checks that cert is a valid CA certificate: IsCA must be true
// and KeyUsageCertSign must be set. The IsCA guard is intentional — this
// function may be called directly (e.g. from validateCACerts) without the
// IsCA pre-filter that validateCerts applies.
// isWeakSignatureAlg reports whether alg is SHA-1, MD5, or MD2 — all rejected
// by rustls as insufficiently secure.
func isWeakSignatureAlg(alg x509.SignatureAlgorithm) bool {
switch alg {
case x509.MD2WithRSA, x509.MD5WithRSA, x509.SHA1WithRSA, x509.DSAWithSHA1, x509.ECDSAWithSHA1:
return true
}
return false
}

// validateCACert checks that cert is a valid CA certificate: IsCA must be true,
// KeyUsageCertSign must be set, and the cert must not use weak crypto.
func validateCACert(cert *x509.Certificate) error {
if !cert.IsCA {
return fmt.Errorf("certificate %q: IsCA is false", cert.Subject.CommonName)
return fmt.Errorf("certificate %q: not a CA certificate (CA:TRUE required for trust anchors)", cert.Subject.CommonName)
}
if err := checkExpiry(cert); err != nil {
return err
}
if cert.KeyUsage&x509.KeyUsageCertSign == 0 {
return fmt.Errorf("certificate %q: missing KeyUsageCertSign", cert.Subject.CommonName)
}
if isWeakSignatureAlg(cert.SignatureAlgorithm) {
return fmt.Errorf("certificate %q: weak signature algorithm %s", cert.Subject.CommonName, cert.SignatureAlgorithm)
}
if rsaKey, ok := cert.PublicKey.(*rsa.PublicKey); ok && rsaKey.N.BitLen() < 1024 {
return fmt.Errorf("certificate %q: RSA key is %d bits, minimum is 1024", cert.Subject.CommonName, rsaKey.N.BitLen())
}
if len(cert.UnhandledCriticalExtensions) > 0 {
oidStrs := make([]string, len(cert.UnhandledCriticalExtensions))
for i, oid := range cert.UnhandledCriticalExtensions {
parts := make([]string, len(oid))
for j, n := range oid {
parts[j] = strconv.Itoa(n)
}
oidStrs[i] = strings.Join(parts, ".")
}
return fmt.Errorf("certificate %q: has unknown critical extensions: %s",
cert.Subject.CommonName, strings.Join(oidStrs, ", "))
}
Comment on lines +128 to +145
return nil
}

Expand All @@ -134,64 +162,14 @@ func validateCACerts(certs []*x509.Certificate) error {
return nil
}

// validateLeafCert checks rustls rules for end-entity (non-CA) certificates:
// must not be self-signed, must carry a SubjectAltName, and must have
// extendedKeyUsage serverAuth.
func validateLeafCert(cert *x509.Certificate) error {
if err := checkExpiry(cert); err != nil {
return err
}
// rustls rejects self-signed end-entity certificates
if isSelfSigned(cert) {
return fmt.Errorf("certificate %q: self-signed certificate cannot be used as a leaf cert", cert.Subject.CommonName)
}
hasSAN := len(cert.DNSNames) > 0 || len(cert.IPAddresses) > 0 ||
len(cert.URIs) > 0 || len(cert.EmailAddresses) > 0
if !hasSAN {
return fmt.Errorf("certificate %q: missing SubjectAltName extension", cert.Subject.CommonName)
}
for _, eku := range cert.ExtKeyUsage {
if eku == x509.ExtKeyUsageServerAuth {
return nil
}
}
return fmt.Errorf("certificate %q: missing extendedKeyUsage serverAuth", cert.Subject.CommonName)
}

func validateCerts(certs []*x509.Certificate) error {
var errs []string
for _, cert := range certs {
var err error
if cert.IsCA {
err = validateCACert(cert)
} else {
err = validateLeafCert(cert)
}
if err != nil {
errs = append(errs, err.Error())
}
}
if len(errs) > 0 {
return fmt.Errorf("cert validation failed:\n %s", strings.Join(errs, "\n "))
}
return nil
}

// validateCertsBySource applies rustls rules to each certificate and includes
// the source field in error messages so the user can locate the problematic cert.
// All initdata cert fields accept any cert valid under rustls rules:
// CA certs require keyCertSign; leaf certs require SAN + serverAuth EKU and
// must not be self-signed.
// validateCertsBySource applies CA certificate rules to each embedded cert and
// includes the source field in error messages so the user can locate the
// problematic cert. All initdata cert fields are trust anchor positions —
// only CA certificates (CA:TRUE, keyCertSign) are accepted.
func validateCertsBySource(entries []certEntry) error {
var errs []string
for _, e := range entries {
var err error
if e.cert.IsCA {
err = validateCACert(e.cert)
} else {
err = validateLeafCert(e.cert)
}
if err != nil {
if err := validateCACert(e.cert); err != nil {
errs = append(errs, fmt.Sprintf("%s: %s", e.source, err.Error()))
}
}
Expand Down
149 changes: 46 additions & 103 deletions cmd/initdata/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/base64"
"encoding/pem"
"math/big"
Expand Down Expand Up @@ -67,31 +68,6 @@ func makeTestLeafCert(t *testing.T, caCert *x509.Certificate, caKey *rsa.Private
return cert, pemBytes
}

// makeValidLeafCert creates a leaf cert that passes rustls rules (SAN + serverAuth EKU).
func makeValidLeafCert(t *testing.T, caCert *x509.Certificate, caKey *rsa.PrivateKey) *x509.Certificate {
t.Helper()
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf("generate key: %v", err)
}
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(3),
Subject: pkix.Name{CommonName: "Test Valid Leaf"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
IsCA: false,
BasicConstraintsValid: true,
DNSNames: []string{"test.example.com"},
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}
der, err := x509.CreateCertificate(rand.Reader, tmpl, caCert, &key.PublicKey, caKey)
if err != nil {
t.Fatalf("create cert: %v", err)
}
cert, _ := x509.ParseCertificate(der)
return cert
}

func writeTempPEM(t *testing.T, dir, name string, pemData []byte) string {
t.Helper()
path := filepath.Join(dir, name)
Expand Down Expand Up @@ -188,8 +164,9 @@ func TestValidateCACert_ValidCA(t *testing.T) {
func TestValidateCACert_LeafCert(t *testing.T) {
caCert, caKey, _ := makeTestCACert(t)
leaf, _ := makeTestLeafCert(t, caCert, caKey)
if err := validateCACert(leaf); err == nil {
t.Error("validateCACert() should reject leaf cert")
err := validateCACert(leaf)
if err == nil || !strings.Contains(err.Error(), "not a CA certificate") {
t.Errorf("validateCACert() should reject leaf cert with 'not a CA certificate', got: %v", err)
}
}

Expand Down Expand Up @@ -265,108 +242,74 @@ func TestIsSelfSigned_SelfIssuedNotSelfSigned(t *testing.T) {
}
}

func TestValidateLeafCert_Expired(t *testing.T) {
caCert, caKey, _ := makeTestCACert(t)
key, _ := rsa.GenerateKey(rand.Reader, 2048)
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(10), Subject: pkix.Name{CommonName: "Expired Leaf"},
NotBefore: time.Now().Add(-48 * time.Hour), NotAfter: time.Now().Add(-time.Hour),
IsCA: false, BasicConstraintsValid: true,
DNSNames: []string{"kbs.example.com"},
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}
der, _ := x509.CreateCertificate(rand.Reader, tmpl, caCert, &key.PublicKey, caKey)
leaf, _ := x509.ParseCertificate(der)
err := validateLeafCert(leaf)
if err == nil || !strings.Contains(err.Error(), "expired") {
t.Errorf("expected expired error, got: %v", err)
}
}

func TestValidateLeafCert_Valid(t *testing.T) {
caCert, caKey, _ := makeTestCACert(t)
leaf := makeValidLeafCert(t, caCert, caKey)
if err := validateLeafCert(leaf); err != nil {
t.Errorf("validateLeafCert() unexpected error for valid leaf: %v", err)
func TestValidateCACert_SHA1Rejected(t *testing.T) {
cert, _, _ := makeTestCACert(t)
cert.SignatureAlgorithm = x509.SHA1WithRSA
err := validateCACert(cert)
if err == nil || !strings.Contains(err.Error(), "weak signature") {
t.Errorf("expected weak signature error for SHA-1, got: %v", err)
}
}

func TestValidateLeafCert_NoSAN(t *testing.T) {
caCert, caKey, _ := makeTestCACert(t)
leaf, _ := makeTestLeafCert(t, caCert, caKey) // no SAN, no EKU
err := validateLeafCert(leaf)
if err == nil || !strings.Contains(err.Error(), "SubjectAltName") {
t.Errorf("expected SubjectAltName error, got: %v", err)
func TestValidateCACert_MD5Rejected(t *testing.T) {
cert, _, _ := makeTestCACert(t)
cert.SignatureAlgorithm = x509.MD5WithRSA
err := validateCACert(cert)
if err == nil || !strings.Contains(err.Error(), "weak signature") {
t.Errorf("expected weak signature error for MD5, got: %v", err)
}
}

func TestValidateLeafCert_NoServerAuth(t *testing.T) {
caCert, caKey, _ := makeTestCACert(t)
key, _ := rsa.GenerateKey(rand.Reader, 2048)
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(4),
Subject: pkix.Name{CommonName: "No EKU Leaf"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
IsCA: false,
BasicConstraintsValid: true,
DNSNames: []string{"test.example.com"},
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
}
der, _ := x509.CreateCertificate(rand.Reader, tmpl, caCert, &key.PublicKey, caKey)
leaf, _ := x509.ParseCertificate(der)
err := validateLeafCert(leaf)
if err == nil || !strings.Contains(err.Error(), "serverAuth") {
t.Errorf("expected serverAuth error, got: %v", err)
func TestValidateCACert_WeakRSAKeyRejected(t *testing.T) {
// Construct a cert struct directly with a 512-bit modulus so we don't need
// to generate a real small key (which newer Go versions may reject).
n := new(big.Int).SetBit(new(big.Int), 511, 1) // 512-bit number
cert, _, _ := makeTestCACert(t)
cert.PublicKey = &rsa.PublicKey{N: n, E: 65537}
err := validateCACert(cert)
if err == nil || !strings.Contains(err.Error(), "1024") {
t.Errorf("expected RSA key size error, got: %v", err)
}
}

func TestValidateLeafCert_SelfSigned(t *testing.T) {
key, _ := rsa.GenerateKey(rand.Reader, 2048)
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(5),
Subject: pkix.Name{CommonName: "Self-Signed Leaf"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
IsCA: false,
BasicConstraintsValid: true,
DNSNames: []string{"test.example.com"},
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
func TestValidateCACert_UnknownCriticalExtensionsRejected(t *testing.T) {
cert, _, _ := makeTestCACert(t)
cert.UnhandledCriticalExtensions = []asn1.ObjectIdentifier{{1, 2, 3, 4}}
err := validateCACert(cert)
if err == nil || !strings.Contains(err.Error(), "unknown critical extensions") {
t.Errorf("expected unknown critical extensions error, got: %v", err)
}
// self-signed: parent == self
der, _ := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
leaf, _ := x509.ParseCertificate(der)
err := validateLeafCert(leaf)
if err == nil || !strings.Contains(err.Error(), "self-signed") {
t.Errorf("expected self-signed error, got: %v", err)
if err != nil && !strings.Contains(err.Error(), "1.2.3.4") {
t.Errorf("error should include OID in dot notation, got: %v", err)
}
}

func TestValidateCerts_ValidLeafAccepted(t *testing.T) {
func TestValidateCACerts_LeafRejected(t *testing.T) {
caCert, caKey, _ := makeTestCACert(t)
leaf := makeValidLeafCert(t, caCert, caKey)
if err := validateCerts([]*x509.Certificate{caCert, leaf}); err != nil {
t.Errorf("validateCerts() should accept CA + valid leaf: %v", err)
leaf, _ := makeTestLeafCert(t, caCert, caKey)
err := validateCACerts([]*x509.Certificate{caCert, leaf})
if err == nil || !strings.Contains(err.Error(), "not a CA certificate") {
t.Errorf("validateCACerts() should reject non-CA cert, got: %v", err)
}
}

func TestValidateCerts_AllValid(t *testing.T) {
func TestValidateCACerts_AllValid(t *testing.T) {
cert1, _, _ := makeTestCACert(t)
cert2, _, _ := makeTestCACert(t)
if err := validateCerts([]*x509.Certificate{cert1, cert2}); err != nil {
t.Errorf("validateCerts() unexpected error: %v", err)
if err := validateCACerts([]*x509.Certificate{cert1, cert2}); err != nil {
t.Errorf("validateCACerts() unexpected error: %v", err)
}
}

func TestValidateCerts_OneInvalid(t *testing.T) {
func TestValidateCACerts_OneInvalid(t *testing.T) {
caCert, caKey, _ := makeTestCACert(t)
leaf, _ := makeTestLeafCert(t, caCert, caKey)
err := validateCerts([]*x509.Certificate{caCert, leaf})
err := validateCACerts([]*x509.Certificate{caCert, leaf})
if err == nil {
t.Fatal("validateCerts() should fail with one leaf cert")
t.Fatal("validateCACerts() should fail with one non-CA cert")
}
if !strings.Contains(err.Error(), "Test Leaf") {
t.Errorf("error should name the failing cert, got: %v", err)
if !strings.Contains(err.Error(), "not a CA certificate") {
t.Errorf("error should identify non-CA cert, got: %v", err)
}
}

Expand Down
Loading
Loading