security(pki): harden DER trust boundaries
Enforce canonical single-object DER validation across CSR, certificate, SPKI, CRL and persisted credential boundaries. Reject malformed, ambiguous and type-confused encodings while preserving streaming aggregate processing and existing PKI semantics.
This commit is contained in:
@@ -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<PkiId> 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.
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<AlgorithmIdentity> 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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
* </p>
|
||||
*/
|
||||
// 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.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*
|
||||
* @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<Instant> 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<Instant> thisUpdate,
|
||||
Optional<Instant> 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<Instant> thisUpdate, Optional<Instant> 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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
@@ -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 == '/';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
* </p>
|
||||
*/
|
||||
// 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 {
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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<SignatureWorkflow> plan,
|
||||
SubjectPublicKeyInfo issuerPublicKey, CancellationSignal cancellation) throws IOException {
|
||||
X509CertificateHolder issuerCertificate, Instant expectedThisUpdate,
|
||||
Optional<Instant> 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];
|
||||
|
||||
@@ -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;
|
||||
* </p>
|
||||
*/
|
||||
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<AttributeValue> issuerCertAttr = attributes.get(BcX509Attributes.ISSUER_CERT_DER);
|
||||
Optional<AttributeValue> 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);
|
||||
|
||||
@@ -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<PKCS10CertificationRequest> 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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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<UnaryOperator<StatusObject>> 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<StatusObject> 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<RevocationRecord> records, Map<PkiId, Credential> 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<Instant> 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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user