ebicsclient 1.3.3__tar.gz → 1.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/PKG-INFO +10 -5
  2. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/README.md +9 -4
  3. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/docs/05-zkb-onboarding.md +22 -5
  4. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/docs/11-certificate-profiles.md +8 -1
  5. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/pyproject.toml +1 -1
  6. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/src/ebicsclient/__init__.py +2 -0
  7. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/src/ebicsclient/client.py +10 -5
  8. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/src/ebicsclient/keys.py +60 -8
  9. ebicsclient-1.4.0/src/ebicsclient/letter.py +411 -0
  10. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tests/test_h005.py +12 -0
  11. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tests/test_keys.py +52 -0
  12. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tests/test_letter.py +61 -10
  13. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/uv.lock +1 -1
  14. ebicsclient-1.3.3/src/ebicsclient/letter.py +0 -344
  15. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/.github/workflows/ci.yml +0 -0
  16. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/.github/workflows/release.yml +0 -0
  17. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/.gitignore +0 -0
  18. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/CLAUDE.md +0 -0
  19. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/CONTRIBUTING.md +0 -0
  20. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/LICENSE.md +0 -0
  21. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/docs/01-protocol-and-formats.md +0 -0
  22. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/docs/02-licensing-strategy.md +0 -0
  23. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/docs/03-library-landscape.md +0 -0
  24. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/docs/04-implementation-plan.md +0 -0
  25. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/docs/06-engineering-conventions.md +0 -0
  26. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/docs/07-handshake-testing.md +0 -0
  27. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/docs/08-parity-and-xsd-findings.md +0 -0
  28. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/docs/09-zkb-test-platform-settings.md +0 -0
  29. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/docs/10-btf-order-types.md +0 -0
  30. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/examples/zkb_handshake.py +0 -0
  31. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/src/ebicsclient/certificates.py +0 -0
  32. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/src/ebicsclient/crypto.py +0 -0
  33. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/src/ebicsclient/errors.py +0 -0
  34. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/src/ebicsclient/formats/__init__.py +0 -0
  35. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/src/ebicsclient/formats/camt.py +0 -0
  36. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/src/ebicsclient/formats/camt052.py +0 -0
  37. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/src/ebicsclient/formats/camt053.py +0 -0
  38. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/src/ebicsclient/formats/camt054.py +0 -0
  39. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/src/ebicsclient/formats/container.py +0 -0
  40. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/src/ebicsclient/formats/pain002.py +0 -0
  41. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/src/ebicsclient/models.py +0 -0
  42. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/src/ebicsclient/protocol/__init__.py +0 -0
  43. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/src/ebicsclient/protocol/h005.py +0 -0
  44. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/src/ebicsclient/py.typed +0 -0
  45. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/src/ebicsclient/transport.py +0 -0
  46. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tests/crypto_helpers.py +0 -0
  47. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tests/data/camt052_zkb_sample.xml +0 -0
  48. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tests/data/camt053_zkb_sample.xml +0 -0
  49. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tests/data/camt054_collective_zkb_sample.xml +0 -0
  50. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tests/data/camt054_credit_zkb_sample.xml +0 -0
  51. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tests/data/camt054_debit_zkb_sample.xml +0 -0
  52. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tests/data/htd_zkb_sample.xml +0 -0
  53. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tests/data/pain002_accp.xml +0 -0
  54. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tests/data/pain002_actc.xml +0 -0
  55. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tests/data/pain002_part.xml +0 -0
  56. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tests/data/pain002_rjct.xml +0 -0
  57. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tests/fixtures/parity/hia.xml +0 -0
  58. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tests/fixtures/parity/hpb.xml +0 -0
  59. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tests/fixtures/parity/ini.xml +0 -0
  60. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tests/fixtures/parity/meta.json +0 -0
  61. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tests/test_c14n_vectors.py +0 -0
  62. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tests/test_camt052_054.py +0 -0
  63. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tests/test_camt053.py +0 -0
  64. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tests/test_certificates.py +0 -0
  65. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tests/test_client.py +0 -0
  66. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tests/test_container.py +0 -0
  67. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tests/test_crypto.py +0 -0
  68. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tests/test_errors.py +0 -0
  69. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tests/test_input_validation.py +0 -0
  70. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tests/test_pain002.py +0 -0
  71. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tests/test_php_parity.py +0 -0
  72. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tests/test_schema_validation.py +0 -0
  73. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tests/test_transport.py +0 -0
  74. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tools/fetch-schemas.py +0 -0
  75. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tools/php-parity/README.md +0 -0
  76. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tools/php-parity/composer.json +0 -0
  77. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tools/php-parity/composer.lock +0 -0
  78. {ebicsclient-1.3.3 → ebicsclient-1.4.0}/tools/php-parity/generate.php +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ebicsclient
