Skip to content

Commit 8994bdd

Browse files
committed
fix: prevent CSR extensions from overriding CA-managed key usage in copy_extensions
When copy_extensions is enabled, CSR-provided X.509v3 extensions are copied into the certificate template's ExtraExtensions. Go's x509.CreateCertificate gives ExtraExtensions precedence over struct fields for the same OID, which means an attacker-crafted CSR could inject KeyUsage, ExtKeyUsage, and other security-critical extensions that silently override the signing profile's intended restrictions. Add a blocklist of CA-managed extension OIDs (KeyUsage, ExtKeyUsage, BasicConstraints, SKI, AKI, SAN, Name Constraints, CRL Distribution Points, Authority Info Access, Certificate Policies, Issuer Alt Name) that are never copied from CSR extensions. These extensions are authoritatively set by the CA's signing profile via FillTemplate and must not be overridable by a CSR submitter. Non-standard/private-use extensions continue to be copied through when copy_extensions is enabled.
1 parent b898d2f commit 8994bdd

3 files changed

Lines changed: 387 additions & 1 deletion

File tree

signer/local/local_test.go

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1636,3 +1636,307 @@ func TestLint(t *testing.T) {
16361636
})
16371637
}
16381638
}
1639+
1640+
// createCSRWithExtensions generates a CSR containing the given raw X.509v3
1641+
// extensions. This simulates an attacker injecting extensions into a CSR.
1642+
func createCSRWithExtensions(t *testing.T, extraExts []pkix.Extension) []byte {
1643+
t.Helper()
1644+
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
1645+
if err != nil {
1646+
t.Fatalf("generating key: %v", err)
1647+
}
1648+
1649+
tmpl := &x509.CertificateRequest{
1650+
Subject: pkix.Name{CommonName: "test.example.com"},
1651+
DNSNames: []string{"test.example.com"},
1652+
ExtraExtensions: extraExts,
1653+
}
1654+
1655+
csrDER, err := x509.CreateCertificateRequest(rand.Reader, tmpl, key)
1656+
if err != nil {
1657+
t.Fatalf("creating CSR: %v", err)
1658+
}
1659+
1660+
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrDER})
1661+
}
1662+
1663+
// marshalKeyUsage encodes a KeyUsage bitstring into the DER value expected
1664+
// inside an X.509v3 KeyUsage extension (OID 2.5.29.15).
1665+
func marshalKeyUsage(t *testing.T, ku x509.KeyUsage) []byte {
1666+
t.Helper()
1667+
// KeyUsage is a bit string where bits are numbered from the MSB.
1668+
// Go's x509.KeyUsage constants are already bit positions from MSB.
1669+
var b [2]byte
1670+
b[0] = byte(ku >> 8) // not used for standard usages below bit 8
1671+
// Standard key usages fit in one byte for the common ones:
1672+
// digitalSignature(0), contentCommitment(1), keyEncipherment(2),
1673+
// dataEncipherment(3), keyAgreement(4), keyCertSign(5), cRLSign(6),
1674+
// encipherOnly(7)
1675+
b[0] = 0
1676+
b[1] = 0
1677+
if ku&x509.KeyUsageDigitalSignature != 0 {
1678+
b[0] |= 0x80
1679+
}
1680+
if ku&x509.KeyUsageContentCommitment != 0 {
1681+
b[0] |= 0x40
1682+
}
1683+
if ku&x509.KeyUsageKeyEncipherment != 0 {
1684+
b[0] |= 0x20
1685+
}
1686+
if ku&x509.KeyUsageDataEncipherment != 0 {
1687+
b[0] |= 0x10
1688+
}
1689+
if ku&x509.KeyUsageKeyAgreement != 0 {
1690+
b[0] |= 0x08
1691+
}
1692+
if ku&x509.KeyUsageCertSign != 0 {
1693+
b[0] |= 0x04
1694+
}
1695+
if ku&x509.KeyUsageCRLSign != 0 {
1696+
b[0] |= 0x02
1697+
}
1698+
if ku&x509.KeyUsageEncipherOnly != 0 {
1699+
b[0] |= 0x01
1700+
}
1701+
if ku&x509.KeyUsageDecipherOnly != 0 {
1702+
b[1] |= 0x80
1703+
}
1704+
1705+
// Determine padding bits: find the lowest set bit
1706+
var usedBytes int
1707+
var padBits int
1708+
if b[1] != 0 {
1709+
usedBytes = 2
1710+
for i := 0; i < 8; i++ {
1711+
if b[1]&(1<<uint(i)) != 0 {
1712+
padBits = i
1713+
break
1714+
}
1715+
}
1716+
} else {
1717+
usedBytes = 1
1718+
for i := 0; i < 8; i++ {
1719+
if b[0]&(1<<uint(i)) != 0 {
1720+
padBits = i
1721+
break
1722+
}
1723+
}
1724+
}
1725+
1726+
bs := asn1.BitString{
1727+
Bytes: b[:usedBytes],
1728+
BitLength: usedBytes*8 - padBits,
1729+
}
1730+
val, err := asn1.Marshal(bs)
1731+
if err != nil {
1732+
t.Fatalf("marshaling KeyUsage: %v", err)
1733+
}
1734+
return val
1735+
}
1736+
1737+
// marshalExtKeyUsage encodes ExtKeyUsage OIDs into the DER value expected
1738+
// inside an X.509v3 ExtKeyUsage extension (OID 2.5.29.37).
1739+
func marshalExtKeyUsage(t *testing.T, ekus []asn1.ObjectIdentifier) []byte {
1740+
t.Helper()
1741+
val, err := asn1.Marshal(ekus)
1742+
if err != nil {
1743+
t.Fatalf("marshaling ExtKeyUsage: %v", err)
1744+
}
1745+
return val
1746+
}
1747+
1748+
// TestCopyExtensionsDoesNotOverrideKeyUsage verifies that when
1749+
// copy_extensions is enabled, a CSR containing injected KeyUsage and
1750+
// ExtKeyUsage extensions cannot override the signing profile's intended
1751+
// key usage values. This is a regression test for a vulnerability where
1752+
// Go's x509.CreateCertificate gave ExtraExtensions precedence over
1753+
// struct fields, allowing CSR-provided KU/EKU to bypass profile restrictions.
1754+
func TestCopyExtensionsDoesNotOverrideKeyUsage(t *testing.T) {
1755+
// Profile allows only: digitalSignature + serverAuth
1756+
profilePolicy := &config.Signing{
1757+
Default: &config.SigningProfile{
1758+
Usage: []string{"digital signature", "server auth"},
1759+
Expiry: 1 * time.Hour,
1760+
ExpiryString: "1h",
1761+
CopyExtensions: true,
1762+
},
1763+
}
1764+
1765+
s, err := NewSignerFromFile(testCaFile, testCaKeyFile, profilePolicy)
1766+
if err != nil {
1767+
t.Fatalf("creating signer: %v", err)
1768+
}
1769+
1770+
// Attacker CSR injects keyCertSign + cRLSign and codeSigning + emailProtection
1771+
attackerKU := x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageCRLSign
1772+
attackerEKUs := []asn1.ObjectIdentifier{
1773+
{1, 3, 6, 1, 5, 5, 7, 3, 3}, // codeSigning
1774+
{1, 3, 6, 1, 5, 5, 7, 3, 4}, // emailProtection
1775+
}
1776+
1777+
csrPEM := createCSRWithExtensions(t, []pkix.Extension{
1778+
{
1779+
Id: asn1.ObjectIdentifier{2, 5, 29, 15}, // KeyUsage
1780+
Critical: true,
1781+
Value: marshalKeyUsage(t, attackerKU),
1782+
},
1783+
{
1784+
Id: asn1.ObjectIdentifier{2, 5, 29, 37}, // ExtKeyUsage
1785+
Critical: false,
1786+
Value: marshalExtKeyUsage(t, attackerEKUs),
1787+
},
1788+
})
1789+
1790+
certPEM, err := s.Sign(signer.SignRequest{
1791+
Hosts: []string{"test.example.com"},
1792+
Request: string(csrPEM),
1793+
})
1794+
if err != nil {
1795+
t.Fatalf("signing: %v", err)
1796+
}
1797+
1798+
cert, err := helpers.ParseCertificatePEM(certPEM)
1799+
if err != nil {
1800+
t.Fatalf("parsing signed cert: %v", err)
1801+
}
1802+
1803+
// The cert MUST have only the profile's KeyUsage (digitalSignature),
1804+
// NOT the attacker's keyCertSign or cRLSign.
1805+
if cert.KeyUsage&x509.KeyUsageCertSign != 0 {
1806+
t.Errorf("certificate has keyCertSign — attacker's KeyUsage was not filtered")
1807+
}
1808+
if cert.KeyUsage&x509.KeyUsageCRLSign != 0 {
1809+
t.Errorf("certificate has cRLSign — attacker's KeyUsage was not filtered")
1810+
}
1811+
if cert.KeyUsage&x509.KeyUsageDigitalSignature == 0 {
1812+
t.Errorf("certificate missing digitalSignature from profile")
1813+
}
1814+
1815+
// The cert MUST have only the profile's EKU (serverAuth),
1816+
// NOT the attacker's codeSigning or emailProtection.
1817+
for _, eku := range cert.ExtKeyUsage {
1818+
switch eku {
1819+
case x509.ExtKeyUsageCodeSigning:
1820+
t.Errorf("certificate has codeSigning EKU — attacker's ExtKeyUsage was not filtered")
1821+
case x509.ExtKeyUsageEmailProtection:
1822+
t.Errorf("certificate has emailProtection EKU — attacker's ExtKeyUsage was not filtered")
1823+
}
1824+
}
1825+
1826+
foundServerAuth := false
1827+
for _, eku := range cert.ExtKeyUsage {
1828+
if eku == x509.ExtKeyUsageServerAuth {
1829+
foundServerAuth = true
1830+
break
1831+
}
1832+
}
1833+
if !foundServerAuth {
1834+
t.Errorf("certificate missing serverAuth EKU from profile")
1835+
}
1836+
}
1837+
1838+
// TestCopyExtensionsAllowsNonManagedOIDs verifies that CopyExtensions
1839+
// still copies through non-security-critical, non-CA-managed extensions
1840+
// (e.g., private-use OIDs). This ensures the blocklist does not break
1841+
// legitimate use cases for copy_extensions.
1842+
func TestCopyExtensionsAllowsNonManagedOIDs(t *testing.T) {
1843+
profilePolicy := &config.Signing{
1844+
Default: &config.SigningProfile{
1845+
Usage: []string{"digital signature", "server auth"},
1846+
Expiry: 1 * time.Hour,
1847+
ExpiryString: "1h",
1848+
CopyExtensions: true,
1849+
},
1850+
}
1851+
1852+
s, err := NewSignerFromFile(testCaFile, testCaKeyFile, profilePolicy)
1853+
if err != nil {
1854+
t.Fatalf("creating signer: %v", err)
1855+
}
1856+
1857+
// A private-use extension that should be copied through
1858+
customOID := asn1.ObjectIdentifier{1, 2, 3, 4, 5, 6, 7}
1859+
customValue, _ := asn1.Marshal("custom-extension-value")
1860+
1861+
csrPEM := createCSRWithExtensions(t, []pkix.Extension{
1862+
{
1863+
Id: customOID,
1864+
Value: customValue,
1865+
},
1866+
})
1867+
1868+
certPEM, err := s.Sign(signer.SignRequest{
1869+
Hosts: []string{"test.example.com"},
1870+
Request: string(csrPEM),
1871+
})
1872+
if err != nil {
1873+
t.Fatalf("signing: %v", err)
1874+
}
1875+
1876+
cert, err := helpers.ParseCertificatePEM(certPEM)
1877+
if err != nil {
1878+
t.Fatalf("parsing signed cert: %v", err)
1879+
}
1880+
1881+
found := false
1882+
for _, ext := range cert.Extensions {
1883+
if ext.Id.Equal(customOID) {
1884+
found = true
1885+
if !bytes.Equal(ext.Value, customValue) {
1886+
t.Errorf("custom extension value mismatch: got %x, want %x", ext.Value, customValue)
1887+
}
1888+
break
1889+
}
1890+
}
1891+
if !found {
1892+
t.Errorf("custom extension (OID %s) was not copied into the certificate", customOID)
1893+
}
1894+
}
1895+
1896+
// TestCopyExtensionsDisabledDoesNotCopy verifies that when copy_extensions
1897+
// is false (the default), CSR extensions are NOT copied — whether they are
1898+
// CA-managed or not.
1899+
func TestCopyExtensionsDisabledDoesNotCopy(t *testing.T) {
1900+
profilePolicy := &config.Signing{
1901+
Default: &config.SigningProfile{
1902+
Usage: []string{"digital signature", "server auth"},
1903+
Expiry: 1 * time.Hour,
1904+
ExpiryString: "1h",
1905+
CopyExtensions: false,
1906+
},
1907+
}
1908+
1909+
s, err := NewSignerFromFile(testCaFile, testCaKeyFile, profilePolicy)
1910+
if err != nil {
1911+
t.Fatalf("creating signer: %v", err)
1912+
}
1913+
1914+
customOID := asn1.ObjectIdentifier{1, 2, 3, 4, 5, 6, 7}
1915+
customValue, _ := asn1.Marshal("should-not-appear")
1916+
1917+
csrPEM := createCSRWithExtensions(t, []pkix.Extension{
1918+
{
1919+
Id: customOID,
1920+
Value: customValue,
1921+
},
1922+
})
1923+
1924+
certPEM, err := s.Sign(signer.SignRequest{
1925+
Hosts: []string{"test.example.com"},
1926+
Request: string(csrPEM),
1927+
})
1928+
if err != nil {
1929+
t.Fatalf("signing: %v", err)
1930+
}
1931+
1932+
cert, err := helpers.ParseCertificatePEM(certPEM)
1933+
if err != nil {
1934+
t.Fatalf("parsing signed cert: %v", err)
1935+
}
1936+
1937+
for _, ext := range cert.Extensions {
1938+
if ext.Id.Equal(customOID) {
1939+
t.Errorf("custom extension (OID %s) should NOT be in cert when CopyExtensions is false", customOID)
1940+
}
1941+
}
1942+
}

