Skip to content

Feature/emv sm adapter#718

Merged
ar merged 17 commits into
mainfrom
feature/emv-sm-adapter
May 18, 2026
Merged

Feature/emv sm adapter#718
ar merged 17 commits into
mainfrom
feature/emv-sm-adapter

Conversation

@ar
Copy link
Copy Markdown
Member

@ar ar commented May 16, 2026

PR Description — feature/emv-sm-adapter

Summary

Expands org.jpos.security.SMAdapter's effective API with a complete EMV cryptographic surface, organised under a new EMVSMAdapter<T> sub-interface. Everything new lives on the sub-interface and on a small set of new value records — the existing SMAdapter<T> interface is byte-identical to main after the entire branch.

What's new

New public sub-interface: EMVSMAdapter<T> extends SMAdapter<T>

14 new methods across three families:

AC / Cryptogram (built on existing internal helpers)

Method Returns
generateCVC3(T, …, MKDMethod) String (5-digit CVC3)
generatedCVV(String accountNo, T, …, MKDMethod) String (3-digit dCVV)
generateApplicationCryptogram(MKDMethod, SKDMethod, T imkac, …) 8-byte ARQC/TC/AAC
deriveICCMasterKey(MKDMethod, T imk, pan, psn) EMVDerivedKey<T>
deriveEMVSessionKey(SKDMethod, T iccMk, atc, upn) EMVDerivedKey<T>
generateARPC(T key, arqc, ARPCMethod, arc, propAuthData) (overload) byte[] 8 or 4

Secure Messaging

Method Returns
deriveSecureMessagingSessionKey(MKDMethod, SKDMethod, T imk, pan, psn, atc, arqc) EMVDerivedKey<T>
generateSM_MAC(T sessionKey, byte[] data) (overload) 8-byte MAC
encryptSecureMessagingPIN(T sessionKey, EncryptedPIN newPIN, T kd1, byte destFormat, PaddingMethod, EncryptedPIN currentPIN, T udkAc) EncryptedPIN

Offline Data Authentication (new RSA + EMV cert recovery code)

Method Returns EMV spec
recoverIssuerPublicKey(EMVCAPublicKey, byte[] cert, byte[] remainder, byte[] exponent, String pan) EMVIssuerPublicKey Book 2 §6.3
recoverICCPublicKey(EMVIssuerPublicKey, byte[] cert, byte[] remainder, byte[] exponent, byte[] sad, String pan) EMVICCPublicKey Book 2 §6.4
verifySDA(EMVIssuerPublicKey, byte[] ssad, byte[] sad) 2-byte DAC Book 2 §5.4
verifyDDA(EMVICCPublicKey, byte[] sdad, byte[] ddolData) ICC Dynamic Number Book 2 §6.5
verifyCDA(EMVICCPublicKey, byte[] sdad, byte[] un, byte[] ac, byte[] txnData) EMVCDAResult (ICC Dynamic Number + CID) Book 2 §6.6

New public records (in org.jpos.security)

All Java 25 records with content-aware equals / hashCode / toString and defensive copies of byte[] fields on construction and access. Hex rendering in toString for safe logging.

Record Fields
EMVDerivedKey<T> T key, byte[] kcv
EMVCAPublicKey byte[] rid, byte index, byte[] modulus, byte[] exponent, byte hashAlgInd, byte pkAlgInd
EMVIssuerPublicKey byte[] issuerIdentifier, byte[] expirationDate, byte[] serialNumber, byte[] modulus, byte[] exponent, byte hashAlgInd, byte pkAlgInd
EMVICCPublicKey byte[] applicationPan, byte[] expirationDate, byte[] serialNumber, byte[] modulus, byte[] exponent, byte hashAlgInd, byte pkAlgInd
EMVCDAResult byte[] iccDynamicNumber, byte cid

Architectural conventions

  • *Impl extension pattern — every public method on BaseSMAdapter is a logging/error-handling wrapper that delegates to a protected *Impl. Defaults throw SMException("Operation not supported in: …") so third-party HSM adapters (Thales, Atalla, Futurex) keep compiling without code changes.
  • JCESecurityModule extends BaseSMAdapter<SecureDESKey> is the software adapter and overrides every *Impl it can support. Reuses existing internal helpers (calculateARQC, calculateCVC3, calculatedCVV, deriveICCMasterKey, deriveCommonSK_*, deriveSK_*, paddingISO9797Method2, calculateMACISO9797Alg3, calculateARPC, translatePINExt, encryptToLMK, decryptFromLMK).
  • TR-31 / X9.143 forward compatibility — every method returning or accepting a derived symmetric key is parametric on T. The verb-level API never refers to SecureDESKey directly. A future HSM adapter speaking ANSI X9.143 / TR-31 key blocks would extend BaseSMAdapter<SecureKeyBlock> without touching the interface.

Visibility lifts on JCESecurityModule

Six internal helpers widened from privateprotected to enable byte-for-byte tests against the internal derivation paths and to give HSM-backed subclasses extension hooks. All binary-compatible widenings; no member ever narrowed.

Helper Lifted in commit
deriveCommonSK_AC PR 5
deriveSK_MK PR 5
deriveSK_VISA PR 7
deriveCommonSK_SM PR 7
paddingISO9797Method2 PR 8
calculateMACISO9797Alg3 PR 8

Testing approach

  • PRs 1–8b cross-check new public methods against the known reference vectors already in JCESecurityModuleTest.java. The new methods are proven bit-for-bit equivalent to the existing verify* / integrated paths.
  • PRs 9–13 use a self-consistent synthetic RSA fixture chain: 1024-bit CA + 768-bit Issuer + 512-bit ICC keypairs generated once at @BeforeAll, certificates constructed byte-by-byte per the EMV Book 2 table layouts, signed via BigInteger.modPow, then recovered with the new method. Negative tests use a decrypt-mutate-resign helper so each corruption case is a single-line lambda.