3
- Version: 1.3.3
3
+ Version: 1.4.0
4
4
  Summary: Pure-Python client for the EBICS 3.0 (H005) banking protocol
5
5
  Project-URL: Homepage, https://github.com/schulluk/ebicsclient
6
6
  Project-URL: Repository, https://github.com/schulluk/ebicsclient
@@ -124,8 +124,9 @@ validated live against Zürcher Kantonalbank (ZKB).
124
124
  **What it provides today:**
125
125
 
126
126
  - **Key ceremony** — INI/HIA/HPB with X.509 key transmission (self-signed *mit Schlüsseln* or
127
- CA-issued *mit Zertifikaten*), the printable initialisation letter (HTML/PDF), and bank-key
128
- **pinning** across sessions
127
+ CA-issued *mit Zertifikaten*), the printable EBICS 3.0 initialisation letters (INI + HIA,
128
+ HTML/PDF, carrying the certificates and their SHA-256 DER fingerprints per spec 4.4.1.2.3),
129
+ and bank-key **pinning** across sessions
129
130
  - **Read** — statement/report downloads with parsers into typed models: **camt.053** end-of-day
130
131
  statements, **camt.052** intraday reports, **camt.054** booking advices (incl. QRR/SCOR/LSV),
131
132
  and **pain.002** payment status reports
@@ -237,8 +238,12 @@ a real bank statement.
237
238
  - [x] Authentication signature (inclusive Canonical XML 1.0 + RSA-SHA256)
238
239
  - [x] HTTPS transport (TLS 1.2 floor, certifi fallback via the optional `tls` extra)
239
240
  - [x] INI/HIA/HPB handshake
240
- - [x] X.509 key transmission: **mit Schlüsseln** (self-signed) and **mit Zertifikaten** (CA certs)
241
- - [x] Initialisation letter (HTML, or PDF via the optional `pdf` extra)
241
+ - [x] X.509 key transmission: **mit Schlüsseln** (self-signed, deterministic certificates) and
242
+ **mit Zertifikaten** (CA certs)
243
+ - [x] Initialisation letters (INI + HIA) with certificate SHA-256 DER fingerprints per the
244
+ EBICS 3.0 spec (HTML, or PDF via the optional `pdf` extra)
245
+ - [ ] Production activation on the printed letters (in progress — the pre-1.4.0 letter carried
246
+ the EBICS 2.x public-key hash, which banks no longer match; see the 1.4.0 notes)
242
247
  - [x] Bank-key pinning across sessions (`hpb(pinned=...)`)
243
248
 
244
249
  **Milestone 2 — Read** (validated live on ZKB)
@@ -6,8 +6,9 @@ validated live against Zürcher Kantonalbank (ZKB).
6
6
  **What it provides today:**
7
7
 
