iOS App Attest
A Ruby gem for verifying iOS App Attest tokens - Apple's device attestation mechanism for iOS apps.
Installation
Add this line to your application's Gemfile:
gem 'ios_app_attest'
And then execute:
$ bundle install
Or install it yourself as:
$ gem install ios_app_attest
Usage
Configuration
Configure the gem with your app's specific settings:
IosAppAttest.configure do |config|
config.app_id = "TEAM_ID.BUNDLE_ID" # Your Apple Team ID and Bundle ID
config.encryption_key = ENV.fetch("IOS_APP_ATTEST_TOKEN").byteslice(0, 32) # Your encryption key (32 bytes)
end
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.
Complete Attestation Flow
1. Generating and Storing Challenge Nonces
When a client requests a nonce, your server generates it and stores it in Redis with a TTL:
# Create a Redis client for nonce storage
redis = Redis.new(url: ENV["REDIS_URL"] || "redis://localhost:6379/0")
# Create a nonce generator
nonce_generator = IosAppAttest::NonceGenerator.new(
redis_client: redis,
logger: Rails.logger, # Optional
expiry_seconds: 300 # Optional: Nonce expiry time in seconds (default: 120)
)
# Generate a nonce (this also stores it in Redis with the configured TTL)
nonce_data = nonce_generator.generate
# The nonce_data contains:
# - challenge_nonce_id: A unique identifier for the challenge (used as Redis key)
# - challenge_nonce: The encrypted challenge nonce (base64 encoded)
# - initialization_vector: The IV used for encryption (base64 encoded)
# Send this data to the client for attestation
2. Client-Side Processing
# On the client side (iOS app):
# 1. Receive the nonce data from the server
# 2. Decrypt the challenge_nonce using the same encryption key:
# a. Base64 decode the challenge_nonce and initialization_vector
# b. Use AES-256-CBC with the shared encryption key to decrypt the challenge
# 3. Use the decrypted nonce in the App Attestation process
# 4. Send the attestation object back to the server along with the original nonce data
3. Server-Side Verification
When the client sends back the attestation object along with the original nonce data, the server:
# Attestation parameters from the client
attestation_params = {
attestation_object: "base64_encoded_attestation_object",
key_id: "base64_encoded_key_id",
challenge_nonce: "base64_encoded_challenge", # The encrypted challenge nonce (base64 encoded)
initialization_vector: "base64_encoded_initialization_vector", # The IV used for encryption (base64 encoded)
challenge_nonce_id: "challenge_id" # The unique identifier for the challenge
}
# Create a verifier with Redis client for nonce verification
verifier = IosAppAttest::Verifier.new(
attestation_params,
redis_client: redis, # Redis client for nonce verification (required for TTL check)
logger: Rails.logger # Optional: Logger for error logging
)
begin
# Verify the attestation - this process includes:
# 1. Decrypting the challenge nonce using the encryption key
# 2. Checking if the nonce exists in Redis (validates TTL)
# 3. Validating the attestation structure and certificates
# 4. Verifying the nonce matches what was used in the attestation
public_key, receipt = verifier.verify
# Use the public_key and receipt for further processing
# e.g., store the public_key for future authentications
rescue IosAppAttest::CertificateError => e
# Handle certificate validation errors
puts "Certificate validation failed: #{e.message}"
rescue IosAppAttest::ChallengeError => e
# Handle challenge validation errors
puts "Challenge validation failed: #{e.message}"
rescue IosAppAttest::AttestationError => e
# Handle attestation format errors
puts "Attestation format invalid: #{e.message}"
rescue IosAppAttest::AppIdentityError => e
# Handle app identity validation errors
puts "App identity validation failed: #{e.message}"
rescue IosAppAttest::NonceError => e
# Handle nonce validation errors
puts "Nonce validation failed: #{e.message}"
rescue IosAppAttest::VerificationError => e
# Handle other verification errors
puts "Verification failed: #{e.message}"
end
Complete Flow
Here's the complete flow for implementing iOS App Attestation in your application:
-
Server-side: Generate a challenge nonce
nonce_data = nonce_generator.generate # Returns: { challenge_nonce_id:, challenge_nonce:, initialization_vector: }
-
Send to Client: Send the nonce data to your iOS client
-
Client-side: The iOS client uses the nonce to generate an attestation using Apple's DeviceCheck framework
// Swift code (client-side) let service = DCAppAttestService.shared if service.isSupported { // Generate a new key pair service.generateKey { keyId, error in // Use the keyId and challenge to generate an attestation service.attestKey(keyId, clientDataHash: challengeHash) { attestation, error in // Send attestation, keyId, and challenge data back to server } } }
-
Server-side: Verify the attestation
verifier = IosAppAttest::Verifier.new( attestation_params, redis_client: redis ) public_key, receipt = verifier.verify # Store the public_key for future authentications
-
Future Assertions: For subsequent requests, the client generates assertions that can be verified using the stored public key
Parameter Reference
The library uses the following parameter names:
Parameter Name | Description |
---|---|
challenge_nonce_id |
Unique identifier for the challenge nonce |
challenge_nonce |
Base64-encoded encrypted challenge nonce |
initialization_vector |
Base64-encoded initialization vector used for encryption |
attestation_object |
Base64-encoded attestation object from Apple's DeviceCheck framework |
key_id |
Base64-encoded key ID generated by the client |
Development
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.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/chaitra-mudili/ios-app-attest-ruby.
License
The gem is available as open source under the terms of the MIT License.