Auto-suggest name for new Passkeys

- Use plain client data json everywhere
- Expose AAGUID and backed_up on Credentials
- Make attestation verifiers configurable
- Auto-suggest names for passkeys and show icons
This commit is contained in:
Stanko K.R.
2026-02-19 10:56:22 +01:00
parent b23660d897
commit fbe9252db1
53 changed files with 666 additions and 172 deletions
@@ -38,7 +38,7 @@
class ActionPack::WebAuthn::Authenticator::Attestation
attr_reader :authenticator_data, :format, :attestation_statement
delegate :credential_id, :public_key, :public_key_bytes, :sign_count, to: :authenticator_data
delegate :credential_id, :public_key, :public_key_bytes, :sign_count, :aaguid, :backed_up?, to: :authenticator_data
def self.decode(bytes)
cbor = ActionPack::WebAuthn::CborDecoder.decode(bytes)
@@ -25,10 +25,10 @@
# In addition to the base Response validations, this class verifies:
#
# * The client data type is "webauthn.create"
# * The attestation format is "none" (other formats require certificate verification)
# * The attestation format has a registered verifier
# * The attestation statement passes format-specific verification
#
class ActionPack::WebAuthn::Authenticator::AttestationResponse < ActionPack::WebAuthn::Authenticator::Response
SUPPORTED_ATTESTATION_FORMATS = %w[ none ].freeze
attr_reader :attestation_object
def initialize(attestation_object:, **attributes)
@@ -43,9 +43,13 @@ class ActionPack::WebAuthn::Authenticator::AttestationResponse < ActionPack::Web
raise InvalidResponseError, "Client data type is not webauthn.create"
end
unless SUPPORTED_ATTESTATION_FORMATS.include?(attestation.format)
verifier = ActionPack::WebAuthn.attestation_verifiers[attestation.format]
unless verifier
raise InvalidResponseError, "Unsupported attestation format: #{attestation.format}"
end
verifier.verify!(attestation, client_data_json: client_data_json)
end
def attestation
@@ -0,0 +1,24 @@
# = Action Pack WebAuthn None Attestation Verifier
#
# Verifies attestation responses with the "none" format, which indicates the
# authenticator did not provide any attestation statement. This is the default
# format used by most consumer authenticators.
#
# == Implementing Custom Verifiers
#
# To support other attestation formats (e.g., "packed", "fido-u2f"), implement
# a class with the same +verify!+ interface and register it:
#
# ActionPack::WebAuthn.register_attestation_verifier("packed", MyPackedVerifier.new)
#
# The +verify!+ method receives the decoded +Attestation+ object and the raw
# +client_data_json+ bytes. Raise +InvalidResponseError+ if verification fails.
#
class ActionPack::WebAuthn::Authenticator::AttestationVerifiers::None
def verify!(attestation, client_data_json:)
if attestation.attestation_statement.present?
raise ActionPack::WebAuthn::Authenticator::Response::InvalidResponseError,
"Attestation statement must be empty for 'none' format"
end
end
end
@@ -57,7 +57,7 @@ class ActionPack::WebAuthn::Authenticator::Data
BACKUP_STATE_FLAG = 0x10
ATTESTED_CREDENTIAL_DATA_FLAG = 0x40
attr_reader :bytes, :relying_party_id_hash, :flags, :sign_count, :credential_id, :public_key_bytes
attr_reader :bytes, :relying_party_id_hash, :flags, :sign_count, :aaguid, :credential_id, :public_key_bytes
class << self
def wrap(data)
@@ -81,10 +81,13 @@ class ActionPack::WebAuthn::Authenticator::Data
sign_count = bytes[position, SIGN_COUNT_LENGTH].pack("C*").unpack1("N")
position += SIGN_COUNT_LENGTH
aaguid = nil
credential_id = nil
public_key_bytes = nil
if flags & ATTESTED_CREDENTIAL_DATA_FLAG != 0
aaguid_bytes = bytes[position, AAGUID_LENGTH].pack("C*")
aaguid = aaguid_bytes.unpack("H8H4H4H4H12").join("-")
position += AAGUID_LENGTH
credential_id_length = bytes[position, CREDENTIAL_ID_LENGTH_BYTES].pack("C*").unpack1("n")
@@ -101,17 +104,19 @@ class ActionPack::WebAuthn::Authenticator::Data
relying_party_id_hash: relying_party_id_hash,
flags: flags,
sign_count: sign_count,
aaguid: aaguid,
credential_id: credential_id,
public_key_bytes: public_key_bytes
)
end
end
def initialize(bytes:, relying_party_id_hash:, flags:, sign_count:, credential_id:, public_key_bytes:)
def initialize(bytes:, relying_party_id_hash:, flags:, sign_count:, aaguid: nil, credential_id:, public_key_bytes:)
@bytes = bytes
@relying_party_id_hash = relying_party_id_hash
@flags = flags
@sign_count = sign_count
@aaguid = aaguid
@credential_id = credential_id
@public_key_bytes = public_key_bytes
end