app-store-server-library 0.0.1 → 0.0.3

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: c393a6e8b61ce0a2639b8d70cd63f7157d6d3493bb5ca1aacf84bd40e562116e
4
+ data.tar.gz: ac9b3992dfff499a76e604804e66e9516c320f02706124c47a75e0c256098182
5
5
  SHA512:
6
- metadata.gz: 4f96b3130f5c882be04265e4314725c8d2074594c8bfd3d46ad42ff37b96f9c10211019f94eed55ae1ff556353440ae17000cfd216ce6ccaec4fc4942e2d9ad5
7
- data.tar.gz: e27151ab5ca970b3e4ab4d0125d6bc1c403fd65a960aae1176b10c4e189fe6f3cdf790025313c7c0954b6f46183c0045aeb348149df3c0ae7214d8779b74fee3
6
+ metadata.gz: b1b59770239a30efe2c889e3ae6478be50262b21b5d5099e44b379b3dffbfbecd8058288a106d5d287ca247e55d3585781d926b1c76bfe3a3878677808012c09
7
+ data.tar.gz: 8bc948d2348b30e00bfb965eb41fead0056cc8b3317c8516bd081ed9c20e5fa28c4dbcfd13cc5541f7bdc1dddf8c9b1adc5db82ebb9749b429626677335081b3
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,126 @@
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
+ payload = decoded_jwt['data'] || decoded_jwt['summary'] || decoded_jwt['externalPurchaseToken']
48
+ app_apple_id = payload['appAppleId']
49
+ bundle_id = payload['bundleId']
50
+ environment = payload['environment']
51
+ if payload['externalPurchaseId']
52
+ environment = payload['externalPurchaseId']&.start_with?('SANDBOX') ? ENVIRONMENTS[:sandbox] : ENVIRONMENTS[:production]
53
+ end
54
+ verify_notification(bundle_id, app_apple_id, environment)
55
+ decoded_jwt
56
+ end
57
+
58
+ def verify_and_decode_app_transaction(signed_app_transaction)
59
+ decoded_app_transaction = verify_jwt(signed_app_transaction) do |t|
60
+ t['receiptCreationDate'].nil? ? Time.now : Time.parse(t['receiptCreationDate'])
61
+ end
62
+ environment = decoded_app_transaction['receiptType']
63
+ if @bundle_id != decoded_app_transaction['bundleId'] || (@environment == :production && @app_apple_id != decoded_app_transaction['appAppleId'])
64
+ raise VerificationException, :invalid_app_identifier
65
+ end
66
+ raise VerificationException, :invalid_environment if @environment != environment
67
+
68
+ decoded_app_transaction
69
+ end
70
+
71
+ private
72
+
73
+ def verify_jwt(jws)
74
+ decoded_jwt = JWT.decode(jws, nil, false)
75
+ payload = decoded_jwt[0]
76
+ chain = decoded_jwt[1]['x5c'] || []
77
+ raise VerificationException, :invalid_chain_length if chain.size != 3
78
+
79
+ certificate_chain = chain[0..1].map { |cert| OpenSSL::X509::Certificate.new(Base64.decode64(cert)) }
80
+ effective_date = payload['signedDate'] ? Time.at(payload['signedDate'] / 1000) : Time.current
81
+ public_key = verify_certificate_chain(root_certificates, certificate_chain[0], certificate_chain[1], effective_date)
82
+ JWT.decode(jws, public_key, true, algorithm: 'ES256')
83
+
84
+ payload
85
+ rescue JWT::DecodeError, JWT::VerificationError => e
86
+ raise VerificationException, :verification_failure
87
+ end
88
+
89
+ def verify_certificate_chain(trusted_roots, leaf, intermediate, effective_date)
90
+ root_cert = trusted_roots.find do |root|
91
+ intermediate.verify(root.public_key) && intermediate.issuer == root.subject
92
+ rescue OpenSSL::X509::CertificateError => _e
93
+ next
94
+ end
95
+
96
+ validity = !root_cert.nil?
97
+ validity &&= leaf.verify(intermediate.public_key) && leaf.issuer == intermediate.subject
98
+ validity &&= intermediate.extensions.any? { |ext| ext.oid == 'basicConstraints' && ext.value.start_with?('CA:TRUE') }
99
+ validity &&= leaf.extensions.any? { |ext| ext.oid == '1.2.840.113635.100.6.11.1' }
100
+ validity &&= intermediate.extensions.any? { |ext| ext.oid == '1.2.840.113635.100.6.2.1' }
101
+
102
+ raise VerificationException, :verification_failure unless validity
103
+
104
+ check_dates(leaf, effective_date)
105
+ check_dates(intermediate, effective_date)
106
+ check_dates(root_cert, effective_date)
107
+
108
+ leaf.public_key
109
+ end
110
+
111
+ def check_dates(cert, effective_date)
112
+ valid_from = cert.not_before
113
+ valid_to = cert.not_after
114
+ return unless valid_from > effective_date + MAX_SKEW || valid_to < effective_date - MAX_SKEW
115
+
116
+ raise VerificationException, :invalid_certificate
117
+ end
118
+
119
+ def verify_notification(bundle_id, app_apple_id, environment)
120
+ if @bundle_id != bundle_id || (environment == ENVIRONMENTS[:production] && @app_apple_id != app_apple_id)
121
+ raise VerificationException, :invalid_app_identifier
122
+ end
123
+ raise VerificationException, :invalid_environment if @environment != environment
124
+ end
125
+ end
126
+ 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,14 +1,14 @@
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.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Illia Kasianenko
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-08-15 00:00:00.000000000 Z
11
+ date: 2024-08-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jwt
@@ -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: