Merge pull request #362 from cconlon/akidTest
Add round trip test for WolfSSLCertificate.setAuthorityKeyId()pull/371/head
commit
5cc5248d0f
|
|
@ -815,7 +815,14 @@ public class WolfSSLCertificate implements Serializable {
|
|||
* Sets the Authority Key Identifier extension for this WolfSSLCertificate,
|
||||
* used when generating X509v3 certificates.
|
||||
*
|
||||
* @param akid Byte array containing Authority Key Identifier.
|
||||
* With native wolfSSL with the AKID encoder fix in PR #10370, the bytes are
|
||||
* wrapped in SEQUENCE { [0] keyIdentifier } per RFC 5280 4.2.1.1. Older
|
||||
* native wolfSSL builds without the fix emit a malformed AKID extension
|
||||
* that parsers reject.
|
||||
*
|
||||
* @param akid Raw key identifier bytes (ex: the SHA-1 hash of the issuer
|
||||
* public key, or the issuer SubjectKeyIdentifier value). Bytes
|
||||
* must not be a pre-encoded AuthorityKeyIdentifier SEQUENCE.
|
||||
*
|
||||
* @throws IllegalStateException if WolfSSLCertificate has been freed.
|
||||
* @throws WolfSSLException if invalid arguments or native JNI error occurs.
|
||||
|
|
@ -851,6 +858,10 @@ public class WolfSSLCertificate implements Serializable {
|
|||
* Sets the Authority Key Identifier extension for this WolfSSLCertificate
|
||||
* using the issuer certificate.
|
||||
*
|
||||
* The keyIdentifier value used is the issuer SubjectKeyIdentifier
|
||||
* extension contents if present, otherwise the SHA-1 hash of the issuer
|
||||
* SubjectPublicKeyInfo.
|
||||
*
|
||||
* @param issuer Issuer certificate used to derive the Authority Key ID.
|
||||
*
|
||||
* @throws IllegalStateException if WolfSSLCertificate has been freed.
|
||||
|
|
|
|||
|
|
@ -32,7 +32,9 @@ import java.util.Date;
|
|||
import java.time.Instant;
|
||||
import java.time.Duration;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
|
|
@ -897,6 +899,260 @@ public class WolfSSLCertificateTest {
|
|||
x509.free();
|
||||
}
|
||||
|
||||
/* Round trip test for setAuthorityKeyId(byte[]) and
|
||||
* setAuthorityKeyIdEx(WolfSSLCertificate).
|
||||
*
|
||||
* RFC 5280 requires the extension OCTET STRING to wrap a SEQUENCE { [0]
|
||||
* keyIdentifier OCTET STRING }. If the encoder writes the raw key ID bytes
|
||||
* directly into the OCTET STRING, the resulting cert is malformed and
|
||||
* parsers will reject it.
|
||||
*
|
||||
* This test signs a cert after calling each setter, runs the DER through
|
||||
* java.security.cert.CertificateFactory, pulls the AKID extension via
|
||||
* getExtensionValue("2.5.29.35"), asserts the inner bytes start with
|
||||
* the expected SEQUENCE { [0] keyId }, and that the embedded keyId
|
||||
* matches what was supplied. */
|
||||
@Test
|
||||
public void testWolfSSLCertificateAuthorityKeyIdRoundtrip()
|
||||
throws WolfSSLException, WolfSSLJNIException, IOException,
|
||||
CertificateException {
|
||||
|
||||
Assume.assumeTrue(WolfSSL.FileSystemEnabled());
|
||||
|
||||
/* Raw 20-byte key identifier passed to setAuthorityKeyId(byte[]). */
|
||||
final byte[] akidRaw = new byte[] {
|
||||
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19,
|
||||
0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23
|
||||
};
|
||||
|
||||
/* SKID stamped on the issuer cert. The AKID derived by
|
||||
* setAuthorityKeyIdEx() must match this value exactly. */
|
||||
final byte[] issuerSkid = new byte[] {
|
||||
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39,
|
||||
0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x41, 0x42, 0x43
|
||||
};
|
||||
|
||||
/* setAuthorityKeyId(byte[]) round trip. buildSignedCertWithAkid()
|
||||
* returns null when the setter is NOT_COMPILED_IN at runtime
|
||||
* (older native wolfSSL). */
|
||||
byte[] derCert = buildSignedCertWithAkid(akidRaw, null);
|
||||
Assume.assumeTrue("AKID setter not compiled in native wolfSSL",
|
||||
derCert != null);
|
||||
|
||||
/* Check if the native encoder did not wrap the raw keyId in
|
||||
* SEQUENCE { [0] keyId } per RFC 5280 4.2.1.1, skip rather than fail.
|
||||
* The encoder bug lives in wolfSSL prior to PR #10370, not wolfssljni.
|
||||
* We want the test here to run for wolfSSL versions that are fixed,
|
||||
* but not for broken versions. */
|
||||
Assume.assumeTrue("Native wolfSSL AKID encoder lacks RFC 5280 " +
|
||||
"wrapping fix (wolfSSL PR #10370); upgrade native wolfSSL.",
|
||||
akidExtensionWellFormed(derCert));
|
||||
|
||||
assertAkidExtensionMatches(derCert, akidRaw);
|
||||
|
||||
/* setAuthorityKeyIdEx(WolfSSLCertificate issuer) round trip test */
|
||||
WolfSSLCertificate issuerCert =
|
||||
new WolfSSLCertificate(caCertPem, WolfSSL.SSL_FILETYPE_PEM);
|
||||
try {
|
||||
try {
|
||||
issuerCert.setSubjectKeyId(issuerSkid);
|
||||
} catch (WolfSSLException e) {
|
||||
if (isNotCompiledIn(e)) {
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
byte[] derCertEx = buildSignedCertWithAkid(null, issuerCert);
|
||||
if (derCertEx != null) {
|
||||
assertAkidExtensionMatches(derCertEx, issuerSkid);
|
||||
}
|
||||
|
||||
} finally {
|
||||
issuerCert.free();
|
||||
}
|
||||
}
|
||||
|
||||
/* Returns true iff the AKID extension on this DER cert decodes as a well
|
||||
* formed SEQUENCE { [0] keyId } per RFC 5280 4.2.1.1. Returns false if the
|
||||
* extension is missing, malformed, or the inner content begins with
|
||||
* anything other than the SEQUENCE tag (which is the unfixed encoder
|
||||
* symptom: raw key id bytes written directly into the OCTET STRING). Used
|
||||
* by testWolfSSLCertificateAuthorityKeyIdRoundtrip() to skip on native
|
||||
* wolfSSL builds without the AKID encoder fix.
|
||||
*
|
||||
* X509Certificate.getExtensionValue() returns the DER-encoded extnValue,
|
||||
* an OCTET STRING wrapping the inner extension bytes:
|
||||
* 04 LL <inner-bytes>
|
||||
*
|
||||
* For a properly formed AKID, <inner-bytes> starts with 0x30 (SEQUENCE);
|
||||
* for an unfixed encoder it is a raw 20-byte keyId starting with whatever
|
||||
* happens to be byte 0 of the keyId. */
|
||||
private boolean akidExtensionWellFormed(byte[] derCert)
|
||||
throws CertificateException {
|
||||
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
X509Certificate jdkCert = (X509Certificate)cf.generateCertificate(
|
||||
new ByteArrayInputStream(derCert));
|
||||
|
||||
if (jdkCert == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] extWrapped = jdkCert.getExtensionValue("2.5.29.35");
|
||||
if (extWrapped == null || extWrapped.length < 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Skip outer OCTET STRING wrapper. Short-form length only, since real
|
||||
* AKID extensions fit in <128 bytes. */
|
||||
if ((extWrapped[0] & 0xFF) != 0x04) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int outerLen = extWrapped[1] & 0xFF;
|
||||
if (outerLen >= 0x80 || extWrapped.length != 2 + outerLen) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Inner must start with SEQUENCE tag (0x30) for a properly formed
|
||||
* AuthorityKeyIdentifier. */
|
||||
return (extWrapped[2] & 0xFF) == 0x30;
|
||||
}
|
||||
|
||||
/* Build, populate, AKID-stamp, and sign a cert. Returns the DER bytes or
|
||||
* null if the AKID setter returned NOT_COMPILED_IN at runtime (native
|
||||
* wolfSSL pre PR 10370). Exactly one of {akidRaw, issuerForEx} must be non
|
||||
* null to select which AKID setter to exercise. */
|
||||
private byte[] buildSignedCertWithAkid(byte[] akidRaw,
|
||||
WolfSSLCertificate issuerForEx) throws WolfSSLException,
|
||||
WolfSSLJNIException, IOException {
|
||||
|
||||
WolfSSLCertificate x509 = new WolfSSLCertificate();
|
||||
WolfSSLX509Name subjectName = null;
|
||||
WolfSSLCertificate issuer = null;
|
||||
|
||||
try {
|
||||
Instant now = Instant.now();
|
||||
x509.setNotBefore(Date.from(now));
|
||||
x509.setNotAfter(Date.from(now.plus(Duration.ofDays(365))));
|
||||
x509.setSerialNumber(BigInteger.valueOf(0xCAFE));
|
||||
|
||||
subjectName = GenerateTestSubjectName();
|
||||
x509.setSubjectName(subjectName);
|
||||
|
||||
if (akidRaw != null) {
|
||||
issuer = new WolfSSLCertificate(caCertPem,
|
||||
WolfSSL.SSL_FILETYPE_PEM);
|
||||
x509.setIssuerName(issuer);
|
||||
}
|
||||
else {
|
||||
x509.setIssuerName(issuerForEx);
|
||||
}
|
||||
|
||||
x509.setPublicKey(cliKeyPubDer, WolfSSL.RSAk,
|
||||
WolfSSL.SSL_FILETYPE_ASN1);
|
||||
|
||||
try {
|
||||
if (akidRaw != null) {
|
||||
x509.setAuthorityKeyId(akidRaw);
|
||||
}
|
||||
else {
|
||||
x509.setAuthorityKeyIdEx(issuerForEx);
|
||||
}
|
||||
|
||||
} catch (WolfSSLException e) {
|
||||
if (isNotCompiledIn(e)) {
|
||||
return null;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
x509.signCert(caKeyDer, WolfSSL.RSAk,
|
||||
WolfSSL.SSL_FILETYPE_ASN1, "SHA256");
|
||||
|
||||
byte[] der = x509.getDer();
|
||||
assertNotNull("getDer() returned null after signing", der);
|
||||
assertTrue("getDer() returned empty bytes", der.length > 0);
|
||||
|
||||
return der;
|
||||
|
||||
} finally {
|
||||
if (subjectName != null) {
|
||||
subjectName.free();
|
||||
}
|
||||
if (issuer != null) {
|
||||
issuer.free();
|
||||
}
|
||||
x509.free();
|
||||
}
|
||||
}
|
||||
|
||||
/* Parse DER cert with JDK CertificateFactory and assert the AKID extension
|
||||
* (OID 2.5.29.35) decodes as a properly formed SEQUENCE {
|
||||
* [0] keyIdentifier OCTET STRING } whose keyId matches expectedKeyId. */
|
||||
private void assertAkidExtensionMatches(byte[] derCert,
|
||||
byte[] expectedKeyId) throws CertificateException {
|
||||
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
X509Certificate jdkCert = (X509Certificate)cf.generateCertificate(
|
||||
new ByteArrayInputStream(derCert));
|
||||
assertNotNull("CertificateFactory rejected the DER cert " +
|
||||
"(likely malformed AKID extension)", jdkCert);
|
||||
|
||||
/* getExtensionValue() returns the DER-encoded extnValue, which per
|
||||
* X.509 is an OCTET STRING whose contents are the actual extension
|
||||
* value. So the bytes look like:
|
||||
* 04 LL <inner-bytes>
|
||||
*
|
||||
* For AKID, <inner-bytes> must be the AuthorityKeyIdentifier
|
||||
* SEQUENCE per RFC 5280:
|
||||
* 30 LL 80 KL <keyId>
|
||||
*/
|
||||
byte[] extWrapped = jdkCert.getExtensionValue("2.5.29.35");
|
||||
assertNotNull("AKID extension (2.5.29.35) was not present in cert",
|
||||
extWrapped);
|
||||
|
||||
/* Strip the outer OCTET STRING wrapper. */
|
||||
assertTrue("AKID extension envelope too short: " + extWrapped.length,
|
||||
extWrapped.length >= 2);
|
||||
assertEquals("AKID extension envelope is not OCTET STRING",
|
||||
0x04, extWrapped[0] & 0xFF);
|
||||
|
||||
int outerLen = extWrapped[1] & 0xFF;
|
||||
/* Short-form length only; AKID for 20-byte keyId fits in <128. */
|
||||
assertTrue("Unexpected long-form length in AKID envelope",
|
||||
outerLen < 0x80);
|
||||
assertEquals("AKID envelope length mismatch",
|
||||
extWrapped.length - 2, outerLen);
|
||||
|
||||
/* Inner extension structure for a 20-byte keyId is exactly 24 bytes:
|
||||
* SEQUENCE (0x30) length 22 (0x16)
|
||||
* [0] (0x80) length 20 (0x14)
|
||||
* <20 keyId bytes>
|
||||
*/
|
||||
byte[] inner = new byte[outerLen];
|
||||
System.arraycopy(extWrapped, 2, inner, 0, outerLen);
|
||||
assertEquals("AKID inner length (expected SEQUENCE { [0] keyId } " +
|
||||
"wrapping " + expectedKeyId.length + " bytes = " +
|
||||
(expectedKeyId.length + 4) + " total)",
|
||||
expectedKeyId.length + 4, inner.length);
|
||||
assertEquals("AKID inner not a SEQUENCE, encoder likely wrote " +
|
||||
"raw keyId bytes directly into the extension OCTET STRING",
|
||||
0x30, inner[0] & 0xFF);
|
||||
assertEquals("AKID SEQUENCE length byte unexpected",
|
||||
expectedKeyId.length + 2, inner[1] & 0xFF);
|
||||
assertEquals("AKID keyIdentifier tag is not [0] context-specific",
|
||||
0x80, inner[2] & 0xFF);
|
||||
assertEquals("AKID keyIdentifier length byte unexpected",
|
||||
expectedKeyId.length, inner[3] & 0xFF);
|
||||
|
||||
byte[] decodedKeyId = new byte[expectedKeyId.length];
|
||||
System.arraycopy(inner, 4, decodedKeyId, 0, expectedKeyId.length);
|
||||
assertArrayEquals("AKID keyIdentifier bytes do not match input",
|
||||
expectedKeyId, decodedKeyId);
|
||||
}
|
||||
|
||||
/* Quick sanity check on certificate bytes. Loads cert into new
|
||||
* WolfSSLCertificate object, tries to get various elements and
|
||||
* simply verify if not null / etc. */
|
||||
|
|
|
|||
Loading…
Reference in New Issue