8
8
  - **Key ceremony** — INI/HIA/HPB with X.509 key transmission (self-signed *mit Schlüsseln* or
9
- CA-issued *mit Zertifikaten*), the printable initialisation letter (HTML/PDF), and bank-key
10
- **pinning** across sessions
9
+ CA-issued *mit Zertifikaten*), the printable EBICS 3.0 initialisation letters (INI + HIA,
10
+ HTML/PDF, carrying the certificates and their SHA-256 DER fingerprints per spec 4.4.1.2.3),
11
+ and bank-key **pinning** across sessions
11
12
  - **Read** — statement/report downloads with parsers into typed models: **camt.053** end-of-day
12
13
  statements, **camt.052** intraday reports, **camt.054** booking advices (incl. QRR/SCOR/LSV),
13
14
  and **pain.002** payment status reports
@@ -119,8 +120,12 @@ a real bank statement.
119
120
  - [x] Authentication signature (inclusive Canonical XML 1.0 + RSA-SHA256)
120
121
  - [x] HTTPS transport (TLS 1.2 floor, certifi fallback via the optional `tls` extra)
121
122
  - [x] INI/HIA/HPB handshake
122
- - [x] X.509 key transmission: **mit Schlüsseln** (self-signed) and **mit Zertifikaten** (CA certs)
123
- - [x] Initialisation letter (HTML, or PDF via the optional `pdf` extra)
123
+ - [x] X.509 key transmission: **mit Schlüsseln** (self-signed, deterministic certificates) and
124
+ **mit Zertifikaten** (CA certs)
125
+ - [x] Initialisation letters (INI + HIA) with certificate SHA-256 DER fingerprints per the
126
+ EBICS 3.0 spec (HTML, or PDF via the optional `pdf` extra)
127
+ - [ ] Production activation on the printed letters (in progress — the pre-1.4.0 letter carried
128
+ the EBICS 2.x public-key hash, which banks no longer match; see the 1.4.0 notes)
124
129
  - [x] Bank-key pinning across sessions (`hpb(pinned=...)`)
125
130
 
126
131
  **Milestone 2 — Read** (validated live on ZKB)
@@ -12,11 +12,17 @@ activation PIN**.
12
12
  1. **Generate** your three RSA keypairs (A006 sig, X002 auth, E002 enc) → encrypted keyring.
13
13
  2. **INI** — send your signature public key (`ebicsUnsecuredRequest`, order type `INI`).
14
14
  3. **HIA** — send your authentication + encryption public keys (order type `HIA`).
15
- 4. **Initialisierungsbriefe** — `make_ini_letter()` renders the letter (covering all three keys) as
16
- **printable HTML** by default (no dependency) or as **PDF** with the optional `pdf` extra; `AUTO`
17
- picks PDF when that extra is installed, else HTML. It contains the SHA-256 hashes of *your* public
18
- keys. **Sign it by hand (legally valid signature)** and mail to ZKB Kompetenzcenter Services. ZKB
19
- verifies the signature against documents on file, then activates and confirms your access. See
15
+ 4. **Initialisierungsbriefe** — `make_ini_letter()` renders **both** EBICS 3.0 letters (the INI
16
+ letter with the A006 signature certificate; the HIA letter with the X002 and E002 certificates,
17
+ on its own page) as **printable HTML** by default (no dependency) or as **PDF** with the optional
18
+ `pdf` extra; `AUTO` picks PDF when that extra is installed, else HTML. Per the EBICS 3.0 spec
19
+ (section 4.4.1.2.3) each letter shows the certificate in PEM **and the SHA-256 hash of its DER
20
+ encoding** in uppercase hex — *not* the EBICS 2.x public-key (`exponent modulus`) hash; the bank
21
+ compares those fingerprints against the certificates it received over INI/HIA. The default
22
+ self-signed certificates are **deterministic**, so the letter always reproduces exactly the
23
+ certificates the requests transmitted, from the keyring alone. **Sign each page by hand (legally
24
+ valid signature)** and mail to ZKB Kompetenzcenter Services. ZKB verifies the signature against
25
+ documents on file, then activates and confirms your access. See
20
26
  [doc 07](07-handshake-testing.md) for the end-to-end test walkthrough.