Every negative-path test cross-references a specific EMV-mandated validation step (header 0x6A, format byte, trailer 0xBC, hash algorithm indicator, pad pattern 0xBB, hash binding, plus CDA's AC and transaction-data-hash bindings).

  • Validate pad patterns in issuer and ICC PK certificate recovery.
  • Reject DDA SDAD whose LDD ≠ LDN + 1.
  • Defensive copies of byte[] fields in all EMV records—on construction (compact constructor) and on every accessor. Equality/hash semantics and key/result contents are no longer externally mutable.
  • Regression tests for each hardening.

Files

Status Path
NEW jpos/src/main/java/org/jpos/security/EMVSMAdapter.java
NEW jpos/src/main/java/org/jpos/security/EMVDerivedKey.java
NEW jpos/src/main/java/org/jpos/security/EMVCAPublicKey.java
NEW jpos/src/main/java/org/jpos/security/EMVIssuerPublicKey.java
NEW jpos/src/main/java/org/jpos/security/EMVICCPublicKey.java
NEW jpos/src/main/java/org/jpos/security/EMVCDAResult.java
NEW jpos/src/test/java/org/jpos/security/EMVDerivedKeyTest.java
NEW jpos/src/test/java/org/jpos/security/EMVCAPublicKeyTest.java
NEW jpos/src/test/java/org/jpos/security/EMVIssuerPublicKeyTest.java
NEW jpos/src/test/java/org/jpos/security/EMVICCPublicKeyTest.java
NEW jpos/src/test/java/org/jpos/security/EMVCDAResultTest.java
MODIFIED jpos/src/main/java/org/jpos/security/BaseSMAdapter.java (wrappers + default-throw *Impl per method)
MODIFIED jpos/src/main/java/org/jpos/security/jceadapter/JCESecurityModule.java (overrides; visibility lifts)
MODIFIED jpos/src/test/java/org/jpos/security/jceadapter/JCESecurityModuleTest.java (140+ new tests)
UNCHANGED jpos/src/main/java/org/jpos/security/SMAdapter.java (byte-identical to main)

EMV spec coverage

EMV 4.4 Book 2 Section Implementation
Issuer Public Key recovery §6.3 recoverIssuerPublicKey
ICC Public Key recovery §6.4 recoverICCPublicKey
Signed Static Application Data §5.4 verifySDA
Dynamic Data Authentication §6.5 verifyDDA
ICC Dynamic Data §6.5.2 used by verifyDDA / verifyCDA
Combined Data Authentication §6.6 verifyCDA
EMV ICC Master Key derivation §A1.4 already in JCESecurityModule, now reachable via deriveICCMasterKey
Common Session Key derivation §A1.3.1 already internal, now reachable via deriveEMVSessionKey / deriveSecureMessagingSessionKey

ar added 17 commits May 14, 2026 21:07
Add an EMVSMAdapter<T> sub-interface of SMAdapter<T> that declares
generation counterparts for verifyCVC3 and verifydCVV. BaseSMAdapter
implements the new interface with the usual *Impl hook pattern; the
defaults throw SMException so third-party adapters keep compiling.
JCESecurityModule overrides the hooks by delegating to the existing
internal calculateCVC3 / calculatedCVV helpers - no new crypto logic.

The existing SMAdapter interface is untouched; signatures and behavior
of verifyARQC, generateARPC, verifyCVC3, verifydCVV, generateSM_MAC
and translatePINGenerateSM_MAC are preserved.

Tests added in JCESecurityModuleTest cover Option A and Option B paths
for both methods, including known-vector matches against the values
the existing verify tests already accept, and round-trip generate/verify
checks.
Add a generation counterpart to verifyARQC on the EMVSMAdapter<T>
sub-interface. The method computes the 8-byte Application Cryptogram
(ARQC / TC / AAC - the three share the MAC primitive; their semantic
distinction lives in the Cryptogram Information Data byte carried in
the supplied txnData, not in the computation).

BaseSMAdapter gains the public wrapper plus a protected
generateApplicationCryptogramImpl hook that throws SMException by
default, so third-party adapters keep compiling. JCESecurityModule
overrides the hook with a one-line delegate to the existing
calculateARQC routine - no new crypto logic; the value returned is
exactly what verifyARQCImpl already compares against.

Tests cover all six SKDMethod x padding combinations already used by
the verifyARQC suite (VSDC/MCHIP/EMV_CSKD x P00/P80), a round-trip
through verifyARQC, and the unsupported-SKDMethod exception path.
SMAdapter.java is untouched; all new surface lives on EMVSMAdapter.
Introduce the two foundation pieces the upcoming EMV key-derivation
APIs will rely on, with no new public method on EMVSMAdapter yet:

  - EMVDerivedKey<T extends SecureKey>(T key, byte[] kcv) - public
    record in org.jpos.security, parametric on the SecureKey subtype
    so that future HSM adapters speaking ANSI X9.143 / TR-31 key
    blocks can plug in alongside the software JCE adapter without
    leaking SecureDESKey through the verb-level API. The record
    overrides equals / hashCode / toString to compare the kcv array
    by content (and render it as hex) instead of by identity,
    eliminating the default-record array-equality footgun.

  - JCESecurityModule.calculateKCV(Key) - protected helper computing
    the standard 3-byte DES / Triple-DES Key Check Value
    (E_K(0x00..00)[0..3]). Placed adjacent to deriveICCMasterKey so
    the ICC-MK / KCV / parity helpers stay clustered. An AES variant
    is deferred until AES IMKs land in jPOS.

No change to SMAdapter, EMVSMAdapter, BaseSMAdapter, or any existing
JCESecurityModule routine. Tests cover the content-equality contract
of the record and verify calculateKCV matches the standard KCV
definition without relying on an externally-sourced vector.
…ue instead

PR 3 introduced a calculateKCV(Key) helper on JCESecurityModule that
duplicated an existing protected calculateKeyCheckValue(Key) at
JCESecurityModule.java:1528 - same algorithm (E_K(0x00..00)[0..3]),
already in use by encryptToLMK. Remove the duplicate and repoint the
PR 3 test at the original helper.

Side benefit: calculateKeyCheckValue had no happy-path test before this
(only a negative test exercising an invalid algorithm). The renamed
test now fills that coverage gap. The EMVDerivedKey record introduced
in PR 3 is unaffected and stays in place.
Add the first public EMV key-derivation API. From a wrapped Issuer
Master Key, the cardholder PAN, and the PAN Sequence Number, return
the corresponding ICC Master Key wrapped in the adapter's native key
representation, paired with its 3-byte Key Check Value.

  EMVDerivedKey<T> deriveICCMasterKey(MKDMethod mkdm, T imk,
                                      String pan, String psn)

Returned type is EMVDerivedKey<T> so future ANSI X9.143 / TR-31 HSM
adapters can plug in alongside the software JCE adapter without
leaking SecureDESKey through the verb-level API. The JCE
implementation reuses the existing internal path verbatim:

  decryptFromLMK(imk) -> formatPANPSN(pan, psn, mkdm)
     -> deriveICCMasterKey(Key, byte[])  (EMV v4.2 Book 2 A1.4)
     -> encryptToLMK(keyLength, keyType, clearIcc)

encryptToLMK already computes the KCV and stores it on the resulting
SecureDESKey, so the new public method does not duplicate any
cryptography. The derived key inherits the IMK's keyType and
keyLength, keeping its usage family intact. Parity adjustment stays
internal (no caller knob).

EMVDerivedKey's type parameter is left unbounded (rather than
<T extends SecureKey>) to stay compatible with the rest of the
org.jpos.security public surface (SMAdapter<T>, BaseSMAdapter<T>),
which uses unbounded T. The TR-31 design rule "all EMV public APIs
parametric on T" is preserved; the SecureKey bound is convention,
not compile-time-enforced.

