ios_app_attest 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8e8e4a0dac87b3cff72d1f82ab119a3a631226aa5fd6debfe3698e13152fb551
4
+ data.tar.gz: 88d54eb7e43e0c6ce051fba922d18cdd43e0fc83811247020aef149241875492
5
+ SHA512:
6
+ metadata.gz: 98b702ff259f7987941d32830f581e9e300cf6685f8519dca6ce67757cddfe8960e5dd535b6886435690568c90de4ed3264a8a98f3080970370c024909c01915
7
+ data.tar.gz: '009d9bbfcf4743ee3eb8bb4f12ce490f30e5ce367061231588ffaa276bd12be113a386a3c3634f56d282592bb127dc5287d3291447591cc8c8c2a959775b6fd2'
data/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2025-08-11
9
+
10
+ ### Added
11
+ - Initial release of the ios_app_attest gem
12
+ - Core functionality for verifying iOS App Attestation tokens
13
+ - Support for nonce validation with Redis
14
+ - Comprehensive error handling with specific error subclasses
15
+ - Configuration options for app ID and encryption key
16
+ - Extensive YARD documentation for all public methods and classes
17
+ - Detailed examples in the examples/ directory
18
+ - Security policy in SECURITY.md
19
+ - Comprehensive test suite with 83 passing tests
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Chaitra Mudili
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,195 @@
1
+ # iOS App Attest
2
+
3
+ A Ruby gem for verifying iOS App Attest tokens - Apple's device attestation mechanism for iOS apps.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'ios_app_attest'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ ```bash
16
+ $ bundle install
17
+ ```
18
+
19
+ Or install it yourself as:
20
+
21
+ ```bash
22
+ $ gem install ios_app_attest
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ### Configuration
28
+
29
+ Configure the gem with your app's specific settings:
30
+
31
+ ```ruby
32
+ IosAppAttest.configure do |config|
33
+ config.app_id = "TEAM_ID.BUNDLE_ID" # Your Apple Team ID and Bundle ID
34
+ config.encryption_key = ENV.fetch("IOS_APP_ATTEST_TOKEN").byteslice(0, 32) # Your encryption key (32 bytes)
35
+ end
36
+ ```
37
+
38
+ > Note: The Apple App Attestation root CA certificate and App Attest OID ("1.2.840.113635.100.8.2") are now hardcoded in the gem for security and convenience.
39
+
40
+ ### Complete Attestation Flow
41
+
42
+ #### 1. Generating and Storing Challenge Nonces
43
+
44
+ When a client requests a nonce, your server generates it and stores it in Redis with a TTL:
45
+
46
+ ```ruby
47
+ # Create a Redis client for nonce storage
48
+ redis = Redis.new(url: ENV["REDIS_URL"] || "redis://localhost:6379/0")
49
+
50
+ # Create a nonce generator
51
+ nonce_generator = IosAppAttest::NonceGenerator.new(
52
+ redis_client: redis,
53
+ logger: Rails.logger, # Optional
54
+ expiry_seconds: 300 # Optional: Nonce expiry time in seconds (default: 120)
55
+ )
56
+
57
+ # Generate a nonce (this also stores it in Redis with the configured TTL)
58
+ nonce_data = nonce_generator.generate
59
+
60
+ # The nonce_data contains:
61
+ # - challenge_nonce_id: A unique identifier for the challenge (used as Redis key)
62
+ # - challenge_nonce: The encrypted challenge nonce (base64 encoded)
63
+ # - initialization_vector: The IV used for encryption (base64 encoded)
64
+
65
+ # Send this data to the client for attestation
66
+ ```
67
+
68
+ #### 2. Client-Side Processing
69
+
70
+ ```
71
+ # On the client side (iOS app):
72
+ # 1. Receive the nonce data from the server
73
+ # 2. Decrypt the challenge_nonce using the same encryption key:
74
+ # a. Base64 decode the challenge_nonce and initialization_vector
75
+ # b. Use AES-256-CBC with the shared encryption key to decrypt the challenge
76
+ # 3. Use the decrypted nonce in the App Attestation process
77
+ # 4. Send the attestation object back to the server along with the original nonce data
78
+ ```
79
+
80
+ #### 3. Server-Side Verification
81
+
82
+ When the client sends back the attestation object along with the original nonce data, the server:
83
+
84
+ ```ruby
85
+ # Attestation parameters from the client
86
+ attestation_params = {
87
+ attestation_object: "base64_encoded_attestation_object",
88
+ key_id: "base64_encoded_key_id",
89
+ challenge_nonce: "base64_encoded_challenge", # The encrypted challenge nonce (base64 encoded)
90
+ initialization_vector: "base64_encoded_initialization_vector", # The IV used for encryption (base64 encoded)
91
+ challenge_nonce_id: "challenge_id" # The unique identifier for the challenge
92
+ }
93
+
94
+ # Create a verifier with Redis client for nonce verification
95
+ verifier = IosAppAttest::Verifier.new(
96
+ attestation_params,
97
+ redis_client: redis, # Redis client for nonce verification (required for TTL check)
98
+ logger: Rails.logger # Optional: Logger for error logging
99
+ )
100
+
101
+ begin
102
+ # Verify the attestation - this process includes:
103
+ # 1. Decrypting the challenge nonce using the encryption key
104
+ # 2. Checking if the nonce exists in Redis (validates TTL)
105
+ # 3. Validating the attestation structure and certificates
106
+ # 4. Verifying the nonce matches what was used in the attestation
107
+ public_key, receipt = verifier.verify
108
+
109
+ # Use the public_key and receipt for further processing
110
+ # e.g., store the public_key for future authentications
111
+ rescue IosAppAttest::CertificateError => e
112
+ # Handle certificate validation errors
113
+ puts "Certificate validation failed: #{e.message}"
114
+ rescue IosAppAttest::ChallengeError => e
115
+ # Handle challenge validation errors
116
+ puts "Challenge validation failed: #{e.message}"
117
+ rescue IosAppAttest::AttestationError => e
118
+ # Handle attestation format errors
119
+ puts "Attestation format invalid: #{e.message}"
120
+ rescue IosAppAttest::AppIdentityError => e
121
+ # Handle app identity validation errors
122
+ puts "App identity validation failed: #{e.message}"
123
+ rescue IosAppAttest::NonceError => e
124
+ # Handle nonce validation errors
125
+ puts "Nonce validation failed: #{e.message}"
126
+ rescue IosAppAttest::VerificationError => e
127
+ # Handle other verification errors
128
+ puts "Verification failed: #{e.message}"
129
+ end
130
+ ```
131
+
132
+ ### Complete Flow
133
+
134
+ Here's the complete flow for implementing iOS App Attestation in your application:
135
+
136
+ 1. **Server-side**: Generate a challenge nonce
137
+ ```ruby
138
+ nonce_data = nonce_generator.generate
139
+ # Returns: { challenge_nonce_id:, challenge_nonce:, initialization_vector: }
140
+ ```
141
+
142
+ 2. **Send to Client**: Send the nonce data to your iOS client
143
+
144
+ 3. **Client-side**: The iOS client uses the nonce to generate an attestation using Apple's DeviceCheck framework
145
+ ```swift
146
+ // Swift code (client-side)
147
+ let service = DCAppAttestService.shared
148
+ if service.isSupported {
149
+ // Generate a new key pair
150
+ service.generateKey { keyId, error in
151
+ // Use the keyId and challenge to generate an attestation
152
+ service.attestKey(keyId, clientDataHash: challengeHash) { attestation, error in
153
+ // Send attestation, keyId, and challenge data back to server
154
+ }
155
+ }
156
+ }
157
+ ```
158
+
159
+ 4. **Server-side**: Verify the attestation
160
+ ```ruby
161
+ verifier = IosAppAttest::Verifier.new(
162
+ attestation_params,
163
+ redis_client: redis
164
+ )
165
+
166
+ public_key, receipt = verifier.verify
167
+ # Store the public_key for future authentications
168
+ ```
169
+
170
+ 5. **Future Assertions**: For subsequent requests, the client generates assertions that can be verified using the stored public key
171
+
172
+ ### Parameter Reference
173
+
174
+ The library uses the following parameter names:
175
+
176
+ | Parameter Name | Description |
177
+ |----------------|-------------|
178
+ | `challenge_nonce_id` | Unique identifier for the challenge nonce |
179
+ | `challenge_nonce` | Base64-encoded encrypted challenge nonce |
180
+ | `initialization_vector` | Base64-encoded initialization vector used for encryption |
181
+ | `attestation_object` | Base64-encoded attestation object from Apple's DeviceCheck framework |
182
+ | `key_id` | Base64-encoded key ID generated by the client |
183
+
184
+
185
+ ## Development
186
+
187
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
188
+
189
+ ## Contributing
190
+
191
+ Bug reports and pull requests are welcome on GitHub at https://github.com/chaitra-mudili/ios-app-attest-ruby.
192
+
193
+ ## License
194
+
195
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IosAppAttest
4
+ # Configuration class for IosAppAttest
5
+ #
6
+ # This class holds all configuration parameters required for the iOS App Attest
7
+ # verification process. It includes the app identifier and encryption key for challenge nonce encryption/decryption.
8
+ #
9
+ # @example
10
+ # IosAppAttest.configure do |config|
11
+ # config.app_id = "TEAM_ID.BUNDLE_ID"
12
+ # config.encryption_key = ENV["IOS_APP_ATTEST_TOKEN"].byteslice(0, 32)
13
+ # end
14
+ #
15
+ # @attr [String] app_id The Apple Team ID and Bundle ID in the format "TEAM_ID.BUNDLE_ID"
16
+ # @attr [String] encryption_key The encryption key used for challenge nonce encryption (32 bytes)
17
+ class Configuration
18
+ attr_accessor :app_id, :encryption_key
19
+
20
+ # Initialize a new Configuration instance with default values
21
+ #
22
+ # Creates a new configuration object with all attributes set to nil.
23
+ # These attributes must be set before using the configuration with validators.
24
+ #
25
+ # @return [IosAppAttest::Configuration] A new Configuration instance with nil values
26
+ def initialize
27
+ @app_id = nil
28
+ @encryption_key = nil
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Error classes for iOS App Attest verification
4
+ #
5
+ # This file defines a hierarchy of error classes for the iOS App Attest verification process.
6
+ # The hierarchy is designed to allow for specific error handling based on the type of error
7
+ # that occurred during verification.
8
+ #
9
+ # The main error categories are:
10
+ # - ConfigurationError: For configuration-related errors
11
+ # - VerificationError: Base class for all verification errors
12
+ # - NonceError: For nonce-related errors
13
+ # - CertificateError: For certificate-related errors
14
+ # - ChallengeError: For challenge-related errors
15
+ # - AppIdentityError: For app identity-related errors
16
+ # - AttestationError: For attestation format-related errors
17
+ #
18
+ # Each category has specific subclasses for more granular error handling.
19
+ #
20
+ module IosAppAttest
21
+ # Base error class for all IosAppAttest errors
22
+ class Error < StandardError; end
23
+
24
+ # Error raised when configuration is invalid or incomplete
25
+ #
26
+ # This error is raised when the configuration object is missing required
27
+ # parameters or contains invalid values.
28
+ class ConfigurationError < Error; end
29
+
30
+ # Error raised when attestation verification fails
31
+ #
32
+ # This is the base class for all verification-related errors.
33
+ # More specific error subclasses should be used when possible.
34
+ class VerificationError < Error; end
35
+
36
+ # Error raised when nonce validation fails
37
+ #
38
+ # This error is raised when there are issues with the challenge nonce
39
+ # during the verification process, such as expired, already used,
40
+ # or not found nonces.
41
+ class NonceError < VerificationError
42
+ # Error raised when a nonce has expired
43
+ class Expired < NonceError; end
44
+
45
+ # Error raised when a nonce has already been used
46
+ class AlreadyUsed < NonceError; end
47
+
48
+ # Error raised when a nonce is not found
49
+ class NotFound < NonceError; end
50
+ end
51
+
52
+ # Error raised when certificate validation fails
53
+ #
54
+ # This error is raised when there are issues with the certificate
55
+ # during the verification process, such as invalid chain, expired certificate,
56
+ # or invalid certificate structure.
57
+ class CertificateError < VerificationError
58
+ # Error raised when certificate chain validation fails
59
+ class ChainInvalid < CertificateError; end
60
+
61
+ # Error raised when certificate has expired
62
+ class Expired < CertificateError; end
63
+
64
+ # Error raised when certificate is not yet valid
65
+ class NotYetValid < CertificateError; end
66
+
67
+ # Error raised when certificate structure is invalid
68
+ class InvalidStructure < CertificateError; end
69
+ end
70
+
71
+ # Error raised when challenge validation fails
72
+ #
73
+ # This error is raised when there are issues with the challenge
74
+ # during the verification process, such as invalid signature,
75
+ # invalid format, or key ID mismatch.
76
+ class ChallengeError < VerificationError
77
+ # Error raised when challenge signature is invalid
78
+ class InvalidSignature < ChallengeError; end
79
+
80
+ # Error raised when challenge format is invalid
81
+ class InvalidFormat < ChallengeError; end
82
+
83
+ # Error raised when key ID doesn't match
84
+ class KeyIdMismatch < ChallengeError; end
85
+ end
86
+
87
+ # Error raised when app identity validation fails
88
+ #
89
+ # This error is raised when there are issues with the app identity
90
+ # during the verification process, such as ID mismatch or invalid format.
91
+ class AppIdentityError < VerificationError
92
+ # Error raised when app ID doesn't match
93
+ class IdMismatch < AppIdentityError; end
94
+
95
+ # Error raised when app identity format is invalid
96
+ class InvalidFormat < AppIdentityError; end
97
+ end
98
+
99
+ # Error raised when attestation format is invalid
100
+ #
101
+ # This error is raised when there are issues with the attestation format
102
+ # during the verification process, such as invalid structure or missing data.
103
+ class AttestationError < VerificationError
104
+ # Error raised when attestation structure is invalid
105
+ class InvalidStructure < AttestationError; end
106
+
107
+ # Error raised when attestation data is missing
108
+ class MissingData < AttestationError; end
109
+ end
110
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "base64"
5
+ require "securerandom"
6
+
7
+ module IosAppAttest
8
+ # Generates and manages challenge nonces for iOS App Attestation
9
+ #
10
+ # The NonceGenerator class is responsible for creating secure random nonces,
11
+ # encrypting them, and storing them in Redis for later validation during
12
+ # the attestation verification process.
13
+ #
14
+ # This class uses IosAppAttest::NonceError for error handling.
15
+ #
16
+ # @example Basic usage
17
+ # redis = Redis.new
18
+ # generator = IosAppAttest::NonceGenerator.new(redis_client: redis)
19
+ # nonce_data = generator.generate
20
+ #
21
+ # @example With custom expiry time
22
+ # generator = IosAppAttest::NonceGenerator.new(
23
+ # redis_client: redis,
24
+ # expiry_seconds: 300
25
+ # )
26
+ # nonce_data = generator.generate
27
+ class NonceGenerator
28
+
29
+ attr_reader :redis_client, :logger, :expiry_seconds
30
+
31
+ # Initialize the nonce generator
32
+ # @param redis_client [Object] Redis client for nonce storage
33
+ # @param logger [Object] Logger instance (optional)
34
+ # @param expiry_seconds [Integer] Nonce expiry time in seconds (default: 120)
35
+ def initialize(redis_client:, logger: nil, expiry_seconds: 120)
36
+ @redis_client = redis_client
37
+ @logger = logger
38
+ @expiry_seconds = expiry_seconds
39
+ end
40
+
41
+ # Generate a new nonce and store it in Redis
42
+ #
43
+ # This method generates a cryptographically secure random nonce,
44
+ # encrypts it using AES-256-CBC, and stores it in Redis for later validation.
45
+ # The nonce is stored with an expiry time specified during initialization.
46
+ #
47
+ # @return [Hash] Hash containing:
48
+ # - :challenge_nonce_id [String] A unique identifier for the challenge
49
+ # - :challenge_nonce [String] Base64-encoded encrypted challenge nonce
50
+ # - :initialization_vector [String] Base64-encoded initialization vector
51
+ # @raise [IosAppAttest::NonceError] If nonce generation fails due to Redis errors or configuration issues
52
+ def generate
53
+ begin
54
+ store_nonce_in_redis
55
+ encrypted_nonce, iv = encrypt
56
+ rescue IosAppAttest::Error => error
57
+ # Re-raise IosAppAttest errors directly
58
+ log_error("IosAppAttest nonce generation failed: #{error}")
59
+ raise error
60
+ rescue StandardError => error
61
+ # Wrap other errors in NonceGenerationError
62
+ log_error("IosAppAttest nonce generation failed: #{error}")
63
+ raise IosAppAttest::NonceError, "Nonce generation failed: #{error.message}"
64
+ end
65
+
66
+ {
67
+ challenge_nonce_id: nonce_id,
68
+ challenge_nonce: base64_encode(encrypted_nonce),
69
+ initialization_vector: base64_encode(iv)
70
+ }
71
+ end
72
+
73
+ private
74
+
75
+ # Encrypt the raw nonce using AES-256-CBC
76
+ def encrypt
77
+ cipher.encrypt
78
+ iv = cipher.random_iv
79
+ cipher.key = encryption_key
80
+ cipher.iv = iv
81
+ encrypted = cipher.update(raw_nonce) + cipher.final
82
+ [encrypted, iv]
83
+ end
84
+
85
+ # Get AES cipher
86
+ def cipher
87
+ @cipher ||= OpenSSL::Cipher::AES256.new(:CBC)
88
+ end
89
+
90
+ # Generate a random nonce
91
+ def raw_nonce
92
+ @raw_nonce ||= SecureRandom.random_bytes(32)
93
+ end
94
+
95
+ # Generate a unique nonce ID
96
+ def nonce_id
97
+ @nonce_id ||= SecureRandom.uuid
98
+ end
99
+
100
+ # Store the nonce in Redis with expiry
101
+ def store_nonce_in_redis
102
+ redis_client.set("nonce:#{nonce_id}", base64_encode(raw_nonce), ex: expiry_seconds)
103
+ end
104
+
105
+ # Get encryption key from configuration
106
+ def encryption_key
107
+ key = IosAppAttest.configuration.encryption_key
108
+ raise IosAppAttest::NonceError, "Encryption key not configured" unless key
109
+ key
110
+ end
111
+
112
+ # Encode base64 string
113
+ def base64_encode(base64_string)
114
+ Base64.strict_encode64(base64_string)
115
+ end
116
+
117
+ # Log error if logger is available
118
+ def log_error(message)
119
+ logger&.error(message)
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IosAppAttest
4
+ module Validators
5
+ # Validates app identity using authentication data
6
+ #
7
+ # This validator is responsible for verifying the application identity
8
+ # by checking the authentication data from the attestation object.
9
+ # It validates the relying party ID hash, sign count, AAGUID, and credential ID.
10
+ #
11
+ # @example
12
+ # validator = IosAppAttest::Validators::AppIdentityValidator.new(config)
13
+ # validator.validate(auth_data, key_id)
14
+ class AppIdentityValidator < BaseValidator
15
+ # Verify app identity using authentication data
16
+ #
17
+ # This method performs the following validations:
18
+ # 1. Unpacks the authentication data to extract required components
19
+ # 2. Verifies the relying party ID hash matches the configured app ID
20
+ # 3. Ensures the sign count is zero (required for initial attestation)
21
+ # 4. Validates the AAGUID matches the expected Apple App Attest value
22
+ # 5. Verifies the credential ID matches the provided key ID
23
+ #
24
+ # @param auth_data [String] The authentication data from the attestation object
25
+ # @param key_id [String] The key ID from attestation parameters
26
+ # @raise [IosAppAttest::AppIdentityError] If any app identity verification check fails
27
+ def validate(auth_data, key_id)
28
+ rp_id_hash, sign_count, aaguid, credential_id = unpack_auth_data(auth_data)
29
+
30
+ # Verify relying party ID hash
31
+ unless rp_id_hash == sha256.digest(app_id)
32
+ raise IosAppAttest::AppIdentityError, 'App ID verification failed'
33
+ end
34
+
35
+ # Verify sign count is zero (first attestation)
36
+ unless sign_count.zero?
37
+ raise IosAppAttest::AppIdentityError, 'Sign counter must be zero for initial attestation'
38
+ end
39
+
40
+ # Verify AAGUID
41
+ unless validate_aaguid(aaguid)
42
+ raise IosAppAttest::AppIdentityError, 'Invalid AAGUID for App Attestation'
43
+ end
44
+
45
+ # Verify credential ID matches key ID
46
+ unless key_id == Base64.strict_encode64(credential_id)
47
+ raise IosAppAttest::AppIdentityError, 'Credential ID does not match key ID'
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ # Unpack required objects from authentication data
54
+ #
55
+ # This method parses the binary authentication data according to the
56
+ # WebAuthn/FIDO2 specification format to extract the components needed
57
+ # for app identity validation.
58
+ #
59
+ # @param auth_data [String] The authentication data from the attestation object
60
+ # @return [Array] Array containing [rp_id_hash, sign_count, aaguid, credential_id]
61
+ def unpack_auth_data(auth_data)
62
+ (rp_id_hash, flags, sign_count, trailing_bytes) =
63
+ auth_data.unpack('a32c1N1a*')
64
+
65
+ (aaguid, credential_id_length, trailing_bytes) =
66
+ trailing_bytes.unpack('a16na*')
67
+
68
+ (credential_id, credential_public_key) =
69
+ trailing_bytes.unpack("a#{credential_id_length}a*")
70
+
71
+ [rp_id_hash, sign_count, aaguid, credential_id]
72
+ end
73
+
74
+ # Validate AAGUID (Authenticator Attestation Globally Unique Identifier)
75
+ #
76
+ # This method checks if the AAGUID matches the expected value for
77
+ # Apple App Attestation. In non-production environments, it also
78
+ # accepts the development AAGUID value.
79
+ #
80
+ # @param aaguid [String] The AAGUID extracted from authentication data
81
+ # @return [Boolean] True if AAGUID is valid for App Attestation
82
+ def validate_aaguid(aaguid)
83
+ expected_aaguid = "appattest\x00\x00\x00\x00\x00\x00\x00"
84
+
85
+ # Allow development AAGUID in non-production environments
86
+ if ENV['IOS_APP_ATTEST_ENV'] != 'production'
87
+ aaguid == 'appattestdevelop' || aaguid == expected_aaguid
88
+ else
89
+ aaguid == expected_aaguid
90
+ end
91
+ end
92
+
93
+ # Get app ID from configuration
94
+ #
95
+ # This method retrieves the application identifier from the configuration.
96
+ # The app ID is used to verify the relying party ID hash in the authentication data.
97
+ #
98
+ # @return [String] The configured application identifier
99
+ def app_id
100
+ config.app_id
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IosAppAttest
4
+ module Validators
5
+ # Validates attestation structure and format
6
+ #
7
+ # This validator is responsible for verifying the structure and format
8
+ # of the attestation object received from the Apple App Attest service.
9
+ # It ensures the attestation contains all required keys and has the correct format.
10
+ #
11
+ # @example
12
+ # validator = IosAppAttest::Validators::AttestationValidator.new(config)
13
+ # validator.validate(attestation)
14
+ class AttestationValidator < BaseValidator
15
+ # Validate the attestation object structure
16
+ # @param attestation [Hash] The decoded attestation object
17
+ # @raise [IosAppAttest::AttestationError] If attestation structure is invalid
18
+ def validate(attestation)
19
+ required_keys = %w[fmt attStmt authData]
20
+ missing_keys = required_keys - attestation.keys.map(&:to_s)
21
+
22
+ if missing_keys.any?
23
+ raise IosAppAttest::AttestationError,
24
+ "Missing required attestation keys: #{missing_keys.join(', ')}"
25
+ end
26
+
27
+ unless attestation['fmt'] == 'apple-appattest'
28
+ raise IosAppAttest::AttestationError,
29
+ "Invalid attestation format: expected 'apple-appattest'"
30
+ end
31
+ end
32
+
33
+ # Extract receipt from attestation statement
34
+ #
35
+ # This method extracts the App Store receipt from the attestation statement.
36
+ # The receipt is used for additional verification with Apple's servers.
37
+ #
38
+ # @param attestation [Hash] The decoded attestation object
39
+ # @return [String] The App Store receipt data
40
+ def extract_receipt(attestation)
41
+ attestation['attStmt']['receipt']
42
+ end
43
+
44
+ # Extract authentication data from attestation
45
+ #
46
+ # This method extracts the authentication data from the attestation object.
47
+ # The authentication data contains important information like the relying party ID hash,
48
+ # sign count, AAGUID, and credential ID needed for verification.
49
+ #
50
+ # @param attestation [Hash] The decoded attestation object
51
+ # @return [String] The authentication data used for identity verification
52
+ def extract_auth_data(attestation)
53
+ attestation['authData']
54
+ end
55
+ end
56
+ end
57
+ end