Class: Pedicel::Base

Inherits:
Object
  • Object
show all
Defined in:
lib/pedicel/base.rb

Direct Known Subclasses

EC, RSA

Constant Summary collapse

SUPPORTED_VERSIONS =
[:EC_v1].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(token, config: Pedicel::DEFAULT_CONFIG) ⇒ Base

Returns a new instance of Base.



11
12
13
14
15
16
17
# File 'lib/pedicel/base.rb', line 11

def initialize(token, config: Pedicel::DEFAULT_CONFIG)
  validation = Validator::Token.new(token)
  validation.validate

  @token  = validation.output
  @config = config
end

Instance Attribute Details

#configObject (readonly)

Returns the value of attribute config.



9
10
11
# File 'lib/pedicel/base.rb', line 9

def config
  @config
end

Class Method Details

.extract_certificates(signature:, intermediate_oid: , leaf_oid: ) ⇒ Object

Raises:



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/pedicel/base.rb', line 163

def self.extract_certificates(signature:,
                              intermediate_oid: Pedicel::DEFAULT_CONFIG[:oid_intermediate_certificate],
                              leaf_oid:         Pedicel::DEFAULT_CONFIG[:oid_leaf_certificate])
  leafs, intermediates, others = [], [], []

  signature.certificates.each do |certificate|
    leaf_or_intermediate = false

    certificate.extensions.each do |extension|
      case extension.oid
      when intermediate_oid
        intermediates << certificate
        leaf_or_intermediate = true
      when leaf_oid
        leafs << certificate
        leaf_or_intermediate = true
      end
    end

    others << certificate unless leaf_or_intermediate
  end

  raise SignatureError, "no unique leaf certificate found (OID #{leaf_oid})" unless leafs.length == 1
  raise SignatureError, "no unique intermediate certificate found (OID #{intermediate_oid})" unless intermediates.length == 1
  raise SignatureError, "too many certificates found in the signature: #{others.map(&:subject).join('; ')}" if others.length > 1

  [leafs.first, intermediates.first, others.first]
end

.verify_root_certificate(root:, trusted_root:) ⇒ Object

Raises:



192
193
194
195
196
# File 'lib/pedicel/base.rb', line 192

def self.verify_root_certificate(root:, trusted_root:)
  raise SignatureError, 'root certificate is not trusted' unless root.to_der == trusted_root.to_der

  true
end

.verify_signed_time(signature:, now: Time.now, few_min: ) ⇒ Object

Raises:



219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/pedicel/base.rb', line 219

def self.verify_signed_time(signature:, now: Time.now, few_min: Pedicel::DEFAULT_CONFIG[:replay_threshold_seconds])
  # Inspect the CMS signing time of the signature, as defined by section
  # 11.3 of RFC 5652. If the time signature and the transaction time differ
  # by more than a few minutes, it's possible that the token is a replay
  # attack.
  # https://developer.apple.com/library/content/documentation/PassKit/Reference/PaymentTokenJSON/PaymentTokenJSON.html

  unless signature.signers.length == 1
    raise SignatureError, 'not 1 signer, unable to determine signing time'
  end
  signed_time = signature.signers.first.signed_time

  # Time objects. DST aware. Ignoring leap seconds. Both ends included.
  return true if signed_time.between?(now - few_min, now + few_min)

  diff = signed_time - now
  if diff.negative?
    raise SignatureError, "signature too old; signed #{-diff}s ago"
  end
  raise SignatureError, "signature too new; signed #{diff}s in the future"
end

.verify_x509_chain(root:, intermediate:, leaf:) ⇒ Object



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/pedicel/base.rb', line 198

def self.verify_x509_chain(root:, intermediate:, leaf:)
  # Specifically, ensure that the signature was created using the private
  # key corresponding to the leaf certificate, that the leaf certificate is
  # signed by the intermediate CA, and that the intermediate CA is signed by
  # the Apple Root CA - G3.

  unless root.verify(root.public_key)
    raise SignatureError, 'invalid chain due to root'
  end

  unless intermediate.verify(root.public_key)
    raise SignatureError, 'invalid chain due to intermediate'
  end

  unless leaf.verify(intermediate.public_key)
    raise SignatureError, 'invalid chain due to leaf'
  end

  true
end

Instance Method Details

#application_dataObject



39
40
41
42
43
# File 'lib/pedicel/base.rb', line 39