21
27
  5. **HPB** — download ZKB's public keys and **verify them against the hashes printed on p.2 of the
22
28
  Bankparameterdaten letter** (in `../local/`).
@@ -48,3 +54,14 @@ Contact: **support.epayment@zkb.ch / +41 44 293 99 15** (Kompetenzcenter Service
48
54
 
49
55
  - Confirm **plain keys** ("mit Schlüsseln") are fine for download-only — assume yes, proceed.
50
56
  - Profile **T** (no distributed signature) — irrelevant since download-only.
57
+
58
+ ## Production watch-item: `BankPubKeyDigests` hash format
59
+
60
+ The EBICS 3.0 spec (section 5.5.1.1) defines the `BankPubKeyDigests` values in requests as the
61
+ SHA-256 over the bank **certificate's DER** — but this client sends the EBICS 2.x public-key
62
+ (`exponent modulus`) digest, and the ZKB **test platform accepted that in every live
63
+ transaction** (downloads and uploads). ZKB's published Bankparameterdaten hashes are also
64
+ public-key hashes, so HPB pinning stays in the `e m` format. If ZKB **production** ever rejects
65
+ requests with `EBICS_BANK_PUBKEY_UPDATE_REQUIRED` despite fresh HPB keys, this mismatch is the
66
+ first suspect: the fix is to retain the bank *certificates* from the HPB response and send
67
+ their DER SHA-256 fingerprints instead.
@@ -6,7 +6,14 @@ subscriber's certificate comes from and how the bank's certificate is trusted:
6
6
 
7
7
  - **mit Schlüsseln** (key-based) — the certificate is **self-signed** by the key it carries.
8
8
  Identity is established out of band: the signed initialisation letter, or online activation.
9
- This is the default and what the ZKB validation used.
9
+ This is the default and what the ZKB validation used. The self-signed certificates are
10
+ **deterministic** (fixed validity 2020-01-01 → 9999-12-31, key-derived serial, deterministic
11
+ PKCS#1 v1.5 signature): regenerating one for the same key yields **byte-identical DER**. This
12
+ matters because the EBICS 3.0 initialisation letters print the SHA-256 hash of the DER-encoded
13
+ certificate (spec section 4.4.1.2.3) and the bank compares it against the certificate INI/HIA
14
+ transmitted — determinism lets any later session reproduce that certificate from the keyring
15
+ alone, without persisting certificates. (The Swiss Market Practice Guidelines section 6.1
16
+ explicitly allow unlimited certificate validity.)
10
17
  - **mit Zertifikaten** (certificate-based) — the certificate is **issued by a CA the bank
11
18
  trusts**, and the bank validates the chain. Common at German and French banks.
12
19
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ebicsclient"
7
- version = "1.3.3"
7
+ version = "1.4.0"
8
8
  description = "Pure-Python client for the EBICS 3.0 (H005) banking protocol"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -39,6 +39,7 @@ from ebicsclient.errors import (
39
39
  from ebicsclient.keys import (
40
40
  CertificateUsage,
41
41
  bank_key_hashes,
42
+ certificate_fingerprint,
42
43
  deserialize_keyring,
43
44
  generate_keyring,
44
45
  generate_self_signed_certificate,
@@ -139,6 +140,7 @@ __all__ = [
139
140
  "User",
140
141
  "__version__",
141
142
  "bank_key_hashes",
143
+ "certificate_fingerprint",
142
144
  "deserialize_keyring",
143
145
  "generate_keyring",
144
146
  "generate_self_signed_certificate",
@@ -93,18 +93,22 @@ class Client:
93
93
  output_format: OutputFormat = OutputFormat.AUTO,
94
94
  branding: str = "ebicsClient",
95
95
  ) -> Letter:
96
- """Render the initialisation letter to print, sign, and send to the bank.
96
+ """Render the initialisation letters (INI and HIA) to print, sign, and send.
97
97
 
98
- The letter carries the subscriber's public-key hashes so the bank can verify, out
99
- of band, the keys it received electronically over INI and HIA.
98
+ The letters carry the subscriber's certificates and their SHA-256 DER
99
+ fingerprints (EBICS 3.0 spec, section 4.4.1.2.3) so the bank can verify, out of
100
+ band, the certificates it received electronically over INI and HIA. They are
101
+ rendered from this client's certificate provider, so the printed fingerprints
102
+ match the certificates :meth:`ini` and :meth:`hia` transmitted.
100
103
 
101
104
  Args:
102
105
  output_format: The output format. ``AUTO`` renders PDF when the optional
103
106
  ``pdf`` extra is installed, otherwise HTML.
104
- branding: A name shown in the letter's footer; defaults to ``"ebicsClient"``.
107
+ branding: A name shown in the letters' footer; defaults to ``"ebicsClient"``.
105
108
 
106
109
  Returns:
107
- The rendered letter (format, media type, and content bytes).
110
+ The rendered letters (format, media type, and content bytes) — the INI
111
+ letter and the HIA letter, each on its own page.
108
112
 
109
113
  Raises:
110
114
  MissingDependencyError: PDF output was requested without the ``pdf`` extra.
@@ -113,6 +117,7 @@ class Client:
113
117
  self._bank,
114
118
  self._user,
115
119
  self._keyring,
120
+ certificate_provider=self._certificate_provider,
116
121
  output_format=output_format,
117
122
  branding=branding,
118
123
  )
@@ -38,9 +38,19 @@ _PUBLIC_EXPONENT = 65537
38
38
  _KEYRING_FORMAT_VERSION = 1
39
39
  _KEY_FIELDS = ("signature", "authentication", "encryption")
40
40
 
41
- # Self-signed certificates for the key-based ("mit Schlüsseln") profile are long-lived;
42
- # the bank only extracts the public key from them.
43
- _CERTIFICATE_VALIDITY_DAYS = 365 * 10
41
+ # Self-signed certificates for the key-based ("mit Schlüsseln") profile are DETERMINISTIC:
42
+ # regenerating one for the same key always yields byte-identical DER. That matters because
43
+ # EBICS 3.0 initialisation letters carry the SHA-256 hash of the DER-encoded certificate
44
+ # (EBICS 3.0 spec, section 4.4.1.2.3), and the bank compares it against the certificate it
45
+ # received over INI/HIA — so the letter must be able to reproduce that exact certificate
46
+ # from the keyring alone, in any later session. Determinism comes from fixed validity
47
+ # dates, a serial derived from the key, and RSA PKCS#1 v1.5 signing (which is itself
48
+ # deterministic). The Swiss Market Practice Guidelines EBICS 3.0 (section 6.1) explicitly
49
+ # allow any validity value including unlimited (9999-12-31).
50
+ _CERTIFICATE_NOT_BEFORE = datetime.datetime(2020, 1, 1, tzinfo=datetime.UTC)
51
+ _CERTIFICATE_NOT_AFTER = datetime.datetime(9999, 12, 31, 23, 59, 59, tzinfo=datetime.UTC)
52
+ # RFC 5280 caps serial numbers at 20 octets; 16 digest bytes stay comfortably below.
53
+ _CERTIFICATE_SERIAL_BYTES = 16
44
54
 
45
55
 
46
56
  def _require_passphrase(passphrase: object) -> None:
@@ -109,13 +119,20 @@ def _key_usage(usage: CertificateUsage) -> x509.KeyUsage:
109
119
  def generate_self_signed_certificate(
110
120
  private_key: rsa.RSAPrivateKey, common_name: str, usage: CertificateUsage
111
121
  ) -> x509.Certificate:
112
- """Generate a self-signed X.509 certificate carrying an EBICS public key.
122
+ """Generate a deterministic self-signed X.509 certificate carrying an EBICS public key.
113
123
 
114
124
  EBICS 3.0 (H005) transmits public keys as X.509 certificates (``ds:X509Data``). In the
115
125
  key-based ("mit Schlüsseln") profile the certificate is self-signed by the very key it
116
126
  carries, and the bank extracts the public key from it. The bank *does* validate the
117
127
  certificate's Key Usage against its EBICS role, so it is set from ``usage``.
118
128
 
129
+ The certificate is **deterministic**: the same key, name, and usage always produce
130
+ byte-identical DER (fixed validity, key-derived serial, deterministic PKCS#1 v1.5
131
+ signature). The EBICS 3.0 initialisation letters carry the SHA-256 hash of this DER
132
+ (see :func:`certificate_fingerprint`), and the bank compares it against the certificate
133
+ delivered over INI/HIA — determinism lets the letter reproduce that certificate from
134
+ the keyring alone, without persisting certificates.
135
+
119
136
  Args:
120
137
  private_key: The key pair to certify; its public half is embedded and it self-signs.
121
138
  common_name: The subject/issuer common name (e.g. the subscriber's User ID).
@@ -125,20 +142,55 @@ def generate_self_signed_certificate(
125
142
  The self-signed certificate.
126
143
  """
127
144
  name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, common_name)])
128
- now = datetime.datetime.now(datetime.UTC)
129
145
  return (
130
146
  x509.CertificateBuilder()
131
147
  .subject_name(name)
132
148
  .issuer_name(name)
133
149
  .public_key(private_key.public_key())
134
- .serial_number(x509.random_serial_number())
135
- .not_valid_before(now - datetime.timedelta(days=1))
136
- .not_valid_after(now + datetime.timedelta(days=_CERTIFICATE_VALIDITY_DAYS))
150
+ .serial_number(_deterministic_serial(private_key.public_key(), common_name, usage))
151
+ .not_valid_before(_CERTIFICATE_NOT_BEFORE)
152
+ .not_valid_after(_CERTIFICATE_NOT_AFTER)
137
153
  .add_extension(_key_usage(usage), critical=True)
138
154
  .sign(private_key, hashes.SHA256())
139
155
  )
140
156
 
141
157
 
158
+ def _deterministic_serial(
159
+ public_key: rsa.RSAPublicKey, common_name: str, usage: CertificateUsage
160
+ ) -> int:
161
+ # A serial derived from the certified key (plus name and role) keeps regeneration
162
+ # byte-identical while staying unique per certificate. RFC 5280 requires a positive
163
+ # serial of at most 20 octets; 16 digest bytes always satisfy both (`or 1` covers the
164
+ # astronomically unlikely all-zero digest).
165
+ digest = hashlib.sha256(
166
+ b"|".join(
167
+ (
168
+ _fingerprint_data(public_key.public_numbers()),
169
+ common_name.encode("utf-8"),
170
+ usage.value.encode("ascii"),
171
+ )
172
+ )
173
+ ).digest()
174
+ return int.from_bytes(digest[:_CERTIFICATE_SERIAL_BYTES], "big") or 1
175
+
176
+
177
+ def certificate_fingerprint(certificate: x509.Certificate) -> bytes:
178
+ """Compute the SHA-256 fingerprint of a certificate's DER encoding.
179
+
180
+ This is the hash the EBICS 3.0 initialisation letters print (spec section 4.4.1.2.3):
181
+ the SHA-256 of the certificate in DER binary format, shown as uppercase hexadecimal.
182
+ The bank compares it against the certificate received over INI/HIA before activating
183
+ the subscriber.
184
+
185
+ Args:
186
+ certificate: The certificate to fingerprint.
187
+
188
+ Returns:
189
+ The 32-byte SHA-256 digest of the DER-encoded certificate.
190
+ """
191
+ return certificate.fingerprint(hashes.SHA256())
192
+
193
+
142
194
  def bank_key_hashes(bank_keys: BankKeys) -> BankKeyHashes:
143
195
  """Compute the pinning hashes for a bank's public keys.
144
196