|
32 | 32 | import java.time.Instant; |
33 | 33 | import java.time.Duration; |
34 | 34 | import java.security.cert.CertificateException; |
| 35 | +import java.security.cert.CertificateFactory; |
35 | 36 | import java.security.cert.X509Certificate; |
| 37 | +import java.io.ByteArrayInputStream; |
36 | 38 | import java.security.spec.PKCS8EncodedKeySpec; |
37 | 39 | import java.security.spec.InvalidKeySpecException; |
38 | 40 | import java.security.interfaces.RSAPrivateKey; |
@@ -897,6 +899,248 @@ public void testWolfSSLCertificateExtensionSetters() |
897 | 899 | x509.free(); |
898 | 900 | } |
899 | 901 |
|
| 902 | + /* Round trip test for setAuthorityKeyId(byte[]) and |
| 903 | + * setAuthorityKeyIdEx(WolfSSLCertificate). |
| 904 | + * |
| 905 | + * RFC 5280 requires the extension OCTET STRING to wrap a SEQUENCE { [0] |
| 906 | + * keyIdentifier OCTET STRING }. If the encoder writes the raw key-id bytes |
| 907 | + * directly into the OCTET STRING, the resulting cert is malformed and |
| 908 | + * strict parsers will reject it. |
| 909 | + * |
| 910 | + * This test signs a cert after calling each setter, runs the DER through |
| 911 | + * java.security.cert.CertificateFactory, pulls the AKID extension via |
| 912 | + * getExtensionValue("2.5.29.35"), asserts the inner bytes start with |
| 913 | + * the expected SEQUENCE { [0] keyId }, and that the embedded keyId |
| 914 | + * matches what was supplied. */ |
| 915 | + @Test |
| 916 | + public void testWolfSSLCertificateAuthorityKeyIdRoundtrip() |
| 917 | + throws WolfSSLException, WolfSSLJNIException, IOException, |
| 918 | + CertificateException { |
| 919 | + |
| 920 | + Assume.assumeTrue(WolfSSL.FileSystemEnabled()); |
| 921 | + |
| 922 | + /* Raw 20-byte key identifier passed to setAuthorityKeyId(byte[]). */ |
| 923 | + final byte[] akidRaw = new byte[] { |
| 924 | + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, |
| 925 | + 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23 |
| 926 | + }; |
| 927 | + |
| 928 | + /* SKID stamped on the issuer cert. The AKID derived by |
| 929 | + * setAuthorityKeyIdEx() must match this value exactly. */ |
| 930 | + final byte[] issuerSkid = new byte[] { |
| 931 | + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, |
| 932 | + 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x41, 0x42, 0x43 |
| 933 | + }; |
| 934 | + |
| 935 | + /* setAuthorityKeyId(byte[]) round-trip. buildSignedCertWithAkid() |
| 936 | + * returns null when the setter is NOT_COMPILED_IN at runtime |
| 937 | + * (older native wolfSSL). */ |
| 938 | + byte[] derCert = buildSignedCertWithAkid(akidRaw, null); |
| 939 | + Assume.assumeTrue("AKID setter not compiled in native wolfSSL", |
| 940 | + derCert != null); |
| 941 | + |
| 942 | + /* Behavioral probe: if the native encoder did not wrap the raw |
| 943 | + * keyId in SEQUENCE { [0] keyId } per RFC 5280 4.2.1.1, skip |
| 944 | + * rather than fail. The encoder bug lives in wolfSSL (see |
| 945 | + * wolfSSL PR #10370), not wolfssljni. We want the test here to run |
| 946 | + * for wolfSSL versions that are fixed, but not for broken versions. */ |
| 947 | + Assume.assumeTrue("Native wolfSSL AKID encoder lacks RFC 5280 " + |
| 948 | + "wrapping fix (wolfSSL PR #10370); upgrade native wolfSSL.", |
| 949 | + akidExtensionWellFormed(derCert)); |
| 950 | + |
| 951 | + assertAkidExtensionMatches(derCert, akidRaw); |
| 952 | + |
| 953 | + /* setAuthorityKeyIdEx(WolfSSLCertificate issuer) round-trip. */ |
| 954 | + WolfSSLCertificate issuerCert = |
| 955 | + new WolfSSLCertificate(caCertPem, WolfSSL.SSL_FILETYPE_PEM); |
| 956 | + try { |
| 957 | + try { |
| 958 | + issuerCert.setSubjectKeyId(issuerSkid); |
| 959 | + } catch (WolfSSLException e) { |
| 960 | + if (isNotCompiledIn(e)) { |
| 961 | + return; |
| 962 | + } |
| 963 | + throw e; |
| 964 | + } |
| 965 | + |
| 966 | + byte[] derCertEx = buildSignedCertWithAkid(null, issuerCert); |
| 967 | + if (derCertEx != null) { |
| 968 | + assertAkidExtensionMatches(derCertEx, issuerSkid); |
| 969 | + } |
| 970 | + |
| 971 | + } finally { |
| 972 | + issuerCert.free(); |
| 973 | + } |
| 974 | + } |
| 975 | + |
| 976 | + /* Behavioral probe: returns true iff the AKID extension on this DER |
| 977 | + * cert decodes as a well-formed SEQUENCE { [0] keyId } per RFC 5280 |
| 978 | + * 4.2.1.1. Returns false if the extension is missing, malformed, or |
| 979 | + * the inner content begins with anything other than the SEQUENCE tag |
| 980 | + * (which is the unfixed-encoder symptom - raw key id bytes written |
| 981 | + * directly into the OCTET STRING). Used by |
| 982 | + * testWolfSSLCertificateAuthorityKeyIdRoundtrip to skip on native |
| 983 | + * wolfSSL builds without the AKID encoder fix. |
| 984 | + * |
| 985 | + * X509Certificate.getExtensionValue() returns the DER-encoded extnValue, |
| 986 | + * an OCTET STRING wrapping the inner extension bytes: |
| 987 | + * 04 LL <inner-bytes> |
| 988 | + * For a well-formed AKID, <inner-bytes> starts with 0x30 (SEQUENCE); |
| 989 | + * for an unfixed encoder it is a raw 20-byte keyId starting with |
| 990 | + * whatever happens to be byte 0 of the keyId. */ |
| 991 | + private boolean akidExtensionWellFormed(byte[] derCert) |
| 992 | + throws CertificateException { |
| 993 | + |
| 994 | + CertificateFactory cf = CertificateFactory.getInstance("X.509"); |
| 995 | + X509Certificate jdkCert = (X509Certificate)cf.generateCertificate( |
| 996 | + new ByteArrayInputStream(derCert)); |
| 997 | + if (jdkCert == null) { |
| 998 | + return false; |
| 999 | + } |
| 1000 | + byte[] extWrapped = jdkCert.getExtensionValue("2.5.29.35"); |
| 1001 | + if (extWrapped == null || extWrapped.length < 4) { |
| 1002 | + return false; |
| 1003 | + } |
| 1004 | + /* Skip outer OCTET STRING wrapper. Short-form length only, since |
| 1005 | + * realistic AKID extensions fit comfortably in <128 bytes. */ |
| 1006 | + if ((extWrapped[0] & 0xFF) != 0x04) { |
| 1007 | + return false; |
| 1008 | + } |
| 1009 | + int outerLen = extWrapped[1] & 0xFF; |
| 1010 | + if (outerLen >= 0x80 || extWrapped.length != 2 + outerLen) { |
| 1011 | + return false; |
| 1012 | + } |
| 1013 | + /* Inner must start with SEQUENCE tag (0x30) for a well-formed |
| 1014 | + * AuthorityKeyIdentifier. */ |
| 1015 | + return (extWrapped[2] & 0xFF) == 0x30; |
| 1016 | + } |
| 1017 | + |
| 1018 | + /* Build, populate, AKID-stamp, and sign a cert. Returns the DER bytes, |
| 1019 | + * or null if the AKID setter returned NOT_COMPILED_IN at runtime (older |
| 1020 | + * native wolfSSL). Exactly one of {akidRaw, issuerForEx} must be non-null |
| 1021 | + * to select which AKID setter to exercise. */ |
| 1022 | + private byte[] buildSignedCertWithAkid(byte[] akidRaw, |
| 1023 | + WolfSSLCertificate issuerForEx) throws WolfSSLException, |
| 1024 | + WolfSSLJNIException, IOException { |
| 1025 | + |
| 1026 | + WolfSSLCertificate x509 = new WolfSSLCertificate(); |
| 1027 | + WolfSSLX509Name subjectName = null; |
| 1028 | + WolfSSLCertificate issuer = null; |
| 1029 | + |
| 1030 | + try { |
| 1031 | + Instant now = Instant.now(); |
| 1032 | + x509.setNotBefore(Date.from(now)); |
| 1033 | + x509.setNotAfter(Date.from(now.plus(Duration.ofDays(365)))); |
| 1034 | + x509.setSerialNumber(BigInteger.valueOf(0xCAFE)); |
| 1035 | + |
| 1036 | + subjectName = GenerateTestSubjectName(); |
| 1037 | + x509.setSubjectName(subjectName); |
| 1038 | + |
| 1039 | + issuer = new WolfSSLCertificate(caCertPem, |
| 1040 | + WolfSSL.SSL_FILETYPE_PEM); |
| 1041 | + x509.setIssuerName(issuer); |
| 1042 | + |
| 1043 | + x509.setPublicKey(cliKeyPubDer, WolfSSL.RSAk, |
| 1044 | + WolfSSL.SSL_FILETYPE_ASN1); |
| 1045 | + |
| 1046 | + try { |
| 1047 | + if (akidRaw != null) { |
| 1048 | + x509.setAuthorityKeyId(akidRaw); |
| 1049 | + } |
| 1050 | + else { |
| 1051 | + x509.setAuthorityKeyIdEx(issuerForEx); |
| 1052 | + } |
| 1053 | + |
| 1054 | + } catch (WolfSSLException e) { |
| 1055 | + if (isNotCompiledIn(e)) { |
| 1056 | + return null; |
| 1057 | + } |
| 1058 | + throw e; |
| 1059 | + } |
| 1060 | + |
| 1061 | + x509.signCert(caKeyDer, WolfSSL.RSAk, |
| 1062 | + WolfSSL.SSL_FILETYPE_ASN1, "SHA256"); |
| 1063 | + |
| 1064 | + byte[] der = x509.getDer(); |
| 1065 | + assertNotNull("getDer() returned null after signing", der); |
| 1066 | + assertTrue("getDer() returned empty bytes", der.length > 0); |
| 1067 | + |
| 1068 | + return der; |
| 1069 | + |
| 1070 | + } finally { |
| 1071 | + if (subjectName != null) { |
| 1072 | + subjectName.free(); |
| 1073 | + } |
| 1074 | + if (issuer != null) { |
| 1075 | + issuer.free(); |
| 1076 | + } |
| 1077 | + x509.free(); |
| 1078 | + } |
| 1079 | + } |
| 1080 | + |
| 1081 | + /* Parse DER cert with JDK CertificateFactory and assert the AKID |
| 1082 | + * extension (OID 2.5.29.35) decodes as a well-formed SEQUENCE { |
| 1083 | + * [0] keyIdentifier OCTET STRING } whose keyId matches expectedKeyId. */ |
| 1084 | + private void assertAkidExtensionMatches(byte[] derCert, |
| 1085 | + byte[] expectedKeyId) throws CertificateException { |
| 1086 | + |
| 1087 | + CertificateFactory cf = CertificateFactory.getInstance("X.509"); |
| 1088 | + X509Certificate jdkCert = (X509Certificate)cf.generateCertificate( |
| 1089 | + new ByteArrayInputStream(derCert)); |
| 1090 | + assertNotNull("CertificateFactory rejected the DER cert " + |
| 1091 | + "(likely malformed AKID extension)", jdkCert); |
| 1092 | + |
| 1093 | + /* getExtensionValue() returns the DER-encoded extnValue, which per |
| 1094 | + * X.509 is an OCTET STRING whose contents are the actual extension |
| 1095 | + * value. So the bytes look like: |
| 1096 | + * 04 LL <inner-bytes> |
| 1097 | + * For AKID, <inner-bytes> must be the AuthorityKeyIdentifier |
| 1098 | + * SEQUENCE per RFC 5280: |
| 1099 | + * 30 LL 80 KL <keyId> |
| 1100 | + */ |
| 1101 | + byte[] extWrapped = jdkCert.getExtensionValue("2.5.29.35"); |
| 1102 | + assertNotNull("AKID extension (2.5.29.35) was not present in cert", |
| 1103 | + extWrapped); |
| 1104 | + |
| 1105 | + /* Strip the outer OCTET STRING wrapper. */ |
| 1106 | + assertTrue("AKID extension envelope too short: " + extWrapped.length, |
| 1107 | + extWrapped.length >= 2); |
| 1108 | + assertEquals("AKID extension envelope is not OCTET STRING", |
| 1109 | + 0x04, extWrapped[0] & 0xFF); |
| 1110 | + int outerLen = extWrapped[1] & 0xFF; |
| 1111 | + /* Short-form length only; AKID for 20-byte keyId fits in <128. */ |
| 1112 | + assertTrue("Unexpected long-form length in AKID envelope", |
| 1113 | + outerLen < 0x80); |
| 1114 | + assertEquals("AKID envelope length mismatch", |
| 1115 | + extWrapped.length - 2, outerLen); |
| 1116 | + |
| 1117 | + /* Inner extension structure for a 20-byte keyId is exactly 24 bytes: |
| 1118 | + * SEQUENCE (0x30) length 22 (0x16) |
| 1119 | + * [0] (0x80) length 20 (0x14) |
| 1120 | + * <20 keyId bytes> |
| 1121 | + */ |
| 1122 | + byte[] inner = new byte[outerLen]; |
| 1123 | + System.arraycopy(extWrapped, 2, inner, 0, outerLen); |
| 1124 | + assertEquals("AKID inner length (expected SEQUENCE { [0] keyId } " + |
| 1125 | + "wrapping " + expectedKeyId.length + " bytes = " + |
| 1126 | + (expectedKeyId.length + 4) + " total)", |
| 1127 | + expectedKeyId.length + 4, inner.length); |
| 1128 | + assertEquals("AKID inner not a SEQUENCE — encoder likely wrote " + |
| 1129 | + "raw keyId bytes directly into the extension OCTET STRING", |
| 1130 | + 0x30, inner[0] & 0xFF); |
| 1131 | + assertEquals("AKID SEQUENCE length byte unexpected", |
| 1132 | + expectedKeyId.length + 2, inner[1] & 0xFF); |
| 1133 | + assertEquals("AKID keyIdentifier tag is not [0] context-specific", |
| 1134 | + 0x80, inner[2] & 0xFF); |
| 1135 | + assertEquals("AKID keyIdentifier length byte unexpected", |
| 1136 | + expectedKeyId.length, inner[3] & 0xFF); |
| 1137 | + |
| 1138 | + byte[] decodedKeyId = new byte[expectedKeyId.length]; |
| 1139 | + System.arraycopy(inner, 4, decodedKeyId, 0, expectedKeyId.length); |
| 1140 | + assertArrayEquals("AKID keyIdentifier bytes do not match input", |
| 1141 | + expectedKeyId, decodedKeyId); |
| 1142 | + } |
| 1143 | + |
900 | 1144 | /* Quick sanity check on certificate bytes. Loads cert into new |
901 | 1145 | * WolfSSLCertificate object, tries to get various elements and |
902 | 1146 | * simply verify if not null / etc. */ |
|
0 commit comments