diff --git a/src/main/java/org/kopi/ebics/certificate/CertificateManager.java b/src/main/java/org/kopi/ebics/certificate/CertificateManager.java index ed887e16..838c7d6f 100644 --- a/src/main/java/org/kopi/ebics/certificate/CertificateManager.java +++ b/src/main/java/org/kopi/ebics/certificate/CertificateManager.java @@ -44,19 +44,31 @@ public class CertificateManager { public CertificateManager(EbicsUser user) { this.user = user; + this.signatureVersion = SignatureVersion.A005.getVersion(); generator = new X509Generator(); } /** - * Creates the certificates for the user + * Creates the certificates for the user using the default A005 signature version. * @throws GeneralSecurityException * @throws IOException */ public void create() throws GeneralSecurityException, IOException { + create(SignatureVersion.A005.getVersion()); + } + + /** + * Creates the certificates for the user using the specified signature version. + * @param signatureVersion the EBICS signature version (A005 or A006) + * @throws GeneralSecurityException + * @throws IOException + */ + public void create(String signatureVersion) throws GeneralSecurityException, IOException { + this.signatureVersion = signatureVersion; Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.DAY_OF_YEAR, X509Constants.DEFAULT_DURATION); - createA005Certificate(new Date(calendar.getTimeInMillis())); + createSignatureCertificate(new Date(calendar.getTimeInMillis()), signatureVersion); createX002Certificate(new Date(calendar.getTimeInMillis())); createE002Certificate(new Date(calendar.getTimeInMillis())); setUserCertificates(); @@ -76,7 +88,7 @@ private void setUserCertificates() { } /** - * Creates the signature certificate. + * Creates the signature certificate using the default A005 algorithm. * @param end the expiration date of a the certificate. * @throws GeneralSecurityException * @throws IOException @@ -87,10 +99,27 @@ public void createA005Certificate(Date end) throws GeneralSecurityException, IOE a005PrivateKey = keypair.getPrivate(); } + /** + * Creates the signature certificate using the specified signature version. + * @param end the expiration date of the certificate. + * @param signatureVersion the EBICS signature version (A005 or A006) + * @throws GeneralSecurityException + * @throws IOException + */ + public void createSignatureCertificate(Date end, String signatureVersion) throws GeneralSecurityException, IOException { + KeyPair keypair = KeyUtil.makeKeyPair(X509Constants.EBICS_KEY_SIZE); + a005Certificate = generator.generateSignatureCertificate(keypair, user.getDN(), new Date(), end, signatureVersion); + a005PrivateKey = keypair.getPrivate(); + } + X509Certificate getA005Certificate() { return a005Certificate; } + X509Certificate getSignatureCertificate() { + return a005Certificate; + } + /** * Creates the authentication certificate. * @param end the expiration date of a certificate. @@ -144,16 +173,31 @@ public void save(File directory, PasswordCallback pwdCallBack) public void load(File path, PasswordCallback pwdCallBack) throws GeneralSecurityException, IOException { + load(path, pwdCallBack, SignatureVersion.A005.getVersion()); + } + + /** + * Loads user certificates from a given key store using the specified signature version alias. + * @param path the key store path + * @param pwdCallBack the password call back + * @param signatureVersion the EBICS signature version (A005 or A006) used as the keystore alias + * @throws GeneralSecurityException + * @throws IOException + */ + public void load(File path, PasswordCallback pwdCallBack, String signatureVersion) + throws GeneralSecurityException, IOException + { + this.signatureVersion = signatureVersion; KeyStoreManager loader; loader = new KeyStoreManager(); loader.load(path, pwdCallBack.getPassword()); - a005Certificate = loader.getCertificate(user.getUserId() + "-A005"); + a005Certificate = loader.getCertificate(user.getUserId() + "-" + signatureVersion); x002Certificate = loader.getCertificate(user.getUserId() + "-X002"); e002Certificate = loader.getCertificate(user.getUserId() + "-E002"); - a005PrivateKey = loader.getPrivateKey(user.getUserId() + "-A005"); + a005PrivateKey = loader.getPrivateKey(user.getUserId() + "-" + signatureVersion); x002PrivateKey = loader.getPrivateKey(user.getUserId() + "-X002"); e002PrivateKey = loader.getPrivateKey(user.getUserId() + "-E002"); setUserCertificates(); @@ -191,7 +235,7 @@ public void writePKCS12Certificate(char[] password, OutputStream fos) keystore = KeyStore.getInstance("PKCS12", new BouncyCastleProvider()); keystore.load(null, null); - keystore.setKeyEntry(user.getUserId() + "-A005", + keystore.setKeyEntry(user.getUserId() + "-" + signatureVersion, a005PrivateKey, password, new X509Certificate[] {a005Certificate}); @@ -212,6 +256,7 @@ public void writePKCS12Certificate(char[] password, OutputStream fos) private final X509Generator generator; private final EbicsUser user; + private String signatureVersion; private X509Certificate a005Certificate; private X509Certificate e002Certificate; diff --git a/src/main/java/org/kopi/ebics/certificate/SignatureVersion.java b/src/main/java/org/kopi/ebics/certificate/SignatureVersion.java new file mode 100644 index 00000000..901017a1 --- /dev/null +++ b/src/main/java/org/kopi/ebics/certificate/SignatureVersion.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2026 Uwe Maurer + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +package org.kopi.ebics.certificate; + +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.spec.MGF1ParameterSpec; +import java.security.spec.PSSParameterSpec; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +/** + * Encapsulates signature algorithm details for EBICS signature versions. + * + *
Two implementations are provided as constants: + *
X509Certificate from a given
- * KeyPair and limit dates validations
+ * KeyPair and limit dates validations, using the default signature algorithm.
* @param keypair the given key pair
* @param issuer the certificate issuer
* @param notBefore the begin validity date
@@ -140,6 +169,30 @@ public X509Certificate generate(KeyPair keypair,
Date notAfter,
CertificateKeyUsage keyusage)
throws GeneralSecurityException, IOException
+ {
+ return generate(keypair, issuer, notBefore, notAfter, keyusage, X509Constants.SIGNATURE_ALGORITHM);
+ }
+
+ /**
+ * Returns an X509Certificate from a given
+ * KeyPair and limit dates validations
+ * @param keypair the given key pair
+ * @param issuer the certificate issuer
+ * @param notBefore the begin validity date
+ * @param notAfter the end validity date
+ * @param keyusage the certificate key usage
+ * @param signatureAlgorithm the certificate signature algorithm
+ * @return the X509 certificate
+ * @throws GeneralSecurityException
+ * @throws IOException
+ */
+ public X509Certificate generate(KeyPair keypair,
+ String issuer,
+ Date notBefore,
+ Date notAfter,
+ CertificateKeyUsage keyusage,
+ String signatureAlgorithm)
+ throws GeneralSecurityException, IOException
{
X509V3CertificateGenerator generator;
BigInteger serial;
@@ -154,7 +207,7 @@ public X509Certificate generate(KeyPair keypair,
generator.setNotAfter(notAfter);
generator.setSubjectDN(new X509Principal(issuer));
generator.setPublicKey(keypair.getPublic());
- generator.setSignatureAlgorithm(X509Constants.SIGNATURE_ALGORITHM);
+ generator.setSignatureAlgorithm(signatureAlgorithm);
generator.addExtension(X509Extensions.BasicConstraints,
false,
new BasicConstraints(true));
diff --git a/src/main/java/org/kopi/ebics/client/User.java b/src/main/java/org/kopi/ebics/client/User.java
index e1a37186..1c1585c6 100644
--- a/src/main/java/org/kopi/ebics/client/User.java
+++ b/src/main/java/org/kopi/ebics/client/User.java
@@ -35,6 +35,7 @@
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.kopi.ebics.certificate.CertificateManager;
+import org.kopi.ebics.certificate.SignatureVersion;
import org.kopi.ebics.exception.EbicsException;
import org.kopi.ebics.interfaces.EbicsPartner;
import org.kopi.ebics.interfaces.EbicsUser;
@@ -511,8 +512,12 @@ public byte[] authenticate(byte[] digest) throws GeneralSecurityException {
*/
@Override
public byte[] sign(byte[] digest) throws GeneralSecurityException {
- Signature signature = Signature.getInstance("SHA256WithRSA", BouncyCastleProvider.PROVIDER_NAME);
- signature.initSign(a005PrivateKey);
+ return sign(digest, SignatureVersion.A005.getVersion());
+ }
+
+ @Override
+ public byte[] sign(byte[] digest, String signatureVersion) throws GeneralSecurityException {
+ Signature signature = SignatureVersion.lookup(signatureVersion).createSignature(a005PrivateKey);
signature.update(removeOSSpecificChars(digest));
return signature.sign();
}
diff --git a/src/main/java/org/kopi/ebics/interfaces/EbicsUser.java b/src/main/java/org/kopi/ebics/interfaces/EbicsUser.java
index d6649fec..fc741d9c 100644
--- a/src/main/java/org/kopi/ebics/interfaces/EbicsUser.java
+++ b/src/main/java/org/kopi/ebics/interfaces/EbicsUser.java
@@ -154,14 +154,27 @@ public interface EbicsUser {
byte[] authenticate(byte[] digest) throws GeneralSecurityException;
/**
- * Signs the given digest with the private A005 key.
+ * Signs the given digest with the private signature key using A005 (PKCS1-v1_5).
* @param digest
* @return the signature
* @throws IOException
- * @throws GeneralSecurityException
+ * @throws GeneralSecurityException
*/
byte[] sign(byte[] digest) throws IOException, GeneralSecurityException;
+ /**
+ * Signs the given digest with the private signature key using the specified version.
+ * A005 uses EMSA-PKCS1-v1_5 with SHA-256, A006 uses EMSA-PSS with SHA-256.
+ * @param digest the data to sign
+ * @param signatureVersion the signature version (A005 or A006)
+ * @return the signature
+ * @throws IOException
+ * @throws GeneralSecurityException
+ */
+ default byte[] sign(byte[] digest, String signatureVersion) throws IOException, GeneralSecurityException {
+ return sign(digest);
+ }
+
/**
* Uses the E001 key to decrypt the given secret key.
* @param encryptedKey the given secret key
diff --git a/src/main/java/org/kopi/ebics/xml/EbicsXmlFactory.java b/src/main/java/org/kopi/ebics/xml/EbicsXmlFactory.java
index ddd8137d..1be4d7bf 100644
--- a/src/main/java/org/kopi/ebics/xml/EbicsXmlFactory.java
+++ b/src/main/java/org/kopi/ebics/xml/EbicsXmlFactory.java
@@ -1159,13 +1159,14 @@ public static EbicsRequestDocument.EbicsRequest.Body createEbicsRequestBody(
* @return the DataTransferRequestType XML object
*/
public static DataTransferRequestType createDataTransferRequestType(
- DataEncryptionInfo dataEncryptionInfo, SignatureData signatureData, String digestValue) {
+ DataEncryptionInfo dataEncryptionInfo, SignatureData signatureData, String digestValue,
+ String signatureVersion) {
DataTransferRequestType newDataTransferRequestType = DataTransferRequestType.Factory.newInstance();
newDataTransferRequestType.setDataEncryptionInfo(dataEncryptionInfo);
newDataTransferRequestType.setSignatureData(signatureData);
if (digestValue != null) {
var digest = DataDigestType.Factory.newInstance();
- digest.setSignatureVersion("A005");
+ digest.setSignatureVersion(signatureVersion);
digest.setStringValue(digestValue);
newDataTransferRequestType.setDataDigest(digest);
}
@@ -1173,6 +1174,15 @@ public static DataTransferRequestType createDataTransferRequestType(
return newDataTransferRequestType;
}
+ /**
+ * @deprecated Use {@link #createDataTransferRequestType(DataEncryptionInfo, SignatureData, String, String)} instead.
+ */
+ @Deprecated
+ public static DataTransferRequestType createDataTransferRequestType(
+ DataEncryptionInfo dataEncryptionInfo, SignatureData signatureData, String digestValue) {
+ return createDataTransferRequestType(dataEncryptionInfo, signatureData, digestValue, "A005");
+ }
+
/**
* Create the StaticHeaderType XML object
*
diff --git a/src/main/java/org/kopi/ebics/xml/SPRRequestElement.java b/src/main/java/org/kopi/ebics/xml/SPRRequestElement.java
index d636ccd0..c0ae1f89 100644
--- a/src/main/java/org/kopi/ebics/xml/SPRRequestElement.java
+++ b/src/main/java/org/kopi/ebics/xml/SPRRequestElement.java
@@ -118,7 +118,8 @@ public void buildInitialization() throws EbicsException {
dataEncryptionInfo = EbicsXmlFactory.createDataEncryptionInfo(true,
encryptionPubKeyDigest,
generateTransactionKey());
- dataTransfer = EbicsXmlFactory.createDataTransferRequestType(dataEncryptionInfo, signatureData, null);
+ dataTransfer = EbicsXmlFactory.createDataTransferRequestType(dataEncryptionInfo, signatureData, null,
+ session.getConfiguration().getSignatureVersion());
body = EbicsXmlFactory.createEbicsRequestBody(dataTransfer);
request = EbicsXmlFactory.createEbicsRequest(session.getConfiguration().getRevision(),
session.getConfiguration().getVersion(),
diff --git a/src/main/java/org/kopi/ebics/xml/UploadInitializationRequestElement.java b/src/main/java/org/kopi/ebics/xml/UploadInitializationRequestElement.java
index cca19d28..1bafd049 100644
--- a/src/main/java/org/kopi/ebics/xml/UploadInitializationRequestElement.java
+++ b/src/main/java/org/kopi/ebics/xml/UploadInitializationRequestElement.java
@@ -138,7 +138,7 @@ public void buildInitialization() throws EbicsException {
throw new EbicsException(e);
}
var dataTransfer = EbicsXmlFactory.createDataTransferRequestType(dataEncryptionInfo,
- signatureData, digest);
+ signatureData, digest, session.getConfiguration().getSignatureVersion());
var body = EbicsXmlFactory.createEbicsRequestBody(dataTransfer);
var request = EbicsXmlFactory.createEbicsRequest(session.getConfiguration().getRevision(),
session.getConfiguration().getVersion(), header, body);
diff --git a/src/main/java/org/kopi/ebics/xml/UserSignature.java b/src/main/java/org/kopi/ebics/xml/UserSignature.java
index 0f665c02..f66b9fea 100644
--- a/src/main/java/org/kopi/ebics/xml/UserSignature.java
+++ b/src/main/java/org/kopi/ebics/xml/UserSignature.java
@@ -61,7 +61,7 @@ public void build() throws EbicsException {
byte[] signature;
try {
- signature = user.sign(toSign);
+ signature = user.sign(toSign, signatureVersion);
} catch (IOException e) {
throw new EbicsException(e.getMessage());
} catch (GeneralSecurityException e) {
diff --git a/src/test/java/org/kopi/ebics/certificate/SignatureVersionTest.java b/src/test/java/org/kopi/ebics/certificate/SignatureVersionTest.java
new file mode 100644
index 00000000..f79ab9ea
--- /dev/null
+++ b/src/test/java/org/kopi/ebics/certificate/SignatureVersionTest.java
@@ -0,0 +1,309 @@
+package org.kopi.ebics.certificate;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.Security;
+import java.security.Signature;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.RSAPublicKey;
+import java.util.Calendar;
+import java.util.Date;
+
+import javax.security.auth.x500.X500Principal;
+
+import org.apache.xml.security.Init;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.kopi.ebics.exception.EbicsException;
+import org.kopi.ebics.interfaces.EbicsPartner;
+import org.kopi.ebics.interfaces.EbicsUser;
+import org.kopi.ebics.interfaces.PasswordCallback;
+
+class SignatureVersionTest {
+
+ @BeforeAll
+ static void setup() {
+ Init.init();
+ Security.addProvider(new BouncyCastleProvider());
+ }
+
+ @Test
+ void lookupRejectsNull() {
+ assertThrows(IllegalArgumentException.class, () -> SignatureVersion.lookup(null));
+ }
+
+ @Test
+ void lookupRejectsInvalidVersion() {
+ assertThrows(IllegalArgumentException.class, () -> SignatureVersion.lookup("a006"));
+ assertThrows(IllegalArgumentException.class, () -> SignatureVersion.lookup("A007"));
+ assertThrows(IllegalArgumentException.class, () -> SignatureVersion.lookup(""));
+ }
+
+ @Test
+ void lookupReturnsCorrectInstances() {
+ assertSame(SignatureVersion.A005, SignatureVersion.lookup("A005"));
+ assertSame(SignatureVersion.A006, SignatureVersion.lookup("A006"));
+ }
+
+ @Test
+ void a005SignatureAlgorithm() {
+ assertEquals("SHA256WithRSA", SignatureVersion.A005.getSignatureAlgorithm());
+ }
+
+ @Test
+ void a006SignatureAlgorithm() {
+ assertEquals("SHA256withRSA/PSS", SignatureVersion.A006.getSignatureAlgorithm());
+ }
+
+ @Test
+ void a005CertificateAlgorithm() {
+ assertEquals("SHA256WithRSAEncryption", SignatureVersion.A005.getCertificateSignatureAlgorithm());
+ }
+
+ @Test
+ void a006CertificateAlgorithm() {
+ assertEquals("SHA256WithRSAAndMGF1", SignatureVersion.A006.getCertificateSignatureAlgorithm());
+ }
+
+ @Test
+ void a005VersionString() {
+ assertEquals("A005", SignatureVersion.A005.getVersion());
+ }
+
+ @Test
+ void a006VersionString() {
+ assertEquals("A006", SignatureVersion.A006.getVersion());
+ }
+
+ @Test
+ void a005SignAndVerify() throws Exception {
+ KeyPair keyPair = KeyUtil.makeKeyPair(2048);
+ byte[] data = "EBICS test message for A005 signature".getBytes();
+
+ Signature signer = SignatureVersion.A005.createSignature(keyPair.getPrivate());
+ signer.update(data);
+ byte[] sig = signer.sign();
+
+ assertNotNull(sig);
+ assertTrue(sig.length > 0);
+
+ Signature verifier = SignatureVersion.A005.createVerifySignature(keyPair.getPublic());
+ verifier.update(data);
+ assertTrue(verifier.verify(sig), "A005 signature verification must succeed");
+ }
+
+ @Test
+ void a006SignAndVerify() throws Exception {
+ KeyPair keyPair = KeyUtil.makeKeyPair(2048);
+ byte[] data = "EBICS test message for A006 signature".getBytes();
+
+ Signature signer = SignatureVersion.A006.createSignature(keyPair.getPrivate());
+ signer.update(data);
+ byte[] sig = signer.sign();
+
+ assertNotNull(sig);
+ assertTrue(sig.length > 0);
+
+ Signature verifier = SignatureVersion.A006.createVerifySignature(keyPair.getPublic());
+ verifier.update(data);
+ assertTrue(verifier.verify(sig), "A006 signature verification must succeed");
+ }
+
+ @Test
+ void a005AndA006SignaturesAreNotInterchangeable() throws Exception {
+ KeyPair keyPair = KeyUtil.makeKeyPair(2048);
+ byte[] data = "EBICS cross-version test".getBytes();
+
+ // Sign with A005
+ Signature a005Signer = SignatureVersion.A005.createSignature(keyPair.getPrivate());
+ a005Signer.update(data);
+ byte[] a005Sig = a005Signer.sign();
+
+ // Verify with A006 should fail
+ Signature a006Verifier = SignatureVersion.A006.createVerifySignature(keyPair.getPublic());
+ a006Verifier.update(data);
+ assertFalse(a006Verifier.verify(a005Sig),
+ "A005 signature must not verify as A006");
+
+ // Sign with A006
+ Signature a006Signer = SignatureVersion.A006.createSignature(keyPair.getPrivate());
+ a006Signer.update(data);
+ byte[] a006Sig = a006Signer.sign();
+
+ // Verify with A005 should fail
+ Signature a005Verifier = SignatureVersion.A005.createVerifySignature(keyPair.getPublic());
+ a005Verifier.update(data);
+ assertFalse(a005Verifier.verify(a006Sig),
+ "A006 signature must not verify as A005");
+ }
+
+ @Test
+ void a005CertificateGeneration() throws Exception {
+ var generator = new X509Generator();
+ KeyPair keyPair = KeyUtil.makeKeyPair(2048);
+ Date now = new Date();
+ Calendar cal = Calendar.getInstance();
+ cal.add(Calendar.DAY_OF_YEAR, 365);
+
+ X509Certificate cert = generator.generateA005Certificate(keyPair, "CN=test-a005", now, cal.getTime());
+
+ assertNotNull(cert);
+ assertEquals("SHA256WITHRSA", cert.getSigAlgName());
+ assertEquals("CN=test-a005", cert.getSubjectX500Principal().getName(X500Principal.RFC2253));
+ }
+
+ @Test
+ void a006CertificateGeneration() throws Exception {
+ var generator = new X509Generator();
+ KeyPair keyPair = KeyUtil.makeKeyPair(2048);
+ Date now = new Date();
+ Calendar cal = Calendar.getInstance();
+ cal.add(Calendar.DAY_OF_YEAR, 365);
+
+ X509Certificate cert = generator.generateSignatureCertificate(
+ keyPair, "CN=test-a006", now, cal.getTime(), "A006");
+
+ assertNotNull(cert);
+ assertTrue(cert.getSigAlgName().contains("MGF1") || cert.getSigAlgName().contains("PSS"),
+ "A006 certificate must use PSS/MGF1 algorithm, got: " + cert.getSigAlgName());
+ assertEquals("CN=test-a006", cert.getSubjectX500Principal().getName(X500Principal.RFC2253));
+ cert.checkValidity(new Date());
+ cert.verify(keyPair.getPublic());
+ }
+
+ @Test
+ void a006CertificateManagerCreateAndLoad() throws Exception {
+ var user = createStubUser();
+ var manager = new CertificateManager(user);
+ Calendar calendar = Calendar.getInstance();
+ calendar.add(Calendar.DAY_OF_YEAR, X509Constants.DEFAULT_DURATION);
+
+ manager.createSignatureCertificate(new Date(calendar.getTimeInMillis()), "A006");
+
+ var cert = manager.getSignatureCertificate();
+ assertNotNull(cert);
+ assertTrue(cert.getSigAlgName().contains("MGF1") || cert.getSigAlgName().contains("PSS"),
+ "A006 certificate must use PSS/MGF1 algorithm, got: " + cert.getSigAlgName());
+ assertEquals(3, cert.getVersion());
+ }
+
+ @Test
+ void a006SignatureIsDeterministicInLength() throws Exception {
+ KeyPair keyPair = KeyUtil.makeKeyPair(2048);
+ for (int i = 0; i < 5; i++) {
+ byte[] data = ("message " + i).getBytes();
+ Signature signer = SignatureVersion.A006.createSignature(keyPair.getPrivate());
+ signer.update(data);
+ byte[] sig = signer.sign();
+ assertEquals(256, sig.length,
+ "A006 signature length should match RSA key size (2048 bits = 256 bytes)");
+ }
+ }
+
+ @Test
+ void a005SignatureLength() throws Exception {
+ KeyPair keyPair = KeyUtil.makeKeyPair(2048);
+ byte[] data = "test".getBytes();
+ Signature signer = SignatureVersion.A005.createSignature(keyPair.getPrivate());
+ signer.update(data);
+ byte[] sig = signer.sign();
+ assertEquals(256, sig.length,
+ "A005 signature length should match RSA key size (2048 bits = 256 bytes)");
+ }
+
+ @Test
+ void pkcs12RoundTripWithA006Certificate() throws Exception {
+ var user = createStubUser();
+ var manager = new CertificateManager(user);
+
+ manager.create("A006");
+
+ var baos = new ByteArrayOutputStream();
+ char[] password = "test".toCharArray();
+ manager.writePKCS12Certificate(password, baos);
+
+ var bais = new ByteArrayInputStream(baos.toByteArray());
+ var keyStore = KeyStore.getInstance("PKCS12", new BouncyCastleProvider());
+ keyStore.load(bais, password);
+
+ var cert = (X509Certificate) keyStore.getCertificate("-A006");
+ assertNotNull(cert, "Signature certificate must be loadable from PKCS12 store under -A006 alias");
+ assertTrue(cert.getSigAlgName().contains("MGF1") || cert.getSigAlgName().contains("PSS"),
+ "Loaded A006 certificate must use PSS/MGF1, got: " + cert.getSigAlgName());
+
+ assertNull(keyStore.getCertificate("-A005"),
+ "A006 keystore should not have an -A005 alias");
+ }
+
+ @Test
+ void pkcs12RoundTripWithA005Certificate() throws Exception {
+ var user = createStubUser();
+ var manager = new CertificateManager(user);
+
+ manager.create("A005");
+
+ var baos = new ByteArrayOutputStream();
+ char[] password = "test".toCharArray();
+ manager.writePKCS12Certificate(password, baos);
+
+ var bais = new ByteArrayInputStream(baos.toByteArray());
+ var keyStore = KeyStore.getInstance("PKCS12", new BouncyCastleProvider());
+ keyStore.load(bais, password);
+
+ var cert = (X509Certificate) keyStore.getCertificate("-A005");
+ assertNotNull(cert, "Signature certificate must be loadable from PKCS12 store under -A005 alias");
+ assertEquals("SHA256WITHRSA", cert.getSigAlgName());
+ }
+
+ @Test
+ void lookupSignAndVerifyRoundTrip() throws Exception {
+ // Test the full lookup-based flow as used by User.sign()
+ KeyPair keyPair = KeyUtil.makeKeyPair(2048);
+ byte[] data = "lookup round-trip test".getBytes();
+
+ for (String version : new String[]{"A005", "A006"}) {
+ SignatureVersion sv = SignatureVersion.lookup(version);
+ Signature signer = sv.createSignature(keyPair.getPrivate());
+ signer.update(data);
+ byte[] sig = signer.sign();
+
+ Signature verifier = sv.createVerifySignature(keyPair.getPublic());
+ verifier.update(data);
+ assertTrue(verifier.verify(sig), version + " lookup round-trip failed");
+ }
+ }
+
+ private EbicsUser createStubUser() {
+ return new EbicsUser() {
+ @Override public RSAPublicKey getA005PublicKey() { return null; }
+ @Override public RSAPublicKey getE002PublicKey() { return null; }
+ @Override public RSAPublicKey getX002PublicKey() { return null; }
+ @Override public byte[] getA005Certificate() throws EbicsException { return new byte[0]; }
+ @Override public byte[] getX002Certificate() throws EbicsException { return new byte[0]; }
+ @Override public byte[] getE002Certificate() throws EbicsException { return new byte[0]; }
+ @Override public void setA005Certificate(X509Certificate c) {}
+ @Override public void setX002Certificate(X509Certificate c) {}
+ @Override public void setE002Certificate(X509Certificate c) {}
+ @Override public void setA005PrivateKey(PrivateKey k) {}
+ @Override public void setX002PrivateKey(PrivateKey k) {}
+ @Override public void setE002PrivateKey(PrivateKey k) {}
+ @Override public String getSecurityMedium() { return ""; }
+ @Override public EbicsPartner getPartner() { return null; }
+ @Override public String getUserId() { return ""; }
+ @Override public String getName() { return "test-name"; }
+ @Override public String getDN() { return "CN=test-dn"; }
+ @Override public PasswordCallback getPasswordCallback() { return null; }
+ @Override public byte[] authenticate(byte[] digest) throws GeneralSecurityException { return new byte[0]; }
+ @Override public byte[] sign(byte[] digest) throws IOException, GeneralSecurityException { return new byte[0]; }
+ @Override public byte[] decrypt(byte[] encryptedKey, byte[] transactionKey) { return new byte[0]; }
+ };
+ }
+}