Key pairs and self-signed certificates are generated on the fly for each of + * ML-DSA-44/65/87 via {@link SelfSignedCertGenerator}, rather than loading a + * pre-generated keystore committed as a binary test resource (see SANTUARIO-634). + * The test requires BouncyCastle on the runtime classpath to supply the ML-DSA + * JCA provider; compile-time BC classes are deliberately avoided so the default + * build (without {@code -P bouncycastle}) still compiles cleanly. + * + *
Run with the Maven {@code bouncycastle} profile: + *
mvn test -Dtest=XMLSignatureMLDSATest -P bouncycastle+ */ +class XMLSignatureMLDSATest extends XMLSignatureAbstract { + + static final char[] KEY_PASSWORD = "security".toCharArray(); + + private static boolean mlDsaAvailable; + private static boolean bcAddedForTheTest; + private static KeyStore keyStore; + + @BeforeAll + static void setUp() { + Security.insertProviderAt( + new org.apache.jcp.xml.dsig.internal.dom.XMLDSigRI(), 1); + + if (Security.getProvider("BC") == null) { + try { + Class> cls = Class.forName("org.bouncycastle.jce.provider.BouncyCastleProvider"); + Provider bc = (Provider) cls.getConstructor().newInstance(); + Security.addProvider(bc); + bcAddedForTheTest = true; + } catch (ReflectiveOperationException e) { + mlDsaAvailable = false; + return; + } + } + + try { + keyStore = KeyStore.getInstance("PKCS12"); + keyStore.load(null, null); + for (String alias : new String[]{"ml-dsa-44", "ml-dsa-65", "ml-dsa-87"}) { + String jcaAlgorithm = alias.toUpperCase(); + KeyPairGenerator kpg = KeyPairGenerator.getInstance(jcaAlgorithm, "BC"); + KeyPair keyPair = kpg.generateKeyPair(); + X509Certificate cert = SelfSignedCertGenerator.generate( + keyPair, jcaAlgorithm, "CN=Test " + jcaAlgorithm + ",O=Apache Santuario,C=US", 365); + keyStore.setKeyEntry(alias, keyPair.getPrivate(), KEY_PASSWORD, new Certificate[]{cert}); + } + mlDsaAvailable = true; + } catch (Exception e) { + mlDsaAvailable = false; + } + } + + @AfterAll + static void tearDown() { + if (bcAddedForTheTest) { + Security.removeProvider("BC"); + } + } + + @ParameterizedTest + @CsvSource({ + XMLSignature.ALGO_ID_SIGNATURE_MLDSA_44 + ",ml-dsa-44", + XMLSignature.ALGO_ID_SIGNATURE_MLDSA_65 + ",ml-dsa-65", + XMLSignature.ALGO_ID_SIGNATURE_MLDSA_87 + ",ml-dsa-87", + }) + void testMLDSASignAndVerify(String signatureAlgorithmURI, String alias) throws Exception { + Assumptions.assumeTrue(mlDsaAvailable, "ML-DSA requires BouncyCastle 1.81+"); + byte[] signedXml = doSignWithJcpApi(signatureAlgorithmURI, alias, false); + Assertions.assertNotNull(signedXml); + assertValidSignatureWithJcpApi(signedXml, false); + } + + @ParameterizedTest + @CsvSource({ + XMLSignature.ALGO_ID_SIGNATURE_MLDSA_44 + ",ml-dsa-44", + XMLSignature.ALGO_ID_SIGNATURE_MLDSA_65 + ",ml-dsa-65", + XMLSignature.ALGO_ID_SIGNATURE_MLDSA_87 + ",ml-dsa-87", + }) + void testMLDSATamperedSignatureRejected(String signatureAlgorithmURI, String alias) throws Exception { + Assumptions.assumeTrue(mlDsaAvailable, "ML-DSA requires BouncyCastle 1.81+"); + byte[] signedXml = doSignWithJcpApi(signatureAlgorithmURI, alias, false); + + byte[] tamperedXml = flipByteInSignatureValue(signedXml); + + boolean coreValidity = validateSignatureWithJcpApi(tamperedXml, new KeySelectors.RawX509KeySelector()); + Assertions.assertFalse(coreValidity, "A tampered SignatureValue must not validate"); + } + + @ParameterizedTest + @CsvSource({ + XMLSignature.ALGO_ID_SIGNATURE_MLDSA_44 + ",ml-dsa-44", + XMLSignature.ALGO_ID_SIGNATURE_MLDSA_65 + ",ml-dsa-65", + XMLSignature.ALGO_ID_SIGNATURE_MLDSA_87 + ",ml-dsa-87", + }) + void testMLDSAWrongPublicKeyRejected(String signatureAlgorithmURI, String alias) throws Exception { + Assumptions.assumeTrue(mlDsaAvailable, "ML-DSA requires BouncyCastle 1.81+"); + byte[] signedXml = doSignWithJcpApi(signatureAlgorithmURI, alias, false); + + KeyPairGenerator kpg = KeyPairGenerator.getInstance(alias.toUpperCase(), "BC"); + PublicKey wrongPublicKey = kpg.generateKeyPair().getPublic(); + + KeySelector wrongKeySelector = new KeySelector() { + @Override + public KeySelectorResult select(KeyInfo keyInfo, Purpose purpose, AlgorithmMethod method, + XMLCryptoContext context) throws KeySelectorException { + return () -> wrongPublicKey; + } + }; + + boolean coreValidity = validateSignatureWithJcpApi(signedXml, wrongKeySelector); + Assertions.assertFalse(coreValidity, "Verification against the wrong public key must not validate"); + } + + /** + * Decodes the <SignatureValue> text content, flips one byte, and re-serializes - + * simulates an attacker (or transport bug) corrupting the signature bytes while leaving + * the rest of the document, including the embedded certificate, intact. + */ + private byte[] flipByteInSignatureValue(byte[] signedXml) throws Exception { + Document doc; + try (ByteArrayInputStream is = new ByteArrayInputStream(signedXml)) { + doc = XMLUtils.read(is, false); + } + NodeList sigValues = doc.getElementsByTagNameNS(Constants.SignatureSpecNS, "SignatureValue"); + Assertions.assertEquals(1, sigValues.getLength(), "Expected exactly one SignatureValue element"); + Element sigValueElement = (Element) sigValues.item(0); + + byte[] sigBytes = Base64.getMimeDecoder().decode(sigValueElement.getTextContent()); + sigBytes[sigBytes.length / 2] ^= (byte) 0xFF; + String tamperedBase64 = Base64.getEncoder().encodeToString(sigBytes); + + // Replace the SignatureValue element's text content in place + NodeList children = sigValueElement.getChildNodes(); + for (int i = children.getLength() - 1; i >= 0; i--) { + sigValueElement.removeChild(children.item(i)); + } + Text newText = doc.createTextNode(tamperedBase64); + sigValueElement.appendChild(newText); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + XMLUtils.outputDOMc14nWithComments(doc, bos); + return bos.toByteArray(); + } + + @Override + KeyStore getKeyStore() { + return keyStore; + } + + @Override + char[] getKeyPassword() { + return KEY_PASSWORD; + } +} diff --git a/src/test/java/org/apache/xml/security/test/stax/signature/StaxMLDSAKeyValueInboundTest.java b/src/test/java/org/apache/xml/security/test/stax/signature/StaxMLDSAKeyValueInboundTest.java new file mode 100644 index 000000000..5b93e604f --- /dev/null +++ b/src/test/java/org/apache/xml/security/test/stax/signature/StaxMLDSAKeyValueInboundTest.java @@ -0,0 +1,313 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.xml.security.test.stax.signature; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PublicKey; +import java.util.ArrayList; +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.xml.namespace.QName; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +import org.apache.xml.security.stax.ext.InboundXMLSec; +import org.apache.xml.security.stax.ext.SecurePart; +import org.apache.xml.security.stax.ext.XMLSec; +import org.apache.xml.security.stax.ext.XMLSecurityConstants; +import org.apache.xml.security.stax.ext.XMLSecurityProperties; +import org.apache.xml.security.stax.securityEvent.KeyValueTokenSecurityEvent; +import org.apache.xml.security.stax.securityEvent.SecurityEvent; +import org.apache.xml.security.stax.securityToken.SecurityTokenConstants; +import org.apache.xml.security.test.stax.utils.StAX2DOM; +import org.apache.xml.security.test.stax.utils.XMLSecEventAllocator; +import org.apache.xml.security.utils.Constants; +import org.apache.xml.security.utils.XMLUtils; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.w3c.dom.Text; + +/** + * StAX inbound verification of ML-DSA signatures whose KeyInfo carries the public key as a + * {@code dsig11:DEREncodedKeyValue} (the KeyValue form emitted for key types without a + * structured KeyValue element). No verification key is supplied out of band: the inbound + * processor must resolve the key from the document itself, then verify with it. + */ +class StaxMLDSAKeyValueInboundTest extends AbstractSignatureCreationTest { + + private static final Map
+ * http://www.apache.org/licenses/LICENSE-2.0 + *
+ * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.xml.security.testutils; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.Signature; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.Map; +import java.util.Set; + +/** + * Generates minimal self-signed X.509 v3 certificates using only public JDK APIs. + * + *
The certificate's DER structure is constructed directly from ASN.1/DER primitives + * and then parsed using CertificateFactory. No BouncyCastle, no sun.security.* internals, + * and no --add-opens flags are required. + * This class is designed to eliminate the need for storing test certificates in a keystore + * or truststore. Instead, the certificates are generated dynamically during test execution. + *
+ *These are acceptable constraints for unit and integration tests. + * + *
Adapted from Joze Rihtarsic's {@code SelfSignedCertGenerator} utility contributed in
+ * https://github.com/apache/santuario-xml-security-java/pull/617, and extended here with
+ * ML-DSA (FIPS 204) support per his suggestion on SANTUARIO-634 to avoid committing binary
+ * keystores as test resources.
+ */
+public final class SelfSignedCertGenerator {
+
+ private SelfSignedCertGenerator() {
+ }
+
+ // -------------------------------------------------------------------------
+ // ASN.1 universal tag constants (ITU-T X.690)
+ // -------------------------------------------------------------------------
+
+ private static final int TAG_INTEGER = 0x02;
+ private static final int TAG_BIT_STRING = 0x03;
+ private static final int TAG_OID = 0x06;
+ private static final int TAG_UTF8_STRING = 0x0C;
+ private static final int TAG_PRINTABLE_STRING = 0x13;
+ private static final int TAG_UTC_TIME = 0x17;
+ private static final int TAG_SEQUENCE = 0x30;
+ private static final int TAG_SET = 0x31;
+ /** Context-specific constructed [0] tag — used for the TBSCertificate version field. */
+ private static final int TAG_CONTEXT_0 = 0xA0;
+
+ // -------------------------------------------------------------------------
+ // Pre-built DER encoding constants
+ // -------------------------------------------------------------------------
+
+ /** DER encoding of ASN.1 NULL (05 00). */
+ private static final byte[] DER_NULL = {0x05, 0x00};
+
+ /**
+ * DER encoding of TBSCertificate {@code version} field set to v3 (INTEGER value 2)
+ * wrapped in an [0] EXPLICIT context tag.
+ */
+ private static final byte[] TBS_VERSION_V3 = {
+ (byte) TAG_CONTEXT_0, 0x03, (byte) TAG_INTEGER, 0x01, 0x02
+ };
+
+ // -------------------------------------------------------------------------
+ // Signature algorithm OID strings
+ // -------------------------------------------------------------------------
+
+ /** SHA-256 with RSA Encryption — RFC 4055, OID 1.2.840.113549.1.1.11 */
+ private static final String OID_SHA256_WITH_RSA = "1.2.840.113549.1.1.11";
+ /** SHA-384 with RSA Encryption — RFC 4055, OID 1.2.840.113549.1.1.12 */
+ private static final String OID_SHA384_WITH_RSA = "1.2.840.113549.1.1.12";
+ /** SHA-512 with RSA Encryption — RFC 4055, OID 1.2.840.113549.1.1.13 */
+ private static final String OID_SHA512_WITH_RSA = "1.2.840.113549.1.1.13";
+ /** ECDSA with SHA-256 — RFC 5758, OID 1.2.840.10045.4.3.2 */
+ private static final String OID_SHA256_WITH_ECDSA = "1.2.840.10045.4.3.2";
+ /** ECDSA with SHA-384 — RFC 5758, OID 1.2.840.10045.4.3.3 */
+ private static final String OID_SHA384_WITH_ECDSA = "1.2.840.10045.4.3.3";
+ /** ECDSA with SHA-512 — RFC 5758, OID 1.2.840.10045.4.3.4 */
+ private static final String OID_SHA512_WITH_ECDSA = "1.2.840.10045.4.3.4";
+ /** Ed25519 — RFC 8410, OID 1.3.101.112 */
+ private static final String OID_ED25519 = "1.3.101.112";
+ /** Ed448 — RFC 8410, OID 1.3.101.113 */
+ private static final String OID_ED448 = "1.3.101.113";
+ /** ML-DSA-44 — NIST CSOR / draft-ietf-lamps-dilithium-certificates, OID 2.16.840.1.101.3.4.3.17 */
+ private static final String OID_ML_DSA_44 = "2.16.840.1.101.3.4.3.17";
+ /** ML-DSA-65 — NIST CSOR / draft-ietf-lamps-dilithium-certificates, OID 2.16.840.1.101.3.4.3.18 */
+ private static final String OID_ML_DSA_65 = "2.16.840.1.101.3.4.3.18";
+ /** ML-DSA-87 — NIST CSOR / draft-ietf-lamps-dilithium-certificates, OID 2.16.840.1.101.3.4.3.19 */
+ private static final String OID_ML_DSA_87 = "2.16.840.1.101.3.4.3.19";
+
+ /**
+ * OIDs whose AlgorithmIdentifier MUST have absent (not NULL) parameters — the EdDSA arc
+ * (RFC 8410 §6) and the ML-DSA OIDs (draft-ietf-lamps-dilithium-certificates §5.1).
+ */
+ private static final Set RSA and ECDSA algorithms include a trailing {@code NULL} parameters element
+ * (RFC 4055 §3.2, conventionally also used for ECDSA). EdDSA and ML-DSA algorithms
+ * omit parameters entirely (RFC 8410; draft-ietf-lamps-dilithium-certificates §5.1).
+ */
+ private static final Map A SEQUENCE in ASN.1 represents an ordered collection of elements
+ * The DER tag for a SEQUENCE is 0x30. Thus, for the DN In ASN.1, a SET represents an unordered collection of elements. Although the
+ * abstract syntax does not impose ordering, DER requires all elements inside a SET
+ * to be sorted by their encoded byte values to ensure canonical form. The DER tag for a SET is 0x31. Use in X.509:
+ * TBSCertificate ::= SEQUENCE {
+ * version [0] EXPLICIT INTEGER DEFAULT v1,
+ * serialNumber INTEGER,
+ * signature AlgorithmIdentifier,
+ * issuer Name,
+ * validity Validity,
+ * subject Name,
+ * subjectPublicKeyInfo SubjectPublicKeyInfo
+ * }
+ *
+ */
+ private static byte[] buildTbs(byte[] algId, byte[] name,
+ byte[] spki, int validityDays) {
+ // Serial: milliseconds since epoch — unique enough for test certs
+ byte[] serial = integer(BigInteger.valueOf(System.currentTimeMillis()));
+ byte[] validity = buildValidity(validityDays);
+ // issuer == subject for self-signed
+ return sequence(cat(TBS_VERSION_V3, serial, algId, name, validity, name, spki));
+ }
+
+ private static byte[] buildValidity(int validityDays) {
+ Instant notBefore = Instant.now();
+ Instant notAfter = notBefore.plusSeconds(validityDays * 86_400L);
+ return sequence(cat(utcTime(notBefore), utcTime(notAfter)));
+ }
+
+ // -------------------------------------------------------------------------
+ // DN encoding — CN, C, O, OU attributes (RFC 4519)
+ // -------------------------------------------------------------------------
+
+ /**
+ * Encodes a Name containing a single CN attribute.
+ *
+ *
+ * Name ::= SEQUENCE OF SET OF SEQUENCE { OID, value }
+ *
+ */
+ private static byte[] encodeName(String dn) {
+ ByteArrayOutputStream rdns = new ByteArrayOutputStream();
+ for (String part : dn.split(",")) {
+ String trimmed = part.strip();
+ int eq = trimmed.indexOf('=');
+ if (eq < 0) continue;
+ String key = trimmed.substring(0, eq).strip().toUpperCase();
+ String val = trimmed.substring(eq + 1).strip();
+ byte[] oidBytes;
+ byte[] valueBytes;
+ switch (key) {
+ case "CN":
+ oidBytes = OID_BYTES_CN;
+ valueBytes = tlv(TAG_UTF8_STRING, val.getBytes(StandardCharsets.UTF_8));
+ break;
+ case "C":
+ oidBytes = OID_BYTES_C;
+ // countryName uses PrintableString; ISO 3166-1 alpha-2 codes are ASCII
+ valueBytes = tlv(TAG_PRINTABLE_STRING, val.getBytes(StandardCharsets.US_ASCII));
+ break;
+ case "O":
+ oidBytes = OID_BYTES_O;
+ valueBytes = tlv(TAG_UTF8_STRING, val.getBytes(StandardCharsets.UTF_8));
+ break;
+ case "OU":
+ oidBytes = OID_BYTES_OU;
+ valueBytes = tlv(TAG_UTF8_STRING, val.getBytes(StandardCharsets.UTF_8));
+ break;
+ default:
+ continue; // unsupported attribute — skip
+ }
+ byte[] rdn = set(sequence(cat(oidBytes, valueBytes)));
+ rdns.write(rdn, 0, rdn.length);
+ }
+ if (rdns.size() == 0) {
+ // fallback: treat the whole string as a CN value
+ byte[] cnValue = tlv(TAG_UTF8_STRING, dn.getBytes(StandardCharsets.UTF_8));
+ byte[] rdn = set(sequence(cat(OID_BYTES_CN, cnValue)));
+ rdns.write(rdn, 0, rdn.length);
+ }
+ return sequence(rdns.toByteArray());
+ }
+
+ // -------------------------------------------------------------------------
+ // DER / ASN.1 primitives
+ // -------------------------------------------------------------------------
+
+ /**
+ * Encodes the provided content as an ASN.1 DER SEQUENCE.
+ *
+ *
+ * AttributeTypeAndValue ::= SEQUENCE {
+ * type OBJECT IDENTIFIER,
+ * value DirectoryString
+ * }
+ *
+ *
+ * CN=Test, the inner attribute pair is encoded as:
+ * 30 ... SEQUENCE (AttributeTypeAndValue)
+ * 06 03 55 04 03 OID 2.5.4.3 (commonName)
+ * 0C 04 54 65 73 74 UTF8String "Test"
+ *
+ *
+ * @param content the already‑encoded DER content to wrap in a SEQUENCE
+ * @return the DER‑encoded SEQUENCE (tag 0x30 + length + content)
+ */
+ private static byte[] sequence(byte[] content) {
+ return tlv(TAG_SEQUENCE, content);
+ }
+
+ /**
+ * Encodes the provided content as an ASN.1 DER SET value.
+ *
+ *
+ * Within an X.509 Distinguished Name (DN), each RelativeDistinguishedName (RDN)
+ * is encoded as a SET containing one or more AttributeTypeAndValue structures.
+ * A DN therefore follows the structure:
+ * Name ::= SEQUENCE OF
+ * SET OF
+ * SEQUENCE {
+ * type OBJECT IDENTIFIER, -- e.g., 2.5.4.3 (commonName)
+ * value DirectoryString -- e.g., UTF8String "Test"
+ * }
+ *
+ * @param content the already‑encoded DER content to wrap in a SET
+ * @return the DER-encoded SET (tag 0x31 + length + content)
+ */
+ private static byte[] set(byte[] content) {
+ return tlv(TAG_SET, content);
+ }
+
+ private static byte[] integer(BigInteger value) {
+ // toByteArray() produces two's-complement big-endian; positive integers may
+ // have a leading 0x00 byte if the MSB would otherwise be set — that is correct
+ // DER INTEGER encoding for a non-negative number.
+ return tlv(TAG_INTEGER, value.toByteArray());
+ }
+
+ private static byte[] bitString(byte[] value) {
+ return tlv(TAG_BIT_STRING, cat(new byte[]{0x00}, value)); // 0x00 = zero unused bits
+ }
+
+ // UTCTime covers 2000–2049 (yy < 50 → 20yy). Sufficient for short-lived test certs.
+ private static final DateTimeFormatter UTC_TIME_FMT =
+ DateTimeFormatter.ofPattern("yyMMddHHmmss'Z'").withZone(ZoneOffset.UTC);
+
+ private static byte[] utcTime(Instant instant) {
+ return tlv(TAG_UTC_TIME, UTC_TIME_FMT.format(instant).getBytes(StandardCharsets.US_ASCII));
+ }
+
+ /**
+ * Encodes a DER TLV (Tag–Length–Value) triplet.
+ * Lengths up to 65535 bytes are supported; that is sufficient for all key types
+ * used in practice.
+ */
+ private static byte[] tlv(int tag, byte[] value) {
+ int len = value.length;
+ byte[] lenBytes;
+ if (len < 128) {
+ lenBytes = new byte[]{(byte) len};
+ } else if (len < 256) {
+ lenBytes = new byte[]{(byte) 0x81, (byte) len};
+ } else {
+ lenBytes = new byte[]{(byte) 0x82, (byte) (len >> 8), (byte) (len & 0xFF)};
+ }
+ byte[] out = new byte[1 + lenBytes.length + len];
+ out[0] = (byte) tag;
+ System.arraycopy(lenBytes, 0, out, 1, lenBytes.length);
+ System.arraycopy(value, 0, out, 1 + lenBytes.length, len);
+ return out;
+ }
+
+ /**
+ * Concatenates byte arrays.
+ */
+ private static byte[] cat(byte[]... parts) {
+ int total = 0;
+ for (byte[] p : parts) {
+ total += p.length;
+ }
+ byte[] buf = new byte[total];
+ int pos = 0;
+ for (byte[] p : parts) {
+ System.arraycopy(p, 0, buf, pos, p.length);
+ pos += p.length;
+ }
+ return buf;
+ }
+
+ /**
+ * Encode oid as certificate algorithm identifier.
+ * @param oid
+ * @return
+ */
+ public static byte[] encodeAlgorithmIdentifier(String oid) {
+ // RFC 8410 §6: all OIDs under arc 1.3.101 (X25519, X448, Ed25519, Ed448) MUST omit parameters.
+ // draft-ietf-lamps-dilithium-certificates §5.1: ML-DSA OIDs MUST omit parameters.
+ // RFC 4055 §3.2: RSA signature algorithms MUST include a NULL parameters element.
+ byte[] params = NO_PARAMS_ALGORITHMS.contains(oid) ? new byte[0] : DER_NULL;
+ return sequence(cat(encodeOid(oid), params));
+ }
+
+ /**
+ * Endodes all number values to ASN.1/DER encoded bytearray
+ * @param oid - the value
+ * @return encoded byte array
+ */
+ public static byte[] encodeOid(String oid) {
+ String[] parts = oid.split("\\.");
+ ByteArrayOutputStream body = new ByteArrayOutputStream();
+ body.write(40 * Integer.parseInt(parts[0]) + Integer.parseInt(parts[1]));
+ for (int i = 2; i < parts.length; i++) {
+ byte[] arc = encodeBase128(Long.parseLong(parts[i]));
+ body.write(arc, 0, arc.length);
+ }
+ return tlv(TAG_OID, body.toByteArray());
+ }
+
+ /**
+ * It encodes a non-negative integer using base-128 (variable-length) encoding, which is the standard
+ * way ASN.1/DER encodes OID arc values
+ * @param value the long value
+ * @return ASN.1/DER encoded value
+ */
+ private static byte[] encodeBase128(long value) {
+ byte[] stack = new byte[10];
+ int count = 0;
+ do {
+ stack[count++] = (byte) (value & 0x7F);
+ value >>= 7;
+ } while (value > 0);
+ byte[] result = new byte[count];
+ for (int i = 0; i < count; i++) {
+ result[i] = (byte) (stack[count - 1 - i] | (i < count - 1 ? 0x80 : 0x00));
+ }
+ return result;
+ }
+}