Seven new tests cover the OPTION_A byte-for-byte match against the
internal derivation, IMK-metadata preservation, KCV shape,
determinism, the null-mkdm default, the OPTION_B SHA-1 path on a
long PAN, and the null-PAN rejection. SMAdapter.java continues
byte-identical to main; all new surface lives on EMVSMAdapter.
Add the second public EMV key-derivation primitive on EMVSMAdapter:

  EMVDerivedKey<T> deriveEMVSessionKey(SKDMethod skdm, T iccMk,
                                       byte[] atc, byte[] upn)

The method takes the ICC Master Key produced by PR 4 plus the
per-transaction diversifiers (ATC, optional UN) and returns the
Application Cryptogram session key wrapped in the adapter's native
key representation, paired with its 3-byte Key Check Value. The
SKDMethod dispatch mirrors calculateARQC exactly:

  - VSDC has no session-key derivation; the AC is MAC'd with the
    ICC Master Key directly. For API uniformity the method returns
    the ICC Master Key rewrapped so callers do not need to special-
    case VSDC. ATC and UN are ignored.
  - MCHIP uses deriveSK_MK(clearIcc, atc, upn) - ATC last 2 bytes
    + UPN last 4 bytes + zeros into an 8-byte diversifier, fed into
    deriveCommonSK_SM.
  - EMV_CSKD uses deriveCommonSK_AC(clearIcc, atc) - ATC last 2
    bytes into an 8-byte diversifier, fed into deriveCommonSK_SM.
  - AEPIS_V40 and EMV2000_SKM throw SMException with the same
    message format calculateARQC has always used.

The derived session key inherits the ICC Master Key's keyType and
keyLength. Parity adjustment stays internal (deriveCommonSK_SM and
encryptToLMK both adjust parity).

Visibility lift: deriveCommonSK_AC (line 1014) and deriveSK_MK
(line 1023) are widened from private to protected. This aligns them
with the already-protected deriveICCMasterKey(Key, byte[]) and
enables the byte-for-byte tests below to compare the new public
path against the original derivation without reflection. Widening
access is binary-compatible; no caller breaks.

Seven new tests cover VSDC pass-through, EMV_CSKD byte-for-byte
match against the internal path, MCHIP byte-for-byte match against
the internal path, determinism, ICC-MK metadata preservation, KCV
shape, and the AEPIS_V40 unsupported-SKD exception. SMAdapter.java
remains byte-identical to main; all new public surface lives on
EMVSMAdapter.
Add a 5-arg overload of generateARPC on EMVSMAdapter<T> that skips the
IMK + PAN/PSN derivation and computes the ARPC directly under a
caller-supplied key:

  byte[] generateARPC(T key, byte[] arqc, ARPCMethod arpcMethod,
                      byte[] arc, byte[] propAuthData)

This is the first consumer-facing API that uses an EMVDerivedKey<T>
result from PR 4/5: callers can now run the full derivation chain
deriveICCMasterKey -> (optionally) deriveEMVSessionKey -> generateARPC
without any internal re-derivation.

The implementation in JCESecurityModule is three lines, delegating to
the existing calculateARPC(Key, ...) routine after decrypting the
supplied key from the LMK. No new crypto.

Two intentional differences from the master-key-based generateARPC:

  - The overload is method-name-overloaded by arity (5 params vs 11);
    callers pick at compile time based on which signature they need.
    No call-site ambiguity since the first parameter is T vs MKDMethod.

  - Constraint relaxation. The master-key-based variant rejects
    VSDC+METHOD_2 and MCHIP+METHOD_2 via constraintARPCM because it
    has SKDMethod context. The direct overload is a math primitive
    with no SKDMethod context; it accepts any (key, ARPCMethod)
    combination and lets the caller take responsibility for choosing
    combinations the card will actually accept. Documented in the
    Javadoc and exercised by two NoConstraint tests below.

The Javadoc also calls out which key the caller must pass: the ICC
Master Key directly for VSDC and MCHIP (EMV defines no ARPC-session
derivation in those schemes), and the AC session key from
deriveEMVSessionKey for EMV_CSKD. Getting this wrong is a real
foot-gun without that guidance.

Six new tests in JCESecurityModuleTest:
  - VSDC, MCHIP, EMV_CSKD x METHOD_1 + EMV_CSKD x METHOD_2 each match
    the existing known-vector ARPCs that the master-key tests check
    against, proving the direct path is bit-equivalent.
  - VSDC + METHOD_2 and MCHIP + METHOD_2 succeed (the master-key path
    throws here) and are self-consistent with calculateARPC on the
    same decrypted key, pinning the constraint-relaxation behaviour.