signer/signer.go

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
cferr "github.com/cloudflare/cfssl/errors"
2424
"github.com/cloudflare/cfssl/helpers"
2525
"github.com/cloudflare/cfssl/info"
26+
"github.com/cloudflare/cfssl/log"
2627
)
2728

2829
// Subject contains the information that should be used to override the
@@ -180,6 +181,47 @@ func isCommonAttr(t []int) bool {
180181
return (len(t) == 4 && t[0] == 2 && t[1] == 5 && t[2] == 4 && (t[3] == 3 || (t[3] >= 5 && t[3] <= 11) || t[3] == 17))
181182
}
182183

184+
// caMangedExtensionOIDs is the set of X.509v3 extension OIDs whose values are
185+
// authoritatively determined by the CA's signing profile (via FillTemplate) and
186+
// must never be copied from a CSR. Allowing a CSR to supply these via
187+
// ExtraExtensions would silently override the profile because Go's
188+
// x509.CreateCertificate gives ExtraExtensions precedence over struct fields
189+
// for the same OID.
190+
//
191+
// See: https://pkg.go.dev/crypto/x509#Certificate (ExtraExtensions field).
192+
var caManagedExtensionOIDs = map[string]bool{
193+
// Key Usage (RFC 5280, 4.2.1.3) — set by profile.Usages()
194+
asn1.ObjectIdentifier{2, 5, 29, 15}.String(): true,
195+
// Extended Key Usage (RFC 5280, 4.2.1.12) — set by profile.Usages()
196+
asn1.ObjectIdentifier{2, 5, 29, 37}.String(): true,
197+
// Basic Constraints (RFC 5280, 4.2.1.9) — already handled specially above,
198+
// but included for defense-in-depth
199+
asn1.ObjectIdentifier{2, 5, 29, 19}.String(): true,
200+
// Subject Key Identifier (RFC 5280, 4.2.1.2) — computed by FillTemplate
201+
asn1.ObjectIdentifier{2, 5, 29, 14}.String(): true,
202+
// Authority Key Identifier (RFC 5280, 4.2.1.1) — set by CreateCertificate
203+
// from the issuer
204+
asn1.ObjectIdentifier{2, 5, 29, 35}.String(): true,
205+
// Authority Info Access / OCSP (RFC 5280, 4.2.2.1) — set by profile OCSP URL
206+
asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 1}.String(): true,
207+
// CRL Distribution Points (RFC 5280, 4.2.1.13) — set by profile CRL URL
208+
asn1.ObjectIdentifier{2, 5, 29, 31}.String(): true,
209+
// Certificate Policies (RFC 5280, 4.2.1.4) — set by profile Policies
210+
asn1.ObjectIdentifier{2, 5, 29, 32}.String(): true,
211+
// Name Constraints (RFC 5280, 4.2.1.10) — CA-controlled
212+
asn1.ObjectIdentifier{2, 5, 29, 30}.String(): true,
213+
// Subject Alternative Name (RFC 5280, 4.2.1.6) — handled via Hosts/SANs
214+
asn1.ObjectIdentifier{2, 5, 29, 17}.String(): true,
215+
// Issuer Alternative Name (RFC 5280, 4.2.1.7) — CA-controlled
216+
asn1.ObjectIdentifier{2, 5, 29, 18}.String(): true,
217+
}
218+
219+
// isCAManagedExtension reports whether the given OID is for an extension whose
220+
// value is authoritatively set by the CA and must not be copied from a CSR.
221+
func isCaManagedExtension(oid asn1.ObjectIdentifier) bool {
222+
return caManagedExtensionOIDs[oid.String()]
223+
}
224+
183225
// ParseCertificateRequest takes an incoming certificate request and
184226
// builds a certificate template from it.
185227
func ParseCertificateRequest(s Signer, p *config.SigningProfile, csrBytes []byte) (template *x509.Certificate, err error) {
@@ -247,8 +289,16 @@ func ParseCertificateRequest(s Signer, p *config.SigningProfile, csrBytes []byte
247289
} else if val.Id.Equal(helpers.DelegationUsage) {
248290
template.ExtraExtensions = append(template.ExtraExtensions, val)
249291
} else {
250-
// If the profile has 'copy_extensions' to true then lets add it
292+
// If the profile has 'copy_extensions' to true then copy the
293+
// extension, but never copy CA-managed extensions (KeyUsage,
294+
// ExtKeyUsage, SKI, AKI, etc.) whose values are set by the
295+
// signing profile. Allowing them through would let a CSR
296+
// override the CA's policy via ExtraExtensions precedence.
251297
if p.CopyExtensions {
298+
if isCaManagedExtension(val.Id) {
299+
log.Warningf("copy_extensions: skipping CA-managed extension OID %s from CSR", val.Id)
300+
continue
301+
}
252302
template.ExtraExtensions = append(template.ExtraExtensions, val)
253303
}
254304
}

0 commit comments

Comments
 (0)