Class: GooglePayRuby::SignatureVerifier

Inherits:
Object
  • Object
show all
Defined in:
lib/google_pay_ruby/signature_verifier.rb

Overview

Implements Google Pay ECv2 signature verification as specified in: https://developers.google.com/pay/api/web/guides/resources/payment-data-cryptography

Verification steps:

1. Fetch Google root signing keys
2. Verify intermediate signing key signature against non-expired root keys
3. Verify intermediate signing key hasn't expired
4. Verify message signature using intermediate signing key

Constant Summary collapse

SENDER_ID =
'Google'
PROTOCOL_VERSION =
'ECv2'
GOOGLE_ROOT_SIGNING_KEYS_PROD_URL =
'https://payments.developers.google.com/paymentmethodtoken/keys.json'
GOOGLE_ROOT_SIGNING_KEYS_TEST_URL =
'https://payments.developers.google.com/paymentmethodtoken/test/keys.json'

Instance Method Summary collapse

Constructor Details

#initialize(root_signing_keys: nil, recipient_id:, test: false) ⇒ SignatureVerifier

Returns a new instance of SignatureVerifier.

Parameters:

  • root_signing_keys (Array<Hash>, nil) (defaults to: nil) —

    Pre-fetched root signing keys (ECv2 only). Each hash should have 'keyValue', 'protocolVersion', and optionally 'keyExpiration'. If nil, keys are fetched from Google's public URL.

  • recipient_id (String) —

    The recipient ID used in message signature verification. For merchants: "merchant:" (merchantId from Google Pay & Wallet Console). For gateways: "gateway:".

  • test (Boolean) (defaults to: false) —

    Whether to use Google's test keys URL (default: false).



32
33
34
35
36
# File 'lib/google_pay_ruby/signature_verifier.rb', line 32

def initialize(root_signing_keys: nil, recipient_id:, test: false)
  @root_signing_keys = root_signing_keys
  @recipient_id = recipient_id
  @test = test
end

Instance Method Details

#verify!(token, raw_token_json: nil) ⇒ void

This method returns an undefined value.

Runs all verification steps (1-4) on the given token. Raises GooglePaymentDecryptionError on any verification failure.

Parameters:

  • token (Hash) —

    The full Google Pay payment method token (parsed)

  • raw_token_json (String, nil) (defaults to: nil) —

    The original raw JSON string of the token. When provided, signedKey and signedMessage are extracted from this raw string to preserve the exact byte sequences that Google signed over (e.g. \u003d escapes). JSON.parse decodes \u003d to '=' which changes the signed content and breaks verification.



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/google_pay_ruby/signature_verifier.rb', line 47

def verify!(token, raw_token_json: nil)
  protocol_version = token['protocolVersion'] || token[:protocolVersion]
  unless protocol_version == PROTOCOL_VERSION
    raise GooglePaymentDecryptionError.new(
      "Unsupported protocol version: #{protocol_version}. Only ECv2 is supported."
    )
  end

  intermediate_signing_key = token['intermediateSigningKey'] || token[:intermediateSigningKey]
  unless intermediate_signing_key
    raise GooglePaymentDecryptionError.new('Missing intermediateSigningKey in token')
  end

  signed_key_json = intermediate_signing_key['signedKey'] || intermediate_signing_key[:signedKey]
  signatures = intermediate_signing_key['signatures'] || intermediate_signing_key[:signatures]

  unless signed_key_json && signatures
    raise GooglePaymentDecryptionError.new('Missing signedKey or signatures in intermediateSigningKey')
  end

  # Extract original signedKey and signedMessage from raw JSON if available.
  # This preserves the exact byte sequences (including unicode escapes like \u003d)
  # that Google used when computing signatures.
  if raw_token_json
    raw_signed_key = extract_json_string_value(raw_token_json, 'signedKey')
    raw_signed_message = extract_json_string_value(raw_token_json, 'signedMessage')
    signed_key_json = raw_signed_key if raw_signed_key
    signed_message_for_verify = raw_signed_message
  end

  # Step 2: Verify intermediate signing key signature against non-expired root keys
  verify_intermediate_signing_key_signature!(signed_key_json, signatures)

  # Step 3: Verify intermediate signing key hasn't expired
  verify_intermediate_signing_key_expiration!(signed_key_json)

  # Step 4: Verify message signature using intermediate signing key
  signed_message = signed_message_for_verify || token['signedMessage'] || token[:signedMessage]
  signature = token['signature'] || token[:signature]

  unless signed_message && signature
    raise GooglePaymentDecryptionError.new('Missing signedMessage or signature in token')
  end

  parsed_signed_key = JSON.parse(signed_key_json)
  intermediate_key_value = parsed_signed_key['keyValue']

  verify_message_signature!(signed_message, signature, intermediate_key_value)
end