diff --git a/src/main/java/org/kopi/ebics/client/EbicsClient.java b/src/main/java/org/kopi/ebics/client/EbicsClient.java index d743d31e..484d04ca 100644 --- a/src/main/java/org/kopi/ebics/client/EbicsClient.java +++ b/src/main/java/org/kopi/ebics/client/EbicsClient.java @@ -221,6 +221,58 @@ public User createUser(URL url, String bankName, String hostId, String partnerId } } + /** + * Creates a new EBICS user whose A005/X002/E002 key material is supplied + * externally (HSM, smartcard, or any PKCS#11 token). The library will + * neither call {@code KeyUtil.makeKeyPair(...)} nor write a PKCS#12 file + * for this user — both would conflict with the no-heap-key contract + * that motivates external key custody. + * + *
Bank, partner, and user are persisted via the configured + * {@link org.kopi.ebics.interfaces.SerializationManager} just like the + * heap-key overload. The INI/HIA letters are also produced (they only + * need the public-key bytes that the supplied X.509 certificates expose). + * + * @param externalKeys the externally-managed key material. Must not be + * null; each role accessor must return a non-null + * {@link ExternalKeyProvider.KeyMaterial}. + * @return the created {@link User}. + * @throws IllegalArgumentException if {@code externalKeys} is null or + * partial. + * @since 2.1.0 + */ + public User createUser(URL url, String bankName, String hostId, String partnerId, + String userId, String name, String email, String country, String organization, + boolean useCertificates, PasswordCallback passwordCallback, + ExternalKeyProvider externalKeys) + throws Exception { + if (externalKeys == null) { + throw new IllegalArgumentException("externalKeys must not be null"); + } + log.info(messages.getString("user.create.info", userId)); + + Bank bank = createBank(url, bankName, hostId, useCertificates); + Partner partner = createPartner(bank, partnerId); + try { + User user = new User(partner, userId, name, email, country, organization, + passwordCallback, externalKeys); + createUserDirectories(user); + configuration.getSerializationManager().serialize(bank); + configuration.getSerializationManager().serialize(partner); + configuration.getSerializationManager().serialize(user); + createLetters(user, useCertificates); + users.put(userId, user); + partners.put(partner.getPartnerId(), partner); + banks.put(bank.getHostId(), bank); + + log.info(messages.getString("user.create.success", userId)); + return user; + } catch (Exception e) { + log.error(messages.getString("user.create.error"), e); + throw e; + } + } + private void createLetters(EbicsUser user, boolean useCertificates) throws GeneralSecurityException, IOException, EbicsException { user.getPartner().getBank().setUseCertificate(useCertificates); diff --git a/src/main/java/org/kopi/ebics/client/ExternalKeyProvider.java b/src/main/java/org/kopi/ebics/client/ExternalKeyProvider.java new file mode 100644 index 00000000..1b5bc695 --- /dev/null +++ b/src/main/java/org/kopi/ebics/client/ExternalKeyProvider.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 1990-2012 kopiLeft Development SARL, Bizerte, Tunisia + * + * 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.client; + +import java.security.PrivateKey; +import java.security.cert.X509Certificate; + +/** + * Supplies the A005/X002/E002 key material for a subscriber when the caller + * holds the private keys outside of the JVM heap (HSM, smartcard, or any other + * PKCS#11 token). + * + *
Pass an implementation of this interface to + * {@link EbicsClient#createUser(java.net.URL, String, String, String, String, + * String, String, String, String, boolean, boolean, + * org.kopi.ebics.interfaces.PasswordCallback, ExternalKeyProvider)} or to the + * matching {@link User} constructor; the library will then skip its built-in + * {@code KeyUtil.makeKeyPair(...)} path and install the supplied keys and + * certificates on the {@link User}. This is the seam that allows callers to + * keep DSGVO / FIPS / ISO 27001 commitments under which an EBICS subscriber's + * private signing key never resides outside its hardware token. + * + *
Implementations MUST NOT return a {@link PrivateKey} whose + * {@link PrivateKey#getEncoded()} is non-null when the key is intended to be + * HSM-resident — a non-null encoding indicates the key escaped the token. + * The library does not assert this invariant; callers must. + * + *
All three role methods must return a non-null {@link KeyMaterial}. Returning + * {@code null} from any role causes the consuming overload to throw + * {@link IllegalArgumentException} at the boundary. + * + * @since 2.1.0 + */ +public interface ExternalKeyProvider { + + /** Bank-technical / electronic-signature key (EBICS role A005). */ + KeyMaterial a005(); + + /** Identification & authentication key (EBICS role X002). */ + KeyMaterial x002(); + + /** Encryption key (EBICS role E002). */ + KeyMaterial e002(); + + /** + * A {@code (PrivateKey, X509Certificate)} pair for one EBICS role. + * Both components are required; the compact constructor rejects {@code null}. + */ + record KeyMaterial(PrivateKey privateKey, X509Certificate certificate) { + public KeyMaterial { + if (privateKey == null) { + throw new IllegalArgumentException("privateKey must not be null"); + } + if (certificate == null) { + throw new IllegalArgumentException("certificate must not be null"); + } + } + } +} diff --git a/src/main/java/org/kopi/ebics/client/User.java b/src/main/java/org/kopi/ebics/client/User.java index e1a37186..cfd5406d 100644 --- a/src/main/java/org/kopi/ebics/client/User.java +++ b/src/main/java/org/kopi/ebics/client/User.java @@ -87,6 +87,79 @@ public User(EbicsPartner partner, needSave = true; } + /** + * First-time constructor that installs externally supplied key material + * instead of generating fresh RSA key pairs in-process. + * + *
Use this constructor when the A005/X002/E002 private keys live in an + * HSM, a smartcard, or any other PKCS#11 token and must not be materialised + * on the JVM heap. The supplied {@link ExternalKeyProvider} is consulted + * once for all three roles; the library will not call + * {@code KeyUtil.makeKeyPair(...)} for the same user, so there is no + * heap-resident duplicate of the on-token key. + * + *
The library does not write a PKCS#12 file for an externally-keyed user + * (the private keys may not be exportable). Callers that need to persist a + * record of the public certificates should serialise the certificate bytes + * separately. + * + * @param partner customer in whose name we operate. + * @param userId UserId as obtained from the bank. + * @param name the user name. + * @param email the user email. + * @param country the user country. + * @param organization the user organization or company. + * @param passwordCallback a callback-handler that supplies us with the + * password. This parameter can be null, in which + * case no password is used. + * @param externalKeys the externally-managed key material for A005/X002/E002. + * Must not be null; each role method must return a + * non-null {@link ExternalKeyProvider.KeyMaterial}. + * @throws IllegalArgumentException if {@code externalKeys} is null or any of + * its role accessors return null. + * @since 2.1.0 + */ + public User(EbicsPartner partner, + String userId, + String name, + String email, + String country, + String organization, + PasswordCallback passwordCallback, + ExternalKeyProvider externalKeys) + { + if (externalKeys == null) { + throw new IllegalArgumentException("externalKeys must not be null"); + } + ExternalKeyProvider.KeyMaterial a005 = externalKeys.a005(); + ExternalKeyProvider.KeyMaterial x002 = externalKeys.x002(); + ExternalKeyProvider.KeyMaterial e002 = externalKeys.e002(); + if (a005 == null) { + throw new IllegalArgumentException("externalKeys.a005() returned null"); + } + if (x002 == null) { + throw new IllegalArgumentException("externalKeys.x002() returned null"); + } + if (e002 == null) { + throw new IllegalArgumentException("externalKeys.e002() returned null"); + } + + this.partner = partner; + this.userId = userId; + this.name = name; + this.dn = makeDN(name, email, country, organization); + this.passwordCallback = passwordCallback; + + this.a005PrivateKey = a005.privateKey(); + this.a005Certificate = a005.certificate(); + this.x002PrivateKey = x002.privateKey(); + this.x002Certificate = x002.certificate(); + this.e002PrivateKey = e002.privateKey(); + this.e002Certificate = e002.certificate(); + + needSave = true; + } + /** * Reconstructs a persisted EBICS user. * diff --git a/src/test/java/org/kopi/ebics/client/UserExternalKeysTest.java b/src/test/java/org/kopi/ebics/client/UserExternalKeysTest.java new file mode 100644 index 00000000..c5aeeced --- /dev/null +++ b/src/test/java/org/kopi/ebics/client/UserExternalKeysTest.java @@ -0,0 +1,207 @@ +/* + * Copyright (c) 1990-2012 kopiLeft Development SARL, Bizerte, Tunisia + * + * 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.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.Security; +import java.security.cert.X509Certificate; +import java.util.Calendar; +import java.util.Date; + +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.certificate.X509Generator; +import org.kopi.ebics.interfaces.EbicsPartner; + +/** + * Coverage for the {@link User} constructor that accepts an + * {@link ExternalKeyProvider} (HSM / smartcard / external key custody). + * + *
Three guarantees the upstream patch must preserve: + *