Class: Pedicel::Base
- Inherits:
-
Object
- Object
- Pedicel::Base
- Defined in:
- lib/pedicel/base.rb
Constant Summary collapse
- SUPPORTED_VERSIONS =
[:EC_v1].freeze
Instance Attribute Summary collapse
-
#config ⇒ Object
readonly
Returns the value of attribute config.
Class Method Summary collapse
- .extract_certificates(signature:, intermediate_oid: , leaf_oid: ) ⇒ Object
- .verify_root_certificate(root:, trusted_root:) ⇒ Object
- .verify_signed_time(signature:, now: Time.now, few_min: ) ⇒ Object
- .verify_x509_chain(root:, intermediate:, leaf:) ⇒ Object
Instance Method Summary collapse
- #application_data ⇒ Object
- #decrypt_aes(key:) ⇒ Object
- #encrypted_data ⇒ Object
-
#initialize(token, config: Pedicel::DEFAULT_CONFIG) ⇒ Base
constructor
A new instance of Base.
- #private_key_class ⇒ Object
- #signature ⇒ Object
- #symmetric_algorithm ⇒ Object
- #transaction_id ⇒ Object
- #valid_signature?(now: Time.now) ⇒ Boolean
- #verify_signature(ca_certificate_pem: , now: Time.now) ⇒ Object
- #version ⇒ Object
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
#config ⇒ Object (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
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
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
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_data ⇒ Object
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
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_data ⇒ Object
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_class ⇒ Object
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 |
#signature ⇒ Object
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_algorithm ⇒ Object
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_id ⇒ Object
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
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
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.}" end begin trusted_root = OpenSSL::X509::Certificate.new(ca_certificate_pem) rescue => e raise CertificateError, "invalid trusted root certificate: #{e.}" 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 |
#version ⇒ Object
19 20 21 |
# File 'lib/pedicel/base.rb', line 19 def version @token[:version].to_sym end |