A Ruby gem for verifying iOS App Attest tokens - Apple's device attestation mechanism for iOS apps.
Add this line to your application's Gemfile:
gem 'ios_app_attest'And then execute:
$ bundle installOr install it yourself as:
$ gem install ios_app_attestConfigure 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)
endNote: 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.
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# 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
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}"
endHere'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
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 |
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.
Bug reports and pull requests are welcome on GitHub at https://github.com/chaitra-mudili/ios-app-attest-ruby.
The gem is available as open source under the terms of the MIT License.