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]; } + }; + } +}