app-store-server-library 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: