Class: IosAppAttest::Validators::ChallengeValidator

Inherits:
BaseValidator
  • Object
show all
Defined in:
lib/ios_app_attest/validators/challenge_validator.rb

Overview

Validates challenge nonce and related aspects of the attestation

This validator is responsible for verifying the challenge nonce used in the attestation process. It validates that the nonce is valid, not expired, and matches the expected value. It also verifies the key ID against the certificate’s public key and provides methods for decrypting challenges.

Examples:

validator = IosAppAttest::Validators::ChallengeValidator.new(config, redis_client: redis)
validator.validate_nonce(challenge_id, challenge_decrypted)
validator.validate_challenge(cred_cert, challenge_decrypted, auth_data)
validator.validate_key_id(cred_cert, key_id)

Instance Attribute Summary collapse

Attributes inherited from BaseValidator

#config, #logger

Instance Method Summary collapse

Constructor Details

#initialize(config, redis_client: nil, logger: nil) ⇒ ChallengeValidator

Initialize the challenge validator

This initializes the validator with configuration and optional Redis client. The Redis client is used to store and retrieve nonces for verification. If no Redis client is provided, nonce verification will be skipped.

Parameters:

  • config (IosAppAttest::Configuration)

    Configuration object with encryption keys and OIDs

  • redis_client (Object) (defaults to: nil)

    Redis client for nonce verification (optional)

  • logger (Object) (defaults to: nil)

    Logger instance for logging validation events (optional)



29
30
31
32
# File 'lib/ios_app_attest/validators/challenge_validator.rb', line 29

def initialize(config, redis_client: nil, logger: nil)
  super(config, logger: logger)
  @redis_client = redis_client
end

Instance Attribute Details

#redis_clientObject (readonly)

Returns the value of attribute redis_client.



18
19
20
# File 'lib/ios_app_attest/validators/challenge_validator.rb', line 18

def redis_client
  @redis_client
end

Instance Method Details

#decrypt_challenge(challenge, iv) ⇒ String

Decrypt challenge using AES-256-CBC

This method decrypts the challenge nonce using AES-256-CBC encryption with the provided initialization vector and the encryption key from configuration.

Parameters:

  • challenge (String)

    The encrypted challenge nonce

  • iv (String)

    The initialization vector used for encryption

Returns:

  • (String)

    The decrypted challenge nonce



108
109
110
111
112
113
114
# File 'lib/ios_app_attest/validators/challenge_validator.rb', line 108

def decrypt_challenge(challenge, iv)
  cipher = OpenSSL::Cipher::AES256.new(:CBC)
  cipher.decrypt
  cipher.key = encryption_key
  cipher.iv = iv
  cipher.update(challenge) + cipher.final
end

#validate_challenge(cred_cert, challenge_decrypted, auth_data) ⇒ Object

Verify challenge nonce from certificate

This method verifies that the challenge nonce was correctly incorporated into the attestation by checking that the certificate contains a hash derived from the authentication data and challenge nonce. This ensures the attestation was created specifically for this challenge.

Parameters:

  • cred_cert (OpenSSL::X509::Certificate)

    The credential certificate containing the App Attest extension

  • challenge_decrypted (String)

    The decrypted challenge nonce to verify

  • auth_data (String)

    The authentication data from the attestation

Raises:



69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/ios_app_attest/validators/challenge_validator.rb', line 69

def validate_challenge(cred_cert, challenge_decrypted, auth_data)
  challenge_hash = sha256.digest(challenge_decrypted)

  extension = cred_cert.extensions.find { |e| e.oid == app_attest_oid }
  sequence = OpenSSL::ASN1.decode(OpenSSL::ASN1.decode(extension.to_der).value[1].value)
  to_verify = sequence.value[0].value[0].value

  expected_hash = sha256.digest(auth_data + challenge_hash)
  unless to_verify == expected_hash
    raise IosAppAttest::ChallengeError, 'Challenge verification failed'
  end
end

#validate_key_id(cred_cert, key_id) ⇒ Object

Validate the key ID matches the certificate’s public key

This method verifies that the key ID provided in the attestation parameters matches the hash of the public key from the credential certificate. This ensures the attestation is using the correct key pair.

Parameters:

  • cred_cert (OpenSSL::X509::Certificate)

    The credential certificate containing the public key

  • key_id (String)

    The key ID from attestation parameters to verify

Raises:



91
92
93
94
95
96
97
98
# File 'lib/ios_app_attest/validators/challenge_validator.rb', line 91

def validate_key_id(cred_cert, key_id)
  uncompressed_point_key = cred_cert.public_key.public_key.to_octet_string(:uncompressed)
  expected_key_id = Base64.strict_encode64(sha256.digest(uncompressed_point_key))
  
  unless key_id == expected_key_id
    raise IosAppAttest::ChallengeError, 'Key ID verification failed'
  end
end

#validate_nonce(challenge_id, challenge_decrypted) ⇒ Object

Note:

This method requires a Redis client to be provided during initialization. If no Redis client is available, this validation is skipped.

Validate the challenge nonce against stored value

This method verifies that the provided challenge nonce matches the one previously stored in Redis. After successful validation, the nonce is deleted from Redis to prevent replay attacks.

Parameters:

  • challenge_id (String)

    The challenge nonce ID used as Redis key

  • challenge_decrypted (String)

    The decrypted challenge nonce to validate

Raises:



46
47
48
49
50
51
52
53
54
55
56
# File 'lib/ios_app_attest/validators/challenge_validator.rb', line 46

def validate_nonce(challenge_id, challenge_decrypted)
  return unless redis_client
  
  nonce = redis_client.get("nonce:#{challenge_id}")
  unless nonce && nonce == encode_base64(challenge_decrypted)
    raise IosAppAttest::ChallengeError, "Invalid or expired challenge nonce"
  end
  
  # Delete the nonce after successful validation to prevent replay attacks
  redis_client.del("nonce:#{challenge_id}")
end