SMAdapter.java is byte-identical to main; all new surface lives on
EMVSMAdapter.
Add the Secure Messaging session-key derivation primitive on
EMVSMAdapter<T>:

  EMVDerivedKey<T> deriveSecureMessagingSessionKey(
      MKDMethod mkdm, SKDMethod skdm, T imk,
      String pan, String psn, byte[] atc, byte[] arqc)

The method takes a wrapped Secure Messaging Issuer Master Key
(IMK-SMI or IMK-SMC), the cardholder PAN/PSN, and the per-session
diversifier (ATC for VSDC, ARQC for MCHIP / EMV_CSKD) and returns
the corresponding SM session key wrapped in the adapter's native
key representation, paired with its 3-byte KCV.

Design: a single method handles both the integrity key (SK-SMI) and
the confidentiality key (SK-SMC) because the algorithm is byte-for-
byte the same; only the input IMK differs. The derived key inherits
the IMK's keyType, so the usage family carries through to the
result. This keeps the type surface small (reuses EMVDerivedKey<T>;
no new EMVSecureMessagingKeys record) and lets callers who only
need SK-SMI not have to supply IMK-SMC, and vice versa.

The SKDMethod dispatch mirrors generateSM_MACImpl exactly:

  - VSDC uses deriveSK_VISA(clearMk, atc), the Visa-specific
    ATC-XOR derivation. ATC is required; ARQC is ignored.
  - MCHIP and EMV_CSKD both use deriveCommonSK_SM(clearMk, arqc),
    the common EMV SM session-key method with ARQC as 8-byte
    diversifier. ARQC is required; ATC is ignored.
  - AEPIS_V40 and EMV2000_SKM throw, same format as
    generateSM_MACImpl has always done.

Note: SM session-key derivation is *not* the same algorithm as AC
session-key derivation (PR 5). For MCHIP, AC uses
deriveSK_MK(atc, upn) and SM uses deriveCommonSK_SM(arqc). The two
public methods (deriveEMVSessionKey vs this one) exist as parallel
APIs because the EMV specs prescribe different algorithms for the
two purposes.

Visibility lift: deriveSK_VISA (line 962) and deriveCommonSK_SM
(line 991) are widened from private to protected, mirroring PR 5's
lift on deriveCommonSK_AC and deriveSK_MK. This aligns them with
the already-protected deriveICCMasterKey(Key, byte[]) and enables
the byte-for-byte tests below. Widening access is binary-compatible;
no caller breaks.

