app-store-server-library 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 68c6886cd483c131d26e25b15df4a451e99a37e516411f707ccb93a99b29ab1e
4
- data.tar.gz: 8395d65f9f774a231796b1dd244436e929085cdb77ddc0fa3e46abffdaf382a0
3
+ metadata.gz: 59036d7d3426ae6c07d940f6a46157f1e456f7070e3347a3db9d9ceb9495d063
4
+ data.tar.gz: 222c38a16941624f9592c44cdb43f9ba804e01370b7d77b2bd3c860ed10ad8c7
5
5
  SHA512:
6
- metadata.gz: 4f96b3130f5c882be04265e4314725c8d2074594c8bfd3d46ad42ff37b96f9c10211019f94eed55ae1ff556353440ae17000cfd216ce6ccaec4fc4942e2d9ad5
7
- data.tar.gz: e27151ab5ca970b3e4ab4d0125d6bc1c403fd65a960aae1176b10c4e189fe6f3cdf790025313c7c0954b6f46183c0045aeb348149df3c0ae7214d8779b74fee3
6
+ metadata.gz: 8a69b06b6d7f1250b7f8a22451b3a4455675cc3cd3a2e0692488c75f860217701edaf5a748e3278ae19d5955c0e3b37b146e1fd17136a0795d48d1fb9c8a994b
7
+ data.tar.gz: 05c6ff8cfae8baa8cd670d604459ea1dce7c209e86183272064e84e8f20eccac4c770df0c30e886f7d6f18483b59181242bb0a4fd53ec2a0e1c95bbe2a14f64e
data/README.md ADDED
@@ -0,0 +1,6 @@
1
+ # Apple App Store Server Ruby Library
2
+ The Ruby server library for the [App Store Server API](https://developer.apple.com/documentation/appstoreserverapi) and [App Store Server Notifications](https://developer.apple.com/documentation/appstoreservernotifications).
3
+
4
+ Inspired by [Apple App Store Server Node.js Library](https://github.com/apple/app-store-server-library-node)
5
+
6
+ ## TODO: readme
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+ require 'json'
5
+ require 'base64'
6
+ require 'jwt'
7
+
8
+ module AppStore
9
+ class SignedDataVerifier
10
+ ENVIRONMENTS = {
11
+ sandbox: 'Sandbox',
12
+ production: 'Production',
13
+ xcode: 'Xcode',
14
+ local_testing: 'LocalTesting'
15
+ }.freeze
16
+ MAX_SKEW = 60_000
17
+
18
+ attr_reader :root_certificates, :bundle_id, :environment, :app_apple_id
19
+
20
+ def initialize(root_certificates, environment, bundle_id, app_apple_id = nil)
21
+ @root_certificates = root_certificates.map { |cert| OpenSSL::X509::Certificate.new(cert) }
22
+ @bundle_id = bundle_id
23
+ @environment = environment
24
+ @app_apple_id = app_apple_id
25
+ return unless environment == ENVIRONMENTS[:production] && app_apple_id.nil?
26
+
27
+ raise 'app_apple_id is required when the environment is Production'
28
+ end
29
+
30
+ def verify_and_decode_transaction(signed_transaction_info)
31
+ decoded_jwt = verify_jwt(signed_transaction_info)
32
+ raise VerificationException, :invalid_app_identifier if decoded_jwt['bundleId'] != bundle_id
33
+ raise VerificationException, :invalid_environment if decoded_jwt['environment'] != environment
34
+
35
+ decoded_jwt
36
+ end
37
+
38
+ def verify_and_decode_renewal_info(signed_renewal_info)
39
+ decoded_renewal_info = verify_jwt(signed_renewal_info)
40
+ raise VerificationException, :invalid_environment if decoded_renewal_info['environment'] != @environment
41
+
42
+ decoded_renewal_info
43
+ end
44
+
45
+ def verify_and_decode_notification(signed_payload)
46
+ decoded_jwt = verify_jwt(signed_payload)
47
+ app_apple_id, bundle_id, environment = extract_info(decoded_jwt)
48
+ verify_notification(bundle_id, app_apple_id, environment)
49
+ decoded_jwt
50
+ end
51
+
52
+ def verify_and_decode_app_transaction(signed_app_transaction)
53
+ decoded_app_transaction = verify_jwt(signed_app_transaction) do |t|
54
+ t['receiptCreationDate'].nil? ? Time.now : Time.parse(t['receiptCreationDate'])
55
+ end
56
+ environment = decoded_app_transaction['receiptType']
57
+ if @bundle_id != decoded_app_transaction['bundleId'] || (@environment == :production && @app_apple_id != decoded_app_transaction['appAppleId'])
58
+ raise VerificationException, :invalid_app_identifier
59
+ end
60
+ raise VerificationException, :invalid_environment if @environment != environment
61
+
62
+ decoded_app_transaction
63
+ end
64
+
65
+ private
66
+
67
+ def verify_jwt(jws)
68
+ decoded_jwt = JWT.decode(jws, nil, false)
69
+ payload = decoded_jwt[0]
70
+ chain = decoded_jwt[1]['x5c'] || []
71
+ raise VerificationException, :invalid_chain_length if chain.size != 3
72
+
73
+ certificate_chain = chain[0..1].map { |cert| OpenSSL::X509::Certificate.new(Base64.decode64(cert)) }
74
+ effective_date = payload['signedDate'] ? Time.at(payload['signedDate'] / 1000) : Time.current
75
+ public_key = verify_certificate_chain(root_certificates, certificate_chain[0], certificate_chain[1], effective_date)
76
+ JWT.decode(jws, public_key, true, algorithm: 'ES256')
77
+
78
+ payload
79
+ rescue JWT::DecodeError, JWT::VerificationError => e
80
+ raise VerificationException.new(:verification_failure, e)
81
+ end
82
+
83
+ def verify_certificate_chain(trusted_roots, leaf, intermediate, effective_date)
84
+ root_cert = trusted_roots.find do |root|
85
+ intermediate.verify(root.public_key) && intermediate.issuer == root.subject
86
+ rescue OpenSSL::X509::CertificateError => _e
87
+ next
88
+ end
89
+
90
+ validity = !root_cert.nil?
91
+ validity &&= leaf.verify(intermediate.public_key) && leaf.issuer == intermediate.subject
92
+ validity &&= intermediate.extensions.any? { |ext| ext.oid == 'basicConstraints' && ext.value.start_with?('CA:TRUE') }
93
+ validity &&= leaf.extensions.any? { |ext| ext.oid == '1.2.840.113635.100.6.11.1' }
94
+ validity &&= intermediate.extensions.any? { |ext| ext.oid == '1.2.840.113635.100.6.2.1' }
95
+
96
+ raise VerificationException, :verification_failure unless validity
97
+
98
+ check_dates(leaf, effective_date)
99
+ check_dates(intermediate, effective_date)
100
+ check_dates(root_cert, effective_date)
101
+
102
+ leaf.public_key
103
+ end
104
+
105
+ def check_dates(cert, effective_date)
106
+ valid_from = cert.not_before
107
+ valid_to = cert.not_after
108
+ return unless valid_from > effective_date + MAX_SKEW || valid_to < effective_date - MAX_SKEW
109
+
110
+ raise VerificationException, :invalid_certificate
111
+ end
112
+
113
+ def extract_info(decoded_jwt)
114
+ app_apple_id = decoded_jwt.dig('data', 'appAppleId') ||
115
+ decoded_jwt.dig('summary', 'appAppleId') ||
116
+ decoded_jwt.dig('externalPurchaseToken', 'appAppleId')
117
+ bundle_id = decoded_jwt.dig('data', 'bundleId') ||
118
+ decoded_jwt.dig('summary', 'bundleId') ||
119
+ decoded_jwt.dig('externalPurchaseToken', 'bundleId')
120
+ environment = if decoded_jwt.dig('externalPurchaseToken', 'externalPurchaseId')&.start_with?('SANDBOX')
121
+ ENVIRONMENTS[:sandbox]
122
+ else
123
+ ENVIRONMENTS[:production]
124
+ end
125
+ [app_apple_id, bundle_id, environment]
126
+ end
127
+
128
+ def verify_notification(bundle_id, app_apple_id, environment)
129
+ if @bundle_id != bundle_id || (environment == ENVIRONMENTS[:production] && @app_apple_id != app_apple_id)
130
+ raise VerificationException, :invalid_app_identifier
131
+ end
132
+ raise VerificationException, :invalid_environment if @environment != environment
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppStore
4
+ class VerificationException < StandardError; end
5
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppStore
4
+ module VerificationStatus
5
+ OK = :ok
6
+ VERIFICATION_FAILURE = :verification_failure
7
+ INVALID_APP_IDENTIFIER = :invalid_app_identifier
8
+ INVALID_ENVIRONMENT = :invalid_environment
9
+ INVALID_CHAIN_LENGTH = :invalid_chain_length
10
+ INVALID_CERTIFICATE = :invalid_certificate
11
+ FAILURE = :failure
12
+ end
13
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: app-store-server-library
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Illia Kasianenko
@@ -30,7 +30,11 @@ executables: []
30
30
  extensions: []
31
31
  extra_rdoc_files: []
32
32
  files:
33
- - lib/app_store_server_library.rb
33
+ - README.md
34
+ - lib/app-store-server-library.rb
35
+ - lib/app_store/signed_data_verifier.rb
36
+ - lib/app_store/verification_exception.rb
37
+ - lib/app_store/verification_status.rb
34
38
  homepage: https://github.com/got2be/app-store-server-library
35
39
  licenses: []
36
40
  metadata: