From 5dbb46c3eefae16bf9f67c85824fda33f10309bf Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Tue, 17 Mar 2026 08:49:28 -0700 Subject: [PATCH] Add AIA fields (OCSPServer, IssuingCertificateURL) to certgen.Profile The Profile struct now supports optional OCSPServer and IssuingCertificateURL fields. When populated, these are set on the x509.Certificate template as Authority Information Access extensions before signing. Empty slices are omitted. Co-Authored-By: Claude Opus 4.6 (1M context) --- certlib/certgen/config.go | 19 ++++++-- certlib/certgen/config_test.go | 84 ++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 5 deletions(-) diff --git a/certlib/certgen/config.go b/certlib/certgen/config.go index 2ba6552..edde061 100644 --- a/certlib/certgen/config.go +++ b/certlib/certgen/config.go @@ -131,11 +131,13 @@ func (cs CertificateRequest) Generate() (crypto.PrivateKey, *x509.CertificateReq } type Profile struct { - IsCA bool `yaml:"is_ca"` - PathLen int `yaml:"path_len"` - KeyUse []string `yaml:"key_uses"` - ExtKeyUsages []string `yaml:"ext_key_usages"` - Expiry string `yaml:"expiry"` + IsCA bool `yaml:"is_ca"` + PathLen int `yaml:"path_len"` + 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) { @@ -181,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 } diff --git a/certlib/certgen/config_test.go b/certlib/certgen/config_test.go index 745b5eb..a5a9c1e 100644 --- a/certlib/certgen/config_test.go +++ b/certlib/certgen/config_test.go @@ -81,6 +81,90 @@ func TestRequestFQDNNotDuplicated(t *testing.T) { } } +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},