@@ -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+ }
0 commit comments