From 5c41010d450b47bb9a12466eb8709188cfee899d Mon Sep 17 00:00:00 2001 From: Canaan Etai Date: Thu, 4 Jun 2026 03:04:12 +0100 Subject: [PATCH] feat(vault): decompose Vault into HSM capability interfaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the key-management façade HSM-adapter-ready by splitting it into small capability interfaces, so an adapter implements exactly what its device supports. - Add PINEncryptor, PINTranslator, and Macer. Vault is now their composition, so it keeps the same method set — source-compatible; SoftVault and SealedVault still satisfy Vault unchanged. - PINTranslator documents the PCI PIN Security contract: a conforming hardware implementation must re-encipher the PIN block atomically inside the device so the clear PIN never leaves it; an adapter that cannot (e.g. stock PKCS#11) must not implement the interface, since callers rely on its presence to mean the operation is PIN-secure. - A general-purpose PKCS#11 HSM provides Macer (and possibly PINEncryptor); a payment HSM (Thales payShield, Futurex) additionally provides PINTranslator. Callers type-assert for the narrowest capability they need. Adds capability-detection tests. gofmt/vet clean; ./... builds, vault/flow/gateway pass under -race. --- CHANGELOG.md | 12 ++++++++ vault/capabilities_test.go | 58 +++++++++++++++++++++++++++++++++++ vault/vault.go | 63 ++++++++++++++++++++++++++++++++------ 3 files changed, 124 insertions(+), 9 deletions(-) create mode 100644 vault/capabilities_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 66f01fc..c986bd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ the [versioning policy](https://teqpace-services.github.io/isopace/versioning/). ## [Unreleased] +### Added + +- **`vault` capability interfaces** for HSM adapters. The `vault.Vault` façade is + now composed from `PINEncryptor`, `PINTranslator`, and `Macer`, so a hardware + adapter can implement exactly the operations its device supports — a + general-purpose PKCS#11 HSM provides `Macer` (and possibly `PINEncryptor`), + while a payment HSM additionally provides `PINTranslator`. `Vault` keeps the + same method set, so this is **source-compatible**. `PINTranslator` documents + the PCI PIN Security contract: a conforming hardware implementation must + re-encipher atomically inside the device so the clear PIN never leaves it, and + an adapter that cannot do so (e.g. stock PKCS#11) must not implement it. + ## [0.3.0] - 2026-06-02 The acquirer-profiles release: two more ISO 8583:1987 switch profiles — diff --git a/vault/capabilities_test.go b/vault/capabilities_test.go new file mode 100644 index 0000000..046bb00 --- /dev/null +++ b/vault/capabilities_test.go @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// Copyright (C) 2026 Teqpace Services Ltd. +// +// This file is part of Isopace, a financial transaction framework. +// +// Isopace is dual-licensed: +// - under the GNU Affero General Public License v3.0 or later (see LICENSE); or +// - under a commercial license from Teqpace Services Ltd. (see COMMERCIAL-LICENSE.md). +// +// Authorship is recorded in the AUTHORS file. + +package vault_test + +import ( + "testing" + + "github.com/teqpace-services/isopace/vault" +) + +// The software vault satisfies every capability interface and the full Vault, +// verified from an external package (as an adapter would see them). +var ( + _ vault.PINEncryptor = (*vault.SoftVault)(nil) + _ vault.PINTranslator = (*vault.SoftVault)(nil) + _ vault.Macer = (*vault.SoftVault)(nil) + _ vault.Vault = (*vault.SoftVault)(nil) +) + +// macOnly stands in for a general-purpose (e.g. PKCS#11) HSM adapter that can +// MAC but cannot translate PINs. +type macOnly struct{} + +func (macOnly) GenerateMAC(string, vault.MACAlgorithm, vault.Padding, []byte) ([]byte, error) { + return nil, nil +} + +func (macOnly) VerifyMAC(string, vault.MACAlgorithm, vault.Padding, []byte, []byte) (bool, error) { + return false, nil +} + +func TestCapabilityDetection(t *testing.T) { + // A full software vault advertises every capability. + var v vault.Vault = vault.NewSoftVault() + if _, ok := v.(vault.PINTranslator); !ok { + t.Error("SoftVault should satisfy PINTranslator") + } + if _, ok := v.(vault.Macer); !ok { + t.Error("SoftVault should satisfy Macer") + } + + // A MAC-only adapter must NOT be mistakable for a PIN translator — this is + // exactly the check a switch performs before trusting a vault with PINs. + var m vault.Macer = macOnly{} + if _, ok := m.(vault.PINTranslator); ok { + t.Error("a Macer-only vault must not satisfy PINTranslator") + } +} diff --git a/vault/vault.go b/vault/vault.go index ca0cf69..a10ff66 100644 --- a/vault/vault.go +++ b/vault/vault.go @@ -21,25 +21,70 @@ import ( // ErrUnknownKey is returned when an operation names a key the vault does not hold. var ErrUnknownKey = errors.New("vault: unknown key") -// Vault is the key-management façade: operations name keys by reference rather -// than passing key material, so the same calls work whether the keys live in -// software ([SoftVault]) or in an HSM behind an adapter. A real HSM (e.g. via -// PKCS#11) is a drop-in Vault implementation kept in a separate module so the -// core stays stdlib-only. -type Vault interface { +// The key-management façade is composed from small capability interfaces so a +// hardware adapter can implement exactly the operations its device supports. +// Operations name keys by reference rather than passing key material, so the +// same calls work whether the keys live in software ([SoftVault]) or in an HSM +// behind an adapter (kept in a separate module so the core stays stdlib-only). +// +// A general-purpose PKCS#11 HSM typically provides [Macer] (and possibly +// [PINEncryptor]); a payment HSM additionally provides [PINTranslator]. Callers +// should depend on the narrowest capability they need and type-assert for it: +// +// tr, ok := v.(vault.PINTranslator) +// if !ok { return errors.New("configured vault cannot translate PINs") } + +// PINEncryptor enciphers a CLEAR PIN into a PIN block under a device-resident +// key. Because it takes the clear PIN, it is an issuer-side / trusted-context +// operation (e.g. PIN issuance) — at an acquiring switch the clear PIN must +// never be present, so a switch uses [PINTranslator] instead. +type PINEncryptor interface { // EncryptPINBlock encodes pin for the format and encrypts the 8-byte block // under the named PIN key (3DES ECB). EncryptPINBlock(keyRef string, format PINBlockFormat, pin, pan string) ([]byte, error) - // TranslatePIN decrypts an encrypted PIN block under srcRef, re-encodes it in - // dstFormat, and re-encrypts under dstRef — the classic switch PIN-translate. +} + +// PINTranslator re-enciphers an ENCRYPTED PIN block from one key (and format) to +// another — the classic acquirer/switch PIN-translate. The clear PIN does not +// appear in this interface. +// +// Contract: a conforming hardware implementation MUST perform the translation +// atomically inside the device so the clear PIN never leaves it (PCI PIN +// Security). An adapter that cannot translate without exposing the clear PIN in +// host memory (for example one limited to stock PKCS#11, which has no atomic +// translate mechanism) MUST NOT implement this interface — callers rely on its +// presence to mean the operation is PIN-secure. +type PINTranslator interface { + // TranslatePIN re-enciphers encBlock from srcRef/srcFormat to dstRef/dstFormat. TranslatePIN(srcRef, dstRef string, encBlock []byte, pan string, srcFormat, dstFormat PINBlockFormat) ([]byte, error) +} + +// Macer generates and verifies message authentication codes under a +// device-resident key; the key material never leaves the device. +type Macer interface { // GenerateMAC computes a MAC over data under the named key. GenerateMAC(keyRef string, alg MACAlgorithm, pad Padding, data []byte) ([]byte, error) // VerifyMAC verifies a MAC over data under the named key. VerifyMAC(keyRef string, alg MACAlgorithm, pad Padding, data, mac []byte) (bool, error) } -var _ Vault = (*SoftVault)(nil) +// Vault is the full key-management façade: the composition of every capability, +// implemented by the software backend ([SoftVault], [SealedVault]) and by +// full-function payment HSM adapters. An adapter that supports only some +// capabilities should expose those interface types ([Macer], etc.) rather than +// the full Vault. +type Vault interface { + PINEncryptor + PINTranslator + Macer +} + +var ( + _ Vault = (*SoftVault)(nil) + _ PINEncryptor = (*SoftVault)(nil) + _ PINTranslator = (*SoftVault)(nil) + _ Macer = (*SoftVault)(nil) +) // SoftVault is the in-process Vault: keys are held in memory and operations run // with the Go standard library. It is for development, testing, and conformance