XadesUtil.java
// @formatter:off
package eu.europa.esig.dss.xades.signature;
import java.io.IOException;
import java.math.BigInteger;
import java.security.cert.X509CRL;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECPoint;
import java.util.Arrays;
import org.apache.commons.codec.binary.Base64;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import eu.europa.esig.dss.xml.common.definition.DSSNamespace;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.x509.Extension;
// ########################OVERRIDE_DSS#########################
// ##### DİKKAT: OVERRIDE DEĞİLDİR! ####
// ##### Bu yardımcı metot, XAdES kapsüllü değerler için ####
// ##### Tübitak’ın belirttiği şekilde 76 karakterde bir ####
// ##### satır sonu ekleyerek Base64 formatlar. ####
// ##### Standart Base64 kodlamayı, satır sonlu şekilde ####
// ##### üretmek için kullanılmalıdır. ####
// #############################################################
//
// @param data Base64 ile kodlanacak ikili veri
// @return 76 karakterde bir satır sonlu Base64 metin
/**
* Utility class for common XAdES operations.
* Provides reusable methods for Base64 encoding and XML element creation.
*/
public class XadesUtil {
/**
* The standard encoding URI for DER-encoded certificates and revocation data.
*/
public static final String DER_ENCODING_URI = "http://uri.etsi.org/01903/v1.2.2#DER";
/**
* Formats binary data with Base64 encoding using 76-character line breaks.
* This is the standard format for XAdES encapsulated values.
*
* CRITICAL: Apache Commons Codec Base64 is NOT thread-safe!
* It maintains internal buffers that can be corrupted during concurrent access.
* We MUST create a new instance for each call to avoid data corruption.
*
* @param data The binary data to encode
* @return Base64 encoded string with line breaks
*/
public static String formatWithBase64(byte[] data) {
// Create new instance for each call - Base64 is NOT thread-safe!
// Internal buffer corruption can occur with concurrent encoding
Base64 encoder = new Base64(76, new byte[] { '\n' });
return encoder.encodeToString(data);
}
/**
* Creates an EncapsulatedX509Certificate element with properly formatted Base64
* content.
*
* @param documentDom The parent document
* @param parentElement The parent element to append to
* @param xadesNamespace The XAdES namespace to use
* @param certificateData The DER-encoded certificate bytes
* @return The created Element
*/
public static Element createEncapsulatedCertificateElement(
Document documentDom,
Element parentElement,
DSSNamespace xadesNamespace,
byte[] certificateData) {
String formattedCert = formatWithBase64(certificateData);
Element certElement = documentDom.createElementNS(
xadesNamespace.getUri(),
xadesNamespace.getPrefix() + ":EncapsulatedX509Certificate");
certElement.setAttribute("Encoding", DER_ENCODING_URI);
certElement.setTextContent(formattedCert);
parentElement.appendChild(certElement);
return certElement;
}
/**
* Creates an EncapsulatedCRLValue element with properly formatted Base64
* content.
*
* @param documentDom The parent document
* @param parentElement The parent element to append to
* @param xadesNamespace The XAdES namespace to use
* @param crlData The DER-encoded CRL bytes
* @return The created Element
*/
public static Element createEncapsulatedCRLElement(
Document documentDom,
Element parentElement,
DSSNamespace xadesNamespace,
byte[] crlData) {
String formattedCRL = formatWithBase64(crlData);
Element crlElement = documentDom.createElementNS(
xadesNamespace.getUri(),
xadesNamespace.getPrefix() + ":EncapsulatedCRLValue");
crlElement.setAttribute("Encoding", DER_ENCODING_URI);
crlElement.setTextContent(formattedCRL);
parentElement.appendChild(crlElement);
return crlElement;
}
/**
* Creates an EncapsulatedOCSPValue element with properly formatted Base64
* content.
*
* @param documentDom The parent document
* @param parentElement The parent element to append to
* @param xadesNamespace The XAdES namespace to use
* @param ocspData The DER-encoded OCSP response bytes
* @return The created Element
*/
public static Element createEncapsulatedOCSPElement(
Document documentDom,
Element parentElement,
DSSNamespace xadesNamespace,
byte[] ocspData) {
String formattedOCSP = formatWithBase64(ocspData);
Element ocspElement = documentDom.createElementNS(
xadesNamespace.getUri(),
xadesNamespace.getPrefix() + ":EncapsulatedOCSPValue");
ocspElement.setAttribute("Encoding", DER_ENCODING_URI);
ocspElement.setTextContent(formattedOCSP);
parentElement.appendChild(ocspElement);
return ocspElement;
}
/**
* Creates an EncapsulatedTimeStamp element with properly formatted Base64
* content.
*
* @param documentDom The parent document
* @param parentElement The parent element to append to
* @param xadesNamespace The XAdES namespace to use
* @param timestampData The DER-encoded timestamp token bytes
* @return The created Element
*/
public static Element createEncapsulatedTimestampElement(
Document documentDom,
Element parentElement,
DSSNamespace xadesNamespace,
byte[] timestampData) {
String formattedTimestamp = formatWithBase64(timestampData);
Element timestampElement = documentDom.createElementNS(
xadesNamespace.getUri(),
xadesNamespace.getPrefix() + ":EncapsulatedTimeStamp");
timestampElement.setTextContent(formattedTimestamp);
parentElement.appendChild(timestampElement);
return timestampElement;
}
/**
* Extracts the CRL Number extension from an X509CRL object using Bouncy Castle.
* This method correctly handles the ASN.1/DER structure where the INTEGER value
* is encapsulated within an OCTET STRING.
*
* @param crl The X509CRL object to be processed.
* @return The String representation of the CRL Number.
* @throws IOException If an error occurs during ASN.1/DER parsing.
* @throws IllegalArgumentException If the CRL Number extension is not found or
* has an invalid structure.
*/
public static String extractCrlNumber(X509CRL crl) throws IOException, IllegalArgumentException {
// 1. Retrieve the raw DER-encoded value of the CRL Number extension (OID:
// 2.5.29.20).
// Using Extension.cRLNumber.getId() is cleaner than hardcoding the OID string.
byte[] extensionValue = crl.getExtensionValue(Extension.cRLNumber.getId());
if (extensionValue == null) {
throw new IllegalArgumentException(
"CRL Number extension (" + Extension.cRLNumber.getId() + ") not found in the CRL.");
}
// --- Bouncy Castle ASN.1/DER Decoding ---
// Step 1: The value returned by getExtensionValue() is the DER encoding
// of an OCTET STRING that wraps the actual CRL Number INTEGER.
ASN1OctetString octetString;
try {
// Decode the outer layer (the wrapper OCTET STRING).
octetString = (ASN1OctetString) ASN1Primitive.fromByteArray(extensionValue);
} catch (ClassCastException e) {
throw new IOException("The outer layer of the CRL extension is not an expected OCTET STRING.", e);
}
// Step 2: Decode the contents of the OCTET STRING, which should be the ASN.1
// INTEGER.
try (ASN1InputStream aIn = new ASN1InputStream(octetString.getOctets())) {
// Read the first object inside the OCTET STRING (which should be the INTEGER).
ASN1Primitive primitive = aIn.readObject();
if (!(primitive instanceof ASN1Integer)) {
throw new IOException("The content of the CRL Number extension is not the expected INTEGER type.");
}
// Retrieve the BigInteger value from the ASN1Integer object.
BigInteger crlNumber = ((ASN1Integer) primitive).getPositiveValue();
return crlNumber.toString();
}
}
/**
* EC public key değerini ANSI X9.62'de tanımlanan "uncompressed" formatta
* (0x04 || X || Y) bayt dizisine dönüştürür ve Base64 olarak kodlar.
*
* @param pub Elliptic Curve public key
* @return Base64 olarak kodlanmış, sıkıştırılmamış EC noktası
*/
public static String ecPublicKeyToUncompressedPointBase64(ECPublicKey pub) {
ECPoint w = pub.getW();
BigInteger x = w.getAffineX();
BigInteger y = w.getAffineY();
int fieldSize = pub.getParams().getCurve().getField().getFieldSize();
int coordLen = (fieldSize + 7) / 8;
byte[] xb = toFixedLength(x.toByteArray(), coordLen);
byte[] yb = toFixedLength(y.toByteArray(), coordLen);
byte[] out = new byte[1 + xb.length + yb.length];
out[0] = 0x04; // uncompressed point indicator
System.arraycopy(xb, 0, out, 1, xb.length);
System.arraycopy(yb, 0, out, 1 + xb.length, yb.length);
return java.util.Base64.getEncoder().encodeToString(out);
}
/**
* BigInteger kaynaklı uzunluk sapmalarını gidererek tam olarak belirtilen
* uzunlukta bir bayt dizisi üretir. İmza koordinatları için gereklidir.
*
* @param src Kaynak bayt dizisi
* @param len Hedef uzunluk
* @return len uzunluğunda normalize edilmiş bayt dizisi
*/
private static byte[] toFixedLength(byte[] src, int len) {
// src may contain leading zero due to BigInteger sign bit; normalize to exactly len bytes
if (src.length == len) return src;
if (src.length == len + 1 && src[0] == 0x00) {
return Arrays.copyOfRange(src, 1, src.length);
}
if (src.length < len) {
byte[] dst = new byte[len];
System.arraycopy(src, 0, dst, len - src.length, src.length);
return dst;
}
// src.length > len -> trim leading bytes (shouldn't happen for proper coords)
return Arrays.copyOfRange(src, src.length - len, src.length);
}
/**
* EC public key içinden NamedCurve OID değerini bulur. Standart DER
* kodlamasında ikinci OBJECT IDENTIFIER olarak yer aldığı varsayılır; bulunamazsa
* alan boyutuna göre bilinen eğri OID'leri döner.
*
* @param publicKey EC public key
* @return Eğriye ait OID veya bilinmiyorsa null
*/
public static String extractNamedCurveOID(ECPublicKey publicKey) {
try {
byte[] encoded = publicKey.getEncoded();
int found = 0;
for (int i = 0; i < encoded.length - 2; i++) {
if (encoded[i] == 0x06) { // OBJECT IDENTIFIER
int len = encoded[i + 1] & 0xFF;
byte[] oidBytes = Arrays.copyOfRange(encoded, i + 2, i + 2 + len);
String oid = decodeOID(oidBytes);
found++;
// 2. OID genellikle curve OID’dir
if (found == 2) {
return oid;
}
}
}
} catch (Exception e) {
// ignore
}
int fs = publicKey.getParams().getCurve().getField().getFieldSize();
switch (fs) {
case 256:
return "1.2.840.10045.3.1.7";
case 384:
return "1.3.132.0.34"; // secp384r1
case 521:
return "1.3.132.0.35";
}
return null;
}
/**
* DER kodlu OBJECT IDENTIFIER baytlarını metin formuna dönüştürür.
*
* @param bytes OID içeriğini temsil eden bayt dizisi
* @return Nokta ile ayrılmış OID string'i
*/
private static String decodeOID(byte[] bytes) {
StringBuilder oid = new StringBuilder();
int first = bytes[0] & 0xFF;
oid.append(first / 40).append('.').append(first % 40);
long value = 0;
for (int i = 1; i < bytes.length; i++) {
int b = bytes[i] & 0xFF;
if ((b & 0x80) != 0) {
value = (value << 7) | (b & 0x7F);
} else {
value = (value << 7) | b;
oid.append('.').append(value);
value = 0;
}
}
return oid.toString();
}
}