feat: Add ecdh.Decapsulator interface for hardware token decryption#307
feat: Add ecdh.Decapsulator interface for hardware token decryption#30783noit wants to merge 1 commit intoProtonMail:mainfrom
Conversation
|
Hi @83noit, thanks for the detailed and well-documented PRs. However, I think this is not quite the right architecture for this feature. For RSA decryption with a hardware token, it's possible to set a go-crypto/openpgp/packet/private_key.go Lines 45 to 47 in 2e73b11 That way, the library can call out to the hardware token, rather than the application needing to grab a bunch of values and passing others back in. I would prefer to do the same for ECDH, either by allowing the |
Introduce a Decapsulator interface that allows ECDH key agreement to be performed by an external implementation (e.g. YubiKey, OpenPGP smartcard). This mirrors the existing crypto.Decrypter pattern used for RSA hardware tokens. The library handles KDF and AES key unwrap internally; the Decapsulator only performs the ECDH shared secret computation (Decaps step), matching what hardware tokens return via PSO:DECIPHER. - Add ecdh.Decapsulator interface with single Decaps method - Add ecdh.DecryptWithDecapsulator for the external-decaps flow - Refactor ecdh.Decrypt to share KDF/unwrap logic via internal helper - Wire Decapsulator support into EncryptedKey.Decrypt ECDH path - Test across all 9 ECDH curves + packet-level roundtrip test
3c6f651 to
cd8a955
Compare
|
Thanks for the review @twiss — great call on the interface approach. I've reworked the PR to use a I considered the |
Summary
Adds a
Decapsulatorinterface to theecdhpackage, allowing ECDH key agreement to be performed by an external implementation such as a hardware token (YubiKey, OpenPGP smartcard). This mirrors the existingcrypto.Decrypterpattern already used for RSA hardware token support.This is a revised approach based on @twiss's feedback — using an interface-based design instead of exposing raw MPI getters.
Design
Hardware tokens perform ECDH key agreement internally via PSO:DECIPHER and return only the raw shared secret, never exposing the private scalar. The new
Decapsulatorinterface captures exactly this:The library handles unmarshalling the ephemeral point, KDF (RFC 6637 §8), and AES key unwrap (RFC 3394) internally — the
Decapsulatoronly performs the key agreement step, matching what the hardware actually does.Integration follows the same pattern as RSA's
crypto.Decrypter: set aDecapsulatoras thePrivateKeyfield ofpacket.PrivateKey, andEncryptedKey.Decrypt()dispatches to it automatically.Why
Decapsulatorover a customECDHCurveECDHCurveis inopenpgp/internal/ecc— external callers can't import or implement itECDHCurve.Decaps(ephemeral, secret []byte)takes the private scalar as a parameter, which hardware tokens don't have — the interface signature is semantically wrong for this use caseECDHCurvehas 8 methods;Decapsulatorhas 1Decapsulatoris deliberately generic (notECDHDecapsulator) to allow reuse if x25519/x448 gain similar support laterChanges
ecdh/ecdh.go: AddDecapsulatorinterface,DecryptWithDecapsulator(), refactorDecrypt()to share KDF/unwrap logic via internal helperpacket/encrypted_key.go: ECDH case checks forecdh.Decapsulatorbefore falling back to*ecdh.PrivateKeypacket/private_key.go: UpdatedPrivateKeyfield commentNo existing behaviour is changed. All new code is additive.
Test plan
testDecryptWithDecapsulator— encrypt → decrypt via Decapsulator roundtrip across all 9 ECDH curves (P-256, P-384, P-521, secp256k1, curve25519, x448, brainpoolP256r1, brainpoolP384r1, brainpoolP512r1)TestECDHDecapsulator— packet-level roundtrip: serialize encrypted key → decrypt with Decapsulator-backedPrivateKey→ verify session keygo test ./...— all 22 packages)Relates to ProtonMail/gopenpgp#174.