diff --git a/lib/src/main/java/zeroecho/core/alg/common/sig/SignatureInteropProfile.java b/lib/src/main/java/zeroecho/core/alg/common/sig/SignatureInteropProfile.java
index 9885d90..9afd4a5 100644
--- a/lib/src/main/java/zeroecho/core/alg/common/sig/SignatureInteropProfile.java
+++ b/lib/src/main/java/zeroecho/core/alg/common/sig/SignatureInteropProfile.java
@@ -39,8 +39,6 @@ import java.util.Objects;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1Encoding;
import org.bouncycastle.asn1.ASN1Integer;
-import org.bouncycastle.asn1.ASN1Primitive;
-import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DERSequence;
import zeroecho.core.spec.ContextSpec;
@@ -84,7 +82,10 @@ import zeroecho.core.spec.ContextSpec;
* semantics, not as an optional hint.
*
*/
-public final class SignatureInteropProfile { // NOPMD
+public final class SignatureInteropProfile {
+ private static final int DER_SEQUENCE_TAG = 0x30;
+ private static final int DER_INTEGER_TAG = 0x02;
+ private static final int DER_LONG_FORM_BIT = 0x80;
/**
* Signature byte representation bridging policy.
@@ -192,11 +193,10 @@ public final class SignatureInteropProfile { // NOPMD
*
* @param signature external signature bytes
* @return internal signature bytes suitable for ZeroEcho verification contexts
- * @throws IOException if DER decoding fails
* @throws IllegalArgumentException if the supplied bytes do not match the
* expected external representation
*/
- public byte[] externalToInternalSignature(byte[] signature) throws IOException {
+ public byte[] externalToInternalSignature(byte[] signature) {
Objects.requireNonNull(signature, "signature");
if (signatureRepresentation == SignatureRepresentation.IDENTITY) {
return signature.clone();
@@ -221,21 +221,86 @@ public final class SignatureInteropProfile { // NOPMD
return ecdsaP1363ToDer(signature, internalSignatureLength);
}
- private static byte[] ecdsaDerToP1363(byte[] der, int fixedLength) throws IOException {
- ASN1Primitive primitive = ASN1Primitive.fromByteArray(der);
- ASN1Sequence sequence = ASN1Sequence.getInstance(primitive);
- if (sequence.size() != 2) { // NOPMD
- throw new IllegalArgumentException("ECDSA DER signature must contain exactly two integers");
+ private static byte[] ecdsaDerToP1363(byte[] der, int fixedLength) {
+ if ((fixedLength & 1) != 0 || der.length > fixedLength + 8 || der.length < 8) {
+ throw new IllegalArgumentException("Invalid ECDSA DER signature length");
}
int coordinateLength = fixedLength / 2;
+ int offset = 0;
+ if (Byte.toUnsignedInt(der[offset++]) != DER_SEQUENCE_TAG) {
+ throw new IllegalArgumentException("ECDSA DER signature must be a sequence");
+ }
+ long sequenceLength = readDerLength(der, offset);
+ offset = (int) sequenceLength;
+ int sequenceEnd = Math.addExact(offset, (int) (sequenceLength >>> Integer.SIZE));
+ if (sequenceEnd != der.length) {
+ throw new IllegalArgumentException("ECDSA DER signature contains trailing or truncated data");
+ }
byte[] out = new byte[fixedLength];
- byte[] r = ASN1Integer.getInstance(sequence.getObjectAt(0)).getPositiveValue().toByteArray();
- byte[] s = ASN1Integer.getInstance(sequence.getObjectAt(1)).getPositiveValue().toByteArray();
- copyUnsignedFixed(r, out, 0, coordinateLength);
- copyUnsignedFixed(s, out, coordinateLength, coordinateLength);
+ offset = readPositiveInteger(der, offset, sequenceEnd, out, 0, coordinateLength);
+ offset = readPositiveInteger(der, offset, sequenceEnd, out, coordinateLength, coordinateLength);
+ if (offset != sequenceEnd) {
+ throw new IllegalArgumentException("ECDSA DER signature must contain exactly two integers");
+ }
return out;
}
+ /* High 32 bits carry the decoded length; low 32 bits carry the next offset. */
+ private static long readDerLength(byte[] der, int offset) {
+ int cursor = offset;
+ if (cursor >= der.length) {
+ throw new IllegalArgumentException("Truncated ECDSA DER length");
+ }
+ int first = Byte.toUnsignedInt(der[cursor++]);
+ int length;
+ if (first < DER_LONG_FORM_BIT) {
+ length = first;
+ } else {
+ int octets = first & 0x7f;
+ if (octets == 0 || octets > 2 || cursor + octets > der.length
+ || Byte.toUnsignedInt(der[cursor]) == 0) {
+ throw new IllegalArgumentException("Non-canonical ECDSA DER length");
+ }
+ length = 0;
+ for (int index = 0; index < octets; index++) {
+ length = (length << Byte.SIZE) | Byte.toUnsignedInt(der[cursor++]);
+ }
+ if (length < DER_LONG_FORM_BIT) {
+ throw new IllegalArgumentException("Non-canonical ECDSA DER length");
+ }
+ }
+ return ((long) length << Integer.SIZE) | Integer.toUnsignedLong(cursor);
+ }
+
+ private static int readPositiveInteger(byte[] der, int offset, int limit, byte[] target, int targetOffset,
+ int width) {
+ int cursor = offset;
+ if (cursor >= limit || Byte.toUnsignedInt(der[cursor++]) != DER_INTEGER_TAG) {
+ throw new IllegalArgumentException("ECDSA DER signature must contain INTEGER values");
+ }
+ long encodedLength = readDerLength(der, cursor);
+ cursor = (int) encodedLength;
+ int length = (int) (encodedLength >>> Integer.SIZE);
+ if (length == 0 || length > width + 1 || cursor > limit - length) {
+ throw new IllegalArgumentException("Invalid ECDSA DER integer length");
+ }
+ int first = Byte.toUnsignedInt(der[cursor]);
+ if (first == 0) {
+ if (length == 1 || (der[cursor + 1] & DER_LONG_FORM_BIT) == 0) {
+ throw new IllegalArgumentException("ECDSA DER integer must be positive, nonzero, and minimal");
+ }
+ cursor++;
+ length--;
+ } else if ((first & DER_LONG_FORM_BIT) != 0) {
+ throw new IllegalArgumentException("ECDSA DER integer must be positive");
+ }
+ if (length > width) {
+ throw new IllegalArgumentException("ECDSA integer does not fit into fixed P1363 width");
+ }
+ System.arraycopy(der, cursor, target, targetOffset + width - length, length);
+ return cursor + length;
+ }
+
private static byte[] ecdsaP1363ToDer(byte[] p1363, int fixedLength) throws IOException {
if (p1363.length != fixedLength) {
throw new IllegalArgumentException("Unexpected P1363 signature length: " + p1363.length);
@@ -252,18 +317,6 @@ public final class SignatureInteropProfile { // NOPMD
return new DERSequence(vector).getEncoded(ASN1Encoding.DER);
}
- private static void copyUnsignedFixed(byte[] value, byte[] target, int offset, int width) {
- int start = 0;
- while (start < value.length - 1 && value[start] == 0) {
- start++;
- }
- int len = value.length - start;
- if (len > width) {
- throw new IllegalArgumentException("ECDSA integer does not fit into fixed P1363 width");
- }
- System.arraycopy(value, start, target, offset + width - len, len);
- }
-
private static String requireNonBlank(String value, String label) {
Objects.requireNonNull(value, label);
if (value.isBlank()) {
diff --git a/lib/src/test/java/zeroecho/core/alg/common/sig/SignatureInteropProfilesTest.java b/lib/src/test/java/zeroecho/core/alg/common/sig/SignatureInteropProfilesTest.java
index c090b29..eb228eb 100644
--- a/lib/src/test/java/zeroecho/core/alg/common/sig/SignatureInteropProfilesTest.java
+++ b/lib/src/test/java/zeroecho/core/alg/common/sig/SignatureInteropProfilesTest.java
@@ -36,6 +36,7 @@ package zeroecho.core.alg.common.sig;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
@@ -117,6 +118,41 @@ public final class SignatureInteropProfilesTest {
System.out.println("resolveSha256WithEcdsa_roundTripsDerAndP1363...ok");
}
+ @Test
+ public void ecdsaDerConversionRejectsEveryNonCanonicalIntegerShape() throws Exception {
+ System.out.println("ecdsaDerConversionRejectsEveryNonCanonicalIntegerShape");
+ SignatureInteropProfile profile = SignatureInteropProfiles.resolve("SHA256withECDSA").orElseThrow();
+ byte[] canonical = { 0x30, 0x06, 0x02, 0x01, 0x01, 0x02, 0x01, 0x01 };
+ assertEquals(EcdsaCurveSpec.P256.signFixedLength(), profile.externalToInternalSignature(canonical).length);
+
+ assertThrows(IllegalArgumentException.class,
+ () -> profile.externalToInternalSignature(java.util.Arrays.copyOf(canonical, canonical.length + 1)));
+ assertThrows(IllegalArgumentException.class,
+ () -> profile.externalToInternalSignature(new byte[] { 0x30, (byte) 0x80, 0x02, 0x01, 0x01,
+ 0x02, 0x01, 0x01, 0x00, 0x00 }));
+ assertThrows(IllegalArgumentException.class,
+ () -> profile.externalToInternalSignature(new byte[] { 0x30, 0x06, 0x02, 0x01, (byte) 0xff,
+ 0x02, 0x01, 0x01 }));
+ assertThrows(IllegalArgumentException.class,
+ () -> profile.externalToInternalSignature(new byte[] { 0x30, 0x06, 0x02, 0x01, 0x00,
+ 0x02, 0x01, 0x01 }));
+ assertThrows(IllegalArgumentException.class,
+ () -> profile.externalToInternalSignature(new byte[] { 0x30, 0x07, 0x02, 0x02, 0x00, 0x01,
+ 0x02, 0x01, 0x01 }));
+ byte[] oversized = new byte[40];
+ oversized[0] = 0x30;
+ oversized[1] = 0x26;
+ oversized[2] = 0x02;
+ oversized[3] = 0x21;
+ oversized[4] = 0x01;
+ oversized[37] = 0x02;
+ oversized[38] = 0x01;
+ oversized[39] = 0x01;
+ assertThrows(IllegalArgumentException.class, () -> profile.externalToInternalSignature(oversized));
+ System.out.println("...rejected=trailing, BER, negative, zero, redundant, oversize");
+ System.out.println("ecdsaDerConversionRejectsEveryNonCanonicalIntegerShape...ok");
+ }
+
private static String hexPrefix(byte[] bytes) {
int len = Math.min(bytes.length, 16);
byte[] prefix = new byte[len];
diff --git a/pki/src/main/java/zeroecho/pki/impl/core/CaProofGate.java b/pki/src/main/java/zeroecho/pki/impl/core/CaProofGate.java
index b1535c9..e625e1b 100644
--- a/pki/src/main/java/zeroecho/pki/impl/core/CaProofGate.java
+++ b/pki/src/main/java/zeroecho/pki/impl/core/CaProofGate.java
@@ -66,6 +66,7 @@ import zeroecho.pki.api.audit.Purpose;
import zeroecho.pki.impl.core.async.PkiSigningBus;
import zeroecho.pki.impl.framework.x509.X509AlgorithmRole;
import zeroecho.pki.impl.framework.x509.X509ExecutionPlan;
+import zeroecho.pki.impl.framework.x509.StreamingDerReader;
import zeroecho.pki.impl.framework.x509.bc.BcX509VerificationExecutor;
import zeroecho.pki.impl.framework.x509.bc.BcX509AlgorithmAdapter;
import zeroecho.pki.spi.audit.AuditSink;
@@ -85,6 +86,7 @@ final class CaProofGate {
private static final byte[] MANAGED_KEY_CHALLENGE_DOMAIN = "ZeroEcho/PKI/managed-key-possession/v1\0"
.getBytes(java.nio.charset.StandardCharsets.US_ASCII);
private static final int CHALLENGE_NONCE_BYTES = 32;
+ private static final int MAXIMUM_SPKI_BYTES = 1024 * 1024;
private static final SecureRandom CHALLENGE_RANDOM = new SecureRandom();
private final PublicKeyInfoResolver publicKeyResolver;
@@ -114,11 +116,17 @@ final class CaProofGate {
}
/* default */ SubjectPublicKeyInfo parseRootSpki(EncodedObject spki, FormatId formatId) {
- try {
- return SubjectPublicKeyInfo.getInstance(spki.bytes());
- } catch (RuntimeException ex) { // NOPMD - malformed resolver output fails closed
+ if (spki.encoding() != Encoding.DER) {
throw rejection("CREATE_ROOT_REJECTED", formatId, Optional.empty(), "ROOT_MANAGED_KEY_INVALID");
}
+ byte[] encoded = spki.bytes();
+ try {
+ return parseValidatedSpki(encoded);
+ } catch (IOException | RuntimeException ex) { // NOPMD - malformed resolver output fails closed
+ throw rejection("CREATE_ROOT_REJECTED", formatId, Optional.empty(), "ROOT_MANAGED_KEY_INVALID");
+ } finally {
+ Arrays.fill(encoded, (byte) 0);
+ }
}
/* default */ boolean rootProofIsValid(X509CertificateHolder certificate, EncodedObject expectedSpki) {
@@ -169,6 +177,7 @@ final class CaProofGate {
}
return new ManagedKeyProof(keyRef, formatId, new EncodedObject(Encoding.DER, exactSpki.clone()));
} finally {
+ Arrays.fill(exactSpki, (byte) 0);
Arrays.fill(challenge, (byte) 0);
if (signature != null) {
Arrays.fill(signature, (byte) 0);
@@ -187,7 +196,15 @@ final class CaProofGate {
if (resolved == null || resolved.encoding() != Encoding.DER) {
throw rejection(auditAction, formatId, subjectCaId, "MANAGED_KEY_INVALID");
}
- return new EncodedObject(Encoding.DER, resolved.bytes());
+ byte[] encoded = resolved.bytes();
+ try {
+ parseValidatedSpki(encoded);
+ return new EncodedObject(Encoding.DER, encoded);
+ } catch (IOException | RuntimeException exception) { // NOPMD - hostile resolver output is redacted
+ throw rejection(auditAction, formatId, subjectCaId, "MANAGED_KEY_INVALID");
+ } finally {
+ Arrays.fill(encoded, (byte) 0);
+ }
}
/* default */ PkiException rejection(String action, FormatId formatId, Optional objectId, String code) {
@@ -213,7 +230,7 @@ final class CaProofGate {
private boolean verifyChallenge(byte[] spkiDer, byte[] challenge, byte[] signature) {
try {
- SubjectPublicKeyInfo spki = SubjectPublicKeyInfo.getInstance(spkiDer);
+ SubjectPublicKeyInfo spki = parseValidatedSpki(spkiDer);
BcX509AlgorithmAdapter adapter = new BcX509AlgorithmAdapter(signingBus.authority().bindings());
AlgorithmIdentity keyIdentity = adapter.decode(spki.getAlgorithm(),
X509AlgorithmRole.SUBJECT_PUBLIC_KEY_ALGORITHM);
@@ -230,6 +247,14 @@ final class CaProofGate {
}
}
+ private SubjectPublicKeyInfo parseValidatedSpki(byte[] encoded) throws IOException {
+ new StreamingDerReader().validateSubjectPublicKeyInfo(encoded, MAXIMUM_SPKI_BYTES);
+ SubjectPublicKeyInfo spki = SubjectPublicKeyInfo.getInstance(encoded);
+ new BcX509AlgorithmAdapter(signingBus.authority().bindings()).decode(spki.getAlgorithm(),
+ X509AlgorithmRole.SUBJECT_PUBLIC_KEY_ALGORITHM);
+ return spki;
+ }
+
/**
* Unforgeable package-local result of a completed managed-key challenge.
*/
diff --git a/pki/src/main/java/zeroecho/pki/impl/core/CertificateProfileValidator.java b/pki/src/main/java/zeroecho/pki/impl/core/CertificateProfileValidator.java
index 88d6f1a..9b992ff 100644
--- a/pki/src/main/java/zeroecho/pki/impl/core/CertificateProfileValidator.java
+++ b/pki/src/main/java/zeroecho/pki/impl/core/CertificateProfileValidator.java
@@ -33,6 +33,7 @@
******************************************************************************/
package zeroecho.pki.impl.core;
+import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.MessageDigest;
@@ -72,6 +73,7 @@ import zeroecho.pki.impl.framework.x509.bc.BcX509Attributes;
import zeroecho.pki.impl.framework.x509.bc.BcX509AlgorithmAdapter;
import zeroecho.pki.impl.framework.x509.X509AlgorithmRole;
import zeroecho.pki.impl.framework.x509.X509AuthoritySnapshot;
+import zeroecho.pki.impl.framework.x509.StreamingDerReader;
import zeroecho.core.alg.BootstrapAlgorithmIdentities;
import zeroecho.core.spec.AlgorithmIdentity;
import zeroecho.pki.impl.framework.x509.bc.BcX509ProfileSupport;
@@ -84,6 +86,7 @@ import zeroecho.pki.impl.framework.x509.bc.BcX509ProfileSupport;
final class CertificateProfileValidator {
private static final String EC_FAMILY = "ec";
+ private static final int MAXIMUM_SPKI_BYTES = 1024 * 1024;
private CertificateProfileValidator() {
}
@@ -233,6 +236,7 @@ final class CertificateProfileValidator {
}
byte[] encoded = exactPublicKey.bytes();
try {
+ new StreamingDerReader().validateSubjectPublicKeyInfo(encoded, MAXIMUM_SPKI_BYTES);
SubjectPublicKeyInfo spki = SubjectPublicKeyInfo.getInstance(encoded);
AlgorithmIdentity identity;
try {
@@ -262,7 +266,7 @@ final class CertificateProfileValidator {
}
} catch (PkiException exception) {
throw exception;
- } catch (GeneralSecurityException | IllegalArgumentException exception) {
+ } catch (GeneralSecurityException | IllegalArgumentException | IOException exception) {
throw reject("SUBJECT_KEY_UNSUPPORTED");
} finally {
java.util.Arrays.fill(encoded, (byte) 0);
diff --git a/pki/src/main/java/zeroecho/pki/impl/core/CredentialContent.java b/pki/src/main/java/zeroecho/pki/impl/core/CredentialContent.java
index d31ee34..e934bc8 100644
--- a/pki/src/main/java/zeroecho/pki/impl/core/CredentialContent.java
+++ b/pki/src/main/java/zeroecho/pki/impl/core/CredentialContent.java
@@ -53,10 +53,15 @@ import zeroecho.pki.spi.store.PkiStore;
* Internal individual-certificate adapter around store-owned content.
*/
final class CredentialContent {
+ /* default */ static final int MAXIMUM_CERTIFICATE_BYTES = 1024 * 1024;
+
private CredentialContent() {
}
/* default */ static DurableContentReference stage(PkiStore store, byte[] encoded) {
+ if (encoded.length > MAXIMUM_CERTIFICATE_BYTES) {
+ throw new PkiException("Certificate exceeds artifact limit: code=CERTIFICATE_TOO_LARGE");
+ }
try (ContentSink sink = store.stagedContent().beginContent(Encoding.DER, DurableContentReference.Lifecycle.PERSISTED);
OutputStream output = sink.outputStream()) {
output.write(encoded);
@@ -67,10 +72,11 @@ final class CredentialContent {
}
/* default */ static byte[] materializeForBc(PkiStore store, DurableContentReference reference) {
- if (reference.length() > Integer.MAX_VALUE) {
- throw new PkiException("Credential exceeds BC adapter element domain: code=ADAPTER_ELEMENT_LIMIT_EXCEEDED");
+ long length = reference.length();
+ if (reference.encoding() != Encoding.DER || length <= 0L || length > MAXIMUM_CERTIFICATE_BYTES) {
+ throw new PkiException("Certificate exceeds artifact limit: code=CERTIFICATE_TOO_LARGE");
}
- byte[] result = new byte[(int) reference.length()];
+ byte[] result = new byte[(int) length];
try {
readExact(store, reference, result);
return result;
@@ -83,6 +89,10 @@ final class CredentialContent {
/* default */ static BcX509SignedObjectValidator.CertificateBindings validateCertificate(PkiStore store,
DurableContentReference reference, X509AuthoritySnapshot authority,
Optional expectedSignature) {
+ long length = reference.length();
+ if (reference.encoding() != Encoding.DER || length <= 0L || length > MAXIMUM_CERTIFICATE_BYTES) {
+ throw new PkiException("Certificate exceeds artifact limit: code=CERTIFICATE_TOO_LARGE");
+ }
try (RepeatableContent content = store.stagedContent().openContent(reference)) {
return new BcX509SignedObjectValidator(authority).validateCertificate(content, expectedSignature,
CancellationSignal.NONE);
diff --git a/pki/src/main/java/zeroecho/pki/impl/core/DefaultStatusObjectService.java b/pki/src/main/java/zeroecho/pki/impl/core/DefaultStatusObjectService.java
index ff6bd08..a71d4c0 100644
--- a/pki/src/main/java/zeroecho/pki/impl/core/DefaultStatusObjectService.java
+++ b/pki/src/main/java/zeroecho/pki/impl/core/DefaultStatusObjectService.java
@@ -222,8 +222,12 @@ public final class DefaultStatusObjectService implements StatusObjectService {
SimpleAttributeSet.Builder b = SimpleAttributeSet.builder();
b.putAll(command.attributes());
- b.put(BcX509Attributes.ISSUER_CERT_DER,
- new AttributeValue.BytesValue(CredentialContent.materializeForBc(store, issuerCred.content())));
+ byte[] issuerDer = materializeValidatedCertificate(issuerCred);
+ try {
+ b.put(BcX509Attributes.ISSUER_CERT_DER, new AttributeValue.BytesValue(issuerDer.clone()));
+ } finally {
+ Arrays.fill(issuerDer, (byte) 0);
+ }
b.put(BcX509Attributes.ISSUER_KEYREF, new AttributeValue.StringValue(ca.issuerKeyRef().value()));
StatusObjectGenerateCommand wired = new StatusObjectGenerateCommand(command.issuerCaId(), command.type(),
@@ -248,11 +252,12 @@ public final class DefaultStatusObjectService implements StatusObjectService {
requirePersistableContent(generated.content());
boolean accepted = false;
try {
- byte[] issuerDer = CredentialContent.materializeForBc(store, issuer.content());
+ requireGeneratedMetadata(command, generated);
+ byte[] issuerDer = materializeValidatedCertificate(issuer);
try (RepeatableContent content = store.stagedContent().openContent(generated.content())) {
X509CertificateHolder holder = new X509CertificateHolder(issuerDer);
new BcX509SignedObjectValidator(authority).validateGeneratedCrl(content, signingPlan,
- holder.getSubjectPublicKeyInfo(), CancellationSignal.NONE);
+ holder, generated.thisUpdate(), generated.nextUpdate(), CancellationSignal.NONE);
} finally {
Arrays.fill(issuerDer, (byte) 0);
}
@@ -274,11 +279,20 @@ public final class DefaultStatusObjectService implements StatusObjectService {
private void requirePersistableContent(DurableContentReference content) {
if (content.lifecycle() != DurableContentReference.Lifecycle.PERSISTED
- || !store.stagedContent().contentStoreId().equals(content.storeId())) {
+ || !store.stagedContent().contentStoreId().equals(content.storeId())
+ || content.encoding() != Encoding.DER) {
throw new PkiException("Status content lifecycle invalid: code=STAGED_CONTENT_FOREIGN_RUNTIME");
}
}
+ private static void requireGeneratedMetadata(StatusObjectGenerateCommand command, StatusObject generated) {
+ if (generated.type() != command.type() || !generated.formatId().equals(command.formatId())
+ || !generated.issuerCaId().equals(command.issuerCaId())
+ || generated.nextUpdate().filter(next -> !next.isAfter(generated.thisUpdate())).isPresent()) {
+ throw new PkiException("Status metadata binding invalid: code=STATUS_OBJECT_BINDING_MISMATCH");
+ }
+ }
+
private void releaseRejectedContent(DurableContentReference content) {
try {
store.stagedContent().retireUnownedContent(content);
@@ -443,7 +457,7 @@ public final class DefaultStatusObjectService implements StatusObjectService {
// cause is intentionally removed at this public service boundary.
@SuppressWarnings("PMD.PreserveStackTrace")
private BigInteger certificateSerial(Credential credential) {
- byte[] der = CredentialContent.materializeForBc(store, credential.content());
+ byte[] der = materializeValidatedCertificate(credential);
BigInteger serial;
try {
serial = new X509CertificateHolder(der).getSerialNumber();
@@ -458,6 +472,16 @@ public final class DefaultStatusObjectService implements StatusObjectService {
return serial;
}
+ @SuppressWarnings({ "PMD.AvoidCatchingGenericException", "PMD.PreserveStackTrace" })
+ private byte[] materializeValidatedCertificate(Credential credential) {
+ try {
+ CredentialContent.validateCertificate(store, credential.content(), authority, Optional.empty());
+ return CredentialContent.materializeForBc(store, credential.content());
+ } catch (RuntimeException exception) {
+ throw crlGenerationFailure();
+ }
+ }
+
private static PkiException crlGenerationFailure() {
return new PkiException("CRL generation failed: code=" + CRL_GENERATION_FAILED);
}
diff --git a/pki/src/main/java/zeroecho/pki/impl/framework/x509/StreamingDerReader.java b/pki/src/main/java/zeroecho/pki/impl/framework/x509/StreamingDerReader.java
index 229c38c..3679784 100644
--- a/pki/src/main/java/zeroecho/pki/impl/framework/x509/StreamingDerReader.java
+++ b/pki/src/main/java/zeroecho/pki/impl/framework/x509/StreamingDerReader.java
@@ -33,9 +33,16 @@
******************************************************************************/
package zeroecho.pki.impl.framework.x509;
+import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.time.DateTimeException;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
import java.util.Objects;
+import java.util.Optional;
+import java.util.OptionalLong;
import zeroecho.core.io.CancellationSignal;
import zeroecho.core.io.RepeatableContent;
@@ -58,6 +65,9 @@ import zeroecho.core.io.RepeatableContent;
* fixed depth and comparison-buffer size.
*
*/
+// The aggregate branches implement one closed canonical-DER grammar.
+@SuppressWarnings({ "PMD.CyclomaticComplexity", "PMD.NPathComplexity", "PMD.AvoidLiteralsInIfCondition",
+ "PMD.CommentDefaultAccessModifier" })
public final class StreamingDerReader {
private static final int BUFFER_BYTES = 16 * 1024;
@@ -66,6 +76,76 @@ public final class StreamingDerReader {
private static final int CONTINUATION_BIT = 0x80;
private static final long SHORT_LENGTH_LIMIT = 128L;
+ /**
+ * Validates one complete canonical DER object held in an explicitly bounded
+ * byte array.
+ *
+ *
+ * This convenience entry point is intended for individual adapter values such
+ * as PKCS#10 requests and subject-public-key information. The caller-selected
+ * {@code maximumBytes} is enforced before parsing. Repeatable stream sources,
+ * especially aggregate CRLs, should use
+ * {@link #validate(RepeatableContent, CancellationSignal)} instead.
+ *
+ *
+ * @param encoded complete DER object
+ * @param maximumBytes positive caller-selected element limit in bytes
+ * @return exact encoded length
+ * @throws NullPointerException if {@code encoded} is {@code null}
+ * @throws IllegalArgumentException if {@code maximumBytes} is not positive
+ * @throws IOException if the input exceeds {@code maximumBytes}, is malformed,
+ * non-canonical, truncated, or contains trailing data
+ */
+ public int validate(byte[] encoded, int maximumBytes) throws IOException {
+ Objects.requireNonNull(encoded, "encoded");
+ if (maximumBytes <= 0) {
+ throw new IllegalArgumentException("maximumBytes must be positive");
+ }
+ if (encoded.length > maximumBytes) {
+ throw new IOException("DER object exceeds adapter element limit: code=ADAPTER_ELEMENT_LIMIT_EXCEEDED");
+ }
+ long length = validate(new ByteArrayContent(encoded), CancellationSignal.NONE);
+ try {
+ return Math.toIntExact(length);
+ } catch (ArithmeticException exception) {
+ throw new IOException("DER length overflow: code=CONTENT_LENGTH_OVERFLOW", exception);
+ }
+ }
+
+ /**
+ * Validates one canonical DER {@code SubjectPublicKeyInfo} value, including
+ * the X.509 requirement that its public-key BIT STRING have zero unused bits.
+ *
+ * @param encoded complete DER subject-public-key information
+ * @param maximumBytes positive caller-selected element limit in bytes
+ * @return exact encoded length
+ * @throws NullPointerException if {@code encoded} is {@code null}
+ * @throws IllegalArgumentException if {@code maximumBytes} is not positive
+ * @throws IOException if the value exceeds the limit or is not one canonical
+ * {@code SubjectPublicKeyInfo}
+ */
+ public int validateSubjectPublicKeyInfo(byte[] encoded, int maximumBytes) throws IOException {
+ int length = validate(encoded, maximumBytes);
+ try (CountedInput input = new CountedInput(new ByteArrayInputStream(encoded))) {
+ Header root = HeaderReader.read(input);
+ requireSequence(root);
+ long rootEnd = checkedAdd(input.position(), root.length());
+ Header algorithm = HeaderReader.read(input);
+ requireSequence(algorithm);
+ skip(input, algorithm.length(), CancellationSignal.NONE);
+ Header publicKey = HeaderReader.read(input);
+ if (!publicKey.universal() || publicKey.tagNumber() != 3 || publicKey.constructed()
+ || publicKey.length() < 1L || input.readRequired() != 0) {
+ throw new IOException("Malformed SubjectPublicKeyInfo BIT STRING");
+ }
+ skip(input, publicKey.length() - 1L, CancellationSignal.NONE);
+ if (input.position() != rootEnd || input.position() != length) {
+ throw new IOException("Malformed SubjectPublicKeyInfo structure");
+ }
+ }
+ return length;
+ }
+
/**
* Validates one complete canonical DER object.
*
@@ -108,7 +188,7 @@ public final class StreamingDerReader {
try (CountedInput input = new CountedInput(content.openStream())) {
Header outer = HeaderReader.read(input);
requireSequence(outer);
- long outerEnd = Math.addExact(input.position(), outer.length());
+ long outerEnd = checkedAdd(input.position(), outer.length());
Header tbs = HeaderReader.read(input);
requireSequence(tbs);
long tbsTotalLength = tbs.encodedLength();
@@ -133,7 +213,8 @@ public final class StreamingDerReader {
}
return new SignedObjectLayout(tbs.start(), tbsTotalLength, inner.algorithmOffset(),
inner.algorithmLength(), outerAlgorithm.start(), outerAlgorithm.encodedLength(),
- inner.spkiOffset(), inner.spkiLength(), signatureOffset, signature.length() - 1L);
+ inner.spkiOffset(), inner.spkiLength(), signatureOffset, signature.length() - 1L,
+ inner.issuerOffset(), inner.issuerLength(), inner.thisUpdate(), inner.nextUpdate());
}
}
@@ -150,16 +231,12 @@ public final class StreamingDerReader {
requireUniversal(child, 2);
skip(input, child.length(), cancellation);
child = HeaderReader.read(input);
- } else if (child.universal() && child.tagNumber() == 2) {
- skip(input, child.length(), cancellation);
- child = HeaderReader.read(input);
+ } else {
+ return inspectCrlTbs(input, tbs, child, cancellation);
}
requireSequence(child);
long algorithmOffset = child.start();
long algorithmLength = child.encodedLength();
- if (kind == SignedObjectKind.CRL) {
- return new AlgorithmAndKey(algorithmOffset, algorithmLength, -1L, 0L);
- }
skip(input, child.length(), cancellation);
for (int index = 0; index < 3; index++) {
Header field = HeaderReader.read(input);
@@ -167,7 +244,213 @@ public final class StreamingDerReader {
}
Header spki = HeaderReader.read(input);
requireSequence(spki);
- return new AlgorithmAndKey(algorithmOffset, algorithmLength, spki.start(), spki.encodedLength());
+ return new AlgorithmAndKey(algorithmOffset, algorithmLength, spki.start(), spki.encodedLength(),
+ -1L, 0L, Optional.empty(), Optional.empty());
+ }
+ }
+
+ private static AlgorithmAndKey inspectCrlTbs(CountedInput input, Header tbs, Header firstChild,
+ CancellationSignal cancellation) throws IOException {
+ long tbsEnd = checkedAdd(tbs.valueOffset(), tbs.length());
+ Header child = firstChild;
+ boolean v2 = false;
+ if (child.universal() && child.tagNumber() == 2) {
+ requireCrlVersion(input, child, cancellation);
+ v2 = true;
+ child = readWithin(input, tbsEnd);
+ }
+
+ requireAlgorithmIdentifier(input, child, cancellation);
+ long algorithmOffset = child.start();
+ long algorithmLength = child.encodedLength();
+
+ Header issuer = readWithin(input, tbsEnd);
+ requireSequence(issuer);
+ if (issuer.length() == 0L) {
+ throw new IOException("TBSCertList issuer Name must not be empty");
+ }
+ skip(input, issuer.length(), cancellation);
+
+ Header thisUpdateHeader = readWithin(input, tbsEnd);
+ Instant thisUpdate = readCrlTime(input, thisUpdateHeader);
+ Optional nextUpdate = Optional.empty();
+ Header optional = input.position() < tbsEnd ? readWithin(input, tbsEnd) : null;
+ if (optional != null && isTime(optional)) {
+ Instant parsedNextUpdate = readCrlTime(input, optional);
+ if (!parsedNextUpdate.isAfter(thisUpdate)) {
+ throw new IOException("CRL nextUpdate must follow thisUpdate");
+ }
+ nextUpdate = Optional.of(parsedNextUpdate);
+ optional = input.position() < tbsEnd ? readWithin(input, tbsEnd) : null;
+ }
+ if (optional != null && optional.universal() && optional.tagNumber() == 16) {
+ readRevokedCertificates(input, optional, v2, cancellation);
+ optional = input.position() < tbsEnd ? readWithin(input, tbsEnd) : null;
+ }
+ if (optional != null && optional.contextSpecific(0)) {
+ if (!v2) {
+ throw new IOException("CRL extensions require TBSCertList v2");
+ }
+ readExplicitExtensions(input, optional, cancellation);
+ optional = input.position() < tbsEnd ? readWithin(input, tbsEnd) : null;
+ }
+ if (optional != null || input.position() != tbsEnd) {
+ throw new IOException("Malformed TBSCertList optional fields");
+ }
+ return new AlgorithmAndKey(algorithmOffset, algorithmLength, -1L, 0L, issuer.start(),
+ issuer.encodedLength(), Optional.of(thisUpdate), nextUpdate);
+ }
+
+ private static void requireCrlVersion(CountedInput input, Header version, CancellationSignal cancellation)
+ throws IOException {
+ requireUniversal(version, 2);
+ if (version.constructed() || version.length() != 1L || input.readRequired() != 1) {
+ throw new IOException("TBSCertList version must be canonical v2(1)");
+ }
+ skip(input, version.length() - 1L, cancellation);
+ }
+
+ private static void requireAlgorithmIdentifier(CountedInput input, Header algorithm,
+ CancellationSignal cancellation) throws IOException {
+ requireSequence(algorithm);
+ long end = checkedAdd(algorithm.valueOffset(), algorithm.length());
+ Header oid = readWithin(input, end);
+ requirePrimitiveUniversal(oid, 6);
+ PrimitiveReader.read(input, oid, cancellation);
+ if (input.position() < end) {
+ Header parameters = readWithin(input, end);
+ skip(input, parameters.length(), cancellation);
+ }
+ if (input.position() != end) {
+ throw new IOException("Malformed AlgorithmIdentifier");
+ }
+ }
+
+ private static Instant readCrlTime(CountedInput input, Header time) throws IOException {
+ if (!isTime(time) || time.constructed()) {
+ throw new IOException("Expected TBSCertList Time");
+ }
+ return PrimitiveReader.readTime(input, time.length(), time.tagNumber() == 24).toInstant(ZoneOffset.UTC);
+ }
+
+ private static boolean isTime(Header header) {
+ return header.universal() && (header.tagNumber() == 23 || header.tagNumber() == 24);
+ }
+
+ private static void readRevokedCertificates(CountedInput input, Header revokedCertificates, boolean v2,
+ CancellationSignal cancellation) throws IOException {
+ requireSequence(revokedCertificates);
+ long end = checkedAdd(revokedCertificates.valueOffset(), revokedCertificates.length());
+ while (input.position() < end) {
+ cancellation.throwIfCancelled();
+ Header entry = readWithin(input, end);
+ requireSequence(entry);
+ readRevokedCertificate(input, entry, v2, cancellation);
+ }
+ if (input.position() != end) {
+ throw new IOException("Malformed revokedCertificates");
+ }
+ }
+
+ private static void readRevokedCertificate(CountedInput input, Header entry, boolean v2,
+ CancellationSignal cancellation) throws IOException {
+ long end = checkedAdd(entry.valueOffset(), entry.length());
+ Header serial = readWithin(input, end);
+ readPositiveSerial(input, serial, cancellation);
+ Header revocationDate = readWithin(input, end);
+ readCrlTime(input, revocationDate);
+ if (input.position() < end) {
+ if (!v2) {
+ throw new IOException("CRL entry extensions require TBSCertList v2");
+ }
+ Header extensions = readWithin(input, end);
+ readExtensions(input, extensions, cancellation);
+ }
+ if (input.position() != end) {
+ throw new IOException("Malformed revoked certificate entry");
+ }
+ }
+
+ private static void readPositiveSerial(CountedInput input, Header serial, CancellationSignal cancellation)
+ throws IOException {
+ requirePrimitiveUniversal(serial, 2);
+ if (serial.length() == 0L || serial.length() > 20L) {
+ throw new IOException("CRL serial number must be positive");
+ }
+ int first = input.readRequired();
+ if ((first & CONTINUATION_BIT) != 0 || first == 0 && serial.length() == 1L) {
+ throw new IOException("CRL serial number must be positive");
+ }
+ skip(input, serial.length() - 1L, cancellation);
+ }
+
+ private static void readExplicitExtensions(CountedInput input, Header explicit,
+ CancellationSignal cancellation) throws IOException {
+ if (!explicit.constructed()) {
+ throw new IOException("CRL extensions must be [0] EXPLICIT");
+ }
+ long end = checkedAdd(explicit.valueOffset(), explicit.length());
+ Header extensions = readWithin(input, end);
+ readExtensions(input, extensions, cancellation);
+ if (input.position() != end) {
+ throw new IOException("Malformed explicit CRL extensions");
+ }
+ }
+
+ private static void readExtensions(CountedInput input, Header extensions, CancellationSignal cancellation)
+ throws IOException {
+ requireSequence(extensions);
+ long end = checkedAdd(extensions.valueOffset(), extensions.length());
+ if (input.position() == end) {
+ throw new IOException("Extensions must contain at least one Extension");
+ }
+ while (input.position() < end) {
+ cancellation.throwIfCancelled();
+ Header extension = readWithin(input, end);
+ requireSequence(extension);
+ readExtension(input, extension, cancellation);
+ }
+ if (input.position() != end) {
+ throw new IOException("Malformed Extensions");
+ }
+ }
+
+ private static void readExtension(CountedInput input, Header extension, CancellationSignal cancellation)
+ throws IOException {
+ long end = checkedAdd(extension.valueOffset(), extension.length());
+ Header oid = readWithin(input, end);
+ requirePrimitiveUniversal(oid, 6);
+ PrimitiveReader.read(input, oid, cancellation);
+ Header value = readWithin(input, end);
+ if (value.universal() && value.tagNumber() == 1) {
+ requirePrimitiveUniversal(value, 1);
+ if (value.length() != 1L || input.readRequired() != 0xff) {
+ throw new IOException("Extension critical DEFAULT FALSE must be omitted");
+ }
+ value = readWithin(input, end);
+ }
+ requirePrimitiveUniversal(value, 4);
+ skip(input, value.length(), cancellation);
+ if (input.position() != end) {
+ throw new IOException("Malformed Extension");
+ }
+ }
+
+ private static Header readWithin(CountedInput input, long end) throws IOException {
+ if (input.position() >= end) {
+ throw new IOException("Missing required DER field");
+ }
+ Header header = HeaderReader.read(input);
+ if (checkedAdd(input.position(), header.length()) > end) {
+ throw new IOException("DER field exceeds enclosing structure");
+ }
+ return header;
+ }
+
+ private static void requirePrimitiveUniversal(Header header, int tagNumber) throws IOException {
+ requireUniversal(header, tagNumber);
+ if (header.constructed()) {
+ throw new IOException("Expected primitive DER field");
}
}
@@ -189,7 +472,7 @@ public final class StreamingDerReader {
if (depth >= MAXIMUM_X509_STRUCTURE_DEPTH) {
throw new IOException("DER nesting exceeds X.509 adapter capability");
}
- long end = Math.addExact(input.position(), header.length());
+ long end = checkedAdd(input.position(), header.length());
if (header.constructed()) {
requireConstructedForm(header);
SetOrdering setOrdering = header.universal() && header.tagNumber() == 17
@@ -198,7 +481,7 @@ public final class StreamingDerReader {
while (input.position() < end) {
cancellation.throwIfCancelled();
Header child = HeaderReader.read(input);
- long childEnd = Math.addExact(input.position(), child.length());
+ long childEnd = checkedAdd(input.position(), child.length());
if (childEnd > end) {
throw new IOException("DER child exceeds parent: code=MALFORMED_SIGNED_OBJECT");
}
@@ -232,8 +515,11 @@ public final class StreamingDerReader {
}
private static void skip(CountedInput input, long length, CancellationSignal cancellation) throws IOException {
- byte[] buffer = new byte[BUFFER_BYTES];
long remaining = length;
+ if (remaining == 0L) {
+ return;
+ }
+ byte[] buffer = input.scratch();
while (remaining != 0L) {
cancellation.throwIfCancelled();
int read = input.read(buffer, 0, (int) Math.min(buffer.length, remaining));
@@ -249,6 +535,14 @@ public final class StreamingDerReader {
}
}
+ private static long checkedAdd(long left, long right) throws IOException {
+ try {
+ return Math.addExact(left, right);
+ } catch (ArithmeticException exception) {
+ throw new IOException("DER length overflow: code=CONTENT_LENGTH_OVERFLOW", exception);
+ }
+ }
+
/**
* Supported signed-object grammar.
*/
@@ -272,14 +566,30 @@ public final class StreamingDerReader {
* @param subjectPublicKeyInfoLength SPKI TLV length, or zero for CRLs
* @param signatureOffset signature octets offset
* @param signatureLength signature octets length
+ * @param issuerOffset issuer Name TLV offset, or {@code -1} for certificates
+ * @param issuerLength issuer Name TLV length, or zero for certificates
+ * @param thisUpdate exact CRL {@code thisUpdate}, or empty for certificates
+ * @param nextUpdate exact optional CRL {@code nextUpdate}, or empty when absent
+ * and for certificates
*/
public record SignedObjectLayout(long tbsOffset, long tbsLength, long tbsAlgorithmOffset,
long tbsAlgorithmLength, long outerAlgorithmOffset, long outerAlgorithmLength,
long subjectPublicKeyInfoOffset, long subjectPublicKeyInfoLength, long signatureOffset,
- long signatureLength) {
+ long signatureLength, long issuerOffset, long issuerLength, Optional thisUpdate,
+ Optional nextUpdate) {
+ /**
+ * Creates an immutable layout for one validated signed object.
+ *
+ * @throws NullPointerException if either time container is {@code null}
+ */
+ public SignedObjectLayout {
+ Objects.requireNonNull(thisUpdate, "thisUpdate");
+ Objects.requireNonNull(nextUpdate, "nextUpdate");
+ }
}
- private record AlgorithmAndKey(long algorithmOffset, long algorithmLength, long spkiOffset, long spkiLength) {
+ private record AlgorithmAndKey(long algorithmOffset, long algorithmLength, long spkiOffset, long spkiLength,
+ long issuerOffset, long issuerLength, Optional thisUpdate, Optional nextUpdate) {
}
private record Header(int firstTag, int tagNumber, long length, long start, long valueOffset) {
@@ -295,8 +605,8 @@ public final class StreamingDerReader {
return (firstTag & 0xc0) == 0x80 && tagNumber == expectedTag;
}
- private long encodedLength() {
- return Math.addExact(valueOffset - start, length);
+ private long encodedLength() throws IOException {
+ return checkedAdd(valueOffset - start, length);
}
}
@@ -320,10 +630,20 @@ public final class StreamingDerReader {
if ((octet & 0x7f) == 0) {
throw new IOException("Non-minimal DER high tag");
}
- while ((octet & CONTINUATION_BIT) != 0) {
+ tagNumber = 0;
+ while (true) {
+ if (tagNumber > (Integer.MAX_VALUE >>> 7)) {
+ throw new IOException("DER tag number overflow: code=CONTENT_LENGTH_OVERFLOW");
+ }
+ tagNumber = (tagNumber << 7) | (octet & 0x7f);
+ if ((octet & CONTINUATION_BIT) == 0) {
+ break;
+ }
octet = input.readRequired();
}
- tagNumber = -1;
+ if (tagNumber < HIGH_TAG_NUMBER) {
+ throw new IOException("Non-minimal DER high tag");
+ }
}
return tagNumber;
}
@@ -539,6 +859,7 @@ public final class StreamingDerReader {
private static final class CountedInput extends InputStream {
private final InputStream delegate;
private long position;
+ private byte[] scratch;
private CountedInput(InputStream delegate) {
super();
@@ -549,7 +870,7 @@ public final class StreamingDerReader {
public int read() throws IOException {
int value = delegate.read();
if (value >= 0) {
- position = Math.addExact(position, 1L);
+ position = checkedAdd(position, 1L);
}
return value;
}
@@ -558,7 +879,7 @@ public final class StreamingDerReader {
public int read(byte[] bytes, int offset, int length) throws IOException {
int count = delegate.read(bytes, offset, length);
if (count > 0) {
- position = Math.addExact(position, count);
+ position = checkedAdd(position, count);
}
return count;
}
@@ -579,6 +900,13 @@ public final class StreamingDerReader {
private long position() {
return position;
}
+
+ private byte[] scratch() {
+ if (scratch == null) {
+ scratch = new byte[BUFFER_BYTES];
+ }
+ return scratch;
+ }
}
/**
@@ -596,11 +924,21 @@ public final class StreamingDerReader {
}
switch (header.tagNumber()) {
case 1 -> readBoolean(input, header.length());
- case 2 -> readInteger(input, header.length(), cancellation);
+ case 2, 10 -> readInteger(input, header.length(), cancellation);
case 3 -> readBitString(input, header.length(), cancellation);
+ case 4, 20 -> skip(input, header.length(), cancellation);
case 5 -> readNull(header.length());
case 6 -> readOid(input, header.length(), cancellation);
- default -> skip(input, header.length(), cancellation);
+ case 12 -> readUtf8String(input, header.length(), cancellation);
+ case 18 -> readRestrictedString(input, header.length(), StringKind.NUMERIC, cancellation);
+ case 19 -> readRestrictedString(input, header.length(), StringKind.PRINTABLE, cancellation);
+ case 22 -> readRestrictedString(input, header.length(), StringKind.IA5, cancellation);
+ case 23 -> readTime(input, header.length(), false);
+ case 24 -> readTime(input, header.length(), true);
+ case 26 -> readRestrictedString(input, header.length(), StringKind.VISIBLE, cancellation);
+ case 28 -> readUniversalString(input, header.length(), cancellation);
+ case 30 -> readBmpString(input, header.length(), cancellation);
+ default -> throw new IOException("Unsupported universal DER primitive");
}
}
@@ -677,5 +1015,207 @@ public final class StreamingDerReader {
throw new IOException("Truncated DER OID");
}
}
+
+ private static void readUtf8String(CountedInput input, long length, CancellationSignal cancellation)
+ throws IOException {
+ long remaining = length;
+ while (remaining > 0L) {
+ cancellation.throwIfCancelled();
+ int first = input.readRequired();
+ remaining--;
+ if (first <= 0x7f) {
+ continue;
+ }
+ int continuationCount;
+ int codePoint;
+ int minimum;
+ if (first >= 0xc2 && first <= 0xdf) {
+ continuationCount = 1;
+ codePoint = first & 0x1f;
+ minimum = 0x80;
+ } else if (first >= 0xe0 && first <= 0xef) {
+ continuationCount = 2;
+ codePoint = first & 0x0f;
+ minimum = 0x800;
+ } else if (first >= 0xf0 && first <= 0xf4) {
+ continuationCount = 3;
+ codePoint = first & 0x07;
+ minimum = 0x10000;
+ } else {
+ throw new IOException("Malformed DER UTF8String");
+ }
+ if (remaining < continuationCount) {
+ throw new IOException("Truncated DER UTF8String");
+ }
+ for (int index = 0; index < continuationCount; index++) {
+ int octet = input.readRequired();
+ if ((octet & 0xc0) != 0x80) {
+ throw new IOException("Malformed DER UTF8String");
+ }
+ codePoint = (codePoint << 6) | (octet & 0x3f);
+ }
+ remaining -= continuationCount;
+ if (codePoint < minimum || codePoint > 0x10ffff
+ || codePoint >= 0xd800 && codePoint <= 0xdfff) {
+ throw new IOException("Non-canonical DER UTF8String");
+ }
+ }
+ }
+
+ private static void readRestrictedString(CountedInput input, long length, StringKind kind,
+ CancellationSignal cancellation) throws IOException {
+ for (long index = 0L; index < length; index++) {
+ cancellation.throwIfCancelled();
+ int octet = input.readRequired();
+ if (!kind.accepts(octet)) {
+ throw new IOException("Malformed DER " + kind.label());
+ }
+ }
+ }
+
+ private static LocalDateTime readTime(CountedInput input, long length, boolean generalized)
+ throws IOException {
+ int expectedLength = generalized ? 15 : 13;
+ if (length != expectedLength) {
+ throw new IOException("Non-canonical DER time");
+ }
+ byte[] value = new byte[expectedLength];
+ for (int index = 0; index < value.length; index++) {
+ value[index] = (byte) input.readRequired();
+ }
+ if (value[value.length - 1] != 'Z') {
+ throw new IOException("Non-canonical DER time");
+ }
+ int digitCount = value.length - 1;
+ for (int index = 0; index < digitCount; index++) {
+ if (value[index] < '0' || value[index] > '9') {
+ throw new IOException("Malformed DER time");
+ }
+ }
+ int offset;
+ int year;
+ if (generalized) {
+ year = decimal(value, 0, 4);
+ offset = 4;
+ } else {
+ int shortYear = decimal(value, 0, 2);
+ year = shortYear >= 50 ? 1900 + shortYear : 2000 + shortYear;
+ offset = 2;
+ }
+ try {
+ return LocalDateTime.of(year, decimal(value, offset, 2), decimal(value, offset + 2, 2),
+ decimal(value, offset + 4, 2), decimal(value, offset + 6, 2),
+ decimal(value, offset + 8, 2));
+ } catch (DateTimeException exception) {
+ throw new IOException("Malformed DER time", exception);
+ }
+ }
+
+ private static int decimal(byte[] value, int offset, int length) {
+ int result = 0;
+ for (int index = 0; index < length; index++) {
+ result = result * 10 + value[offset + index] - '0';
+ }
+ return result;
+ }
+
+ private static void readUniversalString(CountedInput input, long length, CancellationSignal cancellation)
+ throws IOException {
+ if ((length & 3L) != 0L) {
+ throw new IOException("Malformed DER UniversalString");
+ }
+ for (long index = 0L; index < length; index += 4L) {
+ cancellation.throwIfCancelled();
+ long codePoint = ((long) input.readRequired() << 24) | ((long) input.readRequired() << 16)
+ | ((long) input.readRequired() << 8) | input.readRequired();
+ if (codePoint > 0x10ffffL || codePoint >= 0xd800L && codePoint <= 0xdfffL) {
+ throw new IOException("Malformed DER UniversalString");
+ }
+ }
+ }
+
+ private static void readBmpString(CountedInput input, long length, CancellationSignal cancellation)
+ throws IOException {
+ if ((length & 1L) != 0L) {
+ throw new IOException("Malformed DER BMPString");
+ }
+ for (long index = 0L; index < length; index += 2L) {
+ cancellation.throwIfCancelled();
+ int codeUnit = (input.readRequired() << 8) | input.readRequired();
+ if (codeUnit >= 0xd800 && codeUnit <= 0xdfff) {
+ throw new IOException("Malformed DER BMPString");
+ }
+ }
+ }
+
+ /** Supported restricted X.509 character-string alphabets. */
+ private enum StringKind {
+ NUMERIC("NumericString") {
+ @Override
+ boolean accepts(int octet) {
+ return octet == ' ' || octet >= '0' && octet <= '9';
+ }
+ },
+ PRINTABLE("PrintableString") {
+ @Override
+ boolean accepts(int octet) {
+ return octet >= 'A' && octet <= 'Z' || octet >= 'a' && octet <= 'z'
+ || octet >= '0' && octet <= '9' || " '()+,-./:=?".indexOf(octet) >= 0;
+ }
+ },
+ IA5("IA5String") {
+ @Override
+ boolean accepts(int octet) {
+ return octet <= 0x7f;
+ }
+ },
+ VISIBLE("VisibleString") {
+ @Override
+ boolean accepts(int octet) {
+ return octet >= 0x20 && octet <= 0x7e;
+ }
+ };
+
+ private final String label;
+
+ StringKind(String label) {
+ this.label = label;
+ }
+
+ abstract boolean accepts(int octet);
+
+ private String label() {
+ return label;
+ }
+ }
+ }
+
+ /** Non-copying adapter whose lifetime is confined to one synchronous call. */
+ private static final class ByteArrayContent implements RepeatableContent {
+ private final byte[] encoded;
+
+ private ByteArrayContent(byte[] encoded) {
+ this.encoded = encoded;
+ }
+
+ @Override
+ public InputStream openStream() {
+ return new ByteArrayInputStream(encoded);
+ }
+
+ @Override
+ public OptionalLong length() {
+ return OptionalLong.of(encoded.length);
+ }
+
+ @Override
+ public String contentId() {
+ return "bounded-byte-array";
+ }
+
+ @Override
+ public void close() {
+ // Caller retains ownership of the bounded array.
+ }
}
}
diff --git a/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/BcX509AlgorithmAdapter.java b/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/BcX509AlgorithmAdapter.java
index 72a5ef8..6cca5fb 100644
--- a/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/BcX509AlgorithmAdapter.java
+++ b/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/BcX509AlgorithmAdapter.java
@@ -35,7 +35,6 @@ package zeroecho.pki.impl.framework.x509.bc;
import java.io.IOException;
import java.util.Objects;
-import java.util.Arrays;
import org.bouncycastle.asn1.ASN1Encoding;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
@@ -140,29 +139,4 @@ public final class BcX509AlgorithmAdapter {
}
}
- /**
- * Requires one complete canonical DER object before a BC structure parser is
- * invoked.
- *
- * @param encoded complete DER object
- * @throws IllegalArgumentException for trailing data, BER forms, or
- * non-canonical DER
- */
- /* default */ static void requireCanonicalDer(byte[] encoded) {
- Objects.requireNonNull(encoded, "encoded");
- ASN1Primitive primitive;
- try {
- primitive = ASN1Primitive.fromByteArray(encoded);
- } catch (IOException exception) {
- throw new IllegalArgumentException("Invalid canonical DER object", exception);
- }
- try {
- byte[] canonical = primitive.getEncoded(ASN1Encoding.DER);
- if (!Arrays.equals(encoded, canonical)) {
- throw new IllegalArgumentException("Input is not one canonical DER object");
- }
- } catch (IOException exception) {
- throw new IllegalArgumentException("Invalid canonical DER object", exception);
- }
- }
}
diff --git a/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/BcX509CertificationRequestParser.java b/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/BcX509CertificationRequestParser.java
index 7637d75..a78f70d 100644
--- a/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/BcX509CertificationRequestParser.java
+++ b/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/BcX509CertificationRequestParser.java
@@ -33,11 +33,9 @@
******************************************************************************/
package zeroecho.pki.impl.framework.x509.bc;
-import java.io.ByteArrayInputStream;
-import java.io.InputStreamReader;
-import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
+import java.util.Base64;
import java.util.HashSet;
import java.util.HexFormat;
import java.util.List;
@@ -60,9 +58,6 @@ import org.bouncycastle.asn1.x509.Extensions;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
-import org.bouncycastle.util.io.pem.PemObject;
-import org.bouncycastle.util.io.pem.PemReader;
-
import zeroecho.pki.api.EncodedObject;
import zeroecho.pki.api.Encoding;
import zeroecho.pki.api.PkiException;
@@ -74,6 +69,7 @@ import zeroecho.pki.api.request.CertificationRequest;
import zeroecho.pki.api.request.ParsedCertificationRequest;
import zeroecho.pki.api.request.SubjectAlternativeName;
import zeroecho.pki.api.request.SubjectRdn;
+import zeroecho.pki.impl.framework.x509.StreamingDerReader;
import zeroecho.pki.spi.framework.CertificationRequestParser;
/**
@@ -133,8 +129,13 @@ import zeroecho.pki.spi.framework.CertificationRequestParser;
public final class BcX509CertificationRequestParser implements CertificationRequestParser {
private static final int MAXIMUM_SUBJECT_DER_BYTES = 16 * 1024;
private static final int MAXIMUM_SAN_DER_BYTES = 32 * 1024;
+ /* default */ static final int MAXIMUM_CSR_DER_BYTES = 1024 * 1024;
+ private static final int MAXIMUM_CSR_ENCODED_BYTES = 2 * 1024 * 1024;
private static final int MAXIMUM_SUBJECT_RDNS = 32;
private static final int SINGLE_ATTRIBUTE_VALUE = 1;
+ private static final String CERTIFICATE_REQUEST = "CERTIFICATE REQUEST";
+ private static final String NEW_CERTIFICATE_REQUEST = "NEW CERTIFICATE REQUEST";
+ private static final String CSR_PEM_INVALID = "Invalid PEM: code=CSR_PEM_INVALID";
/**
* Parses a PKCS#10 certification request into the normalized PKI request
@@ -179,12 +180,20 @@ public final class BcX509CertificationRequestParser implements CertificationRequ
}
EncodedObject enc = request.encoded();
- byte[] csrDer = toDer(enc);
+ byte[] csrDer;
+ try {
+ csrDer = toDer(enc);
+ } catch (IllegalArgumentException exception) {
+ if (enc.encoding() != Encoding.DER && enc.encoding() != Encoding.PEM) {
+ throw exception;
+ }
+ throw new PkiException("Invalid PKCS#10 certification request: code=CSR_MALFORMED");
+ }
byte[] spki = null;
try {
PKCS10CertificationRequest csr;
try {
- BcX509AlgorithmAdapter.requireCanonicalDer(csrDer);
+ new StreamingDerReader().validate(csrDer, MAXIMUM_CSR_DER_BYTES);
csr = new PKCS10CertificationRequest(csrDer);
} catch (Exception ex) {
throw new PkiException("Invalid PKCS#10 certification request: code=CSR_MALFORMED");
@@ -364,20 +373,21 @@ public final class BcX509CertificationRequestParser implements CertificationRequ
*/
private static byte[] toDer(EncodedObject obj) {
Objects.requireNonNull(obj, "obj");
-
- return switch (obj.encoding()) {
- case DER -> obj.bytes();
- case PEM -> pemToDer(obj);
- default -> throw new IllegalArgumentException("Unsupported CSR encoding: " + obj.encoding());
- };
- }
-
- private static byte[] pemToDer(EncodedObject obj) {
- byte[] pemBytes = obj.bytes();
+ byte[] encoded = obj.bytes();
+ if (obj.encoding() == Encoding.DER) {
+ return requireDecodedLimit(encoded);
+ }
try {
- return readPemContentOrThrow(pemBytes);
+ if (encoded.length > MAXIMUM_CSR_ENCODED_BYTES) {
+ throw new IllegalArgumentException("CSR encoding exceeds element limit");
+ }
+ return switch (obj.encoding()) {
+ case DER -> throw new IllegalStateException("DER handled before PEM conversion");
+ case PEM -> readPemContentOrThrow(encoded);
+ default -> throw new IllegalArgumentException("Unsupported CSR encoding: " + obj.encoding());
+ };
} finally {
- java.util.Arrays.fill(pemBytes, (byte) 0);
+ java.util.Arrays.fill(encoded, (byte) 0);
}
}
@@ -385,9 +395,8 @@ public final class BcX509CertificationRequestParser implements CertificationRequ
* Reads the binary content of a PEM object.
*
*
- * The input is interpreted as US-ASCII text and parsed using {@link PemReader}.
- * If the PEM container is syntactically valid and contains an object, the raw
- * decoded content of that object is returned.
+ * The input is interpreted as US-ASCII and must contain exactly one accepted
+ * PKCS#10 PEM object with whitespace only outside the object.
*
*
*
@@ -401,20 +410,89 @@ public final class BcX509CertificationRequestParser implements CertificationRequ
* @throws IllegalArgumentException if the PEM input is empty or cannot be
* parsed
*/
+ @SuppressWarnings("PMD.AvoidThrowingNewInstanceOfSameException")
private static byte[] readPemContentOrThrow(byte[] pemBytes) {
Objects.requireNonNull(pemBytes, "pemBytes");
-
- try (PemReader reader = new PemReader(
- new InputStreamReader(new ByteArrayInputStream(pemBytes), StandardCharsets.US_ASCII))) {
-
- PemObject pemObject = reader.readPemObject();
- if (pemObject == null) {
- throw new IllegalArgumentException("Empty PEM");
+ for (byte value : pemBytes) {
+ if (value < 0) {
+ throw new IllegalArgumentException(CSR_PEM_INVALID);
}
- return pemObject.getContent();
- } catch (java.io.IOException ex) {
- throw new IllegalArgumentException("Invalid PEM: code=CSR_PEM_INVALID");
}
+ String text = new String(pemBytes, java.nio.charset.StandardCharsets.US_ASCII);
+ int begin = skipWhitespace(text, 0);
+ String label;
+ String certificateRequestBegin = "-----BEGIN " + CERTIFICATE_REQUEST + "-----";
+ String newCertificateRequestBegin = "-----BEGIN " + NEW_CERTIFICATE_REQUEST + "-----";
+ if (text.startsWith(certificateRequestBegin, begin)) {
+ label = CERTIFICATE_REQUEST;
+ } else if (text.startsWith(newCertificateRequestBegin, begin)) {
+ label = NEW_CERTIFICATE_REQUEST;
+ } else {
+ throw new IllegalArgumentException(CSR_PEM_INVALID);
+ }
+ int bodyStart = begin + "-----BEGIN ".length() + label.length() + "-----".length();
+ if (bodyStart >= text.length() || text.charAt(bodyStart) != '\n'
+ && (text.charAt(bodyStart) != '\r' || bodyStart + 1 >= text.length()
+ || text.charAt(bodyStart + 1) != '\n')) {
+ throw new IllegalArgumentException(CSR_PEM_INVALID);
+ }
+ String endMarker = "-----END " + label + "-----";
+ int end = text.indexOf(endMarker, bodyStart);
+ if (end <= bodyStart || text.charAt(end - 1) != '\n'
+ || skipWhitespace(text, end + endMarker.length()) != text.length()) {
+ throw new IllegalArgumentException(CSR_PEM_INVALID);
+ }
+ StringBuilder compact = new StringBuilder(end - bodyStart);
+ for (int index = bodyStart; index < end; index++) {
+ char value = text.charAt(index);
+ if (isWhitespace(value)) {
+ continue;
+ }
+ if (isBase64(value) || value == '=') {
+ compact.append(value);
+ } else {
+ throw new IllegalArgumentException(CSR_PEM_INVALID);
+ }
+ }
+ if (compact.isEmpty() || (compact.length() + 3L) / 4L * 3L > MAXIMUM_CSR_DER_BYTES + 2L) {
+ throw new IllegalArgumentException(CSR_PEM_INVALID);
+ }
+ byte[] decoded;
+ try {
+ decoded = Base64.getDecoder().decode(compact.toString());
+ } catch (IllegalArgumentException exception) {
+ throw new IllegalArgumentException(CSR_PEM_INVALID);
+ }
+ if (!Base64.getEncoder().encodeToString(decoded).contentEquals(compact)) {
+ java.util.Arrays.fill(decoded, (byte) 0);
+ throw new IllegalArgumentException(CSR_PEM_INVALID);
+ }
+ return requireDecodedLimit(decoded);
+ }
+
+ private static byte[] requireDecodedLimit(byte[] decoded) {
+ if (decoded.length > MAXIMUM_CSR_DER_BYTES) {
+ java.util.Arrays.fill(decoded, (byte) 0);
+ throw new IllegalArgumentException("CSR DER exceeds element limit");
+ }
+ return decoded;
+ }
+
+ private static int skipWhitespace(String value, int offset) {
+ int index = offset;
+ while (index < value.length() && isWhitespace(value.charAt(index))) {
+ index++;
+ }
+ return index;
+ }
+
+ private static boolean isWhitespace(char value) {
+ return value == ' ' || value == '\t' || value == '\r' || value == '\n';
+ }
+
+ private static boolean isBase64(char value) {
+ return value >= 'A' && value <= 'Z' || value >= 'a' && value <= 'z'
+ || value >= '0' && value <= '9' || value == '+' || value == '/';
}
/**
diff --git a/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/BcX509CredentialIssuerBackend.java b/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/BcX509CredentialIssuerBackend.java
index 67f0993..92be442 100644
--- a/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/BcX509CredentialIssuerBackend.java
+++ b/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/BcX509CredentialIssuerBackend.java
@@ -41,6 +41,7 @@ import java.security.MessageDigest;
import java.time.Duration;
import java.util.Date;
import java.util.HexFormat;
+import java.util.Optional;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.DEROctetString;
@@ -57,6 +58,8 @@ import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import zeroecho.core.spec.AlgorithmIdentity;
+import zeroecho.core.io.CancellationSignal;
+import zeroecho.core.io.ImmutableByteContent;
import zeroecho.core.io.RepeatableContent;
import zeroecho.pki.api.EncodedObject;
import zeroecho.pki.api.Encoding;
@@ -78,6 +81,8 @@ import zeroecho.pki.impl.core.ValidatedCaCertificateRequest;
import zeroecho.pki.impl.core.ValidatedCertificateRequest;
import zeroecho.pki.impl.core.async.PkiSigningBus;
import zeroecho.pki.impl.core.attr.SimpleAttributeSet;
+import zeroecho.pki.impl.framework.x509.StreamingDerReader;
+import zeroecho.pki.impl.framework.x509.X509AlgorithmRole;
import zeroecho.pki.spi.framework.CredentialIssuerBackend;
import zeroecho.pki.spi.store.ContentSink;
@@ -124,9 +129,12 @@ import zeroecho.pki.spi.store.ContentSink;
*
*/
// PMD cannot infer that retaining backend causes would violate the redaction contract.
-@SuppressWarnings("PMD.PreserveStackTrace")
+@SuppressWarnings({ "PMD.PreserveStackTrace", "PMD.CyclomaticComplexity" })
public final class BcX509CredentialIssuerBackend implements CredentialIssuerBackend {
+ private static final int MAXIMUM_CERTIFICATE_BYTES = 1024 * 1024;
+ private static final int MAXIMUM_SPKI_BYTES = 1024 * 1024;
+
private final PkiSigningBus signingBus;
private final AlgorithmIdentity signatureIdentity;
private final Duration signingTtl;
@@ -342,7 +350,7 @@ public final class BcX509CredentialIssuerBackend implements CredentialIssuerBack
SubjectPublicKeyInfo spki;
PkiId publicKeyId;
try {
- spki = SubjectPublicKeyInfo.getInstance(subjectSpki);
+ spki = parseSubjectPublicKeyInfo(subjectSpki);
publicKeyId = new PkiId("spki:" + sha256Hex(subjectSpki));
} finally {
java.util.Arrays.fill(subjectSpki, (byte) 0);
@@ -394,6 +402,9 @@ public final class BcX509CredentialIssuerBackend implements CredentialIssuerBack
}
private DurableContentReference stageCertificate(byte[] certificate) {
+ if (certificate.length > MAXIMUM_CERTIFICATE_BYTES) {
+ throw new PkiException("Certificate exceeds artifact limit: code=CERTIFICATE_TOO_LARGE");
+ }
try (ContentSink sink = signingBus.beginContent(Encoding.DER, DurableContentReference.Lifecycle.PERSISTED);
OutputStream output = sink.outputStream()) {
output.write(certificate);
@@ -406,7 +417,7 @@ public final class BcX509CredentialIssuerBackend implements CredentialIssuerBack
private void validateGeneratedCertificate(DurableContentReference reference, PkiBusContentSigner signer) {
try (RepeatableContent content = signingBus.openContent(reference)) {
new BcX509SignedObjectValidator(signingBus.authority()).validateGeneratedCertificate(content,
- signer.executionPlan(), zeroecho.core.io.CancellationSignal.NONE);
+ signer.executionPlan(), CancellationSignal.NONE);
} catch (IOException | IllegalArgumentException exception) {
signingBus.releaseContent(reference);
throw new PkiException("Certificate postcondition failed: code=BACKEND_RESULT_SUBSTITUTION", exception);
@@ -414,10 +425,11 @@ public final class BcX509CredentialIssuerBackend implements CredentialIssuerBack
}
private byte[] materialize(DurableContentReference reference) {
- if (reference.length() > Integer.MAX_VALUE) {
- throw new PkiException("Certificate exceeds BC adapter element domain: code=ADAPTER_ELEMENT_LIMIT_EXCEEDED");
+ long length = reference.length();
+ if (reference.encoding() != Encoding.DER || length <= 0L || length > MAXIMUM_CERTIFICATE_BYTES) {
+ throw new PkiException("Certificate exceeds artifact limit: code=CERTIFICATE_TOO_LARGE");
}
- byte[] result = new byte[(int) reference.length()];
+ byte[] result = new byte[(int) length];
try {
readExact(reference, result);
return result;
@@ -442,10 +454,12 @@ public final class BcX509CredentialIssuerBackend implements CredentialIssuerBack
}
}
- private static X509CertificateHolder parseIssuerCertificateOrThrow(byte[] issuerCertDer) {
+ private X509CertificateHolder parseIssuerCertificateOrThrow(byte[] issuerCertDer) {
try {
+ new BcX509SignedObjectValidator(signingBus.authority()).validateCertificate(
+ new ImmutableByteContent(issuerCertDer), Optional.empty(), CancellationSignal.NONE);
return new X509CertificateHolder(issuerCertDer);
- } catch (Exception ex) {
+ } catch (IOException | IllegalArgumentException ex) {
throw new PkiException("Invalid issuer certificate: code=ISSUER_CERTIFICATE_INVALID");
}
}
@@ -473,15 +487,27 @@ public final class BcX509CredentialIssuerBackend implements CredentialIssuerBack
}
}
- private static SubjectPublicKeyInfo parseSubjectPublicKeyInfo(EncodedObject encoded) {
+ private SubjectPublicKeyInfo parseSubjectPublicKeyInfo(EncodedObject encoded) {
byte[] bytes = encoded.bytes();
try {
- return SubjectPublicKeyInfo.getInstance(bytes);
+ return parseSubjectPublicKeyInfo(bytes);
} finally {
java.util.Arrays.fill(bytes, (byte) 0);
}
}
+ private SubjectPublicKeyInfo parseSubjectPublicKeyInfo(byte[] bytes) {
+ try {
+ new StreamingDerReader().validateSubjectPublicKeyInfo(bytes, MAXIMUM_SPKI_BYTES);
+ SubjectPublicKeyInfo spki = SubjectPublicKeyInfo.getInstance(bytes);
+ new BcX509AlgorithmAdapter(signingBus.authority().bindings()).decode(spki.getAlgorithm(),
+ X509AlgorithmRole.SUBJECT_PUBLIC_KEY_ALGORITHM);
+ return spki;
+ } catch (IOException | IllegalArgumentException exception) {
+ throw new PkiException("Invalid subject public key: code=SUBJECT_KEY_UNSUPPORTED");
+ }
+ }
+
private static String fingerprintEncoded(EncodedObject encoded) {
byte[] bytes = encoded.bytes();
try {
diff --git a/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/BcX509ProofOfPossessionVerifier.java b/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/BcX509ProofOfPossessionVerifier.java
index 713bca8..3ddacd2 100644
--- a/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/BcX509ProofOfPossessionVerifier.java
+++ b/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/BcX509ProofOfPossessionVerifier.java
@@ -49,6 +49,7 @@ import zeroecho.pki.api.request.ProofOfPossessionStatus;
import zeroecho.pki.impl.framework.x509.X509AlgorithmRole;
import zeroecho.pki.impl.framework.x509.X509AuthoritySnapshot;
import zeroecho.pki.impl.framework.x509.X509ExecutionPlan;
+import zeroecho.pki.impl.framework.x509.StreamingDerReader;
import zeroecho.pki.spi.framework.ProofOfPossessionVerifier;
/**
@@ -199,7 +200,8 @@ public final class BcX509ProofOfPossessionVerifier implements ProofOfPossessionV
try {
PKCS10CertificationRequest csr;
try {
- BcX509AlgorithmAdapter.requireCanonicalDer(csrDer);
+ new StreamingDerReader().validate(csrDer,
+ BcX509CertificationRequestParser.MAXIMUM_CSR_DER_BYTES);
csr = new PKCS10CertificationRequest(csrDer);
} catch (Exception ex) {
return new ProofOfPossessionResult(ProofOfPossessionStatus.FAILED, Optional.of("Invalid CSR"));
diff --git a/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/BcX509SignedObjectValidator.java b/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/BcX509SignedObjectValidator.java
index 3292ede..c5daf28 100644
--- a/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/BcX509SignedObjectValidator.java
+++ b/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/BcX509SignedObjectValidator.java
@@ -36,16 +36,19 @@ package zeroecho.pki.impl.framework.x509.bc;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
+import java.time.Instant;
import java.util.Objects;
import java.util.Optional;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
+import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.operator.OperatorCreationException;
import zeroecho.core.io.CancellationSignal;
import zeroecho.core.io.ContentSlice;
+import zeroecho.core.io.ImmutableByteContent;
import zeroecho.core.io.RepeatableContent;
import zeroecho.core.spec.AlgorithmIdentity;
import zeroecho.core.spi.AlgorithmExecutionCapability;
@@ -71,6 +74,8 @@ import zeroecho.pki.spi.crypto.SignatureWorkflow;
public final class BcX509SignedObjectValidator {
private static final int COMPARE_BUFFER_BYTES = 4096;
+ private static final int MAXIMUM_CERTIFICATE_BYTES = 1024 * 1024;
+ private static final int MAXIMUM_ELEMENT_BYTES = 1024 * 1024;
private final X509AuthoritySnapshot authority;
private final StreamingDerReader reader;
@@ -100,6 +105,10 @@ public final class BcX509SignedObjectValidator {
Objects.requireNonNull(content, "content");
Objects.requireNonNull(expectedSignature, "expectedSignature");
Objects.requireNonNull(cancellation, "cancellation");
+ long contentLength = content.length().orElse(-1L);
+ if (contentLength <= 0L || contentLength > MAXIMUM_CERTIFICATE_BYTES) {
+ throw new IOException("Certificate exceeds adapter domain: code=ADAPTER_ELEMENT_LIMIT_EXCEEDED");
+ }
StreamingDerReader.SignedObjectLayout layout = reader.inspectSignedObject(content,
StreamingDerReader.SignedObjectKind.CERTIFICATE, cancellation);
ContentSlice tbsAlgorithm = new ContentSlice(content, layout.tbsAlgorithmOffset(),
@@ -141,25 +150,43 @@ public final class BcX509SignedObjectValidator {
/**
* Validates a generated CRL against its exact live signing plan and issuer
- * public key.
+ * certificate.
*
* @param content original complete CRL DER
* @param plan exact non-forgeable SIGN plan used for generation
- * @param issuerPublicKey authorized issuer SPKI
+ * @param issuerCertificate authorized issuer certificate whose exact subject
+ * Name and SPKI must bind the CRL
+ * @param expectedThisUpdate exact {@code thisUpdate} metadata to bind to the
+ * signed TBSCertList
+ * @param expectedNextUpdate exact optional {@code nextUpdate} metadata to bind
+ * to the signed TBSCertList
* @param cancellation operation cancellation signal
* @throws IOException if canonical DER, binding, suite, or signature
* validation fails
* @throws IllegalArgumentException if the plan belongs to another runtime
*/
public void validateGeneratedCrl(RepeatableContent content, X509ExecutionPlan plan,
- SubjectPublicKeyInfo issuerPublicKey, CancellationSignal cancellation) throws IOException {
+ X509CertificateHolder issuerCertificate, Instant expectedThisUpdate,
+ Optional expectedNextUpdate, CancellationSignal cancellation) throws IOException {
Objects.requireNonNull(content, "content");
Objects.requireNonNull(plan, "plan");
- Objects.requireNonNull(issuerPublicKey, "issuerPublicKey");
+ Objects.requireNonNull(issuerCertificate, "issuerCertificate");
+ Objects.requireNonNull(expectedThisUpdate, "expectedThisUpdate");
+ Objects.requireNonNull(expectedNextUpdate, "expectedNextUpdate");
Objects.requireNonNull(cancellation, "cancellation");
authority.authorize(plan, plan.executor(), AlgorithmExecutionCapability.Direction.SIGN);
StreamingDerReader.SignedObjectLayout layout = reader.inspectSignedObject(content,
StreamingDerReader.SignedObjectKind.CRL, cancellation);
+ if (!layout.thisUpdate().equals(Optional.of(expectedThisUpdate))
+ || !layout.nextUpdate().equals(expectedNextUpdate)) {
+ throw new IOException("CRL update times differ from metadata: code=STATUS_OBJECT_BINDING_MISMATCH");
+ }
+ ContentSlice issuer = new ContentSlice(content, layout.issuerOffset(), layout.issuerLength());
+ try (RepeatableContent expectedIssuer = new ImmutableByteContent(issuerCertificate.getSubject().getEncoded())) {
+ if (!equalContent(issuer, expectedIssuer, cancellation)) {
+ throw new IOException("CRL issuer differs from issuer certificate: code=STATUS_OBJECT_BINDING_MISMATCH");
+ }
+ }
ContentSlice tbsAlgorithm = new ContentSlice(content, layout.tbsAlgorithmOffset(),
layout.tbsAlgorithmLength());
ContentSlice outerAlgorithm = new ContentSlice(content, layout.outerAlgorithmOffset(),
@@ -172,6 +199,7 @@ public final class BcX509SignedObjectValidator {
if (!plan.selection().requested().equals(signatureIdentity)) {
throw new IOException("CRL signature differs from execution plan: code=STATUS_OBJECT_BINDING_MISMATCH");
}
+ SubjectPublicKeyInfo issuerPublicKey = issuerCertificate.getSubjectPublicKeyInfo();
AlgorithmIdentity issuerKey = new BcX509AlgorithmAdapter(authority.bindings()).decode(
issuerPublicKey.getAlgorithm(), X509AlgorithmRole.SUBJECT_PUBLIC_KEY_ALGORITHM);
X509SuiteCompatibility.requireCompatible(signatureIdentity, issuerKey);
@@ -231,8 +259,8 @@ public final class BcX509SignedObjectValidator {
}
private static byte[] materializeElement(RepeatableContent content) throws IOException {
- long length = content.length().orElseThrow();
- if (length > Integer.MAX_VALUE) {
+ long length = content.length().orElse(-1L);
+ if (length <= 0L || length > MAXIMUM_ELEMENT_BYTES) {
throw new IOException("X.509 element exceeds adapter domain: code=ADAPTER_ELEMENT_LIMIT_EXCEEDED");
}
byte[] encoded = new byte[(int) length];
diff --git a/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/BcX509StatusObjectGenerator.java b/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/BcX509StatusObjectGenerator.java
index 8b3c1a0..c7d7a53 100644
--- a/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/BcX509StatusObjectGenerator.java
+++ b/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/BcX509StatusObjectGenerator.java
@@ -57,6 +57,7 @@ import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
import zeroecho.core.io.CancellationSignal;
import zeroecho.core.io.ContentSlice;
+import zeroecho.core.io.ImmutableByteContent;
import zeroecho.core.io.RepeatableContent;
import zeroecho.core.spec.AlgorithmIdentity;
import zeroecho.core.spi.AlgorithmExecutionCapability;
@@ -161,6 +162,7 @@ import zeroecho.pki.spi.store.TemporaryUniqueIndex;
*
*/
public final class BcX509StatusObjectGenerator implements StatusObjectGenerator {
+ private static final int MAXIMUM_CERTIFICATE_BYTES = 1024 * 1024;
private static final long EMPTY_CONTENT_LENGTH = 0L;
@@ -252,7 +254,7 @@ public final class BcX509StatusObjectGenerator implements StatusObjectGenerator
}
IssuerMaterial issuerMaterial = extractIssuerMaterialOrThrow(command.attributes());
- Instant thisUpdate = Instant.now();
+ Instant thisUpdate = Instant.now().truncatedTo(ChronoUnit.SECONDS);
Instant nextUpdate = thisUpdate.plus(Duration.ofDays(7));
DurableContentReference entries = null;
DurableContentReference tbs = null;
@@ -514,7 +516,8 @@ public final class BcX509StatusObjectGenerator implements StatusObjectGenerator
* unexpected type, or contain an invalid issuer
* certificate
*/
- private static IssuerMaterial extractIssuerMaterialOrThrow(AttributeSet attributes) {
+ @SuppressWarnings("PMD.PreserveStackTrace")
+ private IssuerMaterial extractIssuerMaterialOrThrow(AttributeSet attributes) {
Optional issuerCertAttr = attributes.get(BcX509Attributes.ISSUER_CERT_DER);
Optional issuerKeyAttr = attributes.get(BcX509Attributes.ISSUER_KEYREF);
@@ -525,14 +528,21 @@ public final class BcX509StatusObjectGenerator implements StatusObjectGenerator
throw new PkiException("Missing issuer keyRef");
}
- byte[] issuerDer = ((AttributeValue.BytesValue) issuerCertAttr.get()).value();
+ byte[] issuerDer = ((AttributeValue.BytesValue) issuerCertAttr.get()).value().clone();
+ if (issuerDer.length > MAXIMUM_CERTIFICATE_BYTES) {
+ throw new PkiException("Invalid issuer certificate");
+ }
KeyRef keyRef = new KeyRef(((AttributeValue.StringValue) issuerKeyAttr.get()).value());
X509CertificateHolder issuerHolder;
try {
+ new BcX509SignedObjectValidator(signingBus.authority()).validateCertificate(
+ new ImmutableByteContent(issuerDer), Optional.empty(), CancellationSignal.NONE);
issuerHolder = new X509CertificateHolder(issuerDer);
- } catch (Exception ex) {
- throw new PkiException("Invalid issuer certificate", ex);
+ } catch (IOException | IllegalArgumentException ex) {
+ throw new PkiException("Invalid issuer certificate");
+ } finally {
+ Arrays.fill(issuerDer, (byte) 0);
}
return new IssuerMaterial(issuerHolder, keyRef);
diff --git a/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/WorkflowProofOfPossessionVerifier.java b/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/WorkflowProofOfPossessionVerifier.java
index 4088320..35f3023 100644
--- a/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/WorkflowProofOfPossessionVerifier.java
+++ b/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/WorkflowProofOfPossessionVerifier.java
@@ -57,6 +57,7 @@ import zeroecho.pki.api.request.ProofOfPossessionStatus;
import zeroecho.pki.impl.framework.x509.X509AlgorithmRole;
import zeroecho.pki.impl.framework.x509.X509AuthoritySnapshot;
import zeroecho.pki.impl.framework.x509.X509ExecutionPlan;
+import zeroecho.pki.impl.framework.x509.StreamingDerReader;
import zeroecho.pki.spi.crypto.SignatureWorkflow;
import zeroecho.core.io.CancellationSignal;
import zeroecho.core.io.ImmutableByteContent;
@@ -267,7 +268,8 @@ public final class WorkflowProofOfPossessionVerifier implements ProofOfPossessio
*/
private static Optional parseCsr(byte[] csrDer) {
try {
- BcX509AlgorithmAdapter.requireCanonicalDer(csrDer);
+ new StreamingDerReader().validate(csrDer,
+ BcX509CertificationRequestParser.MAXIMUM_CSR_DER_BYTES);
return Optional.of(new PKCS10CertificationRequest(csrDer));
} catch (IOException | IllegalArgumentException ex) {
return Optional.empty();
diff --git a/pki/src/test/java/zeroecho/pki/impl/core/CredentialContentBoundaryTest.java b/pki/src/test/java/zeroecho/pki/impl/core/CredentialContentBoundaryTest.java
new file mode 100644
index 0000000..26f5f64
--- /dev/null
+++ b/pki/src/test/java/zeroecho/pki/impl/core/CredentialContentBoundaryTest.java
@@ -0,0 +1,84 @@
+/*******************************************************************************
+ * Copyright (C) 2026, Leo Galambos
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the conditions in the project license are met.
+ *******************************************************************************/
+package zeroecho.pki.impl.core;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.Optional;
+
+import org.junit.jupiter.api.Test;
+
+import zeroecho.pki.api.Encoding;
+import zeroecho.pki.api.PkiException;
+import zeroecho.pki.api.content.DurableContentReference;
+
+final class CredentialContentBoundaryTest {
+
+ @Test
+ void certificateLimitPrecedesStagingValidationAndMaterialization() {
+ System.out.println("certificateLimitPrecedesStagingValidationAndMaterialization");
+ byte[] oversized = new byte[CredentialContent.MAXIMUM_CERTIFICATE_BYTES + 1];
+ DurableContentReference oversizedReference = reference(oversized.length);
+ DurableContentReference negativeLengthReference = reference(-1L);
+ DurableContentReference zeroLengthReference = reference(0L);
+ DurableContentReference nonDerReference = reference(1L, Encoding.PEM);
+ assertThrows(PkiException.class, () -> CredentialContent.stage(null, oversized));
+ assertThrows(PkiException.class, () -> CredentialContent.materializeForBc(null, oversizedReference));
+ assertThrows(PkiException.class,
+ () -> CredentialContent.validateCertificate(null, oversizedReference, null, Optional.empty()));
+ assertThrows(PkiException.class, () -> CredentialContent.materializeForBc(null, negativeLengthReference));
+ assertThrows(PkiException.class,
+ () -> CredentialContent.validateCertificate(null, negativeLengthReference, null, Optional.empty()));
+ assertThrows(PkiException.class, () -> CredentialContent.materializeForBc(null, zeroLengthReference));
+ assertThrows(PkiException.class,
+ () -> CredentialContent.validateCertificate(null, zeroLengthReference, null, Optional.empty()));
+ assertThrows(PkiException.class, () -> CredentialContent.materializeForBc(null, nonDerReference));
+ assertThrows(PkiException.class,
+ () -> CredentialContent.validateCertificate(null, nonDerReference, null, Optional.empty()));
+ System.out.println("...rejected=staging, validation, materialization, invalid length and encoding");
+ System.out.println("certificateLimitPrecedesStagingValidationAndMaterialization...ok");
+ }
+
+ private static DurableContentReference reference(long length) {
+ return reference(length, Encoding.DER);
+ }
+
+ private static DurableContentReference reference(long length, Encoding encoding) {
+ return new DurableContentReference() {
+ @Override
+ public String storeId() {
+ return "test:store";
+ }
+
+ @Override
+ public String contentId() {
+ return "test:oversized";
+ }
+
+ @Override
+ public Encoding encoding() {
+ return encoding;
+ }
+
+ @Override
+ public long length() {
+ return length;
+ }
+
+ @Override
+ public String sha256() {
+ return "0".repeat(64);
+ }
+
+ @Override
+ public Lifecycle lifecycle() {
+ return Lifecycle.PERSISTED;
+ }
+ };
+ }
+}
diff --git a/pki/src/test/java/zeroecho/pki/impl/core/DefaultStatusObjectServiceCrlTest.java b/pki/src/test/java/zeroecho/pki/impl/core/DefaultStatusObjectServiceCrlTest.java
index da6a7fa..4e5244a 100644
--- a/pki/src/test/java/zeroecho/pki/impl/core/DefaultStatusObjectServiceCrlTest.java
+++ b/pki/src/test/java/zeroecho/pki/impl/core/DefaultStatusObjectServiceCrlTest.java
@@ -57,6 +57,7 @@ import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.UnaryOperator;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.CRLReason;
@@ -250,6 +251,23 @@ final class DefaultStatusObjectServiceCrlTest {
EVALUATION_TIME.minusSeconds(1), Optional.empty())),
Map.of(malformed.credentialId(), malformed), false);
+ byte[] certificateWithTrailing = Arrays.copyOf(runtime.credentialBytes(template),
+ runtime.credentialBytes(template).length + 1);
+ Credential trailing = copy(template, "trailing", BcX509CredentialFramework.FORMAT_ID,
+ runtime.stageCredential(certificateWithTrailing));
+ assertCrlFailure(runtime, command,
+ List.of(record(trailing.credentialId(), RevocationState.HELD,
+ EVALUATION_TIME.minusSeconds(1), Optional.empty())),
+ Map.of(trailing.credentialId(), trailing), false);
+
+ byte[] csrInsteadOfCertificate = certificationRequest(generateRsa(), "Type Confusion").getEncoded();
+ Credential typeConfused = copy(template, "type-confused", BcX509CredentialFramework.FORMAT_ID,
+ runtime.stageCredential(csrInsteadOfCertificate));
+ assertCrlFailure(runtime, command,
+ List.of(record(typeConfused.credentialId(), RevocationState.HELD,
+ EVALUATION_TIME.minusSeconds(1), Optional.empty())),
+ Map.of(typeConfused.credentialId(), typeConfused), false);
+
Credential duplicateOne = copy(template, "duplicate-one", BcX509CredentialFramework.FORMAT_ID,
template.content());
Credential duplicateTwo = copy(template, "duplicate-two", BcX509CredentialFramework.FORMAT_ID,
@@ -318,6 +336,93 @@ final class DefaultStatusObjectServiceCrlTest {
}
}
+ @Test
+ void maliciousCompletionMetadataAbortsBeforePersistence(@TempDir Path root) throws Exception {
+ System.out.println("maliciousCompletionMetadataAbortsBeforePersistence");
+ KeyPair rootKey = generateRsa();
+ KeyRef rootKeyRef = new KeyRef("kref:v1:keyring:test:crl-metadata");
+ try (PkiTestRuntime runtime = PkiTestRuntime.create(root, root.resolve("bus.log"),
+ Map.of(rootKeyRef, rootKey))) {
+ PkiId caId = createRoot(runtime, rootKeyRef, "CRL Metadata Root");
+ StatusObjectGenerateCommand command = new StatusObjectGenerateCommand(caId, StatusObjectType.CRL,
+ runtime.framework().formatId(), emptyAttributes());
+ List> mutations = List.of(
+ status -> copyStatus(status, new FormatId("wrong-format"), status.issuerCaId(), status.type(),
+ status.thisUpdate(), status.nextUpdate()),
+ status -> copyStatus(status, status.formatId(), new PkiId("ca:wrong-issuer"), status.type(),
+ status.thisUpdate(), status.nextUpdate()),
+ status -> copyStatus(status, status.formatId(), status.issuerCaId(), StatusObjectType.DELTA_CRL,
+ status.thisUpdate(), status.nextUpdate()),
+ status -> copyStatus(status, status.formatId(), status.issuerCaId(), status.type(),
+ status.thisUpdate(), Optional.of(status.thisUpdate())),
+ status -> copyStatus(status, status.formatId(), status.issuerCaId(), status.type(),
+ status.thisUpdate().plusSeconds(1L), status.nextUpdate()),
+ status -> copyStatus(status, status.formatId(), status.issuerCaId(), status.type(),
+ status.thisUpdate(), Optional.empty()),
+ status -> copyStatus(status, status.formatId(), status.issuerCaId(), status.type(),
+ status.thisUpdate(), status.nextUpdate().map(next -> next.plusSeconds(1L))));
+ int signCount = runtime.submittedSignCount();
+ int statusCount = runtime.store().listStatusObjects(caId).size();
+ AtomicInteger persistenceCalls = new AtomicInteger();
+
+ for (UnaryOperator mutation : mutations) {
+ StatusObjectGenerator delegate = runtime.framework().statusObjectGenerator();
+ StatusObjectGenerator malicious = (wired, entries) -> {
+ X509SignedObjectCompletion completion = delegate.generate(wired, entries);
+ StatusObject modified = mutation.apply(
+ runtime.signingBus().authority().requireStatusCompletion(completion));
+ return runtime.signingBus().authority().completeStatusObject(modified,
+ runtime.signingBus().authority().requireStatusSigningPlan(completion));
+ };
+ DefaultStatusObjectService service = new DefaultStatusObjectService(
+ failingPersistenceStore(runtime.store(), persistenceCalls),
+ frameworkView(runtime.framework(), malicious), runtime.auditSink(), usableResolver(),
+ runtime.signingBus().authority());
+ assertBoundaryFailure(assertThrows(PkiException.class, () -> service.generate(command)));
+ }
+
+ assertEquals(0, persistenceCalls.get());
+ assertEquals(statusCount, runtime.store().listStatusObjects(caId).size());
+ assertEquals(signCount + mutations.size(), runtime.submittedSignCount());
+ System.out.println("...rejected=type, format, issuer, time order, changed/omitted/later signed times");
+ }
+ System.out.println("maliciousCompletionMetadataAbortsBeforePersistence...ok");
+ }
+
+ @Test
+ void generatedCrlIssuerNameMustMatchSelectedIssuerCertificate(@TempDir Path root) throws Exception {
+ System.out.println("generatedCrlIssuerNameMustMatchSelectedIssuerCertificate");
+ KeyPair expectedKey = generateRsa();
+ KeyPair substitutedKey = generateRsa();
+ KeyRef expectedKeyRef = new KeyRef("kref:v1:keyring:test:crl-expected-issuer");
+ KeyRef substitutedKeyRef = new KeyRef("kref:v1:keyring:test:crl-substituted-issuer");
+ try (PkiTestRuntime runtime = PkiTestRuntime.create(root, root.resolve("bus.log"),
+ Map.of(expectedKeyRef, expectedKey, substitutedKeyRef, substitutedKey))) {
+ PkiId expectedCaId = createRoot(runtime, expectedKeyRef, "Expected CRL Issuer");
+ PkiId substitutedCaId = createRoot(runtime, substitutedKeyRef, "Substituted CRL Issuer");
+ Credential substitutedIssuer = runtime.caCredential(runtime.caService().getCa(substitutedCaId), 0);
+ StatusObjectGenerateCommand command = new StatusObjectGenerateCommand(expectedCaId,
+ StatusObjectType.CRL, runtime.framework().formatId(), emptyAttributes());
+ StatusObjectGenerateCommand substituted = crlCommand(runtime, expectedCaId, substitutedIssuer,
+ substitutedKeyRef);
+ StatusObjectGenerator delegate = runtime.framework().statusObjectGenerator();
+ StatusObjectGenerator malicious = (wired, entries) -> delegate.generate(
+ new StatusObjectGenerateCommand(wired.issuerCaId(), wired.type(), wired.formatId(),
+ substituted.attributes()), entries);
+ AtomicInteger persistenceCalls = new AtomicInteger();
+ DefaultStatusObjectService service = new DefaultStatusObjectService(
+ failingPersistenceStore(runtime.store(), persistenceCalls),
+ frameworkView(runtime.framework(), malicious), runtime.auditSink(), usableResolver(),
+ runtime.signingBus().authority());
+
+ assertBoundaryFailure(assertThrows(PkiException.class, () -> service.generate(command)));
+ assertEquals(0, persistenceCalls.get());
+ assertTrue(runtime.store().listStatusObjects(expectedCaId).isEmpty());
+ System.out.println("...rejected=exact issuer Name mismatch");
+ }
+ System.out.println("generatedCrlIssuerNameMustMatchSelectedIssuerCertificate...ok");
+ }
+
private static void assertCrlFailure(PkiTestRuntime runtime, StatusObjectGenerateCommand command,
List records, Map credentials, boolean failListing) {
int signCount = runtime.submittedSignCount();
@@ -486,6 +591,12 @@ final class DefaultStatusObjectServiceCrlTest {
new CaProfileBinding(binding.reference()), CredentialStatus.ISSUED, content, template.attributes());
}
+ private static StatusObject copyStatus(StatusObject source, FormatId formatId, PkiId issuerCaId,
+ StatusObjectType type, Instant thisUpdate, Optional nextUpdate) {
+ return new StatusObject(source.statusObjectId(), formatId, issuerCaId, type, thisUpdate, nextUpdate,
+ source.content(), source.attributes());
+ }
+
private static StatusObjectGenerateCommand crlCommand(PkiTestRuntime runtime, PkiId caId, Credential issuer,
KeyRef keyRef) throws Exception {
AttributeSet attributes = SimpleAttributeSet.builder()
diff --git a/pki/src/test/java/zeroecho/pki/impl/core/H7ProfileEnforcementTest.java b/pki/src/test/java/zeroecho/pki/impl/core/H7ProfileEnforcementTest.java
index 39281ef..85576c0 100644
--- a/pki/src/test/java/zeroecho/pki/impl/core/H7ProfileEnforcementTest.java
+++ b/pki/src/test/java/zeroecho/pki/impl/core/H7ProfileEnforcementTest.java
@@ -60,6 +60,7 @@ import java.util.Set;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.DERBMPString;
+import org.bouncycastle.asn1.DERBitString;
import org.bouncycastle.asn1.DERNull;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.DERSequence;
@@ -250,6 +251,14 @@ final class H7ProfileEnforcementTest {
ed.getPublicKeyData().getBytes());
assertCode("SUBJECT_KEY_PARAMETERS_UNSUPPORTED",
() -> validate(edWithNull.getEncoded(), "Ed25519", policyWithFixedOrganization("Ed25519")));
+
+ byte[] trailing = java.util.Arrays.copyOf(original.getEncoded(), original.getEncoded().length + 1);
+ assertCode("SUBJECT_KEY_UNSUPPORTED",
+ () -> validate(trailing, "RSA", policyWithFixedOrganization("RSA")));
+ DERSequence padded = new DERSequence(new ASN1Encodable[] { original.getAlgorithm(),
+ new DERBitString(original.getPublicKeyData().getBytes(), 1) });
+ assertCode("SUBJECT_KEY_UNSUPPORTED",
+ () -> validate(padded.getEncoded(), "RSA", policyWithFixedOrganization("RSA")));
}
@Test
diff --git a/pki/src/test/java/zeroecho/pki/impl/framework/x509/StreamingDerReaderTest.java b/pki/src/test/java/zeroecho/pki/impl/framework/x509/StreamingDerReaderTest.java
index f2065fd..0add81a 100644
--- a/pki/src/test/java/zeroecho/pki/impl/framework/x509/StreamingDerReaderTest.java
+++ b/pki/src/test/java/zeroecho/pki/impl/framework/x509/StreamingDerReaderTest.java
@@ -33,6 +33,7 @@
******************************************************************************/
package zeroecho.pki.impl.framework.x509;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -55,11 +56,11 @@ import zeroecho.pki.impl.framework.x509.StreamingDerReader.SignedObjectLayout;
final class StreamingDerReaderTest {
private static final byte[] CRL = {
+ 0x30, 0x32,
0x30, 0x26,
- 0x30, 0x1a,
0x02, 0x01, 0x01,
0x30, 0x04, 0x06, 0x02, 0x2a, 0x03,
- 0x30, 0x00,
+ 0x30, 0x0c, 0x31, 0x0a, 0x30, 0x08, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, 0x01, 0x49,
0x17, 0x0d, 0x32, 0x36, 0x30, 0x31, 0x30, 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5a,
0x30, 0x04, 0x06, 0x02, 0x2a, 0x03,
0x03, 0x02, 0x00, (byte) 0xaa
@@ -74,16 +75,87 @@ final class StreamingDerReaderTest {
CancellationSignal.NONE);
System.out.println("...encodedLength=" + reader.validate(content, CancellationSignal.NONE));
assertEquals(2L, layout.tbsOffset());
- assertEquals(28L, layout.tbsLength());
+ assertEquals(40L, layout.tbsLength());
assertEquals(7L, layout.tbsAlgorithmOffset());
assertEquals(6L, layout.tbsAlgorithmLength());
- assertEquals(30L, layout.outerAlgorithmOffset());
+ assertEquals(13L, layout.issuerOffset());
+ assertEquals(14L, layout.issuerLength());
+ assertEquals(42L, layout.outerAlgorithmOffset());
assertEquals(6L, layout.outerAlgorithmLength());
- assertEquals(39L, layout.signatureOffset());
+ assertEquals(51L, layout.signatureOffset());
assertEquals(1L, layout.signatureLength());
+ assertEquals(java.util.Optional.of(java.time.Instant.parse("2026-01-01T00:00:00Z")),
+ layout.thisUpdate());
+ assertEquals(java.util.Optional.empty(), layout.nextUpdate());
System.out.println("validatesCanonicalCrlAndLocatesSignedFields...ok");
}
+ @Test
+ void crlInspectionRejectsStructurallyValidV1Certificate() {
+ System.out.println("crlInspectionRejectsStructurallyValidV1Certificate");
+ StreamingDerReader reader = new StreamingDerReader();
+ byte[] validity = sequence(utcTime("250101000000Z"), utcTime("270101000000Z"));
+ byte[] subjectPublicKeyInfo = sequence(algorithm(), tlv(3, new byte[] { 0, 1 }));
+ byte[] certificate = signedObject(sequence(integer(1), algorithm(), issuer(), validity, issuer(),
+ subjectPublicKeyInfo));
+ ImmutableByteContent content = new ImmutableByteContent(certificate);
+
+ SignedObjectLayout certificateLayout = assertDoesNotThrow(() -> reader.inspectSignedObject(content,
+ SignedObjectKind.CERTIFICATE, CancellationSignal.NONE));
+ assertEquals(java.util.Optional.empty(), certificateLayout.thisUpdate());
+ assertEquals(java.util.Optional.empty(), certificateLayout.nextUpdate());
+ assertThrows(IOException.class, () -> reader.inspectSignedObject(content, SignedObjectKind.CRL,
+ CancellationSignal.NONE));
+ System.out.println("...rejected=certificate TBS is not TBSCertList");
+ System.out.println("crlInspectionRejectsStructurallyValidV1Certificate...ok");
+ }
+
+ @Test
+ void crlInspectionReturnsExactOptionalNextUpdate() throws Exception {
+ System.out.println("crlInspectionReturnsExactOptionalNextUpdate");
+ byte[] encoded = signedObject(sequence(integer(1), algorithm(), issuer(), utcTime("260803120000Z"),
+ utcTime("260810120000Z")));
+
+ SignedObjectLayout layout = new StreamingDerReader().inspectSignedObject(
+ new ImmutableByteContent(encoded), SignedObjectKind.CRL, CancellationSignal.NONE);
+
+ assertEquals(java.util.Optional.of(java.time.Instant.parse("2026-08-03T12:00:00Z")),
+ layout.thisUpdate());
+ assertEquals(java.util.Optional.of(java.time.Instant.parse("2026-08-10T12:00:00Z")),
+ layout.nextUpdate());
+ System.out.println("...thisUpdate=" + layout.thisUpdate().orElseThrow()
+ + " nextUpdate=" + layout.nextUpdate().orElseThrow());
+ System.out.println("crlInspectionReturnsExactOptionalNextUpdate...ok");
+ }
+
+ @Test
+ void crlInspectionRejectsMalformedOptionalTimesEntriesAndExtensions() {
+ System.out.println("crlInspectionRejectsMalformedOptionalTimesEntriesAndExtensions");
+ StreamingDerReader reader = new StreamingDerReader();
+ byte[] thisUpdate = utcTime("260101000000Z");
+ byte[] earlier = utcTime("250101000000Z");
+ byte[] zeroSerialEntry = sequence(integer(0), thisUpdate);
+ byte[] oversizedSerial = new byte[21];
+ oversizedSerial[0] = 1;
+ byte[] oversizedSerialEntry = sequence(tlv(2, oversizedSerial), thisUpdate);
+ byte[] malformedEntry = sequence(integer(1), thisUpdate, tlv(4, new byte[] { 1 }));
+ byte[] falseCritical = explicitExtensions(sequence(sequence(oid(), tlv(1, new byte[] { 0 }),
+ tlv(4, new byte[] { 1 }))));
+ byte[] extensionOnV1 = explicitExtensions(sequence(sequence(oid(), tlv(4, new byte[] { 1 }))));
+
+ assertCrlRejected(reader, integer(0), algorithm(), issuer(), thisUpdate);
+ assertCrlRejected(reader, integer(1), algorithm(), sequence(), thisUpdate);
+ assertCrlRejected(reader, integer(1), algorithm(), issuer(), thisUpdate, earlier);
+ assertCrlRejected(reader, integer(1), algorithm(), issuer(), thisUpdate, thisUpdate);
+ assertCrlRejected(reader, integer(1), algorithm(), issuer(), thisUpdate, sequence(zeroSerialEntry));
+ assertCrlRejected(reader, integer(1), algorithm(), issuer(), thisUpdate, sequence(oversizedSerialEntry));
+ assertCrlRejected(reader, integer(1), algorithm(), issuer(), thisUpdate, sequence(malformedEntry));
+ assertCrlRejected(reader, integer(1), algorithm(), issuer(), thisUpdate, falseCritical);
+ assertCrlRejected(reader, algorithm(), issuer(), thisUpdate, extensionOnV1);
+ System.out.println("...rejected=version, issuer, time order/equality, serial, entry, DEFAULT, v1 extensions");
+ System.out.println("crlInspectionRejectsMalformedOptionalTimesEntriesAndExtensions...ok");
+ }
+
@Test
void rejectsTrailingIndefiniteAndNonMinimalLength() {
System.out.println("rejectsTrailingIndefiniteAndNonMinimalLength");
@@ -116,6 +188,53 @@ final class StreamingDerReaderTest {
System.out.println("rejectsNonCanonicalPrimitiveForms...ok");
}
+ @Test
+ void boundedEntryRejectsOversizeAndHostileHighTags() throws Exception {
+ System.out.println("boundedEntryRejectsOversizeAndHostileHighTags");
+ StreamingDerReader reader = new StreamingDerReader();
+ byte[] highTag31 = { (byte) 0x9f, 0x1f, 0x00 };
+ assertEquals(highTag31.length, reader.validate(highTag31, highTag31.length));
+ assertThrows(IOException.class, () -> reader.validate(highTag31, highTag31.length - 1));
+ assertThrows(IOException.class, () -> reader.validate(new byte[] { 0x1f, 0x1e, 0x00 }, 3));
+ assertThrows(IOException.class, () -> reader.validate(new byte[] { 0x1f, 0x1f, 0x00 }, 3));
+ assertThrows(IOException.class,
+ () -> reader.validate(new byte[] { 0x1f, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+ (byte) 0xff, 0x7f, 0x00 }, 7));
+ System.out.println("...rejected=limit, non-minimal tag, tag overflow");
+ System.out.println("boundedEntryRejectsOversizeAndHostileHighTags...ok");
+ }
+
+ @Test
+ void validatesReachableEnumeratedTimesAndX509Strings() throws Exception {
+ System.out.println("validatesReachableEnumeratedTimesAndX509Strings");
+ StreamingDerReader reader = new StreamingDerReader();
+ byte[] values = { 0x30, 0x1f, 0x0a, 0x01, 0x01, 0x0c, 0x02, (byte) 0xc3, (byte) 0xa9,
+ 0x13, 0x02, 'C', 'N', 0x16, 0x03, 'a', '@', 'b', 0x17, 0x0d,
+ '2', '6', '0', '8', '0', '3', '1', '2', '0', '0', '0', '0', 'Z' };
+ assertEquals(values.length, reader.validate(values, values.length));
+ assertThrows(IOException.class, () -> reader.validate(new byte[] { 0x0a, 0x02, 0x00, 0x01 }, 4));
+ assertThrows(IOException.class,
+ () -> reader.validate(new byte[] { 0x17, 0x0d, '2', '6', '0', '2', '3', '0', '1', '2', '0',
+ '0', '0', '0', 'Z' }, 15));
+ assertThrows(IOException.class,
+ () -> reader.validate(new byte[] { 0x0c, 0x02, (byte) 0xc0, (byte) 0x80 }, 4));
+ assertThrows(IOException.class, () -> reader.validate(new byte[] { 0x13, 0x01, '@' }, 3));
+ System.out.println("...rejected=ENUMERATED, date, UTF-8, PrintableString");
+ System.out.println("validatesReachableEnumeratedTimesAndX509Strings...ok");
+ }
+
+ @Test
+ void subjectPublicKeyInfoRequiresZeroPadBits() throws Exception {
+ System.out.println("subjectPublicKeyInfoRequiresZeroPadBits");
+ StreamingDerReader reader = new StreamingDerReader();
+ byte[] valid = { 0x30, 0x08, 0x30, 0x03, 0x06, 0x01, 0x2a, 0x03, 0x01, 0x00 };
+ byte[] padded = { 0x30, 0x09, 0x30, 0x03, 0x06, 0x01, 0x2a, 0x03, 0x02, 0x01, 0x00 };
+ assertEquals(valid.length, reader.validateSubjectPublicKeyInfo(valid, valid.length));
+ assertThrows(IOException.class, () -> reader.validateSubjectPublicKeyInfo(padded, padded.length));
+ System.out.println("...rejected=nonzero pad bits");
+ System.out.println("subjectPublicKeyInfoRequiresZeroPadBits...ok");
+ }
+
@Test
void enforcesCanonicalSetOrdering() throws Exception {
System.out.println("enforcesCanonicalSetOrdering");
@@ -144,6 +263,26 @@ final class StreamingDerReaderTest {
System.out.println("setOrderingUsesFixedSourcePasses...ok");
}
+ @Test
+ void denseEmptyPrimitiveInputUsesOneStreamingPass() throws Exception {
+ System.out.println("denseEmptyPrimitiveInputUsesOneStreamingPass");
+ byte[] encoded = new byte[4 + 2 * 16_384];
+ encoded[0] = 0x30;
+ encoded[1] = (byte) 0x82;
+ encoded[2] = (byte) ((encoded.length - 4) >>> 8);
+ encoded[3] = (byte) (encoded.length - 4);
+ for (int index = 4; index < encoded.length; index += 2) {
+ encoded[index] = 0x04;
+ encoded[index + 1] = 0x00;
+ }
+ CountingContent content = new CountingContent(encoded);
+ assertEquals(encoded.length, new StreamingDerReader().validate(content, CancellationSignal.NONE));
+ assertEquals(1, content.openCount());
+ assertEquals(1, content.closeCount());
+ System.out.println("...emptyPrimitives=16384 sourcePasses=" + content.openCount());
+ System.out.println("denseEmptyPrimitiveInputUsesOneStreamingPass...ok");
+ }
+
private static byte[] repeatedIntegerSet(int count) {
int valueLength = Math.multiplyExact(count, 3);
byte[] encoded = new byte[Math.addExact(valueLength, 4)];
@@ -159,6 +298,70 @@ final class StreamingDerReaderTest {
return encoded;
}
+ private static void assertCrlRejected(StreamingDerReader reader, byte[]... tbsFields) {
+ byte[] encoded = signedObject(sequence(tbsFields));
+ assertThrows(IOException.class, () -> reader.inspectSignedObject(new ImmutableByteContent(encoded),
+ SignedObjectKind.CRL, CancellationSignal.NONE));
+ }
+
+ private static byte[] signedObject(byte[] tbs) {
+ return sequence(tbs, algorithm(), tlv(3, new byte[] { 0, (byte) 0xaa }));
+ }
+
+ private static byte[] algorithm() {
+ return sequence(oid());
+ }
+
+ private static byte[] issuer() {
+ return sequence(tlv(0x31, sequence(tlv(6, new byte[] { 0x55, 0x04, 0x03 }),
+ tlv(12, new byte[] { 'I' }))));
+ }
+
+ private static byte[] oid() {
+ return tlv(6, new byte[] { 0x2a, 0x03 });
+ }
+
+ private static byte[] integer(int value) {
+ return tlv(2, new byte[] { (byte) value });
+ }
+
+ private static byte[] utcTime(String value) {
+ return tlv(23, value.getBytes(java.nio.charset.StandardCharsets.US_ASCII));
+ }
+
+ private static byte[] explicitExtensions(byte[] extensions) {
+ return tlv(0xa0, extensions);
+ }
+
+ private static byte[] sequence(byte[]... values) {
+ return tlv(0x30, concatenate(values));
+ }
+
+ private static byte[] tlv(int tag, byte[] value) {
+ if (value.length >= 128) {
+ throw new IllegalArgumentException("test DER value too long");
+ }
+ byte[] encoded = new byte[value.length + 2];
+ encoded[0] = (byte) tag;
+ encoded[1] = (byte) value.length;
+ System.arraycopy(value, 0, encoded, 2, value.length);
+ return encoded;
+ }
+
+ private static byte[] concatenate(byte[]... values) {
+ int length = 0;
+ for (byte[] value : values) {
+ length = Math.addExact(length, value.length);
+ }
+ byte[] result = new byte[length];
+ int offset = 0;
+ for (byte[] value : values) {
+ System.arraycopy(value, 0, result, offset, value.length);
+ offset += value.length;
+ }
+ return result;
+ }
+
/** Repeatable test content that accounts for every fixed comparison pass. */
private static final class CountingContent implements RepeatableContent {
private final byte[] encoded;
diff --git a/pki/src/test/java/zeroecho/pki/impl/framework/x509/bc/BcX509CertificationRequestParserDerTest.java b/pki/src/test/java/zeroecho/pki/impl/framework/x509/bc/BcX509CertificationRequestParserDerTest.java
new file mode 100644
index 0000000..eb93752
--- /dev/null
+++ b/pki/src/test/java/zeroecho/pki/impl/framework/x509/bc/BcX509CertificationRequestParserDerTest.java
@@ -0,0 +1,115 @@
+/*******************************************************************************
+ * Copyright (C) 2026, Leo Galambos
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the conditions in the project license are met.
+ *******************************************************************************/
+package zeroecho.pki.impl.framework.x509.bc;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.nio.charset.StandardCharsets;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.util.Base64;
+
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.bouncycastle.pkcs.PKCS10CertificationRequest;
+import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder;
+import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder;
+import org.junit.jupiter.api.Test;
+
+import zeroecho.pki.api.EncodedObject;
+import zeroecho.pki.api.Encoding;
+import zeroecho.pki.api.PkiException;
+import zeroecho.pki.api.request.CertificationRequest;
+
+final class BcX509CertificationRequestParserDerTest {
+
+ @Test
+ void acceptsOnlyExactSingleCsrPemObject() throws Exception {
+ System.out.println("acceptsOnlyExactSingleCsrPemObject");
+ byte[] der = certificationRequest();
+ BcX509CertificationRequestParser parser = new BcX509CertificationRequestParser();
+ for (String label : new String[] { "CERTIFICATE REQUEST", "NEW CERTIFICATE REQUEST" }) {
+ byte[] pem = pem(label, der);
+ assertEquals(Encoding.DER, parser.parse(request(Encoding.PEM, pem)).publicKeyInfo().encoding());
+ }
+ assertThrows(PkiException.class,
+ () -> parser.parse(request(Encoding.PEM, pem("PUBLIC KEY", der))));
+ String missingLineBreak = "-----BEGIN CERTIFICATE REQUEST-----"
+ + Base64.getEncoder().encodeToString(der) + "\n-----END CERTIFICATE REQUEST-----\n";
+ assertThrows(PkiException.class, () -> parser.parse(request(Encoding.PEM,
+ missingLineBreak.getBytes(StandardCharsets.US_ASCII))));
+ byte[] one = pem("CERTIFICATE REQUEST", der);
+ byte[] two = new byte[one.length * 2];
+ System.arraycopy(one, 0, two, 0, one.length);
+ System.arraycopy(one, 0, two, one.length, one.length);
+ assertThrows(PkiException.class, () -> parser.parse(request(Encoding.PEM, two)));
+ System.out.println("...labels=2 rejected=wrong-label, missing-line-break, second-object");
+ System.out.println("acceptsOnlyExactSingleCsrPemObject...ok");
+ }
+
+ @Test
+ void rejectsOversizeAndNonCanonicalDerBeforePkcs10Parsing() throws Exception {
+ System.out.println("rejectsOversizeAndNonCanonicalDerBeforePkcs10Parsing");
+ BcX509CertificationRequestParser parser = new BcX509CertificationRequestParser();
+ byte[] oversized = new byte[BcX509CertificationRequestParser.MAXIMUM_CSR_DER_BYTES + 1];
+ assertThrows(PkiException.class, () -> parser.parse(request(Encoding.DER, oversized)));
+ byte[] der = certificationRequest();
+ byte[] trailing = java.util.Arrays.copyOf(der, der.length + 1);
+ assertThrows(PkiException.class, () -> parser.parse(request(Encoding.DER, trailing)));
+ assertThrows(PkiException.class,
+ () -> parser.parse(request(Encoding.DER, new byte[] { 0x30, (byte) 0x80, 0x00, 0x00 })));
+ assertThrows(PkiException.class,
+ () -> parser.parse(request(Encoding.DER, new byte[] { 0x30, (byte) 0x81, 0x00 })));
+ assertThrows(PkiException.class,
+ () -> parser.parse(request(Encoding.DER, new byte[] { 0x05, 0x00 })));
+ assertThrows(PkiException.class, () -> parser.parse(request(Encoding.DER, excessivelyNestedDer())));
+ System.out.println("...rejected=limit, trailing, indefinite, nonminimal, type, depth");
+ System.out.println("rejectsOversizeAndNonCanonicalDerBeforePkcs10Parsing...ok");
+ }
+
+ private static CertificationRequest request(Encoding encoding, byte[] encoded) {
+ return new CertificationRequest(BcX509CredentialFramework.FORMAT_ID, new EncodedObject(encoding, encoded));
+ }
+
+ private static byte[] pem(String label, byte[] der) {
+ String encoded = "\n-----BEGIN " + label + "-----\n" + Base64.getEncoder().encodeToString(der)
+ + "\n-----END " + label + "-----\n\t";
+ return encoded.getBytes(StandardCharsets.US_ASCII);
+ }
+
+ private static byte[] certificationRequest() throws Exception {
+ KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
+ generator.initialize(2048);
+ KeyPair pair = generator.generateKeyPair();
+ PKCS10CertificationRequestBuilder builder = new JcaPKCS10CertificationRequestBuilder(
+ new X500Name("CN=Bounded CSR"), pair.getPublic());
+ ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").build(pair.getPrivate());
+ PKCS10CertificationRequest request = builder.build(signer);
+ return request.getEncoded();
+ }
+
+ private static byte[] excessivelyNestedDer() {
+ byte[] encoded = { 0x05, 0x00 };
+ for (int depth = 0; depth < 65; depth++) {
+ int headerLength = encoded.length < 128 ? 2 : 3;
+ byte[] wrapped = new byte[headerLength + encoded.length];
+ wrapped[0] = 0x30;
+ if (headerLength == 2) {
+ wrapped[1] = (byte) encoded.length;
+ } else {
+ wrapped[1] = (byte) 0x81;
+ wrapped[2] = (byte) encoded.length;
+ }
+ System.arraycopy(encoded, 0, wrapped, headerLength, encoded.length);
+ encoded = wrapped;
+ }
+ return encoded;
+ }
+}