def application_data
  return nil unless @token[:header][:applicationData]

  [@token[:header][:applicationData]].pack('H*')
end

#decrypt_aes(key:) ⇒ Object

Raises:



57
58
59
60
61
62
63
64
65
66
67
# File 'lib/pedicel/base.rb', line 57

def decrypt_aes(key:)
  raise TokenFormatError, 'no encrypted data present' unless encrypted_data

  if OpenSSL::Cipher.new('aes-256-gcm').respond_to?(:iv_len=)
    # Either because you use Ruby >=2.4's native openssl lib, or if you have
    # a "recent enough" version of the openssl gem available.
    decrypt_aes_openssl(key)
  else
    decrypt_aes_gem(key)
  end
end

#encrypted_dataObject



23
24
25
26
27
# File 'lib/pedicel/base.rb', line 23

def encrypted_data
  return nil unless @token[:data]

  Base64.decode64(@token[:data])
end

#private_key_classObject

Raises:



45
46
47
48
49
# File 'lib/pedicel/base.rb', line 45

def private_key_class
  raise VersionError, "unsupported version: #{version}" unless SUPPORTED_VERSIONS.include?(version)

  { EC_v1: OpenSSL::PKey::EC, RSA_v1: OpenSSL::PKey::RSA }[version]
end

#signatureObject



29
30
31
32
33
# File 'lib/pedicel/base.rb', line 29

def signature
  return nil unless @token[:signature]

  Base64.decode64(@token[:signature])
end

#symmetric_algorithmObject

Raises:



51
52
53
54
55
# File 'lib/pedicel/base.rb', line 51

def symmetric_algorithm
  raise VersionError, "unsupported version: #{version}" unless SUPPORTED_VERSIONS.include?(version)

  { EC_v1: 'aes-256-gcm', RSA_v1: 'aes-128-gcm' }[version]
end

#transaction_idObject



35
36
37
# File 'lib/pedicel/base.rb', line 35

def transaction_id
  [@token[:header][:transactionId]].pack('H*')
end

#valid_signature?(now: Time.now) ⇒ Boolean

Returns:

  • (Boolean)


104
105
106
107
108
# File 'lib/pedicel/base.rb', line 104

def valid_signature?(now: Time.now)
  !!verify_signature(now: now)
rescue
  false
end

#verify_signature(ca_certificate_pem: , now: Time.now) ⇒ Object

Raises:



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/pedicel/base.rb', line 110

def verify_signature(ca_certificate_pem: @config[:trusted_ca_pem], now: Time.now)
  raise SignatureError, 'no signature present' unless signature

  begin
    s = OpenSSL::PKCS7.new(signature)
  rescue => e
    raise SignatureError, "invalid PKCS #7 signature: #{e.message}"
  end

  begin
    trusted_root = OpenSSL::X509::Certificate.new(ca_certificate_pem)
  rescue => e
    raise CertificateError, "invalid trusted root certificate: #{e.message}"
  end

  # 1.a
  # Ensure that the certificates contain the correct custom OIDs: (...).
  # The value for these marker OIDs doesn't matter, only their presence.
  leaf, intermediate, other = self.class.extract_certificates(signature: s,
                                                              intermediate_oid: @config[:oid_intermediate_certificate],
                                                              leaf_oid: @config[:oid_leaf_certificate])
  # Implicit since these are the ones extracted.

  # 1.b
  # Ensure that the root CA is the Apple Root CA - G3. (...)
  if other
    self.class.verify_root_certificate(trusted_root: trusted_root, root: other)
    # Allow no other certificate than the root.
  #else
    # no other certificate is not extracted from the signature, and thus, we
    # trust the trusted root.
  end

  # 1.c
  # Ensure that there is a valid X.509 chain of trust from the signature to
  # the root CA.
  self.class.verify_x509_chain(root: trusted_root, intermediate: intermediate, leaf: leaf)
  # We "only" check the *certificate* chain (from leaf to root). Below (in
  # 1.d) is checked that the signature is created with the leaf.

  # 1.d
  # Validate the token's signature.
  #
  # Implemented in the subclass.
  validate_signature(signature: s, leaf: leaf)

  # 1.e
  # Inspect the CMS signing time of the signature (...)
  self.class.verify_signed_time(signature: s, now: now, few_min: @config[:replay_threshold_seconds])

  self
end

#versionObject



19
20
21
# File 'lib/pedicel/base.rb', line 19

def version
  @token[:version].to_sym
end