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:
2026-08-03 23:48:37 +02:00
parent d5d5bf7a96
commit 64af4519f0
19 changed files with 1484 additions and 150 deletions

View File

@@ -39,8 +39,6 @@ import java.util.Objects;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1Encoding;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DERSequence;
import zeroecho.core.spec.ContextSpec;
@@ -84,7 +82,10 @@ import zeroecho.core.spec.ContextSpec;
* semantics, not as an optional hint.</li>
* </ul>
*/
public final class SignatureInteropProfile { // NOPMD
public final class SignatureInteropProfile {
private static final int DER_SEQUENCE_TAG = 0x30;
private static final int DER_INTEGER_TAG = 0x02;
private static final int DER_LONG_FORM_BIT = 0x80;
/**
* Signature byte representation bridging policy.
@@ -192,11 +193,10 @@ public final class SignatureInteropProfile { // NOPMD
*
* @param signature external signature bytes
* @return internal signature bytes suitable for ZeroEcho verification contexts
* @throws IOException if DER decoding fails
* @throws IllegalArgumentException if the supplied bytes do not match the
* expected external representation
*/
public byte[] externalToInternalSignature(byte[] signature) throws IOException {
public byte[] externalToInternalSignature(byte[] signature) {
Objects.requireNonNull(signature, "signature");
if (signatureRepresentation == SignatureRepresentation.IDENTITY) {
return signature.clone();
@@ -221,21 +221,86 @@ public final class SignatureInteropProfile { // NOPMD
return ecdsaP1363ToDer(signature, internalSignatureLength);
}
private static byte[] ecdsaDerToP1363(byte[] der, int fixedLength) throws IOException {
ASN1Primitive primitive = ASN1Primitive.fromByteArray(der);
ASN1Sequence sequence = ASN1Sequence.getInstance(primitive);
if (sequence.size() != 2) { // NOPMD
throw new IllegalArgumentException("ECDSA DER signature must contain exactly two integers");
private static byte[] ecdsaDerToP1363(byte[] der, int fixedLength) {
if ((fixedLength & 1) != 0 || der.length > fixedLength + 8 || der.length < 8) {
throw new IllegalArgumentException("Invalid ECDSA DER signature length");
}
int coordinateLength = fixedLength / 2;
int offset = 0;
if (Byte.toUnsignedInt(der[offset++]) != DER_SEQUENCE_TAG) {
throw new IllegalArgumentException("ECDSA DER signature must be a sequence");
}
long sequenceLength = readDerLength(der, offset);
offset = (int) sequenceLength;
int sequenceEnd = Math.addExact(offset, (int) (sequenceLength >>> Integer.SIZE));
if (sequenceEnd != der.length) {
throw new IllegalArgumentException("ECDSA DER signature contains trailing or truncated data");
}
byte[] out = new byte[fixedLength];
byte[] r = ASN1Integer.getInstance(sequence.getObjectAt(0)).getPositiveValue().toByteArray();
byte[] s = ASN1Integer.getInstance(sequence.getObjectAt(1)).getPositiveValue().toByteArray();
copyUnsignedFixed(r, out, 0, coordinateLength);
copyUnsignedFixed(s, out, coordinateLength, coordinateLength);
offset = readPositiveInteger(der, offset, sequenceEnd, out, 0, coordinateLength);
offset = readPositiveInteger(der, offset, sequenceEnd, out, coordinateLength, coordinateLength);
if (offset != sequenceEnd) {
throw new IllegalArgumentException("ECDSA DER signature must contain exactly two integers");
}
return out;
}
/* High 32 bits carry the decoded length; low 32 bits carry the next offset. */
private static long readDerLength(byte[] der, int offset) {
int cursor = offset;
if (cursor >= der.length) {
throw new IllegalArgumentException("Truncated ECDSA DER length");
}
int first = Byte.toUnsignedInt(der[cursor++]);
int length;
if (first < DER_LONG_FORM_BIT) {
length = first;
} else {
int octets = first & 0x7f;
if (octets == 0 || octets > 2 || cursor + octets > der.length
|| Byte.toUnsignedInt(der[cursor]) == 0) {
throw new IllegalArgumentException("Non-canonical ECDSA DER length");
}
length = 0;
for (int index = 0; index < octets; index++) {
length = (length << Byte.SIZE) | Byte.toUnsignedInt(der[cursor++]);
}
if (length < DER_LONG_FORM_BIT) {
throw new IllegalArgumentException("Non-canonical ECDSA DER length");
}
}
return ((long) length << Integer.SIZE) | Integer.toUnsignedLong(cursor);
}
private static int readPositiveInteger(byte[] der, int offset, int limit, byte[] target, int targetOffset,
int width) {
int cursor = offset;
if (cursor >= limit || Byte.toUnsignedInt(der[cursor++]) != DER_INTEGER_TAG) {
throw new IllegalArgumentException("ECDSA DER signature must contain INTEGER values");
}
long encodedLength = readDerLength(der, cursor);
cursor = (int) encodedLength;
int length = (int) (encodedLength >>> Integer.SIZE);
if (length == 0 || length > width + 1 || cursor > limit - length) {
throw new IllegalArgumentException("Invalid ECDSA DER integer length");
}
int first = Byte.toUnsignedInt(der[cursor]);
if (first == 0) {
if (length == 1 || (der[cursor + 1] & DER_LONG_FORM_BIT) == 0) {
throw new IllegalArgumentException("ECDSA DER integer must be positive, nonzero, and minimal");
}
cursor++;
length--;
} else if ((first & DER_LONG_FORM_BIT) != 0) {
throw new IllegalArgumentException("ECDSA DER integer must be positive");
}
if (length > width) {
throw new IllegalArgumentException("ECDSA integer does not fit into fixed P1363 width");
}
System.arraycopy(der, cursor, target, targetOffset + width - length, length);
return cursor + length;
}
private static byte[] ecdsaP1363ToDer(byte[] p1363, int fixedLength) throws IOException {
if (p1363.length != fixedLength) {
throw new IllegalArgumentException("Unexpected P1363 signature length: " + p1363.length);
@@ -252,18 +317,6 @@ public final class SignatureInteropProfile { // NOPMD
return new DERSequence(vector).getEncoded(ASN1Encoding.DER);
}
private static void copyUnsignedFixed(byte[] value, byte[] target, int offset, int width) {
int start = 0;
while (start < value.length - 1 && value[start] == 0) {
start++;
}
int len = value.length - start;
if (len > width) {
throw new IllegalArgumentException("ECDSA integer does not fit into fixed P1363 width");
}
System.arraycopy(value, start, target, offset + width - len, len);
}
private static String requireNonBlank(String value, String label) {
Objects.requireNonNull(value, label);
if (value.isBlank()) {