From 5beb2a684b4de06638b928901d0959de40bac647 Mon Sep 17 00:00:00 2001 From: Uwe Maurer Date: Tue, 24 Mar 2026 09:36:46 +0100 Subject: [PATCH] Add A006 (EMSA-PSS) signature support Implement the A006 electronic signature mechanism as defined in the EBICS specification (section 14.1.4.2), using RSA-PSS with SHA-256, MGF1(SHA-256), and salt length 32 bytes. Introduces a SignatureVersion interface with A005 and A006 constant implementations that encapsulate the algorithm differences. The signature version is now read from configuration and threaded through signing, certificate generation, XML serialization, and PKCS12 key storage. To use A006, set signature.version=A006 in config.properties. Closes #44 --- .../ebics/certificate/CertificateManager.java | 57 +++- .../ebics/certificate/SignatureVersion.java | 154 +++++++++ .../kopi/ebics/certificate/X509Generator.java | 65 +++- src/main/java/org/kopi/ebics/client/User.java | 9 +- .../org/kopi/ebics/interfaces/EbicsUser.java | 17 +- .../org/kopi/ebics/xml/EbicsXmlFactory.java | 14 +- .../org/kopi/ebics/xml/SPRRequestElement.java | 3 +- .../UploadInitializationRequestElement.java | 2 +- .../org/kopi/ebics/xml/UserSignature.java | 2 +- .../certificate/SignatureVersionTest.java | 309 ++++++++++++++++++ 10 files changed, 611 insertions(+), 21 deletions(-) create mode 100644 src/main/java/org/kopi/ebics/certificate/SignatureVersion.java create mode 100644 src/test/java/org/kopi/ebics/certificate/SignatureVersionTest.java 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: + *

+ */ +public interface SignatureVersion { + + /** + * EBICS signature version A005: EMSA-PKCS1-v1_5 with SHA-256. + */ + SignatureVersion A005 = new SignatureVersion() { + @Override + public String getVersion() { + return "A005"; + } + + @Override + public String getSignatureAlgorithm() { + return "SHA256WithRSA"; + } + + @Override + public String getCertificateSignatureAlgorithm() { + return "SHA256WithRSAEncryption"; + } + + @Override + public Signature createSignature(PrivateKey privateKey) throws GeneralSecurityException { + Signature signature = Signature.getInstance(getSignatureAlgorithm(), BouncyCastleProvider.PROVIDER_NAME); + signature.initSign(privateKey); + return signature; + } + + @Override + public Signature createVerifySignature(PublicKey publicKey) throws GeneralSecurityException { + Signature signature = Signature.getInstance(getSignatureAlgorithm(), BouncyCastleProvider.PROVIDER_NAME); + signature.initVerify(publicKey); + return signature; + } + }; + + /** + * EBICS signature version A006: EMSA-PSS with SHA-256, MGF1(SHA-256), salt length 32 bytes. + */ + SignatureVersion A006 = new SignatureVersion() { + + private final PSSParameterSpec pssParams = new PSSParameterSpec( + "SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1 + ); + + @Override + public String getVersion() { + return "A006"; + } + + @Override + public String getSignatureAlgorithm() { + return "SHA256withRSA/PSS"; + } + + @Override + public String getCertificateSignatureAlgorithm() { + return "SHA256WithRSAAndMGF1"; + } + + @Override + public Signature createSignature(PrivateKey privateKey) throws GeneralSecurityException { + Signature signature = Signature.getInstance(getSignatureAlgorithm(), BouncyCastleProvider.PROVIDER_NAME); + signature.setParameter(pssParams); + signature.initSign(privateKey); + return signature; + } + + @Override + public Signature createVerifySignature(PublicKey publicKey) throws GeneralSecurityException { + Signature signature = Signature.getInstance(getSignatureAlgorithm(), BouncyCastleProvider.PROVIDER_NAME); + signature.setParameter(pssParams); + signature.initVerify(publicKey); + return signature; + } + }; + + /** + * Returns the EBICS version string (e.g. "A005" or "A006"). + */ + String getVersion(); + + /** + * Returns the JCA signature algorithm name used for signing data. + */ + String getSignatureAlgorithm(); + + /** + * Returns the BouncyCastle algorithm name used for X.509 certificate generation. + */ + String getCertificateSignatureAlgorithm(); + + /** + * Creates and initializes a {@link Signature} instance for signing. + */ + Signature createSignature(PrivateKey privateKey) throws GeneralSecurityException; + + /** + * Creates and initializes a {@link Signature} instance for verification. + */ + Signature createVerifySignature(PublicKey publicKey) throws GeneralSecurityException; + + /** + * Looks up a {@link SignatureVersion} by its EBICS version string. + * + * @param version the version string (e.g. "A005" or "A006") + * @return the corresponding SignatureVersion + * @throws IllegalArgumentException if the version is not supported + */ + static SignatureVersion lookup(String version) { + if ("A005".equals(version)) return A005; + if ("A006".equals(version)) return A006; + throw new IllegalArgumentException( + "Unsupported signature version: " + version + ". Must be A005 or A006."); + } +} diff --git a/src/main/java/org/kopi/ebics/certificate/X509Generator.java b/src/main/java/org/kopi/ebics/certificate/X509Generator.java index a2963c73..f34a5d80 100644 --- a/src/main/java/org/kopi/ebics/certificate/X509Generator.java +++ b/src/main/java/org/kopi/ebics/certificate/X509Generator.java @@ -54,7 +54,7 @@ public class X509Generator { /** - * Generates the signature certificate for the EBICS protocol + * Generates the signature certificate for the EBICS protocol using the default A005 algorithm. * @param keypair the key pair * @param issuer the certificate issuer * @param notBefore the begin validity date @@ -73,7 +73,34 @@ public X509Certificate generateA005Certificate(KeyPair keypair, issuer, notBefore, notAfter, - CertificateKeyUsage.SIGNATURE_KEY_USAGE); + CertificateKeyUsage.SIGNATURE_KEY_USAGE, + X509Constants.SIGNATURE_ALGORITHM); + } + + /** + * Generates the signature certificate for the EBICS protocol using the specified signature version. + * @param keypair the key pair + * @param issuer the certificate issuer + * @param notBefore the begin validity date + * @param notAfter the end validity date + * @param signatureVersion the EBICS signature version (A005 or A006) + * @return the signature certificate + * @throws GeneralSecurityException + * @throws IOException + */ + public X509Certificate generateSignatureCertificate(KeyPair keypair, + String issuer, + Date notBefore, + Date notAfter, + String signatureVersion) + throws GeneralSecurityException, IOException + { + return generate(keypair, + issuer, + notBefore, + notAfter, + CertificateKeyUsage.SIGNATURE_KEY_USAGE, + SignatureVersion.lookup(signatureVersion).getCertificateSignatureAlgorithm()); } /** @@ -96,7 +123,8 @@ public X509Certificate generateX002Certificate(KeyPair keypair, issuer, notBefore, notAfter, - CertificateKeyUsage.AUTHENTICATION_KEY_USAGE); + CertificateKeyUsage.AUTHENTICATION_KEY_USAGE, + X509Constants.SIGNATURE_ALGORITHM); } /** @@ -119,12 +147,13 @@ public X509Certificate generateE002Certificate(KeyPair keypair, issuer, notBefore, notAfter, - CertificateKeyUsage.ENCRYPTION_KEY_USAGE); + CertificateKeyUsage.ENCRYPTION_KEY_USAGE, + X509Constants.SIGNATURE_ALGORITHM); } /** * Returns an 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]; } + }; + } +}