# = Action Pack WebAuthn Public Key Credential # # Represents a WebAuthn public key credential and orchestrates the registration # and authentication ceremonies. During registration (+.register+), it verifies # the attestation response and returns a new credential. During authentication # (+#authenticate+), it verifies the assertion response against the stored # public key. # # == Registration # # credential = ActionPack::WebAuthn::PublicKeyCredential.register( # params[:passkey], # challenge: ActionPack::WebAuthn::Current.challenge, # origin: ActionPack::WebAuthn::Current.origin # ) # # credential.id # => Base64URL-encoded credential ID # credential.public_key # => OpenSSL::PKey::EC # credential.sign_count # => 0 # # == Authentication # # credential = ActionPack::WebAuthn::PublicKeyCredential.new( # id: stored_credential_id, # public_key: stored_public_key, # sign_count: stored_sign_count # ) # # credential.authenticate(params[:passkey]) # # == Ceremony Options # # Use +.creation_options+ and +.request_options+ to generate the JSON options # passed to the browser's +navigator.credentials.create()+ and # +navigator.credentials.get()+ calls. # # == Attributes # # [+id+] # The Base64URL-encoded credential identifier. # # [+public_key+] # The OpenSSL public key for signature verification. # # [+sign_count+] # The signature counter, used for replay detection. # # [+aaguid+] # The authenticator attestation GUID (set during registration). # # [+backed_up+] # Whether the credential is backed up to cloud storage (synced passkey). # # [+transports+] # Transport hints (e.g., "internal", "usb", "ble", "nfc"). # class ActionPack::WebAuthn::PublicKeyCredential attr_reader :id, :public_key, :sign_count, :aaguid, :backed_up, :transports class << self # Returns a RequestOptions object for the authentication ceremony. # Credentials responding to +to_public_key_credential+ are automatically # transformed. def request_options(**attributes) attributes[:credentials] = transform_credentials(attributes[:credentials]) if attributes[:credentials] ActionPack::WebAuthn::PublicKeyCredential::RequestOptions.new(**attributes) end # Returns a CreationOptions object for the registration ceremony. # Credentials in +exclude_credentials+ responding to # +to_public_key_credential+ are automatically transformed. def creation_options(**attributes) attributes[:exclude_credentials] = transform_credentials(attributes[:exclude_credentials]) if attributes[:exclude_credentials] ActionPack::WebAuthn::PublicKeyCredential::CreationOptions.new(**attributes) end # Verifies an attestation response from the browser and returns a new # PublicKeyCredential with the registered credential data. # # Raises +InvalidResponseError+ if the attestation is invalid. def register(params, challenge: ActionPack::WebAuthn::Current.challenge, origin: ActionPack::WebAuthn::Current.origin) response = ActionPack::WebAuthn::Authenticator::AttestationResponse.new( client_data_json: params[:client_data_json], attestation_object: params[:attestation_object], challenge: challenge, origin: origin ) response.validate! new( id: response.attestation.credential_id, public_key: response.attestation.public_key, sign_count: response.attestation.sign_count, aaguid: response.attestation.aaguid, backed_up: response.attestation.backed_up?, transports: Array(params[:transports]) ) end private def transform_credentials(credentials) Array(credentials).map do |credential| if credential.respond_to?(:to_public_key_credential) credential.to_public_key_credential else credential end end end end def initialize(id:, public_key:, sign_count:, aaguid: nil, backed_up: nil, transports: []) @id = id @public_key = public_key @public_key = OpenSSL::PKey.read(public_key) unless public_key.is_a?(OpenSSL::PKey::PKey) @sign_count = sign_count @aaguid = aaguid @backed_up = backed_up @transports = transports end # Verifies an assertion response against this credential's public key. # Updates +sign_count+ and +backed_up+ on success. # # Raises +InvalidResponseError+ if the assertion is invalid. def authenticate(params, challenge: ActionPack::WebAuthn::Current.challenge, origin: ActionPack::WebAuthn::Current.origin) response = ActionPack::WebAuthn::Authenticator::AssertionResponse.new( client_data_json: params[:client_data_json], authenticator_data: params[:authenticator_data], signature: params[:signature], credential: self, challenge: challenge, origin: origin ) response.validate! @sign_count = response.authenticator_data.sign_count @backed_up = response.authenticator_data.backed_up? end # Returns a Hash of the credential data suitable for persisting. def to_h { credential_id: id, public_key: public_key.to_der, sign_count: sign_count, aaguid: aaguid, backed_up: backed_up, transports: transports } end end