Seven new tests cover VSDC/MCHIP/EMV_CSKD SK-SMI byte-for-byte
match against the internal derivation, VSDC SK-SMC byte-for-byte
match (showing the single method handles both families and that
imksmc's keyType carries through), determinism, IMK metadata
preservation, and the AEPIS_V40 unsupported-SKD exception.
SMAdapter.java remains byte-identical to main.
…SMAdapter

Add a 2-arg overload of generateSM_MAC on EMVSMAdapter<T> that skips
the IMK + PAN/PSN derivation and computes the SM MAC directly under a
caller-supplied SK-SMI:

  byte[] generateSM_MAC(T sessionKey, byte[] data)

This closes the SM-MAC derivation chain on the new public API.
Combined with deriveSecureMessagingSessionKey from PR 7, callers can
now run:

  EMVDerivedKey<T> skSmi = deriveSecureMessagingSessionKey(
          mkdm, skdm, imkSmi, pan, psn, atc, arqc);
  byte[]           mac   = generateSM_MAC(skSmi.key(), data);

The result is bit-identical to the existing master-key-based
generateSM_MAC(mkdm, skdm, imkSmi, ...) for the same inputs - this is
proved end-to-end by the four known-vector tests below, which compare
the new derivation+MAC chain against the values already pinned in
testGenerateSM_MACImpl1..4.

Implementation in JCESecurityModule is three lines: decryptFromLMK ->
paddingISO9797Method2 -> calculateMACISO9797Alg3. The padding (ISO/IEC
9797-1 method 2) and MAC algorithm (ISO/IEC 9797-1 algorithm 3,
Triple-DES retail MAC) match what generateSM_MACImpl uses internally
for every SKDMethod, so there is no behavioural divergence to document.

Visibility lifts: paddingISO9797Method2 (line 661) and
calculateMACISO9797Alg3 (line 678) are widened from private to
protected, mirroring the lifts in PR 5 and PR 7. Both are pure-function
ISO 9797 primitives an HSM-backed subclass may want to share or
override. Widening access is binary-compatible; no caller breaks.

Method-name overload (not a new name): the existing public method is
generateSM_MAC (legacy underscore convention); the new signature is
distinguished by arity (2 args vs 8). Java picks the right one at
compile time. Same precedent as PR 6's generateARPC overload.

No PaddingMethod parameter: the JCE adapter implements only ISO 9797
method 2 for SM-MAC today; the Desktop plan's Visa/CCD/M-Chip padding
variants are not in scope here. Callers requiring alternative padding
should pre-pad the data themselves.

Direct SM PIN encrypt is deliberately split off into a separate PR 8b
follow-up. PIN-block format variants (34/35/41/42) and the interplay
between PaddingMethod and PIN block format deserve their own design
pass; bundling them here would over-load this change.

Five new tests:
  - Four known-vector tests (MCHIP/VSDC x long/short data) cross-check
    PR 7's derivation + PR 8's MAC against the existing
    testGenerateSM_MACImpl1..4 vectors (217CF53EA0E7C327,
    5E14A5A5C4B98C0C, E218CC0B7FEC6876, C1F2C04136BD48E6).
  - One determinism test pins same-input -> same-output and confirms
    the MAC is 8 bytes.

SMAdapter.java is byte-identical to main; all new surface lives on
EMVSMAdapter.
Add the SM-PIN-encryption half of the integrated
translatePINGenerateSM_MAC as a stand-alone primitive on
EMVSMAdapter<T>:

  EncryptedPIN encryptSecureMessagingPIN(
          T sessionKey, EncryptedPIN newPIN, T kd1,
          byte destinationPINBlockFormat,
          PaddingMethod paddingMethod,
          EncryptedPIN currentPIN, T udkAc)

The caller supplies the SK-SMC directly (typically from
deriveSecureMessagingSessionKey in PR 7) and a new PIN already
encrypted under an existing key. The method translates the PIN into
destinationPINBlockFormat and re-encrypts it under the SK-SMC for
transmission to the card in an issuer script.

Combined with PR 4 (deriveICCMasterKey), PR 7
(deriveSecureMessagingSessionKey), and PR 8 (direct generateSM_MAC),
every SM operation jPOS supports today via translatePINGenerateSM_MAC
is now reachable as a composition of new primitives. No call from
PR 8b to the integrated method.

Implementation in JCESecurityModule is roughly 10 lines: decrypt the
sessionKey / kd1 / udkAc from the LMK and delegate to the existing
private translatePINExt with the supplied paddingMethod. No new
crypto, no visibility lifts - translatePINExt lives inside
JCESecurityModule so the new *Impl has direct access.

Seven-parameter shape: PIN translation has format-dependent inputs.
FORMAT34/35 use only the first five parameters; FORMAT41 additionally
requires udkAc as the PAN-like diversifier; FORMAT42 additionally
requires both udkAc and currentPIN. Bundling all into one method with
nulls for unused slots mirrors how translatePINExt already works and
avoids multiplying the surface across formats.

Null paddingMethod defaults to PaddingMethod.MCHIP (no SM-specific
padding added), matching translatePINImpl's existing default. The
integrated translatePINGenerateSM_MAC auto-derives padding from the
SKDMethod; the direct method has no SKDMethod context, so the caller
picks. Documented in the Javadoc to avoid surprise.

Five new tests:
  - FORMAT34 (MCHIP) cross-check against testTranslatePINGenerateSM_MACImpl1
    expected PIN block F473D25D9B478970.
  - FORMAT41 (EMV_CSKD + CCD padding) cross-check against
    testTranslatePINGenerateSM_MACImpl2 expected E60663E4B11CDB2DE4667CC9433384B4.
  - FORMAT42 (VSDC + currentPIN + udkAc) cross-check against
    testTranslatePINGenerateSM_MACImpl6 expected 74253653C81CE99140C47C0F7C572473.
  - Null paddingMethod defaults to MCHIP (compare null vs explicit MCHIP).
  - Determinism (same inputs -> same output).

The three known-vector tests pin the new PR 4 + PR 7 + PR 8b
composition to the existing translatePINGenerateSM_MAC test vectors,
proving the new derivation chain is bit-equivalent for the PIN-encrypt
half across all three supported formats.

SMAdapter.java is byte-identical to main; all new public surface
lives on EMVSMAdapter.
…blicKey (impl pending)

First half of PR 9. Adds the public type surface for EMV offline
authentication's first primitive — recovering an Issuer Public Key
from a CA-signed Issuer Public Key Certificate (EMV 4.4 Book 2 §6) —
but deliberately defers the JCESecurityModule implementation to a
follow-up commit.

What lands here:

  - EMVCAPublicKey: public record (rid, index, modulus, exponent,
    hashAlgorithmIndicator, publicKeyAlgorithmIndicator) with
    content-aware equals/hashCode/toString. Same pattern as the
    EMVDerivedKey record from PR 3 — byte[] fields are compared by
    Arrays.equals and rendered via ISOUtil.hexString in toString so
    logs never spill [B@... identity strings.

  - EMVIssuerPublicKey: public record (issuerIdentifier,
    expirationDate, serialNumber, modulus, exponent, hashAlgInd,
    pkAlgInd) with the same overrides.

  - EMVSMAdapter.recoverIssuerPublicKey(...) declared on the interface.
    Javadoc references EMV 4.4 Book 2 §6 and the relevant EMV tags
    (0x90 cert, 0x92 remainder, 0x9F32 exponent), lists all 10
    validation steps the implementation must perform, and documents
    that every validation is a hard failure (no partial result).

  - BaseSMAdapter: public wrapper following the established logging
    pattern; protected recoverIssuerPublicKeyImpl that throws
    SMException("Operation not supported in: ...") by default.

  - Record-shape tests for both records (7 each = 14 new tests):
    accessors, content-based equals, hashCode consistency, hex
    rendering in toString, byte-diff-by-one inequality, unrelated-
    type inequality, null-field tolerance.

Intentional half-feature state: JCESecurityModule is NOT modified in
this commit. Calling jcesecmod.recoverIssuerPublicKey(...) throws
"Operation not supported in: ..." until the follow-up commit lands.
This split keeps each commit independently reviewable — the type
surface here, the ~80-line RSA recovery + validation logic plus its
10 cross-checking tests next.

Note this is the first PR on this branch where the new method is NOT
parametric on <T>. CA and Issuer public keys are clear public values
with no LMK wrapping involved, so the TR-31 / X9.143 constraint that
governs derived-symmetric-key methods does not apply here.

SMAdapter.java remains byte-identical to main.
…rityModule

Second half of PR 9, completing the half-feature state from
93923c1. The JCESecurityModule now actually does the RSA recovery
and the full EMV 4.4 Book 2 §6 validation chain that the previous
commit declared on the EMVSMAdapter interface.

This is the first PR on this branch that introduces new cryptographic
code rather than exposing existing internals. PR 1-8b reused private
or protected helpers already living in JCESecurityModule. PR 9b adds
~100 lines of new code: BigInteger.modPow for the RSA recovery, plus
the EMV cert structural validation, plus four small private helpers
(bigIntegerToFixedLengthBytes, require, requireExpirationFuture,
requireIssuerIdMatches).

The implementation performs the following hard-failing checks, in
order, on the recovered cert payload:

  1. Recovered Data Header == 0x6A
  2. Certificate Format == 0x02 (Issuer Public Key Certificate)
  3. Recovered Data Trailer == 0xBC
  4. Hash Algorithm Indicator == 0x01 (SHA-1) — others throw
  5. Public Key Algorithm Indicator == 0x01 (RSA) — others throw
  6. Length consistency: ipkInCert + remainder.length == NI
                          issuerExp.length == declared ipkExpLen
  7. SHA-1(recovered[1..nca-22] || remainder || exponent) ==
     embedded hash in recovered[nca-21..nca-2]
  8. Expiration date (MMYY BCD) > LocalDate.now() (year/month
     compared in current century — sufficient for tests in 2026
     and EMV CA certs issued post-2010)
  9. Issuer Identifier prefix matches PAN's leftmost digits (with
     trailing 0xF nibbles stripped). pan == null skips this check.

Every failure throws SMException with a message naming the failed
step; no partial result is returned.

New imports in JCESecurityModule:
  - java.math.BigInteger (for modPow)
  - java.time.LocalDate (for expiration check)

The existing static SHA1_MESSAGE_DIGEST field is reused via
.digest(byte[]) on a fully-assembled input, matching the pattern
already established in formatPANPSNOptionB.

Test strategy is self-consistent rather than vector-based: a
@BeforeAll hook generates a 1024-bit CA RSA keypair and a 768-bit
issuer keypair using KeyPairGenerator, constructs a canonical
Issuer Public Key Certificate payload by hand, signs it with the
CA private exponent via BigInteger.modPow, and then recovers it.
Ten new tests:

  - HappyPath: full recovery, all fields match the original payload.
  - NullPAN_SkipsIssuerIdCheck: positive path with pan == null.
  - BadHeader_Throws: 0x6A -> 0x6B, re-sign, expect SMException.
  - BadFormat_Throws: 0x02 -> 0x03.
  - BadTrailer_Throws: 0xBC -> 0xBD.
  - BadHash_Throws: XOR one bit inside the embedded hash field.
  - UnsupportedHashAlgo_Throws: byte 11 = 0x02 instead of 0x01.
  - UnsupportedPKAlgo_Throws: byte 12 = 0x02 instead of 0x01.
  - ExpiredDate_Throws: build a fresh cert with expDate "0101"
    (Jan 2001) and expect the expiration check to reject it.
  - IssuerIdMismatch_Throws: pass a PAN whose first digits don't
    match the issuer identifier embedded in the cert.

The mutateAndSign helper decrypts the happy-cert payload with the
CA public exponent, applies a mutator lambda, and re-signs with the
CA private exponent. This keeps each negative test to one line of
mutation.

768-bit issuer modulus (96 bytes) exceeds NCA-36 (92 bytes), so
the happy-path cert exercises the remainder path. Once a real
consumer demands a no-remainder code path, a follow-up test with
a smaller issuer key can land separately.

:jpos:test 4302/0/70 (4292 + 10 new). SMAdapter.java still
byte-identical to main; all new public surface still lives on
EMVSMAdapter and the two new records from PR 9a.
Second step in the offline-authentication chain after PR 9. Given the
Issuer Public Key recovered from a CA-signed cert (PR 9's output) plus
an ICC PK Certificate signed by that issuer, validate per EMV 4.4
Book 2 §6.4 and return the recovered ICC Public Key.

What lands here:

  - New record EMVICCPublicKey(applicationPan, expirationDate,
    serialNumber, modulus, exponent, hashAlgInd, pkAlgInd). Same
    content-aware override pattern as EMVIssuerPublicKey. The only
    structural difference is the first field: 10-byte BCD Application
    PAN (vs 4-byte Issuer Identifier in PR 9's record).

  - EMVSMAdapter.recoverICCPublicKey(...) declared on the interface.
    Javadoc references EMV §6.4 and tags 0x9F46 / 0x9F47 / 0x9F48,
    documents the SAD inclusion in hash input, and gives the natural
    PR 9 → PR 10 chain as an example.

  - BaseSMAdapter public wrapper logging the inputs; protected
    recoverICCPublicKeyImpl that throws SMException by default.

  - JCESecurityModule override (~90 lines):
      1. RSA recovery cert^e mod n under the *issuer* PK (not the CA)
      2. Header 0x6A
      3. Certificate Format 0x04 (note: differs from PR 9's 0x02)
      4. Trailer 0xBC
      5. Hash algorithm indicator 0x01 (SHA-1)
      6. Public key algorithm indicator 0x01 (RSA)
      7. Length consistency: ipkInCert + remainder.length == NIC
                              iccExp.length == declared iccExpLen
      8. SHA-1(recovered[1..ni-22] || remainder || iccExp || SAD) ==
         embedded hash in recovered[ni-21..ni-2]
      9. Expiration date (MMYY BCD, reuses PR 9's requireExpirationFuture)
     10. Application PAN match (10-byte BCD, F-padded; new helper
         requirePanMatches; pan == null skips)

    Reuses PR 9's bigIntegerToFixedLengthBytes, require, and
    requireExpirationFuture helpers. No new imports.

  - Field offsets in the recovered payload differ from PR 9 because
    the ICC cert carries the full 10-byte PAN where the issuer cert
    carries a 4-byte Issuer Identifier. The "leftmost slot" is NI-42
    bytes wide (vs NCA-36 in PR 9), accounting for the extra 6 bytes.

  - SAD inclusion in the hash input is the key semantic addition vs
    PR 9. It's how the issuer cryptographically commits the ICC
    public key to a specific set of card records assembled by the
    terminal from the AFL. Passing a different SAD at recovery time
    than was used at certificate creation breaks the hash validation.

  - Seventeen new tests: 7 EMVICCPublicKey record-shape tests
    (mirroring EMVIssuerPublicKeyTest) and 10 recovery tests:
      * HappyPath: full PR 9 → PR 10 chain, all fields match
      * NullPAN_SkipsPanCheck
      * BadHeader_Throws / BadFormat_Throws / BadTrailer_Throws
      * BadHash_Throws (XOR a bit in the embedded hash)
      * SADCorruption_Throws — pass a different SAD at recovery;
        unique to ICC PK recovery, pins the SAD-in-hash behavior
      * UnsupportedHashAlgo_Throws
      * ExpiredDate_Throws
      * PanMismatch_Throws

    The recovery tests build on PR 9's CA + Issuer keypair fixtures
    (the issuer's private exponent had to be captured at PR 9 init
    so PR 10 can sign ICC certs with it). A new 512-bit ICC keypair
    is generated at @BeforeAll; 64-byte modulus exceeds NI-42 = 54
    bytes, so the happy fixture exercises the remainder path.

  - The setUpPR10Fixtures method is intentionally NOT a separate
    @BeforeAll — JUnit 5 does not order multiple @BeforeAll methods
    deterministically, so it's invoked from setUpPR9Fixtures at the
    end. This guarantees the issuer keypair is captured before any
    ICC-cert signing runs.

:jpos:test 4319/0/70 (4302 + 17 new). SMAdapter.java still
byte-identical to main; all new public surface lives on EMVSMAdapter
and the three new records (EMVCAPublicKey, EMVIssuerPublicKey,
EMVICCPublicKey).
First offline-data-authentication consumer of the recovered Issuer
Public Key chain from PR 9. Given a recovered Issuer Public Key, the
SSAD blob from EMV tag 0x93, and the Static Application Data the
terminal assembled per Book 3 §10.3, validate the §5.4 structure and
return the 2-byte Data Authentication Code (DAC) the issuer embedded
in the signed payload.

The DAC is the only artifact the terminal carries forward — it goes
into EMV tag 0x9F45 for inclusion in the transaction's data
authentication outcome. Returning byte[] keeps the API minimal; no
new record type is needed.

What lands here:

  - EMVSMAdapter.verifySDA(...) declared on the interface. Javadoc
    references EMV 4.4 Book 2 §5.4, names the DAC return value,
    documents that the caller is responsible for assembling SAD per
    Book 3 §10.3 (records from the AFL + optional Static Data
    Authentication Tag List).

  - BaseSMAdapter: public wrapper following the established logging
    pattern; protected verifySDAImpl that throws SMException by
    default.

  - JCESecurityModule.verifySDAImpl (~55 lines):

      1. RSA recovery: ssad^e mod n under the issuer public key
      2. Header 0x6A
      3. Signed data format 0x03 (note: differs from 0x02 for issuer
         certs / 0x04 for ICC certs)
      4. Trailer 0xBC
      5. Hash algorithm indicator 0x01 (SHA-1)
      6. Pad pattern: every byte in recovered[5..ni-21) equals 0xBB
      7. SHA-1(recovered[1..ni-21) || SAD) == recovered[ni-21..ni-1)

    Reuses PR 9's bigIntegerToFixedLengthBytes and require helpers.
    No new helpers, no new imports.

  - Pad pattern check is mandatory. EMV §5.4 lists it under structural
    verification guidance without strict mandate; this implementation
    treats non-conformant padding as a hard failure rather than
    silently accept a card defect. A lenient variant can land in a
    follow-up if a real-world card surfaces non-0xBB padding.

  - Nine new tests reusing PR 9/10's CA + Issuer keypair fixture:
      * HappyPath: build SSAD with DAC 0x1234, verify, assert returned
      * BadHeader / BadFormat / BadTrailer / BadHash / UnsupportedHashAlgo
        (decrypt-mutate-resign pattern, same as PR 9/10)
      * BadPadding: build SSAD with pad byte 0xBC instead of 0xBB —
        hash is signed-consistent, structural check rejects. Pins the
        mandatory-pad-check decision.
      * SADCorruption: pass a different SAD at verify time than was
        used at signing; expect hash mismatch
      * WrongLength: truncated SSAD, expect early-exit length check

:jpos:test 4328/0/70 (4319 + 9 new). SMAdapter.java still byte-
identical to main; all new public surface lives on EMVSMAdapter.
Last EMV offline-data-authentication verifier before CDA. Given the
recovered ICC Public Key (PR 10's output), the SDAD blob from EMV
tag 0x9F4B, and the DDOL bytes the terminal sent to INTERNAL
AUTHENTICATE, validate per EMV 4.4 Book 2 §6.5 and return the ICC
Dynamic Number the card signed.

DDA proves the card holds the ICC private key — a stronger
authentication signal than SDA (which only proves the issuer signed
static records). The natural caller flow chains all three recovery
+ verify steps: recoverIssuerPublicKey (PR 9) → recoverICCPublicKey
(PR 10) → verifyDDA (PR 12).

What lands here:

  - EMVSMAdapter.verifyDDA(...) declared on the interface. Javadoc
    references EMV §6.5 / §6.5.2 and tag 0x9F4B, names the ICC
    Dynamic Number return value, documents that the caller is
    responsible for DDOL assembly per EMV §10.4.

  - BaseSMAdapter: public wrapper following the established logging
    pattern; protected verifyDDAImpl that throws SMException by
    default.

  - JCESecurityModule.verifyDDAImpl (~60 lines):

      1. RSA recovery: sdad^e mod n under the ICC public key
      2. Header 0x6A
      3. Signed data format 0x05 (note: differs from 0x03 for SSAD,
         0x02 for issuer cert, 0x04 for ICC cert)
      4. Trailer 0xBC
      5. Hash algorithm indicator 0x01 (SHA-1)
      6. ICC Dynamic Data Length (LDD) sanity: in [1, nic-26]
      7. ICC Dynamic Number Length (LDN) sanity: in [2, 8] per §6.5.2
      8. Pad pattern: bytes (4+LDD) to (nic-21) all equal 0xBB
      9. SHA-1(recovered[1..nic-21) || DDOL) == recovered[nic-21..nic-1)

    Reuses PR 9's bigIntegerToFixedLengthBytes and require helpers.
    No new helpers, no new imports.

  - Mandatory pad-pattern check — same rationale as PR 11.

  - Tests reuse PR 9/10's CA + Issuer + ICC keypair fixture. The ICC
    private exponent (pr12IccD) is captured inside setUpPR10Fixtures
    since the keypair is generated there. setUpPR12Fixtures is
    invoked at the end of setUpPR10Fixtures (not as a separate
    @BeforeAll) to guarantee deterministic ordering — JUnit 5 does
    not order multiple @BeforeAll methods.

  - Nine new tests:
      * HappyPath: build SDAD with ICC Dynamic Number 0x12345678,
        recover via the full PR 9 → PR 10 → PR 12 chain, assert
        returned bytes match
      * BadHeader / BadFormat (0x05 → 0x03, SSAD format used by
        mistake) / BadTrailer / BadHash / UnsupportedHashAlgo
      * BadPadding (rebuilt with 0xBC pad — signed consistently;
        structural check rejects)
      * DDOLCorruption (different DDOL at verify time than was used
        at signing; expect hash mismatch)
      * WrongLength (truncated SDAD; early-exit length check)

After this PR the JCE adapter exposes every EMV offline-data-
authentication primitive pre-CDA: PR 9 (recover Issuer PK), PR 10
(recover ICC PK), PR 11 (SDA verify), PR 12 (DDA verify). CDA is a
separate item on the roadmap because it validates the AC alongside
the DDA signature in a single operation.

:jpos:test 4337/0/70 (4328 + 9 new). SMAdapter.java still byte-
identical to main; all new public surface lives on EMVSMAdapter
and the three offline-auth records.
Last EMV offline-data-authentication verifier the spec defines. Given
the recovered ICC Public Key (PR 10's output), the CDA-flavored SDAD
blob from EMV tag 0x9F4B, the terminal's Unpredictable Number, the
Application Cryptogram returned by the card in tag 0x9F26, and the
transaction data the terminal hashed, validate per EMV 4.4 Book 2
§6.6 and return an EMVCDAResult carrying the ICC Dynamic Number and
the Cryptogram Information Data (CID).

CDA is strictly stronger than DDA (PR 12): it proves not only that
the card holds the ICC private key, but that the card's cryptogram
and transaction context are cryptographically bound to the dynamic
signature. The two cross-binding checks unique to CDA are what give
it that property:

  - AC binding: the 8-byte AC embedded inside the SDAD must equal
    the supplied applicationCryptogram. Catches a malicious card
    that signs one AC and returns another out-of-band.
  - Transaction Data Hash binding: the 20-byte hash embedded inside
    the SDAD must equal SHA-1(transactionData). Catches a card that
    signed a different transaction context than what the terminal
    sent.

What lands here:

  - EMVCDAResult: public record (byte[] iccDynamicNumber, byte cid)
    with content-aware equals/hashCode/toString. Two distinct outputs
    justify a record over flat returns; the CID byte is needed by
    the terminal to route the outcome (bits 7-6 select ARQC / TC /
    AAC).

  - EMVSMAdapter.verifyCDA(...) declared on the interface. Javadoc
    references EMV §6.6 / §6.6.2, documents the AC + transaction-
    data-hash bindings, explains that the CDA SDAD shares tag 0x9F4B
    with the DDA SDAD and is distinguished by LDD = LDN + 30.

  - BaseSMAdapter: public wrapper with the established logging
    pattern; protected verifyCDAImpl that throws SMException by
    default.

  - JCESecurityModule.verifyCDAImpl (~90 lines): the most involved
    offline-auth verifier so far. 12 validation steps:

      1. RSA recovery
      2. Header 0x6A
      3. Signed data format 0x05
      4. Trailer 0xBC
      5. Hash algorithm indicator 0x01 (SHA-1)
      6. ICC Dynamic Number Length (LDN) in [2, 8]
      7. CDA LDD == LDN + 30 (strict — DDA used LDN + 1)
      8. Dynamic data fits before hash + trailer
      9. Pad pattern: bytes (4+LDD)..(nic-21) all 0xBB
     10. SHA-1(recovered[1..nic-21) || UN) == embedded hash
     11. AC binding: SDAD-embedded 8-byte AC == supplied AC
     12. Transaction Data Hash binding: SDAD-embedded 20-byte hash
         == SHA-1(transactionData)

    Reuses PR 9's bigIntegerToFixedLengthBytes + require helpers and
    the existing SHA1_MESSAGE_DIGEST static. No new helpers, no new
    imports.

  - Test fixtures extend PR 9 -> PR 10 -> PR 12 chain. PR 13 setup is
    invoked from setUpPR12Fixtures (which is itself invoked from
    setUpPR10Fixtures, which is invoked from setUpPR9Fixtures) so
    JUnit 5's non-deterministic @BeforeAll ordering can't observe a
    null fixture dependency. CDA SDAD is built by hand with LDD=34
    (LDN=4 + 30) and signed with the ICC private exponent captured
    at PR 12 fixture time.

  - Six new EMVCDAResultTest cases (record-shape) plus eleven verify
    tests in JCESecurityModuleTest:

      * HappyPath: full PR 9 -> PR 10 -> PR 13 chain, asserts the
        returned EMVCDAResult matches inputs
      * BadHeader / BadFormat / BadTrailer / UnsupportedHashAlgo /
        BadHash / BadPadding: standard structural-corruption matrix
      * ACMismatch: supply a different AC at verify; expect AC
        binding failure (unique to CDA)
      * TransactionDataMismatch: supply different transactionData;
        expect transaction-data-hash binding failure (unique to CDA)
      * UNCorruption: supply different Unpredictable Number; surfaces
        as outer hash mismatch
      * WrongLength: truncated SDAD; early-exit length check

After this PR the JCE adapter exposes every EMV offline-data-
authentication verifier the spec defines: PR 9 (recover Issuer PK),
PR 10 (recover ICC PK), PR 11 (SDA verify), PR 12 (DDA verify),
PR 13 (CDA verify). Remaining roadmap items are scheme-specific
(MasterCard DSPK / OWHF / ICC Dynamic Number; Visa HCE / LUK /
qVSDC; CAP token).

:jpos:test 4354/0/70 (4337 + 17 new). SMAdapter.java still byte-
identical to main; all new public surface lives on EMVSMAdapter and
the four offline-auth records (EMVCAPublicKey, EMVIssuerPublicKey,
EMVICCPublicKey, EMVCDAResult).
Validate issuer and ICC public key certificate pad patterns, and reject
DDA signed dynamic data whose LDD does not match LDN + 1.

Defensively copy byte-array fields in EMV records on construction and
access so equality/hash semantics and key/result contents cannot be
mutated externally.

Add regression coverage for malformed certificate padding, invalid DDA
dynamic data length, and byte-array defensive copying.
@ar ar merged commit cff528e into main May 18, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant