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.
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IosAppAttest
4
+ module Validators
5
+ # Base class for all validators in the iOS App Attest validation process
6
+ #
7
+ # This class provides common functionality used by all validator classes,
8
+ # including logging, cryptographic utilities, and base64 encoding/decoding.
9
+ # All specific validators inherit from this class.
10
+ #
11
+ # @abstract Subclass and override validation methods to implement specific validation logic
12
+ class BaseValidator
13
+ attr_reader :config, :logger
14
+
15
+ # Initialize the validator
16
+ # @param config [IosAppAttest::Configuration] Configuration object
17
+ # @param logger [Object] Logger instance (optional)
18
+ def initialize(config, logger: nil)
19
+ @config = config
20
+ @logger = logger
21
+ end
22
+
23
+ private
24
+
25
+ # Log error if logger is available
26
+ # @param message [String] The error message to log
27
+ def log_error(message)
28
+ logger&.error(message)
29
+ end
30
+
31
+ # Get SHA256 digest
32
+ # @return [OpenSSL::Digest] SHA256 digest instance
33
+ def sha256
34
+ @sha256 ||= OpenSSL::Digest.new('SHA256')
35
+ end
36
+
37
+ # Decode base64 string
38
+ # @param base64_string [String] The base64 encoded string
39
+ # @return [String] The decoded string
40
+ def decode_base64(base64_string)
41
+ Base64.decode64(base64_string)
42
+ end
43
+
44
+ # Encode base64 string
45
+ # @param string [String] The string to encode
46
+ # @return [String] The base64 encoded string
47
+ def encode_base64(string)
48
+ Base64.strict_encode64(string)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IosAppAttest
4
+ module Validators
5
+ # Validates certificate chain and related aspects
6
+ #
7
+ # This validator is responsible for verifying the certificate chain,
8
+ # validating sequence structures, and ensuring the App Attest OID
9
+ # is present in the certificate.
10
+ #
11
+ # @example
12
+ # validator = IosAppAttest::Validators::CertificateValidator.new(config)
13
+ # cred_cert = validator.validate(attestation)
14
+ class CertificateValidator < BaseValidator
15
+ # Validate the certificate chain
16
+ #
17
+ # This method performs the following validations:
18
+ # 1. Extracts certificates from the attestation statement
19
+ # 2. Verifies the certificate chain against the Apple root CA
20
+ # 3. Validates that the certificate contains the App Attest OID
21
+ #
22
+ # @param attestation [Hash] The decoded attestation object containing x5c certificates
23
+ # @return [OpenSSL::X509::Certificate] The credential certificate if validation succeeds
24
+ # @raise [IosAppAttest::CertificateError] If certificate chain validation fails or App Attest OID is missing
25
+ def validate(attestation)
26
+ att_stmt = attestation['attStmt']
27
+ certificates = att_stmt['x5c'].map { |c| OpenSSL::X509::Certificate.new(c) }
28
+ cred_cert, *chain = certificates
29
+
30
+ context = OpenSSL::X509::StoreContext.new(certificates_store, cred_cert, chain)
31
+ unless context.verify
32
+ raise IosAppAttest::CertificateError,
33
+ "Certificate chain verification failed: #{context.error_string}"
34
+ end
35
+
36
+ verify_app_attest_oid(cred_cert)
37
+ cred_cert
38
+ end
39
+
40
+ # Validate the sequence structure in the certificate
41
+ #
42
+ # This method validates that the certificate extension with the App Attest OID
43
+ # contains a properly structured ASN.1 sequence. This is required for the
44
+ # challenge validation process.
45
+ #
46
+ # @param cred_cert [OpenSSL::X509::Certificate] The credential certificate to validate
47
+ # @raise [IosAppAttest::CertificateError] If sequence structure validation fails
48
+ def validate_sequence(cred_cert)
49
+ extension = cred_cert.extensions.find { |e| e.oid == app_attest_oid }
50
+ sequence = OpenSSL::ASN1.decode(OpenSSL::ASN1.decode(extension.to_der).value[1].value)
51
+
52
+ unless sequence.tag == OpenSSL::ASN1::SEQUENCE && sequence.value.size == 1
53
+ raise IosAppAttest::CertificateError, 'Failed sequence structure validation'
54
+ end
55
+ end
56
+
57
+ # Extract the public key from the certificate
58
+ #
59
+ # This method extracts the public key from the credential certificate
60
+ # in DER (Distinguished Encoding Rules) format for further validation.
61
+ #
62
+ # @param cred_cert [OpenSSL::X509::Certificate] The credential certificate
63
+ # @return [String] The public key in DER format
64
+ def extract_public_key(cred_cert)
65
+ cred_cert.public_key.to_der
66
+ end
67
+
68
+ private
69
+
70
+ # Validate the app attest OID in the certificate
71
+ #
72
+ # This method checks that the certificate contains the Apple App Attest OID
73
+ # extension, which is required for valid App Attestation certificates.
74
+ #
75
+ # @param certificate [OpenSSL::X509::Certificate] The certificate to validate
76
+ # @raise [IosAppAttest::CertificateError] If App Attest OID is missing
77
+ def verify_app_attest_oid(certificate)
78
+ has_oid = certificate.extensions.any? { |ext| ext.oid == app_attest_oid }
79
+ unless has_oid
80
+ raise IosAppAttest::CertificateError, "Missing App Attest OID in certificate"
81
+ end
82
+ end
83
+
84
+ # Create certificates store with hardcoded root CA
85
+ #
86
+ # This method creates an OpenSSL certificate store and adds the hardcoded Apple
87
+ # root CA certificate to it for certificate chain validation.
88
+ #
89
+ # @return [OpenSSL::X509::Store] The certificate store with Apple root CA
90
+ def certificates_store
91
+ root_cert = OpenSSL::X509::Certificate.new(root_ca)
92
+ @certificates_store ||= OpenSSL::X509::Store.new.add_cert(root_cert)
93
+ end
94
+
95
+ # Get hardcoded root CA
96
+ #
97
+ # This method returns the hardcoded Apple App Attestation root CA certificate content.
98
+ # The certificate is stored as a constant to avoid recreating the string on each call.
99
+ #
100
+ # @return [String] The Apple root CA certificate content
101
+ # @raise [IosAppAttest::CertificateError] If the certificate format is invalid
102
+ def root_ca
103
+ APPLE_APP_ATTEST_ROOT_CA
104
+ rescue StandardError => e
105
+ raise IosAppAttest::CertificateError, "Invalid root CA certificate format: #{e.message}"
106
+ end
107
+
108
+ # Apple App Attestation Root CA Certificate
109
+ # This is the official Apple App Attestation root CA certificate
110
+ APPLE_APP_ATTEST_ROOT_CA = <<~CERT
111
+ -----BEGIN CERTIFICATE-----
112
+ MIICITCCAaegAwIBAgIQC/O+DvHN0uD7jG5yH2IXmDAKBggqhkjOPQQDAzBSMSYw
113
+ JAYDVQQDDB1BcHBsZSBBcHAgQXR0ZXN0YXRpb24gUm9vdCBDQTETMBEGA1UECgwK
114
+ QXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTAeFw0yMDAzMTgxODMyNTNa
115
+ Fw00NTAzMTUwMDAwMDBaMFIxJjAkBgNVBAMMHUFwcGxlIEFwcCBBdHRlc3RhdGlv
116
+ biBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJbmMuMRMwEQYDVQQIDApDYWxpZm9y
117
+ bmlhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAERTHhmLW07ATaFQIEVwTtT4dyctdh
118
+ NbJhFs/Ii2FdCgAHGbpphY3+d8qjuDngIN3WVhQUBHAoMeQ/cLiP1sOUtgjqK9au
119
+ Yen1mMEvRq9Sk3Jm5X8U62H+xTD3FE9TgS41o0IwQDAPBgNVHRMBAf8EBTADAQH/
120
+ MB0GA1UdDgQWBBSskRBTM72+aEH/pwyp5frq5eWKoTAOBgNVHQ8BAf8EBAMCAQYw
121
+ CgYIKoZIzj0EAwMDaAAwZQIwQgFGnByvsiVbpTKwSga0kP0e8EeDS4+sQmTvb7vn
122
+ 53O5+FRXgeLhpJ06ysC5PrOyAjEAp5U4xDgEgllF7En3VcE3iexZZtKeYnpqtijV
123
+ oyFraWVIyd/dganmrduC1bmTBGwD
124
+ -----END CERTIFICATE-----
125
+ CERT
126
+
127
+ # Apple App Attestation OID constant
128
+ # This OID identifies the App Attest extension in certificates
129
+ APP_ATTEST_OID = "1.2.840.113635.100.8.2"
130
+
131
+ # Get app attest OID
132
+ #
133
+ # This method returns the hardcoded Apple App Attest OID.
134
+ # The OID is used to identify the App Attest extension in certificates.
135
+ #
136
+ # @return [String] The Apple App Attest OID ("1.2.840.113635.100.8.2")
137
+ def app_attest_oid
138
+ APP_ATTEST_OID
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IosAppAttest
4
+ module Validators
5
+ # Validates challenge nonce and related aspects of the attestation
6
+ #
7
+ # This validator is responsible for verifying the challenge nonce used in the
8
+ # attestation process. It validates that the nonce is valid, not expired, and
9
+ # matches the expected value. It also verifies the key ID against the certificate's
10
+ # public key and provides methods for decrypting challenges.
11
+ #
12
+ # @example
13
+ # validator = IosAppAttest::Validators::ChallengeValidator.new(config, redis_client: redis)
14
+ # validator.validate_nonce(challenge_id, challenge_decrypted)
15
+ # validator.validate_challenge(cred_cert, challenge_decrypted, auth_data)
16
+ # validator.validate_key_id(cred_cert, key_id)
17
+ class ChallengeValidator < BaseValidator
18
+ attr_reader :redis_client
19
+
20
+ # Initialize the challenge validator
21
+ #
22
+ # This initializes the validator with configuration and optional Redis client.
23
+ # The Redis client is used to store and retrieve nonces for verification.
24
+ # If no Redis client is provided, nonce verification will be skipped.
25
+ #
26
+ # @param config [IosAppAttest::Configuration] Configuration object with encryption keys and OIDs
27
+ # @param redis_client [Object] Redis client for nonce verification (optional)
28
+ # @param logger [Object] Logger instance for logging validation events (optional)
29
+ def initialize(config, redis_client: nil, logger: nil)
30
+ super(config, logger: logger)
31
+ @redis_client = redis_client
32
+ end
33
+
34
+ # Validate the challenge nonce against stored value
35
+ #
36
+ # This method verifies that the provided challenge nonce matches the one
37
+ # previously stored in Redis. After successful validation, the nonce is
38
+ # deleted from Redis to prevent replay attacks.
39
+ #
40
+ # @note This method requires a Redis client to be provided during initialization.
41
+ # If no Redis client is available, this validation is skipped.
42
+ #
43
+ # @param challenge_id [String] The challenge nonce ID used as Redis key
44
+ # @param challenge_decrypted [String] The decrypted challenge nonce to validate
45
+ # @raise [IosAppAttest::ChallengeError] If nonce is invalid, expired, or missing
46
+ def validate_nonce(challenge_id, challenge_decrypted)
47
+ return unless redis_client
48
+
49
+ nonce = redis_client.get("nonce:#{challenge_id}")
50
+ unless nonce && nonce == encode_base64(challenge_decrypted)
51
+ raise IosAppAttest::ChallengeError, "Invalid or expired challenge nonce"
52
+ end
53
+
54
+ # Delete the nonce after successful validation to prevent replay attacks
55
+ redis_client.del("nonce:#{challenge_id}")
56
+ end
57
+
58
+ # Verify challenge nonce from certificate
59
+ #
60
+ # This method verifies that the challenge nonce was correctly incorporated into
61
+ # the attestation by checking that the certificate contains a hash derived from
62
+ # the authentication data and challenge nonce. This ensures the attestation was
63
+ # created specifically for this challenge.
64
+ #
65
+ # @param cred_cert [OpenSSL::X509::Certificate] The credential certificate containing the App Attest extension
66
+ # @param challenge_decrypted [String] The decrypted challenge nonce to verify
67
+ # @param auth_data [String] The authentication data from the attestation
68
+ # @raise [IosAppAttest::ChallengeError] If challenge verification fails
69
+ def validate_challenge(cred_cert, challenge_decrypted, auth_data)
70
+ challenge_hash = sha256.digest(challenge_decrypted)
71
+
72
+ extension = cred_cert.extensions.find { |e| e.oid == app_attest_oid }
73
+ sequence = OpenSSL::ASN1.decode(OpenSSL::ASN1.decode(extension.to_der).value[1].value)
74
+ to_verify = sequence.value[0].value[0].value
75
+
76
+ expected_hash = sha256.digest(auth_data + challenge_hash)
77
+ unless to_verify == expected_hash
78
+ raise IosAppAttest::ChallengeError, 'Challenge verification failed'
79
+ end
80
+ end
81
+
82
+ # Validate the key ID matches the certificate's public key
83
+ #
84
+ # This method verifies that the key ID provided in the attestation parameters
85
+ # matches the hash of the public key from the credential certificate. This ensures
86
+ # the attestation is using the correct key pair.
87
+ #
88
+ # @param cred_cert [OpenSSL::X509::Certificate] The credential certificate containing the public key
89
+ # @param key_id [String] The key ID from attestation parameters to verify
90
+ # @raise [IosAppAttest::ChallengeError] If key ID verification fails
91
+ def validate_key_id(cred_cert, key_id)
92
+ uncompressed_point_key = cred_cert.public_key.public_key.to_octet_string(:uncompressed)
93
+ expected_key_id = Base64.strict_encode64(sha256.digest(uncompressed_point_key))
94
+
95
+ unless key_id == expected_key_id
96
+ raise IosAppAttest::ChallengeError, 'Key ID verification failed'
97
+ end
98
+ end
99
+
100
+ # Decrypt challenge using AES-256-CBC
101
+ #
102
+ # This method decrypts the challenge nonce using AES-256-CBC encryption with
103
+ # the provided initialization vector and the encryption key from configuration.
104
+ #
105
+ # @param challenge [String] The encrypted challenge nonce
106
+ # @param iv [String] The initialization vector used for encryption
107
+ # @return [String] The decrypted challenge nonce
108
+ def decrypt_challenge(challenge, iv)
109
+ cipher = OpenSSL::Cipher::AES256.new(:CBC)
110
+ cipher.decrypt
111
+ cipher.key = encryption_key
112
+ cipher.iv = iv
113
+ cipher.update(challenge) + cipher.final
114
+ end
115
+
116
+ private
117
+
118
+ # Apple App Attest OID constant
119
+ # This OID identifies the App Attest extension in certificates
120
+ APP_ATTEST_OID = "1.2.840.113635.100.8.2"
121
+
122
+ # Get app attest OID
123
+ #
124
+ # This method returns the hardcoded Apple App Attest OID.
125
+ # The OID is used to identify the App Attest extension in certificates.
126
+ #
127
+ # @return [String] The Apple App Attest OID ("1.2.840.113635.100.8.2")
128
+ def app_attest_oid
129
+ APP_ATTEST_OID
130
+ end
131
+
132
+ # Get encryption key from configuration
133
+ #
134
+ # This method retrieves the encryption key from the configuration object.
135
+ # The key is used for decrypting challenge nonces.
136
+ #
137
+ # @return [String] The encryption key used for AES-256-CBC decryption
138
+ def encryption_key
139
+ config.encryption_key
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IosAppAttest
4
+ module Validators
5
+ # Utility methods for iOS App Attest validators
6
+ #
7
+ # This module provides common cryptographic and encoding utilities
8
+ # used throughout the iOS App Attest validation process.
9
+ # It includes methods for base64 encoding/decoding, SHA256 hashing,
10
+ # and AES cipher creation.
11
+ module Utils
12
+ # Decode base64 string
13
+ # @param base64_string [String] The base64 encoded string
14
+ # @return [String] The decoded string
15
+ def self.decode_base64(base64_string)
16
+ Base64.strict_decode64(base64_string)
17
+ end
18
+
19
+ # Encode base64 string
20
+ # @param string [String] The string to encode
21
+ # @return [String] The base64 encoded string
22
+ def self.encode_base64(string)
23
+ Base64.strict_encode64(string)
24
+ end
25
+
26
+ # Get SHA256 digest
27
+ # @return [OpenSSL::Digest] SHA256 digest instance
28
+ def self.sha256
29
+ OpenSSL::Digest.new('SHA256')
30
+ end
31
+
32
+ # Create AES cipher
33
+ # @return [OpenSSL::Cipher] AES256-CBC cipher instance
34
+ def self.cipher
35
+ OpenSSL::Cipher::AES256.new(:CBC)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'validators/utils'
4
+ require_relative 'validators/base_validator'
5
+ require_relative 'validators/attestation_validator'
6
+ require_relative 'validators/certificate_validator'
7
+ require_relative 'validators/challenge_validator'
8
+ require_relative 'validators/app_identity_validator'
9
+
10
+ module IosAppAttest
11
+ # Namespace for validators
12
+ module Validators
13
+ end
14
+ end
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pry'
4
+ require_relative 'validators'
5
+
6
+ module IosAppAttest
7
+ # Verifies iOS App Attestation tokens
8
+ #
9
+ # The Verifier class is responsible for validating iOS App Attestation tokens
10
+ # received from iOS clients. It performs a series of validation steps to ensure
11
+ # the attestation is genuine and comes from a valid Apple device.
12
+ #
13
+ # @example Basic usage
14
+ # verifier = IosAppAttest::Verifier.new(attestation_params)
15
+ # public_key, receipt = verifier.verify
16
+ #
17
+ # @example With Redis for nonce validation
18
+ # verifier = IosAppAttest::Verifier.new(
19
+ # attestation_params,
20
+ # redis_client: redis
21
+ # )
22
+ # public_key, receipt = verifier.verify
23
+ class Verifier
24
+
25
+ attr_reader :attestation_params, :redis_client, :logger
26
+
27
+ # Initialize the verifier with attestation parameters
28
+ # @param attestation_params [Hash] The attestation parameters from the client
29
+ # @param redis_client [Object] Redis client for nonce verification (optional)
30
+ # @param logger [Object] Logger instance (optional)
31
+ def initialize(attestation_params, redis_client: nil, logger: nil)
32
+ @attestation_params = attestation_params
33
+ @redis_client = redis_client
34
+ @logger = logger
35
+ initialize_validators
36
+ end
37
+
38
+ # Verify the app attestation
39
+ #
40
+ # This method performs a complete verification of the iOS App Attestation token.
41
+ # It validates the attestation structure, certificate chain, challenge nonce,
42
+ # and app identity. If all validations pass, it returns the public key and receipt.
43
+ #
44
+ # @return [Array<String>] An array containing [public_key, receipt] if verification succeeds
45
+ # @raise [VerificationError] If verification fails for any reason
46
+ # @raise [NonceError] If nonce validation fails
47
+ # @raise [CertificateError] If certificate validation fails
48
+ # @raise [ChallengeError] If challenge validation fails
49
+ # @raise [AppIdentityError] If app identity validation fails
50
+ # @raise [AttestationError] If attestation format is invalid
51
+ def verify
52
+ begin
53
+ # Step 1: Decode the attestation object
54
+ attestation = decode_attestation
55
+
56
+ # Step 2: Validate the challenge nonce if Redis client is provided
57
+ if redis_client
58
+ challenge_validator.validate_nonce(challenge_id, challenge_decrypted)
59
+ end
60
+
61
+ # Step 3: Validate the attestation structure and format
62
+ attestation_validator.validate(attestation)
63
+
64
+ # Step 4: Extract auth_data and receipt
65
+ auth_data = attestation_validator.extract_auth_data(attestation)
66
+ @receipt = attestation_validator.extract_receipt(attestation)
67
+
68
+ # Step 5: Validate the certificate chain and get the credential certificate
69
+ cred_cert = certificate_validator.validate(attestation)
70
+
71
+ # Step 6: Validate the challenge
72
+ challenge_validator.validate_challenge(cred_cert, challenge_decrypted, auth_data)
73
+
74
+ # Step 7: Validate the key ID
75
+ challenge_validator.validate_key_id(cred_cert, key_id)
76
+
77
+ # Step 8: Validate the certificate sequence structure
78
+ certificate_validator.validate_sequence(cred_cert)
79
+
80
+ # Step 9: Verify the app identity
81
+ app_identity_validator.validate(auth_data, key_id)
82
+
83
+ # Step 10: Extract the public key
84
+ @public_key = certificate_validator.extract_public_key(cred_cert)
85
+ rescue IosAppAttest::Error => error
86
+ # Re-raise IosAppAttest errors directly
87
+ log_error("IosAppAttest verification failed: #{error}")
88
+ raise error
89
+ rescue StandardError => error
90
+ # Wrap other errors in VerificationError
91
+ log_error("IosAppAttest verification failed: #{error}")
92
+ raise VerificationError, "Attestation verification failed: #{error.message}"
93
+ end
94
+
95
+ [public_key, receipt]
96
+ end
97
+
98
+ private
99
+
100
+ attr_reader :attestation_validator, :certificate_validator, :challenge_validator,
101
+ :app_identity_validator, :public_key, :receipt
102
+
103
+ # Initialize all validators
104
+ def initialize_validators
105
+ @attestation_validator = Validators::AttestationValidator.new(config, logger: logger)
106
+ @certificate_validator = Validators::CertificateValidator.new(config, logger: logger)
107
+ @challenge_validator = Validators::ChallengeValidator.new(
108
+ config,
109
+ redis_client: redis_client,
110
+ logger: logger
111
+ )
112
+ @app_identity_validator = Validators::AppIdentityValidator.new(config, logger: logger)
113
+ end
114
+
115
+ # Get IosAppAttest configuration
116
+ # @return [IosAppAttest::Configuration] The configuration object
117
+ def config
118
+ IosAppAttest.configuration
119
+ end
120
+
121
+ # Decrypt challenge using AES
122
+ # @return [String] The decrypted challenge
123
+ def challenge_decrypted
124
+ challenge_validator.decrypt_challenge(challenge, iv)
125
+ end
126
+
127
+ #---------------------------
128
+ # Parameter Accessors
129
+ #---------------------------
130
+
131
+ # Get key ID from attestation parameters
132
+ # @return [String] The key ID
133
+ def key_id
134
+ @key_id ||= attestation_params[:key_id] || attestation_params["key_id"]
135
+ end
136
+
137
+ # Get attestation object from attestation parameters
138
+ # @return [String] The decoded attestation object
139
+ def attestation_object
140
+ @attestation_object ||= Validators::Utils.decode_base64(attestation_params[:attestation_object] || attestation_params["attestation_object"])
141
+ end
142
+
143
+ # Get challenge ID from attestation parameters
144
+ # @return [String] The challenge nonce ID
145
+ def challenge_id
146
+ @challenge_id ||= attestation_params[:challenge_nonce_id] || attestation_params["challenge_nonce_id"]
147
+ end
148
+
149
+ # Get challenge from attestation parameters
150
+ # @return [String] The decoded challenge nonce
151
+ def challenge
152
+ @challenge ||= Validators::Utils.decode_base64(attestation_params[:challenge_nonce] || attestation_params["challenge_nonce"])
153
+ end
154
+
155
+ # Get IV from attestation parameters
156
+ # @return [String] The decoded initialization vector
157
+ def iv
158
+ @iv ||= Validators::Utils.decode_base64(attestation_params[:initialization_vector] || attestation_params["initialization_vector"])
159
+ end
160
+
161
+ # Log error if logger is available
162
+ # @param message [String] The error message to log
163
+ def log_error(message)
164
+ logger&.error(message)
165
+ end
166
+
167
+ # Decode the attestation object from base64
168
+ # @return [Hash] The decoded attestation object
169
+ def decode_attestation
170
+ CBOR.decode(attestation_object)
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IosAppAttest
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cbor"
4
+ require "openssl"
5
+ require "base64"
6
+ require "securerandom"
7
+ require_relative "ios_app_attest/version"
8
+ require_relative "ios_app_attest/configuration"
9
+ require_relative "ios_app_attest/errors"
10
+ require_relative "ios_app_attest/validators"
11
+ require_relative "ios_app_attest/verifier"
12
+ require_relative "ios_app_attest/nonce_generator"
13
+
14
+ # Main module for iOS App Attest verification
15
+ #
16
+ # This module provides functionality for verifying iOS App Attest attestations.
17
+ # It includes classes for configuration, verification, nonce generation, and
18
+ # various validators for different aspects of the attestation process.
19
+ #
20
+ # @example Basic usage
21
+ # IosAppAttest.configure do |config|
22
+ # config.app_id = "TEAM123.com.example.app"
23
+ # config.encryption_key = SecureRandom.random_bytes(32)
24
+ # end
25
+ # # Note: App Attest OID ("1.2.840.113635.100.8.2") is hardcoded in the gem
26
+ #
27
+ # verifier = IosAppAttest::Verifier.new
28
+ # verifier.verify(attestation_object, challenge_id, key_id)
29
+ #
30
+ module IosAppAttest
31
+ # Configuration options for the IosAppAttest module
32
+ class << self
33
+ attr_accessor :configuration
34
+
35
+ # Configure the IosAppAttest module
36
+ #
37
+ # This method allows configuration of the IosAppAttest module using a block.
38
+ # It yields the configuration object to the block, allowing for setting
39
+ # various configuration parameters.
40
+ #
41
+ # @example
42
+ # IosAppAttest.configure do |config|
43
+ # config.app_id = "TEAM123.com.example.app"
44
+ # config.encryption_key = SecureRandom.random_bytes(32)
45
+ # end
46
+ # # Note: App Attest OID is hardcoded in the gem
47
+ #
48
+ # @yield [configuration] The configuration object to be modified
49
+ # @return [Configuration] The current configuration object
50
+ def configure
51
+ self.configuration ||= Configuration.new
52
+ yield(configuration) if block_given?
53
+ configuration
54
+ end
55
+ end
56
+
57
+ # Initialize with default configuration
58
+ configure
59
+ end