Module: Portage::Ucp::Ap2::MandateSignature

Defined in:
lib/portage/ucp/ap2/mandate_signature.rb

Overview

Cryptographic verification of a PaymentMandate's signature — the piece MandateGuard's own comment (and PaymentMandate's, before this file existed) flagged as deliberately missing: "no key infrastructure or trust anchor exists in this repo to verify a signature against." A caller that now has a trust anchor (a real PSP adapter, or a platform-provided key resolver) passes it as trusted_keys: to get actual proof instead of shape-only validation.

Same ECDSA/JWK wire conventions as Security::Signature (P-256 mandatory/P-384 optional, raw r||s signature bytes, JWK x/y coordinates) since that curve support is already proven against this gem's RFC 9421 verifier — but deliberately a separate class, not a shared one: a mandate isn't an HTTP request (no method/path/headers to canonicalize, just PaymentMandate#signing_payload), and Security::Signature's own class doc already frames every signing story in this gem as deliberately distinct rather than unified under one abstraction.

Trust anchor: trusted_keys here is the mandate issuer's key set (the shopper's agent, or the agent platform vouching for it) — a different trust root than Security::Signature's trusted_keys (the calling platform's own request-signing key), even though both reuse the same flat-JWK-array-or-#call(kid)-resolver shape (§9's convention, reused again here rather than inventing a second differently-shaped key config for the same job).

Constant Summary collapse

CURVES =
{
  "P-256" => { oid: "1.2.840.10045.3.1.7", coord: 32, digest: "SHA256" },
  "P-384" => { oid: "1.3.132.0.34", coord: 48, digest: "SHA384" }
}.freeze
EC_PUBLIC_KEY_OID =
"1.2.840.10045.2.1".freeze

Class Method Summary collapse

Class Method Details

.verify!(mandate, trusted_keys:) ⇒ true

Returns never a falsy result; raises Portage::Ucp::InvalidMandateError on any failure so a caller can't accidentally treat "didn't check" as "checked and passed" (same convention as Security::Signature.verify!).

Parameters:

  • mandate (PaymentMandate)
  • trusted_keys (Array<Hash>, #call) —

    JWK hash set (or resolver #call(kid) => JWK hash or nil), keyed by kid — see module doc.

Returns:

  • (true) —

    never a falsy result; raises Portage::Ucp::InvalidMandateError on any failure so a caller can't accidentally treat "didn't check" as "checked and passed" (same convention as Security::Signature.verify!).



47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/portage/ucp/ap2/mandate_signature.rb', line 47

def self.verify!(mandate, trusted_keys:)
  jwk = trust_key!(mandate, trusted_keys)
  curve = curve_for!(jwk)
  raw_signature = sized_signature!(mandate, curve)

  key = ec_public_key(jwk, curve)
  der = raw_to_der(raw_signature, curve[:coord])
  verified = key.verify(curve[:digest], der, mandate.signing_payload)
  raise Portage::Ucp::InvalidMandateError, "mandate signature does not verify" unless verified

  true
rescue OpenSSL::PKey::PKeyError, OpenSSL::PKey::EC::Point::Error, OpenSSL::ASN1::ASN1Error => e
  raise Portage::Ucp::InvalidMandateError, "mandate signature verification failed: #{e.message}"
end