webauthn-2.4.0/0000755000175000017500000000000013755257222012370 5ustar pravipraviwebauthn-2.4.0/SECURITY.md0000644000175000017500000000075513755257222014170 0ustar pravipravi# Security Policy ## Supported Versions | Version | Supported | | ------- | ------------------ | | 2.4.z | :white_check_mark: | | 2.3.z | :white_check_mark: | | 2.2.z | :white_check_mark: | | 2.1.z | :x: | | 2.0.z | :x: | | 1.18.z | :white_check_mark: | | < 1.18 | :x: | ## Reporting a Vulnerability If you have discovered a security bug, please send an email to security@cedarcode.com instead of posting to the GitHub issue tracker. Thank you! webauthn-2.4.0/lib/0000755000175000017500000000000013755257222013136 5ustar pravipraviwebauthn-2.4.0/lib/webauthn.rb0000644000175000017500000000055713755257222015307 0ustar pravipravi# frozen_string_literal: true require "webauthn/configuration" require "webauthn/credential" require "webauthn/credential_creation_options" require "webauthn/credential_request_options" require "webauthn/version" module WebAuthn TYPE_PUBLIC_KEY = "public-key" def self.generate_user_id configuration.encoder.encode(SecureRandom.random_bytes(64)) end end webauthn-2.4.0/lib/cose/0000755000175000017500000000000013755257222014067 5ustar pravipraviwebauthn-2.4.0/lib/cose/rsapkcs1_algorithm.rb0000644000175000017500000000257413755257222020221 0ustar pravipravi# frozen_string_literal: true require "cose" require "cose/algorithm/signature_algorithm" require "cose/error" require "cose/key/rsa" require "openssl/signature_algorithm/rsapkcs1" class RSAPKCS1Algorithm < COSE::Algorithm::SignatureAlgorithm attr_reader :hash_function def initialize(*args, hash_function:) super(*args) @hash_function = hash_function end private def signature_algorithm_class OpenSSL::SignatureAlgorithm::RSAPKCS1 end def valid_key?(key) to_cose_key(key).is_a?(COSE::Key::RSA) end def to_pkey(key) case key when COSE::Key::RSA key.to_pkey when OpenSSL::PKey::RSA key else raise(COSE::Error, "Incompatible key for algorithm") end end end COSE::Algorithm.register(RSAPKCS1Algorithm.new(-257, "RS256", hash_function: "SHA256")) COSE::Algorithm.register(RSAPKCS1Algorithm.new(-258, "RS384", hash_function: "SHA384")) COSE::Algorithm.register(RSAPKCS1Algorithm.new(-259, "RS512", hash_function: "SHA512")) # Patch openssl-signature_algorithm gem to support discouraged/deprecated RSA-PKCS#1 with SHA-1 # (RS1 in JOSE/COSE terminology) algorithm needed for WebAuthn. OpenSSL::SignatureAlgorithm::RSAPKCS1.const_set( :ACCEPTED_HASH_FUNCTIONS, OpenSSL::SignatureAlgorithm::RSAPKCS1::ACCEPTED_HASH_FUNCTIONS + ["SHA1"] ) COSE::Algorithm.register(RSAPKCS1Algorithm.new(-65535, "RS1", hash_function: "SHA1")) webauthn-2.4.0/lib/webauthn/0000755000175000017500000000000013755257222014753 5ustar pravipraviwebauthn-2.4.0/lib/webauthn/u2f_migrator.rb0000644000175000017500000000477413755257222017714 0ustar pravipravi# frozen_string_literal: true require 'webauthn/fake_client' require 'webauthn/attestation_statement/fido_u2f' module WebAuthn class U2fMigrator def initialize(app_id:, certificate:, key_handle:, public_key:, counter:) @app_id = app_id @certificate = certificate @key_handle = key_handle @public_key = public_key @counter = counter end def authenticator_data @authenticator_data ||= WebAuthn::FakeAuthenticator::AuthenticatorData.new( rp_id_hash: OpenSSL::Digest::SHA256.digest(@app_id.to_s), credential: { id: credential_id, public_key: credential_cose_key }, sign_count: @counter, user_present: true, user_verified: false, aaguid: WebAuthn::AuthenticatorData::AttestedCredentialData::ZEROED_AAGUID ) end def credential @credential ||= begin hash = authenticator_data.send(:credential) WebAuthn::AuthenticatorData::AttestedCredentialData::Credential.new(hash[:id], hash[:public_key].serialize) end end def attestation_type WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC_OR_ATTCA end def attestation_trust_path @attestation_trust_path ||= [OpenSSL::X509::Certificate.new(Base64.strict_decode64(@certificate))] end private # https://fidoalliance.org/specs/fido-v2.0-rd-20180702/fido-client-to-authenticator-protocol-v2.0-rd-20180702.html#u2f-authenticatorMakeCredential-interoperability # Let credentialId be a credentialIdLength byte array initialized with CTAP1/U2F response key handle bytes. def credential_id Base64.urlsafe_decode64(@key_handle) end # Let x9encodedUserPublicKey be the user public key returned in the U2F registration response message [U2FRawMsgs]. # Let coseEncodedCredentialPublicKey be the result of converting x9encodedUserPublicKey’s value from ANS X9.62 / # Sec-1 v2 uncompressed curve point representation [SEC1V2] to COSE_Key representation ([RFC8152] Section 7). def credential_cose_key decoded_public_key = Base64.strict_decode64(@public_key) if WebAuthn::AttestationStatement::FidoU2f::PublicKey.uncompressed_point?(decoded_public_key) COSE::Key::EC2.new( alg: COSE::Algorithm.by_name("ES256").id, crv: 1, x: decoded_public_key[1..32], y: decoded_public_key[33..-1] ) else raise "expected U2F public key to be in uncompressed point format" end end end end webauthn-2.4.0/lib/webauthn/fake_authenticator.rb0000644000175000017500000000502713755257222021144 0ustar pravipravi# frozen_string_literal: true require "cbor" require "openssl" require "securerandom" require "webauthn/fake_authenticator/attestation_object" require "webauthn/fake_authenticator/authenticator_data" module WebAuthn class FakeAuthenticator def initialize @credentials = {} end def make_credential( rp_id:, client_data_hash:, user_present: true, user_verified: false, attested_credential_data: true, sign_count: nil, extensions: nil ) credential_id, credential_key, credential_sign_count = new_credential sign_count ||= credential_sign_count credentials[rp_id] ||= {} credentials[rp_id][credential_id] = { credential_key: credential_key, sign_count: sign_count + 1 } AttestationObject.new( client_data_hash: client_data_hash, rp_id_hash: hashed(rp_id), credential_id: credential_id, credential_key: credential_key, user_present: user_present, user_verified: user_verified, attested_credential_data: attested_credential_data, sign_count: sign_count, extensions: extensions ).serialize end def get_assertion( rp_id:, client_data_hash:, user_present: true, user_verified: false, aaguid: AuthenticatorData::AAGUID, sign_count: nil, extensions: nil ) credential_options = credentials[rp_id] if credential_options credential_id, credential = credential_options.first credential_key = credential[:credential_key] credential_sign_count = credential[:sign_count] authenticator_data = AuthenticatorData.new( rp_id_hash: hashed(rp_id), user_present: user_present, user_verified: user_verified, aaguid: aaguid, credential: nil, sign_count: sign_count || credential_sign_count, extensions: extensions ).serialize signature = credential_key.sign("SHA256", authenticator_data + client_data_hash) credential[:sign_count] += 1 { credential_id: credential_id, authenticator_data: authenticator_data, signature: signature } else raise "No credentials found for RP #{rp_id}" end end private attr_reader :credentials def new_credential [SecureRandom.random_bytes(16), OpenSSL::PKey::EC.new("prime256v1").generate_key, 0] end def hashed(target) OpenSSL::Digest::SHA256.digest(target) end end end webauthn-2.4.0/lib/webauthn/encoder.rb0000644000175000017500000000167013755257222016723 0ustar pravipravi# frozen_string_literal: true require "base64" module WebAuthn def self.standard_encoder @standard_encoder ||= Encoder.new end class Encoder # https://www.w3.org/TR/webauthn-2/#base64url-encoding STANDARD_ENCODING = :base64url attr_reader :encoding def initialize(encoding = STANDARD_ENCODING) @encoding = encoding end def encode(data) case encoding when :base64 Base64.strict_encode64(data) when :base64url Base64.urlsafe_encode64(data, padding: false) when nil, false data else raise "Unsupported or unknown encoding: #{encoding}" end end def decode(data) case encoding when :base64 Base64.strict_decode64(data) when :base64url Base64.urlsafe_decode64(data) when nil, false data else raise "Unsupported or unknown encoding: #{encoding}" end end end end webauthn-2.4.0/lib/webauthn/attestation_object.rb0000644000175000017500000000234513755257222021171 0ustar pravipravi# frozen_string_literal: true require "cbor" require "forwardable" require "openssl" require "webauthn/attestation_statement" require "webauthn/authenticator_data" module WebAuthn class AttestationObject extend Forwardable def self.deserialize(attestation_object) from_map(CBOR.decode(attestation_object)) end def self.from_map(map) new( authenticator_data: WebAuthn::AuthenticatorData.deserialize(map["authData"]), attestation_statement: WebAuthn::AttestationStatement.from(map["fmt"], map["attStmt"]) ) end attr_reader :authenticator_data, :attestation_statement def initialize(authenticator_data:, attestation_statement:) @authenticator_data = authenticator_data @attestation_statement = attestation_statement end def valid_attested_credential? authenticator_data.attested_credential_data_included? && authenticator_data.attested_credential_data.valid? end def valid_attestation_statement?(client_data_hash) attestation_statement.valid?(authenticator_data, client_data_hash) end def_delegators :authenticator_data, :credential, :aaguid def_delegators :attestation_statement, :attestation_certificate_key_id end end webauthn-2.4.0/lib/webauthn/attestation_statement.rb0000644000175000017500000000270713755257222021731 0ustar pravipravi# frozen_string_literal: true require "webauthn/attestation_statement/android_key" require "webauthn/attestation_statement/android_safetynet" require "webauthn/attestation_statement/fido_u2f" require "webauthn/attestation_statement/none" require "webauthn/attestation_statement/packed" require "webauthn/attestation_statement/tpm" require "webauthn/error" module WebAuthn module AttestationStatement class FormatNotSupportedError < Error; end ATTESTATION_FORMAT_NONE = "none" ATTESTATION_FORMAT_FIDO_U2F = "fido-u2f" ATTESTATION_FORMAT_PACKED = 'packed' ATTESTATION_FORMAT_ANDROID_SAFETYNET = "android-safetynet" ATTESTATION_FORMAT_ANDROID_KEY = "android-key" ATTESTATION_FORMAT_TPM = "tpm" FORMAT_TO_CLASS = { ATTESTATION_FORMAT_NONE => WebAuthn::AttestationStatement::None, ATTESTATION_FORMAT_FIDO_U2F => WebAuthn::AttestationStatement::FidoU2f, ATTESTATION_FORMAT_PACKED => WebAuthn::AttestationStatement::Packed, ATTESTATION_FORMAT_ANDROID_SAFETYNET => WebAuthn::AttestationStatement::AndroidSafetynet, ATTESTATION_FORMAT_ANDROID_KEY => WebAuthn::AttestationStatement::AndroidKey, ATTESTATION_FORMAT_TPM => WebAuthn::AttestationStatement::TPM }.freeze def self.from(format, statement) klass = FORMAT_TO_CLASS[format] if klass klass.new(statement) else raise(FormatNotSupportedError, "Unsupported attestation format '#{format}'") end end end end webauthn-2.4.0/lib/webauthn/authenticator_data/0000755000175000017500000000000013755257222020616 5ustar pravipraviwebauthn-2.4.0/lib/webauthn/authenticator_data/attested_credential_data.rb0000644000175000017500000000353013755257222026144 0ustar pravipravi# frozen_string_literal: true require "bindata" require "cose/key" require "webauthn/error" module WebAuthn class AttestedCredentialDataFormatError < WebAuthn::Error; end class AuthenticatorData < BinData::Record class AttestedCredentialData < BinData::Record AAGUID_LENGTH = 16 ZEROED_AAGUID = 0.chr * AAGUID_LENGTH ID_LENGTH_LENGTH = 2 endian :big string :raw_aaguid, length: AAGUID_LENGTH bit16 :id_length string :id, read_length: :id_length count_bytes_remaining :trailing_bytes_length string :trailing_bytes, length: :trailing_bytes_length # TODO: use keyword_init when we dropped Ruby 2.4 support Credential = Struct.new(:id, :public_key) do def public_key_object COSE::Key.deserialize(public_key).to_pkey end end def self.deserialize(data) read(data) rescue EOFError raise AttestedCredentialDataFormatError end def valid? valid_credential_public_key? end def aaguid raw_aaguid.unpack("H8H4H4H4H12").join("-") end def credential @credential ||= if valid? Credential.new(id, public_key) end end def length if valid? AAGUID_LENGTH + ID_LENGTH_LENGTH + id_length + public_key_length end end private def valid_credential_public_key? cose_key = COSE::Key.deserialize(public_key) !!cose_key.alg && WebAuthn.configuration.algorithms.include?(COSE::Algorithm.find(cose_key.alg).name) end def public_key trailing_bytes[0..public_key_length - 1] end def public_key_length @public_key_length ||= CBOR.encode(CBOR::Unpacker.new(StringIO.new(trailing_bytes)).each.first).length end end end end webauthn-2.4.0/lib/webauthn/configuration.rb0000644000175000017500000000354313755257222020154 0ustar pravipravi# frozen_string_literal: true require "openssl" require "webauthn/encoder" require "webauthn/error" module WebAuthn def self.configuration @configuration ||= Configuration.new end def self.configure yield(configuration) end class RootCertificateFinderNotSupportedError < Error; end class Configuration def self.if_pss_supported(algorithm) OpenSSL::PKey::RSA.instance_methods.include?(:verify_pss) ? algorithm : nil end DEFAULT_ALGORITHMS = ["ES256", if_pss_supported("PS256"), "RS256"].compact.freeze attr_accessor :algorithms attr_accessor :encoding attr_accessor :origin attr_accessor :rp_id attr_accessor :rp_name attr_accessor :verify_attestation_statement attr_accessor :credential_options_timeout attr_accessor :silent_authentication attr_accessor :acceptable_attestation_types attr_reader :attestation_root_certificates_finders def initialize @algorithms = DEFAULT_ALGORITHMS.dup @encoding = WebAuthn::Encoder::STANDARD_ENCODING @verify_attestation_statement = true @credential_options_timeout = 120000 @silent_authentication = false @acceptable_attestation_types = ['None', 'Self', 'Basic', 'AttCA', 'Basic_or_AttCA'] @attestation_root_certificates_finders = [] end # This is the user-data encoder. # Used to decode user input and to encode data provided to the user. def encoder @encoder ||= WebAuthn::Encoder.new(encoding) end def attestation_root_certificates_finders=(finders) if !finders.respond_to?(:each) finders = [finders] end finders.each do |finder| unless finder.respond_to?(:find) raise RootCertificateFinderNotSupportedError, "Finder must implement `find` method" end end @attestation_root_certificates_finders = finders end end end webauthn-2.4.0/lib/webauthn/public_key_credential_with_attestation.rb0000644000175000017500000000123213755257222025270 0ustar pravipravi# frozen_string_literal: true require "webauthn/authenticator_attestation_response" require "webauthn/public_key_credential" module WebAuthn class PublicKeyCredentialWithAttestation < PublicKeyCredential def self.response_class WebAuthn::AuthenticatorAttestationResponse end def verify(challenge, user_verification: nil) super response.verify(encoder.decode(challenge), user_verification: user_verification) true end def public_key if raw_public_key encoder.encode(raw_public_key) end end def raw_public_key response&.authenticator_data&.credential&.public_key end end end webauthn-2.4.0/lib/webauthn/security_utils.rb0000644000175000017500000000126213755257222020370 0ustar pravipravi# frozen_string_literal: true require "securecompare" module WebAuthn module SecurityUtils # Constant time string comparison, for variable length strings. # This code was adapted from Rails ActiveSupport::SecurityUtils # # The values are first processed by SHA256, so that we don't leak length info # via timing attacks. def secure_compare(first_string, second_string) first_string_sha256 = ::Digest::SHA256.digest(first_string) second_string_sha256 = ::Digest::SHA256.digest(second_string) SecureCompare.compare(first_string_sha256, second_string_sha256) && first_string == second_string end module_function :secure_compare end end webauthn-2.4.0/lib/webauthn/credential_request_options.rb0000644000175000017500000000200313755257222022730 0ustar pravipravi# frozen_string_literal: true require "webauthn/credential_options" module WebAuthn def self.credential_request_options warn( "DEPRECATION WARNING: `WebAuthn.credential_request_options` is deprecated."\ " Please use `WebAuthn::Credential.options_for_get` instead." ) CredentialRequestOptions.new.to_h end class CredentialRequestOptions < CredentialOptions attr_accessor :allow_credentials, :extensions, :user_verification def initialize(allow_credentials: [], extensions: nil, user_verification: nil) super() @allow_credentials = allow_credentials @extensions = extensions @user_verification = user_verification end def to_h options = { challenge: challenge, timeout: timeout, allowCredentials: allow_credentials } if extensions options[:extensions] = extensions end if user_verification options[:userVerification] = user_verification end options end end end webauthn-2.4.0/lib/webauthn/fake_client.rb0000644000175000017500000000747013755257222017554 0ustar pravipravi# frozen_string_literal: true require "openssl" require "securerandom" require "webauthn/authenticator_data" require "webauthn/encoder" require "webauthn/fake_authenticator" module WebAuthn class FakeClient TYPES = { create: "webauthn.create", get: "webauthn.get" }.freeze attr_reader :origin, :token_binding def initialize( origin = fake_origin, token_binding: nil, authenticator: WebAuthn::FakeAuthenticator.new, encoding: WebAuthn.configuration.encoding ) @origin = origin @token_binding = token_binding @authenticator = authenticator @encoding = encoding end def create( challenge: fake_challenge, rp_id: nil, user_present: true, user_verified: false, attested_credential_data: true, extensions: nil ) rp_id ||= URI.parse(origin).host client_data_json = data_json_for(:create, encoder.decode(challenge)) client_data_hash = hashed(client_data_json) attestation_object = authenticator.make_credential( rp_id: rp_id, client_data_hash: client_data_hash, user_present: user_present, user_verified: user_verified, attested_credential_data: attested_credential_data, extensions: extensions ) id = if attested_credential_data WebAuthn::AuthenticatorData .deserialize(CBOR.decode(attestation_object)["authData"]) .attested_credential_data .id else "id-for-pk-without-attested-credential-data" end { "type" => "public-key", "id" => internal_encoder.encode(id), "rawId" => encoder.encode(id), "clientExtensionResults" => extensions, "response" => { "attestationObject" => encoder.encode(attestation_object), "clientDataJSON" => encoder.encode(client_data_json) } } end def get(challenge: fake_challenge, rp_id: nil, user_present: true, user_verified: false, sign_count: nil, extensions: nil, user_handle: nil) rp_id ||= URI.parse(origin).host client_data_json = data_json_for(:get, encoder.decode(challenge)) client_data_hash = hashed(client_data_json) assertion = authenticator.get_assertion( rp_id: rp_id, client_data_hash: client_data_hash, user_present: user_present, user_verified: user_verified, sign_count: sign_count, extensions: extensions ) { "type" => "public-key", "id" => internal_encoder.encode(assertion[:credential_id]), "rawId" => encoder.encode(assertion[:credential_id]), "clientExtensionResults" => extensions, "response" => { "clientDataJSON" => encoder.encode(client_data_json), "authenticatorData" => encoder.encode(assertion[:authenticator_data]), "signature" => encoder.encode(assertion[:signature]), "userHandle" => user_handle ? encoder.encode(user_handle) : nil } } end private attr_reader :authenticator, :encoding def data_json_for(method, challenge) data = { type: type_for(method), challenge: internal_encoder.encode(challenge), origin: origin } if token_binding data[:tokenBinding] = token_binding end data.to_json end def encoder @encoder ||= WebAuthn::Encoder.new(encoding) end def internal_encoder WebAuthn.standard_encoder end def hashed(data) OpenSSL::Digest::SHA256.digest(data) end def fake_challenge encoder.encode(SecureRandom.random_bytes(32)) end def fake_origin "http://localhost#{rand(1000)}.test" end def type_for(method) TYPES[method] end end end webauthn-2.4.0/lib/webauthn/authenticator_attestation_response.rb0000644000175000017500000000402613755257222024511 0ustar pravipravi# frozen_string_literal: true require "cbor" require "forwardable" require "uri" require "openssl" require "webauthn/attestation_object" require "webauthn/authenticator_response" require "webauthn/client_data" require "webauthn/encoder" module WebAuthn class AttestationStatementVerificationError < VerificationError; end class AttestationTrustworthinessVerificationError < VerificationError; end class AttestedCredentialVerificationError < VerificationError; end class AuthenticatorAttestationResponse < AuthenticatorResponse extend Forwardable def self.from_client(response) encoder = WebAuthn.configuration.encoder new( attestation_object: encoder.decode(response["attestationObject"]), client_data_json: encoder.decode(response["clientDataJSON"]) ) end attr_reader :attestation_type, :attestation_trust_path def initialize(attestation_object:, **options) super(**options) @attestation_object_bytes = attestation_object end def verify(expected_challenge, expected_origin = nil, user_verification: nil, rp_id: nil) super verify_item(:attested_credential) if WebAuthn.configuration.verify_attestation_statement verify_item(:attestation_statement) end true end def attestation_object @attestation_object ||= WebAuthn::AttestationObject.deserialize(attestation_object_bytes) end def_delegators( :attestation_object, :aaguid, :attestation_statement, :attestation_certificate_key_id, :authenticator_data, :credential ) alias_method :attestation_certificate_key, :attestation_certificate_key_id private attr_reader :attestation_object_bytes def type WebAuthn::TYPES[:create] end def valid_attested_credential? attestation_object.valid_attested_credential? end def valid_attestation_statement? @attestation_type, @attestation_trust_path = attestation_object.valid_attestation_statement?(client_data.hash) end end end webauthn-2.4.0/lib/webauthn/attestation_statement/0000755000175000017500000000000013755257222021376 5ustar pravipraviwebauthn-2.4.0/lib/webauthn/attestation_statement/android_key.rb0000644000175000017500000000436113755257222024217 0ustar pravipravi# frozen_string_literal: true require "android_key_attestation" require "openssl" require "webauthn/attestation_statement/base" module WebAuthn module AttestationStatement class AndroidKey < Base def valid?(authenticator_data, client_data_hash) valid_signature?(authenticator_data, client_data_hash) && matching_public_key?(authenticator_data) && valid_attestation_challenge?(client_data_hash) && all_applications_fields_not_set? && valid_authorization_list_origin? && valid_authorization_list_purpose? && trustworthy?(aaguid: authenticator_data.aaguid) && [attestation_type, attestation_trust_path] end private def matching_public_key?(authenticator_data) attestation_certificate.public_key.to_der == authenticator_data.credential.public_key_object.to_der end def valid_attestation_challenge?(client_data_hash) android_key_attestation.verify_challenge(client_data_hash) rescue AndroidKeyAttestation::ChallengeMismatchError false end def valid_certificate_chain?(aaguid: nil, **_) android_key_attestation.verify_certificate_chain(root_certificates: root_certificates(aaguid: aaguid)) rescue AndroidKeyAttestation::CertificateVerificationError false end def all_applications_fields_not_set? !tee_enforced.all_applications && !software_enforced.all_applications end def valid_authorization_list_origin? tee_enforced.origin == :generated || software_enforced.origin == :generated end def valid_authorization_list_purpose? tee_enforced.purpose == [:sign] || software_enforced.purpose == [:sign] end def tee_enforced android_key_attestation.tee_enforced end def software_enforced android_key_attestation.software_enforced end def attestation_type WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC end def default_root_certificates AndroidKeyAttestation::Statement::GOOGLE_ROOT_CERTIFICATES end def android_key_attestation @android_key_attestation ||= AndroidKeyAttestation::Statement.new(*certificates) end end end end webauthn-2.4.0/lib/webauthn/attestation_statement/tpm.rb0000644000175000017500000000465713755257222022537 0ustar pravipravi# frozen_string_literal: true require "cose/algorithm" require "openssl" require "tpm/key_attestation" require "webauthn/attestation_statement/base" module WebAuthn module AttestationStatement class TPM < Base TPM_V2 = "2.0" COSE_ALG_TO_TPM = { "RS1" => { signature: ::TPM::ALG_RSASSA, hash: ::TPM::ALG_SHA1 }, "RS256" => { signature: ::TPM::ALG_RSASSA, hash: ::TPM::ALG_SHA256 }, "PS256" => { signature: ::TPM::ALG_RSAPSS, hash: ::TPM::ALG_SHA256 }, "ES256" => { signature: ::TPM::ALG_ECDSA, hash: ::TPM::ALG_SHA256 }, }.freeze def valid?(authenticator_data, client_data_hash) attestation_type == ATTESTATION_TYPE_ATTCA && ver == TPM_V2 && valid_key_attestation?( authenticator_data.data + client_data_hash, authenticator_data.credential.public_key_object, authenticator_data.aaguid ) && matching_aaguid?(authenticator_data.attested_credential_data.raw_aaguid) && trustworthy?(aaguid: authenticator_data.aaguid) && [attestation_type, attestation_trust_path] end private def valid_key_attestation?(certified_extra_data, key, aaguid) key_attestation = ::TPM::KeyAttestation.new( statement["certInfo"], signature, statement["pubArea"], certificates, OpenSSL::Digest.digest(cose_algorithm.hash_function, certified_extra_data), signature_algorithm: tpm_algorithm[:signature], hash_algorithm: tpm_algorithm[:hash], root_certificates: root_certificates(aaguid: aaguid) ) key_attestation.valid? && key_attestation.key && key_attestation.key.to_pem == key.to_pem end def valid_certificate_chain?(**_) # Already performed as part of #valid_key_attestation? true end def default_root_certificates ::TPM::KeyAttestation::ROOT_CERTIFICATES end def tpm_algorithm COSE_ALG_TO_TPM[cose_algorithm.name] || raise("Unsupported algorithm #{cose_algorithm.name}") end def ver statement["ver"] end def cose_algorithm @cose_algorithm ||= COSE::Algorithm.find(algorithm) end def attestation_type if raw_certificates ATTESTATION_TYPE_ATTCA else raise "Attestation type invalid" end end end end end webauthn-2.4.0/lib/webauthn/attestation_statement/base.rb0000644000175000017500000001242213755257222022636 0ustar pravipravi# frozen_string_literal: true require "cose/algorithm" require "cose/error" require "cose/rsapkcs1_algorithm" require "openssl" require "webauthn/authenticator_data/attested_credential_data" require "webauthn/error" module WebAuthn module AttestationStatement class UnsupportedAlgorithm < Error; end ATTESTATION_TYPE_NONE = "None" ATTESTATION_TYPE_BASIC = "Basic" ATTESTATION_TYPE_SELF = "Self" ATTESTATION_TYPE_ATTCA = "AttCA" ATTESTATION_TYPE_BASIC_OR_ATTCA = "Basic_or_AttCA" ATTESTATION_TYPES_WITH_ROOT = [ ATTESTATION_TYPE_BASIC, ATTESTATION_TYPE_BASIC_OR_ATTCA, ATTESTATION_TYPE_ATTCA ].freeze class Base AAGUID_EXTENSION_OID = "1.3.6.1.4.1.45724.1.1.4" def initialize(statement) @statement = statement end def valid?(_authenticator_data, _client_data_hash) raise NotImplementedError end def format WebAuthn::AttestationStatement::FORMAT_TO_CLASS.key(self.class) end def attestation_certificate certificates&.first end def certificate_chain if certificates certificates[1..-1] end end def attestation_certificate_key_id raw_subject_key_identifier&.unpack("H*")&.[](0) end private attr_reader :statement def matching_aaguid?(attested_credential_data_aaguid) extension = attestation_certificate&.extensions&.detect { |ext| ext.oid == AAGUID_EXTENSION_OID } if extension # `extension.value` mangles data into ASCII, so we must manually compare bytes # see https://github.com/ruby/openssl/pull/234 extension.to_der[-WebAuthn::AuthenticatorData::AttestedCredentialData::AAGUID_LENGTH..-1] == attested_credential_data_aaguid else true end end def certificates @certificates ||= raw_certificates&.map do |raw_certificate| OpenSSL::X509::Certificate.new(raw_certificate) end end def algorithm statement["alg"] end def raw_certificates statement["x5c"] end def signature statement["sig"] end def attestation_trust_path if certificates&.any? certificates end end def trustworthy?(aaguid: nil, attestation_certificate_key_id: nil) if ATTESTATION_TYPES_WITH_ROOT.include?(attestation_type) configuration.acceptable_attestation_types.include?(attestation_type) && valid_certificate_chain?(aaguid: aaguid, attestation_certificate_key_id: attestation_certificate_key_id) else configuration.acceptable_attestation_types.include?(attestation_type) end end def valid_certificate_chain?(aaguid: nil, attestation_certificate_key_id: nil) attestation_root_certificates_store( aaguid: aaguid, attestation_certificate_key_id: attestation_certificate_key_id ).verify(attestation_certificate, attestation_trust_path) end def attestation_root_certificates_store(aaguid: nil, attestation_certificate_key_id: nil) OpenSSL::X509::Store.new.tap do |store| root_certificates( aaguid: aaguid, attestation_certificate_key_id: attestation_certificate_key_id ).each do |cert| store.add_cert(cert) end end end def root_certificates(aaguid: nil, attestation_certificate_key_id: nil) root_certificates = configuration.attestation_root_certificates_finders.reduce([]) do |certs, finder| if certs.empty? finder.find( attestation_format: format, aaguid: aaguid, attestation_certificate_key_id: attestation_certificate_key_id ) || [] else certs end end if root_certificates.empty? && respond_to?(:default_root_certificates, true) default_root_certificates else root_certificates end end def raw_subject_key_identifier extension = attestation_certificate.extensions.detect { |ext| ext.oid == "subjectKeyIdentifier" } return unless extension ext_asn1 = OpenSSL::ASN1.decode(extension.to_der) ext_value = ext_asn1.value.last OpenSSL::ASN1.decode(ext_value.value).value end def valid_signature?(authenticator_data, client_data_hash, public_key = attestation_certificate.public_key) raise("Incompatible algorithm and key") unless cose_algorithm.compatible_key?(public_key) cose_algorithm.verify( public_key, signature, verification_data(authenticator_data, client_data_hash) ) rescue COSE::Error false end def verification_data(authenticator_data, client_data_hash) authenticator_data.data + client_data_hash end def cose_algorithm @cose_algorithm ||= COSE::Algorithm.find(algorithm).tap do |alg| alg && configuration.algorithms.include?(alg.name) || raise(UnsupportedAlgorithm, "Unsupported algorithm #{algorithm}") end end def configuration WebAuthn.configuration end end end end webauthn-2.4.0/lib/webauthn/attestation_statement/fido_u2f.rb0000644000175000017500000000450113755257222023420 0ustar pravipravi# frozen_string_literal: true require "cose" require "openssl" require "webauthn/attestation_statement/base" require "webauthn/attestation_statement/fido_u2f/public_key" module WebAuthn module AttestationStatement class FidoU2f < Base VALID_ATTESTATION_CERTIFICATE_COUNT = 1 VALID_ATTESTATION_CERTIFICATE_ALGORITHM = COSE::Algorithm.by_name("ES256") VALID_ATTESTATION_CERTIFICATE_KEY_CURVE = COSE::Key::Curve.by_name("P-256") def valid?(authenticator_data, client_data_hash) valid_format? && valid_certificate_public_key? && valid_credential_public_key?(authenticator_data.credential.public_key) && valid_aaguid?(authenticator_data.attested_credential_data.raw_aaguid) && valid_signature?(authenticator_data, client_data_hash) && trustworthy?(attestation_certificate_key_id: attestation_certificate_key_id) && [attestation_type, attestation_trust_path] end private def valid_format? !!(raw_certificates && signature) && raw_certificates.length == VALID_ATTESTATION_CERTIFICATE_COUNT end def valid_certificate_public_key? certificate_public_key.is_a?(OpenSSL::PKey::EC) && certificate_public_key.group.curve_name == VALID_ATTESTATION_CERTIFICATE_KEY_CURVE.pkey_name && certificate_public_key.check_key end def valid_credential_public_key?(public_key_bytes) public_key_u2f(public_key_bytes).valid? end def certificate_public_key attestation_certificate.public_key end def valid_aaguid?(attested_credential_data_aaguid) attested_credential_data_aaguid == WebAuthn::AuthenticatorData::AttestedCredentialData::ZEROED_AAGUID end def algorithm VALID_ATTESTATION_CERTIFICATE_ALGORITHM.id end def verification_data(authenticator_data, client_data_hash) "\x00" + authenticator_data.rp_id_hash + client_data_hash + authenticator_data.credential.id + public_key_u2f(authenticator_data.credential.public_key).to_uncompressed_point end def public_key_u2f(cose_key_data) PublicKey.new(cose_key_data) end def attestation_type WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC_OR_ATTCA end end end end webauthn-2.4.0/lib/webauthn/attestation_statement/fido_u2f/0000755000175000017500000000000013755257222023073 5ustar pravipraviwebauthn-2.4.0/lib/webauthn/attestation_statement/fido_u2f/public_key.rb0000644000175000017500000000212113755257222025542 0ustar pravipravi# frozen_string_literal: true require "cose/algorithm" require "cose/key/ec2" require "webauthn/attestation_statement/base" module WebAuthn module AttestationStatement class FidoU2f < Base class PublicKey COORDINATE_LENGTH = 32 UNCOMPRESSED_FORM_INDICATOR = "\x04" def self.uncompressed_point?(data) data.size && data.length == UNCOMPRESSED_FORM_INDICATOR.length + COORDINATE_LENGTH * 2 && data[0] == UNCOMPRESSED_FORM_INDICATOR end def initialize(data) @data = data end def valid? data.size >= COORDINATE_LENGTH * 2 && cose_key.x.length == COORDINATE_LENGTH && cose_key.y.length == COORDINATE_LENGTH && cose_key.alg == COSE::Algorithm.by_name("ES256").id end def to_uncompressed_point UNCOMPRESSED_FORM_INDICATOR + cose_key.x + cose_key.y end private attr_reader :data def cose_key @cose_key ||= COSE::Key::EC2.deserialize(data) end end end end end webauthn-2.4.0/lib/webauthn/attestation_statement/android_safetynet.rb0000644000175000017500000000404113755257222025424 0ustar pravipravi# frozen_string_literal: true require "safety_net_attestation" require "openssl" require "webauthn/attestation_statement/base" module WebAuthn module AttestationStatement # Implements https://www.w3.org/TR/webauthn-1/#sctn-android-safetynet-attestation class AndroidSafetynet < Base def valid?(authenticator_data, client_data_hash) valid_response?(authenticator_data, client_data_hash) && valid_version? && cts_profile_match? && trustworthy?(aaguid: authenticator_data.aaguid) && [attestation_type, attestation_trust_path] end def attestation_certificate attestation_trust_path.first end private def valid_response?(authenticator_data, client_data_hash) nonce = Digest::SHA256.base64digest(authenticator_data.data + client_data_hash) begin attestation_response .verify(nonce, trusted_certificates: root_certificates(aaguid: authenticator_data.aaguid), time: time) rescue SafetyNetAttestation::Error false end end # TODO: improve once the spec has clarifications https://github.com/w3c/webauthn/issues/968 def valid_version? !statement["ver"].empty? end def cts_profile_match? attestation_response.cts_profile_match? end def valid_certificate_chain?(**_) # Already performed as part of #valid_response? true end def attestation_type WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC end # SafetyNetAttestation returns full chain including root, WebAuthn expects only the x5c certificates def attestation_trust_path attestation_response.certificate_chain[0..-2] end def attestation_response @attestation_response ||= SafetyNetAttestation::Statement.new(statement["response"]) end def default_root_certificates SafetyNetAttestation::Statement::GOOGLE_ROOT_CERTIFICATES end def time Time.now end end end end webauthn-2.4.0/lib/webauthn/attestation_statement/packed.rb0000644000175000017500000000454213755257222023157 0ustar pravipravi# frozen_string_literal: true require "openssl" require "webauthn/attestation_statement/base" module WebAuthn # Implements https://www.w3.org/TR/2018/CR-webauthn-20180807/#packed-attestation module AttestationStatement class Packed < Base # Follows "Verification procedure" def valid?(authenticator_data, client_data_hash) valid_format? && valid_algorithm?(authenticator_data.credential) && valid_ec_public_keys?(authenticator_data.credential) && meet_certificate_requirement? && matching_aaguid?(authenticator_data.attested_credential_data.raw_aaguid) && valid_signature?(authenticator_data, client_data_hash) && trustworthy?(aaguid: authenticator_data.aaguid) && [attestation_type, attestation_trust_path] end private def valid_algorithm?(credential) !self_attestation? || algorithm == COSE::Key.deserialize(credential.public_key).alg end def self_attestation? !raw_certificates end def valid_format? algorithm && signature end def valid_ec_public_keys?(credential) (certificates&.map(&:public_key) || [credential.public_key_object]) .select { |pkey| pkey.is_a?(OpenSSL::PKey::EC) } .all? { |pkey| pkey.check_key } end # Check https://www.w3.org/TR/2018/CR-webauthn-20180807/#packed-attestation-cert-requirements def meet_certificate_requirement? if attestation_certificate subject = attestation_certificate.subject.to_a attestation_certificate.version == 2 && subject.assoc('OU')&.at(1) == "Authenticator Attestation" && attestation_certificate.extensions.find { |ext| ext.oid == 'basicConstraints' }&.value == 'CA:FALSE' else true end end def attestation_type if attestation_trust_path WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC_OR_ATTCA # FIXME: use metadata if available else WebAuthn::AttestationStatement::ATTESTATION_TYPE_SELF end end def valid_signature?(authenticator_data, client_data_hash) super( authenticator_data, client_data_hash, attestation_certificate&.public_key || authenticator_data.credential.public_key_object ) end end end end webauthn-2.4.0/lib/webauthn/attestation_statement/none.rb0000644000175000017500000000052213755257222022661 0ustar pravipravi# frozen_string_literal: true require "webauthn/attestation_statement/base" module WebAuthn module AttestationStatement class None < Base def valid?(*_args) if statement == {} [WebAuthn::AttestationStatement::ATTESTATION_TYPE_NONE, nil] else false end end end end end webauthn-2.4.0/lib/webauthn/public_key_credential/0000755000175000017500000000000013755257222021273 5ustar pravipraviwebauthn-2.4.0/lib/webauthn/public_key_credential/entity.rb0000644000175000017500000000130313755257222023131 0ustar pravipravi# frozen_string_literal: true require "awrence" module WebAuthn class PublicKeyCredential class Entity attr_reader :name, :icon def initialize(name:, icon: nil) @name = name @icon = icon end def as_json to_hash.to_camelback_keys end private def to_hash hash = {} attributes.each do |attribute_name| value = send(attribute_name) if value.respond_to?(:as_json) value = value.as_json end if value hash[attribute_name] = value end end hash end def attributes [:name, :icon] end end end end webauthn-2.4.0/lib/webauthn/public_key_credential/request_options.rb0000644000175000017500000000160513755257222025065 0ustar pravipravi# frozen_string_literal: true require "webauthn/public_key_credential/options" module WebAuthn class PublicKeyCredential class RequestOptions < Options attr_accessor :rp_id, :allow, :user_verification def initialize(rp_id: nil, allow_credentials: nil, allow: nil, user_verification: nil, **keyword_arguments) super(**keyword_arguments) @rp_id = rp_id || configuration.rp_id @allow_credentials = allow_credentials @allow = allow @user_verification = user_verification end def allow_credentials @allow_credentials || allow_credentials_from_allow || [] end private def attributes super.concat([:allow_credentials, :rp_id, :user_verification]) end def allow_credentials_from_allow if allow as_public_key_descriptors(allow) end end end end end webauthn-2.4.0/lib/webauthn/public_key_credential/creation_options.rb0000644000175000017500000000422713755257222025204 0ustar pravipravi# frozen_string_literal: true require "cose/algorithm" require "webauthn/public_key_credential/options" require "webauthn/public_key_credential/rp_entity" require "webauthn/public_key_credential/user_entity" module WebAuthn class PublicKeyCredential class CreationOptions < Options attr_accessor( :attestation, :authenticator_selection, :exclude, :algs, :rp, :user ) def initialize( attestation: nil, authenticator_selection: nil, exclude_credentials: nil, exclude: nil, pub_key_cred_params: nil, algs: nil, rp: {}, user:, **keyword_arguments ) super(**keyword_arguments) @attestation = attestation @authenticator_selection = authenticator_selection @exclude_credentials = exclude_credentials @exclude = exclude @pub_key_cred_params = pub_key_cred_params @algs = algs @rp = if rp.is_a?(Hash) rp[:name] ||= configuration.rp_name rp[:id] ||= configuration.rp_id RPEntity.new(**rp) else rp end @user = if user.is_a?(Hash) UserEntity.new(**user) else user end end def exclude_credentials @exclude_credentials || exclude_credentials_from_exclude end def pub_key_cred_params @pub_key_cred_params || pub_key_cred_params_from_algs end private def attributes super.concat([:rp, :user, :pub_key_cred_params, :attestation, :authenticator_selection, :exclude_credentials]) end def exclude_credentials_from_exclude if exclude as_public_key_descriptors(exclude) end end def pub_key_cred_params_from_algs Array(algs || configuration.algorithms).map do |alg| alg_id = if alg.is_a?(String) || alg.is_a?(Symbol) COSE::Algorithm.by_name(alg.to_s).id else alg end { type: TYPE_PUBLIC_KEY, alg: alg_id } end end end end end webauthn-2.4.0/lib/webauthn/public_key_credential/user_entity.rb0000644000175000017500000000073213755257222024174 0ustar pravipravi# frozen_string_literal: true require "webauthn/public_key_credential/entity" module WebAuthn class PublicKeyCredential class UserEntity < Entity attr_reader :id, :display_name def initialize(id:, display_name: nil, **keyword_arguments) super(**keyword_arguments) @id = id @display_name = display_name || name end private def attributes super.concat([:id, :display_name]) end end end end webauthn-2.4.0/lib/webauthn/public_key_credential/rp_entity.rb0000644000175000017500000000057613755257222023645 0ustar pravipravi# frozen_string_literal: true require "webauthn/public_key_credential/entity" module WebAuthn class PublicKeyCredential class RPEntity < Entity attr_reader :id def initialize(id: nil, **keyword_arguments) super(**keyword_arguments) @id = id end private def attributes super.concat([:id]) end end end end webauthn-2.4.0/lib/webauthn/public_key_credential/options.rb0000644000175000017500000000264013755257222023315 0ustar pravipravi# frozen_string_literal: true require "awrence" require "securerandom" module WebAuthn class PublicKeyCredential class Options CHALLENGE_LENGTH = 32 attr_reader :timeout, :extensions def initialize(timeout: default_timeout, extensions: nil) @timeout = timeout @extensions = extensions end def challenge encoder.encode(raw_challenge) end # Argument wildcard for Ruby on Rails controller automatic object JSON serialization def as_json(*) to_hash.to_camelback_keys end private def to_hash hash = {} attributes.each do |attribute_name| value = send(attribute_name) if value.respond_to?(:as_json) value = value.as_json end if value hash[attribute_name] = value end end hash end def attributes [:challenge, :timeout, :extensions] end def encoder WebAuthn.configuration.encoder end def raw_challenge @raw_challenge ||= SecureRandom.random_bytes(CHALLENGE_LENGTH) end def default_timeout configuration.credential_options_timeout end def configuration WebAuthn.configuration end def as_public_key_descriptors(ids) Array(ids).map { |id| { type: TYPE_PUBLIC_KEY, id: id } } end end end end webauthn-2.4.0/lib/webauthn/authenticator_data.rb0000644000175000017500000000575313755257222021155 0ustar pravipravi# frozen_string_literal: true require "bindata" require "webauthn/authenticator_data/attested_credential_data" require "webauthn/error" module WebAuthn class AuthenticatorDataFormatError < WebAuthn::Error; end class AuthenticatorData < BinData::Record RP_ID_HASH_LENGTH = 32 FLAGS_LENGTH = 1 SIGN_COUNT_LENGTH = 4 endian :big count_bytes_remaining :data_length string :rp_id_hash, length: RP_ID_HASH_LENGTH struct :flags do bit1 :extension_data_included bit1 :attested_credential_data_included bit1 :reserved_for_future_use_4 bit1 :reserved_for_future_use_3 bit1 :reserved_for_future_use_2 bit1 :user_verified bit1 :reserved_for_future_use_1 bit1 :user_present end bit32 :sign_count count_bytes_remaining :trailing_bytes_length string :trailing_bytes, length: :trailing_bytes_length def self.deserialize(data) read(data) rescue EOFError raise AuthenticatorDataFormatError end def data to_binary_s end def valid? (!attested_credential_data_included? || attested_credential_data.valid?) && (!extension_data_included? || extension_data) && valid_length? end def user_flagged? user_present? || user_verified? end def user_present? flags.user_present == 1 end def user_verified? flags.user_verified == 1 end def attested_credential_data_included? flags.attested_credential_data_included == 1 end def extension_data_included? flags.extension_data_included == 1 end def credential if attested_credential_data_included? attested_credential_data.credential end end def attested_credential_data @attested_credential_data ||= AttestedCredentialData.deserialize(trailing_bytes) rescue AttestedCredentialDataFormatError raise AuthenticatorDataFormatError end def extension_data @extension_data ||= CBOR.decode(raw_extension_data) end def aaguid raw_aaguid = attested_credential_data.raw_aaguid unless raw_aaguid == WebAuthn::AuthenticatorData::AttestedCredentialData::ZEROED_AAGUID attested_credential_data.aaguid end end private def valid_length? data_length == base_length + attested_credential_data_length + extension_data_length end def raw_extension_data if extension_data_included? if attested_credential_data_included? trailing_bytes[attested_credential_data.length..-1] else trailing_bytes.snapshot end end end def attested_credential_data_length if attested_credential_data_included? attested_credential_data.length else 0 end end def extension_data_length if extension_data_included? raw_extension_data.length else 0 end end def base_length RP_ID_HASH_LENGTH + FLAGS_LENGTH + SIGN_COUNT_LENGTH end end end webauthn-2.4.0/lib/webauthn/credential_rp_entity.rb0000644000175000017500000000021513755257222021505 0ustar pravipravi# frozen_string_literal: true require "webauthn/credential_entity" module WebAuthn class CredentialRPEntity < CredentialEntity end end webauthn-2.4.0/lib/webauthn/authenticator_response.rb0000644000175000017500000000616313755257222022076 0ustar pravipravi# frozen_string_literal: true require "webauthn/authenticator_data" require "webauthn/client_data" require "webauthn/error" require "webauthn/security_utils" module WebAuthn TYPES = { create: "webauthn.create", get: "webauthn.get" }.freeze class VerificationError < Error; end class AuthenticatorDataVerificationError < VerificationError; end class ChallengeVerificationError < VerificationError; end class OriginVerificationError < VerificationError; end class RpIdVerificationError < VerificationError; end class TokenBindingVerificationError < VerificationError; end class TypeVerificationError < VerificationError; end class UserPresenceVerificationError < VerificationError; end class UserVerifiedVerificationError < VerificationError; end class AuthenticatorResponse def initialize(client_data_json:) @client_data_json = client_data_json end def verify(expected_challenge, expected_origin = nil, user_verification: nil, rp_id: nil) expected_origin ||= WebAuthn.configuration.origin || raise("Unspecified expected origin") rp_id ||= WebAuthn.configuration.rp_id verify_item(:type) verify_item(:token_binding) verify_item(:challenge, expected_challenge) verify_item(:origin, expected_origin) verify_item(:authenticator_data) verify_item(:rp_id, rp_id || rp_id_from_origin(expected_origin)) if !WebAuthn.configuration.silent_authentication verify_item(:user_presence) end if user_verification verify_item(:user_verified) end true end def valid?(*args, **keyword_arguments) verify(*args, **keyword_arguments) rescue WebAuthn::VerificationError false end def client_data @client_data ||= WebAuthn::ClientData.new(client_data_json) end private attr_reader :client_data_json def verify_item(item, *args) if send("valid_#{item}?", *args) true else camelized_item = item.to_s.split('_').map { |w| w.capitalize }.join error_const_name = "WebAuthn::#{camelized_item}VerificationError" raise Object.const_get(error_const_name) end end def valid_type? client_data.type == type end def valid_token_binding? client_data.valid_token_binding_format? end def valid_challenge?(expected_challenge) WebAuthn::SecurityUtils.secure_compare(client_data.challenge, expected_challenge) end def valid_origin?(expected_origin) expected_origin && (client_data.origin == expected_origin) end def valid_rp_id?(rp_id) OpenSSL::Digest::SHA256.digest(rp_id) == authenticator_data.rp_id_hash end def valid_authenticator_data? authenticator_data.valid? rescue WebAuthn::AuthenticatorDataFormatError false end def valid_user_presence? authenticator_data.user_flagged? end def valid_user_verified? authenticator_data.user_verified? end def rp_id_from_origin(expected_origin) URI.parse(expected_origin).host end def type raise NotImplementedError, "Please define #type method in subclass" end end end webauthn-2.4.0/lib/webauthn/credential_entity.rb0000644000175000017500000000023613755257222021007 0ustar pravipravi# frozen_string_literal: true module WebAuthn class CredentialEntity attr_reader :name def initialize(name:) @name = name end end end webauthn-2.4.0/lib/webauthn/credential_options.rb0000644000175000017500000000047513755257222021173 0ustar pravipravi# frozen_string_literal: true require "securerandom" module WebAuthn class CredentialOptions CHALLENGE_LENGTH = 32 def challenge @challenge ||= SecureRandom.random_bytes(CHALLENGE_LENGTH) end def timeout @timeout = WebAuthn.configuration.credential_options_timeout end end end webauthn-2.4.0/lib/webauthn/credential_user_entity.rb0000644000175000017500000000052713755257222022050 0ustar pravipravi# frozen_string_literal: true require "webauthn/credential_entity" module WebAuthn class CredentialUserEntity < CredentialEntity attr_reader :id, :display_name def initialize(id:, display_name: nil, **keyword_arguments) super(**keyword_arguments) @id = id @display_name = display_name || name end end end webauthn-2.4.0/lib/webauthn/error.rb0000644000175000017500000000012613755257222016430 0ustar pravipravi# frozen_string_literal: true module WebAuthn class Error < StandardError; end end webauthn-2.4.0/lib/webauthn/credential_creation_options.rb0000644000175000017500000000477013755257222023061 0ustar pravipravi# frozen_string_literal: true require "cose/algorithm" require "webauthn/credential_options" require "webauthn/credential_rp_entity" require "webauthn/credential_user_entity" module WebAuthn def self.credential_creation_options(rp_name: nil, user_name: "web-user", display_name: "web-user", user_id: "1") warn( "DEPRECATION WARNING: `WebAuthn.credential_creation_options` is deprecated."\ " Please use `WebAuthn::Credential.options_for_create` instead." ) CredentialCreationOptions.new( rp_name: rp_name, user_id: user_id, user_name: user_name, user_display_name: display_name ).to_h end class CredentialCreationOptions < CredentialOptions DEFAULT_RP_NAME = "web-server" attr_accessor :attestation, :authenticator_selection, :exclude_credentials, :extensions def initialize( attestation: nil, authenticator_selection: nil, exclude_credentials: nil, extensions: nil, user_id:, user_name:, user_display_name: nil, rp_name: nil ) super() @attestation = attestation @authenticator_selection = authenticator_selection @exclude_credentials = exclude_credentials @extensions = extensions @user_id = user_id @user_name = user_name @user_display_name = user_display_name @rp_name = rp_name end def to_h options = { challenge: challenge, pubKeyCredParams: pub_key_cred_params, timeout: timeout, user: { id: user.id, name: user.name, displayName: user.display_name }, rp: { name: rp.name } } if attestation options[:attestation] = attestation end if authenticator_selection options[:authenticatorSelection] = authenticator_selection end if exclude_credentials options[:excludeCredentials] = exclude_credentials end if extensions options[:extensions] = extensions end options end def pub_key_cred_params configuration.algorithms.map do |alg_name| { type: "public-key", alg: COSE::Algorithm.by_name(alg_name).id } end end def rp @rp ||= CredentialRPEntity.new(name: rp_name || configuration.rp_name || DEFAULT_RP_NAME) end def user @user ||= CredentialUserEntity.new(id: user_id, name: user_name, display_name: user_display_name) end private attr_reader :user_id, :user_name, :user_display_name, :rp_name def configuration WebAuthn.configuration end end end webauthn-2.4.0/lib/webauthn/public_key_credential_with_assertion.rb0000644000175000017500000000137213755257222024745 0ustar pravipravi# frozen_string_literal: true require "webauthn/authenticator_assertion_response" require "webauthn/public_key_credential" module WebAuthn class PublicKeyCredentialWithAssertion < PublicKeyCredential def self.response_class WebAuthn::AuthenticatorAssertionResponse end def verify(challenge, public_key:, sign_count:, user_verification: nil) super response.verify( encoder.decode(challenge), public_key: encoder.decode(public_key), sign_count: sign_count, user_verification: user_verification ) true end def user_handle if raw_user_handle encoder.encode(raw_user_handle) end end def raw_user_handle response.user_handle end end end webauthn-2.4.0/lib/webauthn/authenticator_assertion_response.rb0000644000175000017500000000413313755257222024160 0ustar pravipravi# frozen_string_literal: true require "webauthn/authenticator_data" require "webauthn/authenticator_response" require "webauthn/encoder" require "webauthn/public_key" module WebAuthn class SignatureVerificationError < VerificationError; end class SignCountVerificationError < VerificationError; end class AuthenticatorAssertionResponse < AuthenticatorResponse def self.from_client(response) encoder = WebAuthn.configuration.encoder user_handle = if response["userHandle"] encoder.decode(response["userHandle"]) end new( authenticator_data: encoder.decode(response["authenticatorData"]), client_data_json: encoder.decode(response["clientDataJSON"]), signature: encoder.decode(response["signature"]), user_handle: user_handle ) end attr_reader :user_handle def initialize(authenticator_data:, signature:, user_handle: nil, **options) super(**options) @authenticator_data_bytes = authenticator_data @signature = signature @user_handle = user_handle end def verify(expected_challenge, expected_origin = nil, public_key:, sign_count:, user_verification: nil, rp_id: nil) super(expected_challenge, expected_origin, user_verification: user_verification, rp_id: rp_id) verify_item(:signature, WebAuthn::PublicKey.deserialize(public_key)) verify_item(:sign_count, sign_count) true end def authenticator_data @authenticator_data ||= WebAuthn::AuthenticatorData.deserialize(authenticator_data_bytes) end private attr_reader :authenticator_data_bytes, :signature def valid_signature?(webauthn_public_key) webauthn_public_key.verify(signature, authenticator_data_bytes + client_data.hash) end def valid_sign_count?(stored_sign_count) normalized_sign_count = stored_sign_count || 0 if authenticator_data.sign_count.nonzero? || normalized_sign_count.nonzero? authenticator_data.sign_count > normalized_sign_count else true end end def type WebAuthn::TYPES[:get] end end end webauthn-2.4.0/lib/webauthn/public_key.rb0000644000175000017500000000370413755257222017432 0ustar pravipravi# frozen_string_literal: true require "cose/algorithm" require "cose/error" require "cose/key" require "cose/rsapkcs1_algorithm" require "webauthn/attestation_statement/fido_u2f/public_key" module WebAuthn class PublicKey class UnsupportedAlgorithm < Error; end def self.deserialize(public_key) cose_key = if WebAuthn::AttestationStatement::FidoU2f::PublicKey.uncompressed_point?(public_key) # Gem version v1.11.0 and lower, used to behave so that Credential#public_key # returned an EC P-256 uncompressed point. # # Because of https://github.com/cedarcode/webauthn-ruby/issues/137 this was changed # and Credential#public_key started returning the unchanged COSE_Key formatted # credentialPublicKey (as in https://www.w3.org/TR/webauthn/#credentialpublickey). # # Given that the credential public key is expected to be stored long-term by the gem # user and later be passed as the public_key argument in the # AuthenticatorAssertionResponse.verify call, we then need to support the two formats. COSE::Key::EC2.new( alg: COSE::Algorithm.by_name("ES256").id, crv: 1, x: public_key[1..32], y: public_key[33..-1] ) else COSE::Key.deserialize(public_key) end new(cose_key: cose_key) end attr_reader :cose_key def initialize(cose_key:) @cose_key = cose_key end def pkey @cose_key.to_pkey end def alg @cose_key.alg end def verify(signature, verification_data) cose_algorithm.verify(pkey, signature, verification_data) rescue COSE::Error false end private def cose_algorithm @cose_algorithm ||= COSE::Algorithm.find(alg) || raise( UnsupportedAlgorithm, "The public key algorithm #{alg} is not among the available COSE algorithms" ) end end end webauthn-2.4.0/lib/webauthn/public_key_credential.rb0000644000175000017500000000256313755257222021626 0ustar pravipravi# frozen_string_literal: true require "webauthn/encoder" module WebAuthn class PublicKeyCredential attr_reader :type, :id, :raw_id, :client_extension_outputs, :response def self.from_client(credential) new( type: credential["type"], id: credential["id"], raw_id: WebAuthn.configuration.encoder.decode(credential["rawId"]), client_extension_outputs: credential["clientExtensionResults"], response: response_class.from_client(credential["response"]) ) end def initialize(type:, id:, raw_id:, client_extension_outputs: {}, response:) @type = type @id = id @raw_id = raw_id @client_extension_outputs = client_extension_outputs @response = response end def verify(*_args) valid_type? || raise("invalid type") valid_id? || raise("invalid id") true end def sign_count authenticator_data&.sign_count end def authenticator_extension_outputs authenticator_data.extension_data if authenticator_data&.extension_data_included? end private def valid_type? type == TYPE_PUBLIC_KEY end def valid_id? raw_id && id && raw_id == WebAuthn.standard_encoder.decode(id) end def authenticator_data response&.authenticator_data end def encoder WebAuthn.configuration.encoder end end end webauthn-2.4.0/lib/webauthn/credential.rb0000644000175000017500000000146313755257222017416 0ustar pravipravi# frozen_string_literal: true require "webauthn/public_key_credential/creation_options" require "webauthn/public_key_credential/request_options" require "webauthn/public_key_credential_with_assertion" require "webauthn/public_key_credential_with_attestation" module WebAuthn module Credential def self.options_for_create(**keyword_arguments) WebAuthn::PublicKeyCredential::CreationOptions.new(**keyword_arguments) end def self.options_for_get(**keyword_arguments) WebAuthn::PublicKeyCredential::RequestOptions.new(**keyword_arguments) end def self.from_create(credential) WebAuthn::PublicKeyCredentialWithAttestation.from_client(credential) end def self.from_get(credential) WebAuthn::PublicKeyCredentialWithAssertion.from_client(credential) end end end webauthn-2.4.0/lib/webauthn/client_data.rb0000644000175000017500000000222413755257222017547 0ustar pravipravi# frozen_string_literal: true require "json" require "openssl" require "webauthn/encoder" require "webauthn/error" module WebAuthn class ClientDataMissingError < Error; end class ClientData VALID_TOKEN_BINDING_STATUSES = ["present", "supported", "not-supported"].freeze def initialize(client_data_json) @client_data_json = client_data_json end def type data["type"] end def challenge WebAuthn.standard_encoder.decode(data["challenge"]) end def origin data["origin"] end def token_binding data["tokenBinding"] end def valid_token_binding_format? if token_binding token_binding.is_a?(Hash) && VALID_TOKEN_BINDING_STATUSES.include?(token_binding["status"]) else true end end def hash OpenSSL::Digest::SHA256.digest(client_data_json) end private attr_reader :client_data_json def data @data ||= begin if client_data_json JSON.parse(client_data_json) else raise ClientDataMissingError, "Client Data JSON is missing" end end end end end webauthn-2.4.0/lib/webauthn/fake_authenticator/0000755000175000017500000000000013755257222020613 5ustar pravipraviwebauthn-2.4.0/lib/webauthn/fake_authenticator/attestation_object.rb0000644000175000017500000000343313755257222025030 0ustar pravipravi# frozen_string_literal: true require "cbor" require "webauthn/fake_authenticator/authenticator_data" module WebAuthn class FakeAuthenticator class AttestationObject def initialize( client_data_hash:, rp_id_hash:, credential_id:, credential_key:, user_present: true, user_verified: false, attested_credential_data: true, sign_count: 0, extensions: nil ) @client_data_hash = client_data_hash @rp_id_hash = rp_id_hash @credential_id = credential_id @credential_key = credential_key @user_present = user_present @user_verified = user_verified @attested_credential_data = attested_credential_data @sign_count = sign_count @extensions = extensions end def serialize CBOR.encode( "fmt" => "none", "attStmt" => {}, "authData" => authenticator_data.serialize ) end private attr_reader( :client_data_hash, :rp_id_hash, :credential_id, :credential_key, :user_present, :user_verified, :attested_credential_data, :sign_count, :extensions ) def authenticator_data @authenticator_data ||= begin credential_data = if attested_credential_data { id: credential_id, public_key: credential_key.public_key } end AuthenticatorData.new( rp_id_hash: rp_id_hash, credential: credential_data, user_present: user_present, user_verified: user_verified, sign_count: 0, extensions: extensions ) end end end end end webauthn-2.4.0/lib/webauthn/fake_authenticator/authenticator_data.rb0000644000175000017500000000625013755257222025006 0ustar pravipravi# frozen_string_literal: true require "cose/key" require "cbor" require "securerandom" module WebAuthn class FakeAuthenticator class AuthenticatorData AAGUID = SecureRandom.random_bytes(16) attr_reader :sign_count def initialize( rp_id_hash:, credential: { id: SecureRandom.random_bytes(16), public_key: OpenSSL::PKey::EC.new("prime256v1").generate_key.public_key }, sign_count: 0, user_present: true, user_verified: !user_present, aaguid: AAGUID, extensions: { "fakeExtension" => "fakeExtensionValue" } ) @rp_id_hash = rp_id_hash @credential = credential @sign_count = sign_count @user_present = user_present @user_verified = user_verified @aaguid = aaguid @extensions = extensions end def serialize rp_id_hash + flags + serialized_sign_count + attested_credential_data + extension_data end private attr_reader :rp_id_hash, :credential, :user_present, :user_verified, :extensions def flags [ [ bit(:user_present), reserved_for_future_use_bit, bit(:user_verified), reserved_for_future_use_bit, reserved_for_future_use_bit, reserved_for_future_use_bit, attested_credential_data_included_bit, extension_data_included_bit ].join ].pack("b*") end def serialized_sign_count [sign_count].pack('L>') end def attested_credential_data @attested_credential_data ||= if credential @aaguid + [credential[:id].length].pack("n*") + credential[:id] + cose_credential_public_key else "" end end def extension_data if extensions CBOR.encode(extensions) else "" end end def bit(flag) if context[flag] "1" else "0" end end def attested_credential_data_included_bit if attested_credential_data.empty? "0" else "1" end end def extension_data_included_bit if extension_data.empty? "0" else "1" end end def reserved_for_future_use_bit "0" end def context { user_present: user_present, user_verified: user_verified } end def cose_credential_public_key case credential[:public_key] when OpenSSL::PKey::RSA key = COSE::Key::RSA.from_pkey(credential[:public_key]) key.alg = -257 when OpenSSL::PKey::EC::Point alg = { COSE::Key::Curve.by_name("P-256").id => -7, COSE::Key::Curve.by_name("P-384").id => -35, COSE::Key::Curve.by_name("P-521").id => -36 } key = COSE::Key::EC2.from_pkey(credential[:public_key]) key.alg = alg[key.crv] end key.serialize end def key_bytes(public_key) public_key.to_bn.to_s(2) end end end end webauthn-2.4.0/lib/webauthn/version.rb0000644000175000017500000000010713755257222016763 0ustar pravipravi# frozen_string_literal: true module WebAuthn VERSION = "2.4.0" end webauthn-2.4.0/.travis.yml0000644000175000017500000000151013755257222014476 0ustar pravipravidist: bionic language: ruby cache: bundler: true directories: - /home/travis/.rvm/ env: - LIBSSL=1.1 RB=2.7.1 - LIBSSL=1.1 RB=2.6.6 - LIBSSL=1.1 RB=2.5.8 - LIBSSL=1.1 RB=2.4.10 - LIBSSL=1.1 RB=ruby-head - LIBSSL=1.0 RB=2.7.1 - LIBSSL=1.0 RB=2.6.6 - LIBSSL=1.0 RB=2.5.8 - LIBSSL=1.0 RB=2.4.10 - LIBSSL=1.0 RB=ruby-head gemfile: - gemfiles/cose_head.gemfile - gemfiles/openssl_head.gemfile - gemfiles/openssl_2_2.gemfile - gemfiles/openssl_2_1.gemfile - gemfiles/openssl_2_0.gemfile matrix: fast_finish: true allow_failures: - env: LIBSSL=1.1 RB=ruby-head - env: LIBSSL=1.0 RB=ruby-head - gemfile: gemfiles/cose_head.gemfile - gemfile: gemfiles/openssl_head.gemfile before_install: - ./script/ci/install-openssl - ./script/ci/install-ruby - gem install bundler -v "~> 2.0" webauthn-2.4.0/CONTRIBUTING.md0000644000175000017500000000410213755257222014616 0ustar pravipravi## Contributing to webauthn-ruby ### How? - Creating a new issue to report a bug - Creating a new issue to suggest a new feature - Commenting on an existing issue to answer an open question - Commenting on an existing issue to ask the reporter for more details to aid reproducing the problem - Improving documentation - Creating a pull request that fixes an issue (see [beginner friendly issues](https://github.com/cedarcode/webauthn-ruby/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)) - Creating a pull request that implements a new feature (worth first creating an issue to discuss the suggested feature) ### Development After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake` to run the tests and code-style checks. You can also run `bin/console` for an interactive prompt that will allow you to experiment. To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). ### Styleguide #### Ruby We use [rubocop](https://rubygems.org/gems/rubocop) to check ruby code style. #### Git commit messages We try to follow [Conventional Commits](https://conventionalcommits.org) specification since `v1.17.0`. On top of `fix` and `feat` types, we also use optional: * __build__: Changes that affect the build system or external dependencies * __ci__: Changes to the CI configuration files and scripts * __docs__: Documentation only changes * __perf__: A code change that improves performance * __refactor__: A code change that neither fixes a bug nor adds a feature * __style__: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) * __test__: Adding missing tests or correcting existing tests Partially inspired in [Angular's Commit Message Guidelines](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-commit-message-guidelines). webauthn-2.4.0/.rubocop.yml0000644000175000017500000001004613755257222014643 0ustar pravipravirequire: - rubocop-rspec inherit_mode: merge: - AllowedNames AllCops: TargetRubyVersion: 2.4 DisabledByDefault: true Exclude: - "gemfiles/**/*" - "vendor/**/*" Bundler: Enabled: true Gemspec: Enabled: true Layout: Enabled: true Layout/ClassStructure: Enabled: true Layout/EmptyLinesAroundAttributeAccessor: Enabled: true Layout/FirstMethodArgumentLineBreak: Enabled: true Layout/LineLength: Max: 120 Exclude: - spec/support/seeds.rb Layout/MultilineAssignmentLayout: Enabled: true Layout/MultilineMethodArgumentLineBreaks: Enabled: true Layout/SpaceAroundMethodCallOperator: Enabled: true Lint: Enabled: true Lint/DeprecatedOpenSSLConstant: Enabled: true Lint/MixedRegexpCaptureTypes: Enabled: true Lint/RaiseException: Enabled: true Lint/StructNewOverride: Enabled: true Lint/BinaryOperatorWithIdenticalOperands: Enabled: true Lint/DuplicateElsifCondition: Enabled: true Lint/DuplicateRescueException: Enabled: true Lint/EmptyConditionalBody: Enabled: true Lint/FloatComparison: Enabled: true Lint/MissingSuper: Enabled: true Lint/OutOfRangeRegexpRef: Enabled: true Lint/SelfAssignment: Enabled: true Lint/TopLevelReturnWithArgument: Enabled: true Lint/UnreachableLoop: Enabled: true Naming: Enabled: true RSpec/Be: Enabled: true RSpec/BeforeAfterAll: Enabled: true RSpec/EmptyExampleGroup: Enabled: true RSpec/EmptyLineAfterExample: Enabled: true RSpec/EmptyLineAfterExampleGroup: Enabled: true RSpec/EmptyLineAfterFinalLet: Enabled: true RSpec/EmptyLineAfterHook: Enabled: true RSpec/EmptyLineAfterSubject: Enabled: true RSpec/HookArgument: Enabled: true RSpec/LeadingSubject: Enabled: true RSpec/NamedSubject: Enabled: true RSpec/ScatteredLet: Enabled: true RSpec/ScatteredSetup: Enabled: true Naming/MethodParameterName: AllowedNames: - rp Security: Enabled: true Style/BlockComments: Enabled: true Style/CaseEquality: Enabled: true Style/ClassAndModuleChildren: Enabled: true Style/ClassMethods: Enabled: true Style/ClassVars: Enabled: true Style/CommentAnnotation: Enabled: true Style/ConditionalAssignment: Enabled: true Style/DefWithParentheses: Enabled: true Style/Dir: Enabled: true Style/EachForSimpleLoop: Enabled: true Style/EachWithObject: Enabled: true Style/EmptyBlockParameter: Enabled: true Style/EmptyCaseCondition: Enabled: true Style/EmptyElse: Enabled: true Style/EmptyLambdaParameter: Enabled: true Style/EmptyLiteral: Enabled: true Style/EvenOdd: Enabled: true Style/ExpandPathArguments: Enabled: true Style/For: Enabled: true Style/FrozenStringLiteralComment: Enabled: true Style/GlobalVars: Enabled: true Style/HashSyntax: Enabled: true Style/IdenticalConditionalBranches: Enabled: true Style/IfInsideElse: Enabled: true Style/InverseMethods: Enabled: true Style/MethodCallWithoutArgsParentheses: Enabled: true Style/MethodDefParentheses: Enabled: true Style/MultilineMemoization: Enabled: true Style/MutableConstant: Enabled: true Style/NestedParenthesizedCalls: Enabled: true Style/OptionalArguments: Enabled: true Style/ParenthesesAroundCondition: Enabled: true Style/RedundantBegin: Enabled: true Style/RedundantConditional: Enabled: true Style/RedundantException: Enabled: true Style/RedundantFreeze: Enabled: true Style/RedundantInterpolation: Enabled: true Style/RedundantParentheses: Enabled: true Style/RedundantPercentQ: Enabled: true Style/RedundantReturn: Enabled: true Style/RedundantSelf: Enabled: true Style/Semicolon: Enabled: true Style/SingleLineMethods: Enabled: true Style/SpecialGlobalVars: Enabled: true Style/SymbolLiteral: Enabled: true Style/TrailingBodyOnClass: Enabled: true Style/TrailingBodyOnMethodDefinition: Enabled: true Style/TrailingBodyOnModule: Enabled: true Style/TrailingMethodEndStatement: Enabled: true Style/TrivialAccessors: Enabled: true Style/UnpackFirst: Enabled: true Style/YodaCondition: Enabled: true Style/ZeroLengthPredicate: Enabled: true webauthn-2.4.0/Gemfile0000644000175000017500000000030313755257222013657 0ustar pravipravi# frozen_string_literal: true source "https://rubygems.org" git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } # Specify your gem's dependencies in webauthn.gemspec gemspec webauthn-2.4.0/README.md0000644000175000017500000004624113755257222013656 0ustar pravipravi__Note__: You are viewing the README for the development version of webauthn-ruby. For the current release version see https://github.com/cedarcode/webauthn-ruby/blob/2-stable/README.md. # webauthn-ruby ![banner](assets/webauthn-ruby.png) [![Gem](https://img.shields.io/gem/v/webauthn.svg?style=flat-square)](https://rubygems.org/gems/webauthn) [![Travis](https://img.shields.io/travis/cedarcode/webauthn-ruby/master.svg?style=flat-square)](https://travis-ci.org/cedarcode/webauthn-ruby) [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-informational.svg?style=flat-square)](https://conventionalcommits.org) [![Join the chat at https://gitter.im/cedarcode/webauthn-ruby](https://badges.gitter.im/cedarcode/webauthn-ruby.svg)](https://gitter.im/cedarcode/webauthn-ruby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) > WebAuthn ruby server library Makes your Ruby/Rails web server become a functional [WebAuthn Relying Party](https://www.w3.org/TR/webauthn/#webauthn-relying-party). Takes care of the [server-side operations](https://www.w3.org/TR/webauthn/#rp-operations) needed to [register](https://www.w3.org/TR/webauthn/#registration) or [authenticate](https://www.w3.org/TR/webauthn/#authentication) a user [credential](https://www.w3.org/TR/webauthn/#public-key-credential), including the necessary cryptographic checks. ## Table of Contents - [Security](#security) - [Background](#background) - [Prerequisites](#prerequisites) - [Install](#install) - [Usage](#usage) - [API](#api) - [Attestation Statement Formats](#attestation-statement-formats) - [Testing Your Integration](#testing-your-integration) - [Contributing](#contributing) - [License](#license) ## Security Please report security vulnerabilities to security@cedarcode.com. _More_: [SECURITY](SECURITY.md) ## Background ### What is WebAuthn? WebAuthn (Web Authentication) is a W3C standard for secure public-key authentication on the Web supported by all leading browsers and platforms. #### Good Intros - [Guide to Web Authentication](https://webauthn.guide) by Duo - [What is WebAuthn?](https://www.yubico.com/webauthn/) by Yubico #### In Depth - WebAuthn [W3C Recommendation](https://www.w3.org/TR/webauthn/) (i.e. "The Standard") - [Web Authentication API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API) in MDN - How to use [WebAuthn in Android apps](https://developers.google.com/identity/fido/android/native-apps) - [Security Benefits for WebAuthn Servers (a.k.a Relying Parties)](https://www.w3.org/TR/webauthn/#sctn-rp-benefits) ## Prerequisites This ruby library will help your Ruby/Rails server act as a conforming [_Relying-Party_](https://www.w3.org/TR/webauthn/#relying-party), in WebAuthn terminology. But for the [_Registration_](https://www.w3.org/TR/webauthn/#registration) and [_Authentication_](https://www.w3.org/TR/webauthn/#authentication) ceremonies to fully work, you will also need to add two more pieces to the puzzle, a conforming [User Agent](https://www.w3.org/TR/webauthn/#conforming-user-agents) + [Authenticator](https://www.w3.org/TR/webauthn/#conforming-authenticators) pair. Known conformant pairs are, for example: - Google Chrome for Android 70+ and Android's Fingerprint-based platform authenticator - Microsoft Edge and Windows 10 platform authenticator - Mozilla Firefox for Desktop and Yubico's Security Key roaming authenticator via USB - Safari in iOS 13.3+ and YubiKey 5 NFC via NFC For a complete list: - User Agents (Clients): [Can I Use: Web Authentication API](https://caniuse.com/#search=webauthn) - Authenticators: [FIDO certified products](https://fidoalliance.org/certification/fido-certified-products) (search for Type=Authenticator and Specification=FIDO2) ## Install Add this line to your application's Gemfile: ```ruby gem 'webauthn' ``` And then execute: $ bundle Or install it yourself as: $ gem install webauthn ## Usage You can find a working example on how to use this gem in a __Rails__ app in [webauthn-rails-demo-app](https://github.com/cedarcode/webauthn-rails-demo-app). If you are migrating an existing application from the legacy FIDO U2F JavaScript API to WebAuthn, also refer to [`docs/u2f_migration.md`](docs/u2f_migration.md). ### Configuration For a Rails application this would go in `config/initializers/webauthn.rb`. ```ruby WebAuthn.configure do |config| # This value needs to match `window.location.origin` evaluated by # the User Agent during registration and authentication ceremonies. config.origin = "https://auth.example.com" # Relying Party name for display purposes config.rp_name = "Example Inc." # Optionally configure a client timeout hint, in milliseconds. # This hint specifies how long the browser should wait for any # interaction with the user. # This hint may be overridden by the browser. # https://www.w3.org/TR/webauthn/#dom-publickeycredentialcreationoptions-timeout # config.credential_options_timeout = 120_000 # You can optionally specify a different Relying Party ID # (https://www.w3.org/TR/webauthn/#relying-party-identifier) # if it differs from the default one. # # In this case the default would be "auth.example.com", but you can set it to # the suffix "example.com" # # config.rp_id = "example.com" # Configure preferred binary-to-text encoding scheme. This should match the encoding scheme # used in your client-side (user agent) code before sending the credential to the server. # Supported values: `:base64url` (default), `:base64` or `false` to disable all encoding. # # config.encoding = :base64url # Possible values: "ES256", "ES384", "ES512", "PS256", "PS384", "PS512", "RS256", "RS384", "RS512", "RS1" # Default: ["ES256", "PS256", "RS256"] # # config.algorithms << "ES384" end ``` ### Credential Registration > The ceremony where a user, a Relying Party, and the user’s client (containing at least one authenticator) work in concert to create a public key credential and associate it with the user’s Relying Party account. Note that this includes employing a test of user presence or user verification. > [[source](https://www.w3.org/TR/webauthn-2/#registration-ceremony)] #### Initiation phase ```ruby # Generate and store the WebAuthn User ID the first time the user registers a credential if !user.webauthn_id user.update!(webauthn_id: WebAuthn.generate_user_id) end options = WebAuthn::Credential.options_for_create( user: { id: user.webauthn_id, name: user.name }, exclude: user.credentials.map { |c| c.webauthn_id } ) # Store the newly generated challenge somewhere so you can have it # for the verification phase. session[:creation_challenge] = options.challenge # Send `options` back to the browser, so that they can be used # to call `navigator.credentials.create({ "publicKey": options })` # # You can call `options.as_json` to get a ruby hash with a JSON representation if needed. # If inside a Rails controller, `render json: options` will just work. # I.e. it will encode and convert the options to JSON automatically. # For your frontend code, you might find @github/webauthn-json npm package useful. # Especially for handling the necessary decoding of the options, and sending the # `PublicKeyCredential` object back to the server. ``` #### Verification phase ```ruby # Assuming you're using @github/webauthn-json package to send the `PublicKeyCredential` object back # in params[:publicKeyCredential]: webauthn_credential = WebAuthn::Credential.from_create(params[:publicKeyCredential]) begin webauthn_credential.verify(session[:creation_challenge]) # Store Credential ID, Credential Public Key and Sign Count for future authentications user.credentials.create!( webauthn_id: webauthn_credential.id, public_key: webauthn_credential.public_key, sign_count: webauthn_credential.sign_count ) rescue WebAuthn::Error => e # Handle error end ``` ### Credential Authentication > The ceremony where a user, and the user’s client (containing at least one authenticator) work in concert to cryptographically prove to a Relying Party that the user controls the credential private key associated with a previously-registered public key credential (see Registration). Note that this includes a test of user presence or user verification. [[source](https://www.w3.org/TR/webauthn-2/#authentication-ceremony)] #### Initiation phase ```ruby options = WebAuthn::Credential.options_for_get(allow: user.credentials.map { |c| c.webauthn_id }) # Store the newly generated challenge somewhere so you can have it # for the verification phase. session[:authentication_challenge] = options.challenge # Send `options` back to the browser, so that they can be used # to call `navigator.credentials.get({ "publicKey": options })` # You can call `options.as_json` to get a ruby hash with a JSON representation if needed. # If inside a Rails controller, `render json: options` will just work. # I.e. it will encode and convert the options to JSON automatically. # For your frontend code, you might find @github/webauthn-json npm package useful. # Especially for handling the necessary decoding of the options, and sending the # `PublicKeyCredential` object back to the server. ``` #### Verification phase You need to look up the stored credential for a user by matching the `id` attribute from the PublicKeyCredential interface returned by the browser to the stored `credential_id`. The corresponding `public_key` and `sign_count` attributes must be passed as keyword arguments to the `verify` method call. ```ruby # Assuming you're using @github/webauthn-json package to send the `PublicKeyCredential` object back # in params[:publicKeyCredential]: webauthn_credential = WebAuthn::Credential.from_get(params[:publicKeyCredential]) stored_credential = user.credentials.find_by(webauthn_id: webauthn_credential.id) begin webauthn_credential.verify( session[:authentication_challenge], public_key: stored_credential.public_key, sign_count: stored_credential.sign_count ) # Update the stored credential sign count with the value from `webauthn_credential.sign_count` stored_credential.update!(sign_count: webauthn_credential.sign_count) # Continue with successful sign in or 2FA verification... rescue WebAuthn::SignCountVerificationError => e # Cryptographic verification of the authenticator data succeeded, but the signature counter was less then or equal # to the stored value. This can have several reasons and depending on your risk tolerance you can choose to fail or # pass authentication. For more information see https://www.w3.org/TR/webauthn/#sign-counter rescue WebAuthn::Error => e # Handle error end ``` ### Extensions > The mechanism for generating public key credentials, as well as requesting and generating Authentication assertions, as defined in Web Authentication API, can be extended to suit particular use cases. Each case is addressed by defining a registration extension and/or an authentication extension. > When creating a public key credential or requesting an authentication assertion, a WebAuthn Relying Party can request the use of a set of extensions. These extensions will be invoked during the requested ceremony if they are supported by the WebAuthn Client and/or the WebAuthn Authenticator. The Relying Party sends the client extension input for each extension in the get() call (for authentication extensions) or create() call (for registration extensions) to the WebAuthn client. [[source](https://www.w3.org/TR/webauthn-2/#sctn-extensions)] Extensions can be requested in the initiation phase in both Credential Registration and Authentication ceremonies by adding the extension parameter when generating the options for create/get: ```ruby # Credential Registration creation_options = WebAuthn::Credential.options_for_create( user: { id: user.webauthn_id, name: user.name }, exclude: user.credentials.map { |c| c.webauthn_id }, extensions: { appidExclude: domain.to_s } ) # OR # Credential Authentication options = WebAuthn::Credential.options_for_get( allow: user.credentials.map { |c| c.webauthn_id }, extensions: { appid: domain.to_s } ) ``` Consequently, after these `options` are sent to the WebAuthn client: > The WebAuthn client performs client extension processing for each extension that the client supports, and augments the client data as specified by each extension, by including the extension identifier and client extension output values. > For authenticator extensions, as part of the client extension processing, the client also creates the CBOR authenticator extension input value for each extension (often based on the corresponding client extension input value), and passes them to the authenticator in the create() call (for registration extensions) or the get() call (for authentication extensions). > The authenticator, in turn, performs additional processing for the extensions that it supports, and returns the CBOR authenticator extension output for each as specified by the extension. Part of the client extension processing for authenticator extensions is to use the authenticator extension output as an input to creating the client extension output. [[source](https://www.w3.org/TR/webauthn-2/#sctn-extensions)] Finally, you can check the values returned for each extension by calling `client_extension_outputs` and `authenticator_extension_outputs` respectively. For example, following the initialization phase for the Credential Authentication ceremony specified in the above example: ```ruby webauthn_credential = WebAuthn::Credential.from_get(credential_get_result_hash) webauthn_credential.client_extension_outputs #=> { "appid" => true } webauthn_credential.authenticator_extension_outputs #=> nil ``` A list of all currently defined extensions: - [Last published version](https://www.w3.org/TR/webauthn-2/#sctn-defined-extensions) - [Next version (in draft)](https://w3c.github.io/webauthn/#sctn-defined-extensions) ## API #### `WebAuthn.generate_user_id` Generates a [WebAuthn User Handle](https://www.w3.org/TR/webauthn-2/#user-handle) that follows the WebAuthn spec recommendations. ```ruby WebAuthn.generate_user_id # "lWoMZTGf_ml2RoY5qPwbwrkxrvTqWjGOxEoYBgxft3zG-LlrICvE-y8bxFi06zMyIOyNsJoWx4Fa2TOqoRmnxA" ``` #### `WebAuthn::Credential.options_for_create(options)` Helper method to build the necessary [PublicKeyCredentialCreationOptions](https://www.w3.org/TR/webauthn-2/#dictdef-publickeycredentialcreationoptions) to be used in the client-side code to call `navigator.credentials.create({ "publicKey": publicKeyCredentialCreationOptions })`. ```ruby creation_options = WebAuthn::Credential.options_for_create( user: { id: user.webauthn_id, name: user.name } exclude: user.credentials.map { |c| c.webauthn_id } ) # Store the newly generated challenge somewhere so you can have it # for the verification phase. session[:creation_challenge] = creation_options.challenge # Send `creation_options` back to the browser, so that they can be used # to call `navigator.credentials.create({ "publicKey": creationOptions })` # # You can call `creation_options.as_json` to get a ruby hash with a JSON representation if needed. # If inside a Rails controller, `render json: creation_options` will just work. # I.e. it will encode and convert the options to JSON automatically. ``` #### `WebAuthn::Credential.options_for_get([options])` Helper method to build the necessary [PublicKeyCredentialRequestOptions](https://www.w3.org/TR/webauthn-2/#dictdef-publickeycredentialrequestoptions) to be used in the client-side code to call `navigator.credentials.get({ "publicKey": publicKeyCredentialRequestOptions })`. ```ruby request_options = WebAuthn::Credential.options_for_get(allow: user.credentials.map { |c| c.webauthn_id }) # Store the newly generated challenge somewhere so you can have it # for the verification phase. session[:authentication_challenge] = request_options.challenge # Send `request_options` back to the browser, so that they can be used # to call `navigator.credentials.get({ "publicKey": requestOptions })` # You can call `request_options.as_json` to get a ruby hash with a JSON representation if needed. # If inside a Rails controller, `render json: request_options` will just work. # I.e. it will encode and convert the options to JSON automatically. ``` #### `WebAuthn::Credential.from_create(credential_create_result)` ```ruby credential_with_attestation = WebAuthn::Credential.from_create(params[:publicKeyCredential]) ``` #### `WebAuthn::Credential.from_get(credential_get_result)` ```ruby credential_with_assertion = WebAuthn::Credential.from_get(params[:publicKeyCredential]) ``` #### `PublicKeyCredentialWithAttestation#verify(challenge)` Verifies the created WebAuthn credential is [valid](https://www.w3.org/TR/webauthn-2/#sctn-registering-a-new-credential). ```ruby credential_with_attestation.verify(session[:creation_challenge]) ``` #### `PublicKeyCredentialWithAssertion#verify(challenge, public_key:, sign_count:)` Verifies the asserted WebAuthn credential is [valid](https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion). Mainly, that the client provided a valid cryptographic signature for the corresponding stored credential public key, among other extra validations. ```ruby credential_with_assertion.verify( session[:authentication_challenge], public_key: stored_credential.public_key, sign_count: stored_credential.sign_count ) ``` #### `PublicKeyCredential#client_extension_outputs` ```ruby credential = WebAuthn::Credential.from_create(params[:publicKeyCredential]) credential.client_extension_outputs ``` #### `PublicKeyCredential#authenticator_extension_outputs` ```ruby credential = WebAuthn::Credential.from_create(params[:publicKeyCredential]) credential.authenticator_extension_outputs ``` ## Attestation ### Attestation Statement Format | Attestation Statement Format | Supported? | | -------- | :--------: | | packed (self attestation) | Yes | | packed (x5c attestation) | Yes | | tpm (x5c attestation) | Yes | | android-key | Yes | | android-safetynet | Yes | | fido-u2f | Yes | | none | Yes | ### Attestation Types You can define what trust policy to enforce by setting `acceptable_attestation_types` config to a subset of `['None', 'Self', 'Basic', 'AttCA', 'Basic_or_AttCA']` and `attestation_root_certificates_finders` to an object that responds to `#find` and returns the corresponding root certificate for each registration. The `#find` method will be called passing keyword arguments `attestation_format`, `aaguid` and `attestation_certificate_key_id`. ## Testing Your Integration The Webauthn spec requires for data that is signed and authenticated. As a result, it can be difficult to create valid test authenticator data when testing your integration. webauthn-ruby exposes [WebAuthn::FakeClient](https://github.com/cedarcode/webauthn-ruby/blob/master/lib/webauthn/fake_client.rb) for you to use in your tests. Example usage can be found in [webauthn-ruby/spec/webauthn/authenticator_assertion_response_spec.rb](https://github.com/cedarcode/webauthn-ruby/blob/master/spec/webauthn/authenticator_assertion_response_spec.rb). ## Contributing See [the contributing file](CONTRIBUTING.md)! Bug reports, feature suggestions, and pull requests are welcome on GitHub at https://github.com/cedarcode/webauthn-ruby. ## License The library is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). webauthn-2.4.0/Appraisals0000644000175000017500000000060013755257222014406 0ustar pravipravi# frozen_string_literal: true appraise "cose_head" do gem "cose", git: "https://github.com/cedarcode/cose-ruby" end appraise "openssl_head" do gem "openssl", git: "https://github.com/ruby/openssl" end appraise "openssl_2_2" do gem "openssl", "~> 2.2.0" end appraise "openssl_2_1" do gem "openssl", "~> 2.1.0" end appraise "openssl_2_0" do gem "openssl", "~> 2.0.0" end webauthn-2.4.0/CHANGELOG.md0000644000175000017500000003270213755257222014205 0ustar pravipravi# Changelog ## [v2.4.0] - 2020-09-03 ### Added - Support for ES256K credentials - `FakeClient#get` accepts `user_handle:` keyword argument ([@lgarron]) ## [v2.3.0] - 2020-06-27 ### Added - Ability to access extension outputs with `PublicKeyCredential#client_extension_outputs` and `PublicKeyCredential#authenticator_extension_outputs` ([@santiagorodriguez96]) ## [v2.2.1] - 2020-06-06 ### Fixed - Fixed compatibility with OpenSSL-C (libssl) v1.0.2 ([@santiagorodriguez96]) ## [v2.2.0] - 2020-03-14 ### Added - Verification step that checks the received credential public key algorithm during registration matches one of the configured algorithms - [EXPERIMENTAL] Attestation trustworthiness verification default steps for "tpm", "android-key" and "android-safetynet" ([@bdewater], [@padulafacundo]). Still manual configuration needed for "packed" and "fido-u2f". Note: Expect possible breaking changes for "EXPERIMENTAL" features. ## [v2.1.0] - 2019-12-30 ### Added - Ability to convert stored credential public key back to a ruby object with `WebAuthn::PublicKey.deserialize(stored_public_key)`, included the validation during de-serialization ([@ssuttner], [@padulafacundo]) - Improved TPM attestation validation by checking "Subject Alternative Name" ([@bdewater]) - Improved SafetyNet attestation validation by checking timestamp ([@padulafacundo]) - [EXPERIMENTAL] Ability to optionally "Assess the attestation trustworthiness" during registration by setting `acceptable_attestation_types` and `attestation_root_certificates_finders` configuration values ([@padulafacundo]) - Ruby 2.7 support without warnings Note: Expect possible breaking changes for "EXPERIMENTAL" features. ## [v2.0.0] - 2019-10-03 ### Added - Smarter new public API methods: - `WebAuthn.generate_user_id` - `WebAuthn::Credential.options_for_create` - `WebAuthn::Credential.options_for_get` - `WebAuthn::Credential.from_create` - `WebAuthn::Credential.from_get` - All the above automatically handle encoding/decoding for necessary values. The specific encoding scheme can be set (or even turned off) in `WebAutnn.configuration.encoding=`. Defaults to `:base64url`. - `WebAuthn::FakeClient#get` better fakes a real client by including `userHandle` in the returned hash. - Expose AAGUID and attestationCertificateKey for MDS lookup during attestation ([@bdewater]) ### Changed - `WebAuthn::AuthenticatorAssertionResponse#verify` no longer accepts `allowed_credentials:` keyword argument. Please replace with `public_key:` and `sign_count:` keyword arguments. If you're not performing sign count verification, signal opt-out with `sign_count: false`. - `WebAuthn::FakeClient#create` and `WebAuthn::FakeClient#get` better fakes a real client by using lowerCamelCase string keys instead of snake_case symbol keys in the returned hash. - `WebAuthn::FakeClient#create` and `WebAuthn::FakeClient#get` better fakes a real client by not padding the returned base64url-encoded `id` value. ### Deprecated - `WebAuthn.credential_creation_options` method. Please consider using `WebAuthn::Credential.options_for_create`. - `WebAuthn.credential_request_options` method. Please consider using `WebAuthn::Credential.options_for_get`. ### Removed - `WebAuthn::AuthenticatorAssertionResponse.new` no longer accepts `credential_id`. No replacement needed, just don't pass it. ### BREAKING CHANGES - `WebAuthn::AuthenticatorAssertionResponse.new` no longer accepts `credential_id`. No replacement needed, just don't pass it. - `WebAuthn::AuthenticatorAssertionResponse#verify` no longer accepts `allowed_credentials:` keyword argument. Please replace with `public_key:` and `sign_count:` keyword arguments. If you're not performing sign count verification, signal opt-out with `sign_count: false`. - `WebAuthn::FakeClient#create` and `WebAuthn::FakeClient#get` better fakes a real client by using lowerCamelCase string keys instead of snake_case symbol keys in the returned hash. - `WebAuthn::FakeClient#create` and `WebAuthn::FakeClient#get` better fakes a real client by not padding the returned base64url-encoded `id` value. ## [v1.18.0] - 2019-07-27 ### Added - Ability to migrate U2F credentials to WebAuthn ([#211](https://github.com/cedarcode/webauthn-ruby/pull/211)) ([@bdewater] + [@jdongelmans]) - Ability to skip attestation statement verification ([#219](https://github.com/cedarcode/webauthn-ruby/pull/219)) ([@MaximeNdutiye]) - Ability to configure default credential options timeout ([#243](https://github.com/cedarcode/webauthn-ruby/pull/243)) ([@MaximeNdutiye]) - AttestedCredentialData presence verification ([#237](https://github.com/cedarcode/webauthn-ruby/pull/237)) - FakeClient learns how to increment sign count ([#225](https://github.com/cedarcode/webauthn-ruby/pull/225)) ### Fixed - Properly verify SafetyNet certificates from input ([#233](https://github.com/cedarcode/webauthn-ruby/pull/233)) ([@bdewater]) - FakeClient default origin URL ([#242](https://github.com/cedarcode/webauthn-ruby/pull/242)) ([@kalebtesfay]) ## [v1.17.0] - 2019-06-18 ### Added - Support ES384, ES512, PS384, PS512, RS384 and RS512 credentials. Off by default. Enable by adding any of them to `WebAuthn.configuration.algorithms` array ([@bdewater]) - Support [Signature Counter](https://www.w3.org/TR/webauthn/#signature-counter) verification ([@bdewater]) ## [v1.16.0] - 2019-06-13 ### Added - Ability to enforce [user verification](https://www.w3.org/TR/webauthn/#user-verification) with extra argument in the `#verify` method. - Support RS1 (RSA w/ SHA-1) credentials. Off by default. Enable by adding `"RS1"` to `WebAuthn.configuration.algorithms` array. - Support PS256 (RSA Probabilistic Signature Scheme w/ SHA-256) credentials. On by default ([@bdewater]) ## [v1.15.0] - 2019-05-16 ### Added - Ability to configure Origin, RP ID and RP Name via `WebAuthn.configure` ## [v1.14.0] - 2019-04-25 ### Added - Support 'tpm' attestation statement - Support RS256 credential public key ## [v1.13.0] - 2019-04-09 ### Added - Verify 'none' attestation statement is really empty. - Verify 'packed' attestation statement certificates start/end dates. - Verify 'packed' attestation statement signature algorithm. - Verify 'fiod-u2f attestation statement AAGUID is zeroed out ([@bdewater]) - Verify 'android-key' attestation statement signature algorithm. - Verify assertion response signature algorithm. - Verify collectedClientData.tokenBinding format. - `WebAuthn.credential_creation_options` now accept `rp_name`, `user_id`, `user_name` and `display_name` as keyword arguments ([@bdewater]) ## [v1.12.0] - 2019-04-03 ### Added - Verification of the attestation certificate public key curve for `fido-u2f` attestation statements. ### Changed - `Credential#public_key` now returns the COSE_Key formatted version of the credential public key, instead of the uncompressed EC point format. Note #1: A `Credential` instance is what is returned in `WebAuthn::AuthenticatorAttestationResponse#credential`. Note #2: You don't need to do any convesion before passing the public key in `AuthenticatorAssertionResponse#verify`'s `allowed_credentials` argument, `#verify` is backwards-compatible and will handle both public key formats properly. ## [v1.11.0] - 2019-03-15 ### Added - `WebAuthn::AuthenticatorAttestationResponse#verify` supports `android-key` attestation statements ([@bdewater]) ### Fixed - Verify matching AAGUID if needed when verifying `packed` attestation statements ([@bdewater]) ## [v1.10.0] - 2019-03-05 ### Added - Parse and make AuthenticatorData's extensionData available ## [v1.9.0] - 2019-02-22 ### Added - Added `#verify`, which can be used for getting a meaningful error raised in case of a verification error, as opposed to `#valid?` which returns `false` ## [v1.8.0] - 2019-01-17 ### Added - Make challenge validation inside `#valid?` method resistant to timing attacks (@tomek-bt) - Support for ruby 2.6 ### Changed - Make current raised exception errors a bit more meaningful to aid debugging ## [v1.7.0] - 2018-11-08 ### Added - _Registration_ ceremony - `WebAuthn::AuthenticatorAttestationResponse` exposes attestation type and trust path via `#attestation_type` and `#attestation_trust_path` methods ([@bdewater]) ## [v1.6.0] - 2018-11-01 ### Added - `FakeAuthenticator` object is now exposed to help you test your WebAuthn implementation ## [v1.5.0] - 2018-10-23 ### Added - Works with ruby 2.3 ([@bdewater]) ## [v1.4.0] - 2018-10-11 ### Added - _Registration_ ceremony - `WebAuthn::AuthenticatorAttestationResponse.valid?` supports `android-safetynet` attestation statements ([@bdewater]) ## [v1.3.0] - 2018-10-11 ### Added - _Registration_ ceremony - `WebAuthn::AuthenticatorAttestationResponse.valid?` supports `packed` attestation statements ([@sorah]) ## [v1.2.0] - 2018-10-08 ### Added - _Registration_ ceremony - `WebAuthn::AuthenticatorAttestationResponse.valid?` returns `true` if either UP or UV authenticator flags are present. - _Authentication_ ceremony - `WebAuthn::AuthenticatorAssertionResponse.valid?` returns `true` if either UP or UV authenticator flags are present. Note: Both additions should help making it compatible with Chrome for Android 70+/Android Fingerprint pair. ## [v1.1.0] - 2018-10-04 ### Added - _Registration_ ceremony - `WebAuthn::AuthenticatorAttestationResponse.valid?` optionally accepts rp_id ([@sorah]) - _Authentication_ ceremony - `WebAuthn::AuthenticatorAssertionResponse.valid?` optionally accepts rp_id. ## [v1.0.0] - 2018-09-07 ### Added - _Authentication_ ceremony - Support multiple credentials per user by letting `WebAuthn::AuthenticatorAssertionResponse.valid?` receive multiple allowed credentials ### Changed - _Registration_ ceremony - Use 32-byte challenge instead of 16-byte - _Authentication_ ceremony - Use 32-byte challenge instead of 16-byte ## [v0.2.0] - 2018-06-08 ### Added - _Registration_ ceremony - `WebAuthn::AuthenticatorAttestationResponse.credential` returns the Credential Public Key for you to store it somehwere for future authentications - _Authentication_ ceremony - `WebAuthn.credential_request_options` returns default options for you to initiate the _Authentication_ - `WebAuthn::AuthenticatorAssertionResponse.valid?` can be used to validate the authenticator assertion. For now it validates: - Signature - Challenge - Origin - User presence - Ceremony Type - Relying-Party ID - Allowed Credential - Works with ruby 2.4 ### Changed - _Registration_ ceremony - `WebAuthn::AuthenticatorAttestationResponse.valid?` now runs additional validations on the Credential Public Key ### Removed - _Registration_ ceremony - `WebAuthn::AuthenticatorAttestationResponse.credential_id` (superseded by `WebAuthn::AuthenticatorAttestationResponse.credential`) ## [v0.1.0] - 2018-05-25 ### Added - _Registration_ ceremony: - `WebAuthn.credential_creation_options` returns default options for you to initiate the _Registration_ - `WebAuthn::AuthenticatorAttestationResponse.valid?` can be used to validate fido-u2f attestations returned by the browser - Works with ruby 2.5 [v2.4.0]: https://github.com/cedarcode/webauthn-ruby/compare/v2.3.0...v2.4.0/ [v2.3.0]: https://github.com/cedarcode/webauthn-ruby/compare/v2.2.1...v2.3.0/ [v2.2.1]: https://github.com/cedarcode/webauthn-ruby/compare/v2.2.0...v2.2.1/ [v2.2.0]: https://github.com/cedarcode/webauthn-ruby/compare/v2.1.0...v2.2.0/ [v2.1.0]: https://github.com/cedarcode/webauthn-ruby/compare/v2.0.0...v2.1.0/ [v2.0.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.18.0...v2.0.0/ [v1.18.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.17.0...v1.18.0/ [v1.17.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.16.0...v1.17.0/ [v1.16.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.15.0...v1.16.0/ [v1.15.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.14.0...v1.15.0/ [v1.14.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.13.0...v1.14.0/ [v1.13.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.12.0...v1.13.0/ [v1.12.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.11.0...v1.12.0/ [v1.11.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.10.0...v1.11.0/ [v1.10.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.9.0...v1.10.0/ [v1.9.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.8.0...v1.9.0/ [v1.8.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.7.0...v1.8.0/ [v1.7.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.6.0...v1.7.0/ [v1.6.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.5.0...v1.6.0/ [v1.5.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.4.0...v1.5.0/ [v1.4.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.3.0...v1.4.0/ [v1.3.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.2.0...v1.3.0/ [v1.2.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.1.0...v1.2.0/ [v1.1.0]: https://github.com/cedarcode/webauthn-ruby/compare/v1.0.0...v1.1.0/ [v1.0.0]: https://github.com/cedarcode/webauthn-ruby/compare/v0.2.0...v1.0.0/ [v0.2.0]: https://github.com/cedarcode/webauthn-ruby/compare/v0.1.0...v0.2.0/ [v0.1.0]: https://github.com/cedarcode/webauthn-ruby/compare/v0.0.0...v0.1.0/ [@bdewater]: https://github.com/bdewater [@jdongelmans]: https://github.com/jdongelmans [@kalebtesfay]: https://github.com/kalebtesfay [@MaximeNdutiye]: https://github.com/MaximeNdutiye [@sorah]: https://github.com/sorah [@ssuttner]: https://github.com/ssuttner [@padulafacundo]: https://github.com/padulafacundo [@santiagorodriguez96]: https://github.com/santiagorodriguez96 [@lgarron]: https://github.com/lgarron webauthn-2.4.0/script/0000755000175000017500000000000013755257222013674 5ustar pravipraviwebauthn-2.4.0/script/ci/0000755000175000017500000000000013755257222014267 5ustar pravipraviwebauthn-2.4.0/script/ci/install-openssl0000755000175000017500000000013713755257222017345 0ustar pravipravi#!/bin/bash set -e if [[ "$LIBSSL" == "1.0" ]]; then sudo apt-get install libssl1.0-dev fi webauthn-2.4.0/script/ci/install-ruby0000755000175000017500000000054413755257222016645 0ustar pravipravi#!/bin/bash set -e source "$HOME/.rvm/scripts/rvm" if [[ "$LIBSSL" == "1.0" ]]; then rvm use --install $RB --autolibs=read-only --disable-binary elif [[ "$LIBSSL" == "1.1" ]]; then rvm use --install $RB --binary --fuzzy fi [[ "`ruby -ropenssl -e 'puts OpenSSL::OPENSSL_VERSION'`" =~ "OpenSSL $LIBSSL" ]] || { echo "Wrong libssl version"; exit 1; } webauthn-2.4.0/.gitignore0000644000175000017500000000031013755257222014352 0ustar pravipravi/.bundle/ /.yardoc /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ # rspec failure tracking .rspec_status /Gemfile.lock /gemfiles/*.gemfile.lock .byebug_history /spec/conformance/metadata.zip webauthn-2.4.0/gemfiles/0000755000175000017500000000000013755257222014163 5ustar pravipraviwebauthn-2.4.0/gemfiles/cose_head.gemfile0000644000175000017500000000022613755257222017427 0ustar pravipravi# This file was generated by Appraisal source "https://rubygems.org" gem "cose", git: "https://github.com/cedarcode/cose-ruby" gemspec path: "../" webauthn-2.4.0/gemfiles/openssl_2_2.gemfile0000644000175000017500000000016613755257222017645 0ustar pravipravi# This file was generated by Appraisal source "https://rubygems.org" gem "openssl", "~> 2.2.0" gemspec path: "../" webauthn-2.4.0/gemfiles/openssl_2_1.gemfile0000644000175000017500000000016613755257222017644 0ustar pravipravi# This file was generated by Appraisal source "https://rubygems.org" gem "openssl", "~> 2.1.0" gemspec path: "../" webauthn-2.4.0/gemfiles/openssl_head.gemfile0000644000175000017500000000022213755257222020155 0ustar pravipravi# This file was generated by Appraisal source "https://rubygems.org" gem "openssl", git: "https://github.com/ruby/openssl" gemspec path: "../" webauthn-2.4.0/gemfiles/openssl_2_0.gemfile0000644000175000017500000000016613755257222017643 0ustar pravipravi# This file was generated by Appraisal source "https://rubygems.org" gem "openssl", "~> 2.0.0" gemspec path: "../" webauthn-2.4.0/docs/0000755000175000017500000000000013755257222013320 5ustar pravipraviwebauthn-2.4.0/docs/u2f_migration.md0000644000175000017500000001207413755257222016413 0ustar pravipravi# Migrating from U2F to WebAuthn The Chromium team [recommends](https://groups.google.com/a/chromium.org/forum/#!msg/security-dev/BGWA1d7a6rI/W2avestmBAAJ) application developers to switch from the U2F API to the WebAuthn API. This document describes how a Ruby application using the [u2f gem by Castle](https://github.com/castle/ruby-u2f) can migrate existing credentials so that their users do not experience interruption or need to re-register their security keys. Note that the migration is one-way: credentials registered using WebAuthn cannot be made compatible with the U2F API. It is recommended to successfully migrate authorization flows before migrating registration flows. ## Migrate registered U2F credentials Assuming you have a registered credential per the u2f gem readme, base64 urlsafe encoded in a database: ```ruby # This domain will be used in all code examples. It's a single-facet app but a multi-facet AppID # (e.g. https://example.com/app-id.json) will work as well. domain = URI("https://login.example.com") u2f_registration = U2F::U2F.new(domain.to_s).register!(u2f_challenge, u2f_register_response) # => # ``` The `U2fMigrator` class quacks like `WebAuthn::AuthenticatorAttestationResponse` and can be used similarly as documented in the [registration verification phase](https://github.com/cedarcode/webauthn-ruby/blob/master/README.md#verification-phase). Of course a `verify` instance method is not implemented, as there is no real interaction with an authenticator. The migrator can be used to convert credentials in real time during authentication while keeping them stored in the U2F format, and in a backfill task to store credentials in the new format, depending on how you are approaching your migration. ```ruby require "webauthn/u2f_migrator" migrated_credential = WebAuthn::U2fMigrator.new( app_id: domain, certificate: u2f_registration.certificate, key_handle: u2f_registration.key_handle, public_key: u2f_registration.public_key, counter: u2f_registration.counter ) migrated_credential.credential.id # => "\x99\xB5LE83I>q.\xE9\x9C\x90l\xED'\xD5E[\xAB\xDE9\xB7\xCD!\x85\x92\x9F{\x13\xA8\x86" migrated_credential.credential.public_key # => "\xA5\x03& \x01!X \xE2P^Q`\xF9\x97\xD9*n<\x14\xDA\xB6a\xEEoK\x03\xACpMb\xED\x8B\x06E\"#!\xED\xC6\x01\x02\"X #C\x97\xAD C\x000\xE7\xD1\xD4%\xCFh\x83\xCD\x9E\xCB\xBC,\"\x1F>\xF6SZ\xA1U\xAB7\xBE\xEB" migrated_credential.authenticator_data.sign_count # => 41 ``` ## Authenticate migrated U2F credentials Following the documentation on the [authentication initiation](https://github.com/cedarcode/webauthn-ruby/blob/master/README.md#authentication), you need to specify the [FIDO AppID extension](https://www.w3.org/TR/webauthn/#sctn-appid-extension) for U2F migratedq credentials. The WebAuthn standard explains: > The FIDO APIs use an alternative identifier for Relying Parties called an _AppID_, and any credentials created using > those APIs will be scoped to that identifier. Without this extension, they would need to be re-registered in order to > be scoped to an RP ID. For the earlier given example `domain` this means: - FIDO AppID: `https://login.example.com` - Valid RP IDs: `login.example.com` (default) and `example.com` ```ruby credential_request_options = WebAuthn.credential_request_options credential_request_options[:extensions] = { appid: domain.to_s } ``` On the frontend, in the resolved value from `navigator.credentials.get({ "publicKey": credentialRequestOptions })` add a call to [getClientExtensionResults()](https://www.w3.org/TR/webauthn/#dom-publickeycredential-getclientextensionresults) and send its result to your backend alongside the `id`/`rawId` and `response` values. If the authenticator used the AppID extension, the returned value will contain `{ "appid": true }`. In the example below, we use `clientExtensionResults`. During authentication verification phase, you must pass either the original AppID or the RP ID as the `rp_id` argument: > If true, the AppID was used and thus, when verifying an assertion, the Relying Party MUST expect the `rpIdHash` to be > the hash of the _AppID_, not the RP ID. ```ruby assertion_response = WebAuthn::AuthenticatorAssertionResponse.new( credential_id: params[:id], authenticator_data: params[:response][:authenticatorData], client_data_json: params[:response][:clientDataJSON], signature: params[:response][:signature], ) assertion_response.verify( expected_challenge, allowed_credentials: [credential], rp_id: params[:clientExtensionResults][:appid] ? domain.to_s : domain.host, ) ``` webauthn-2.4.0/bin/0000755000175000017500000000000013755257222013140 5ustar pravipraviwebauthn-2.4.0/bin/setup0000755000175000017500000000020313755257222014221 0ustar pravipravi#!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' set -vx bundle install # Do any other automated setup that you need to do here webauthn-2.4.0/bin/console0000755000175000017500000000056513755257222014536 0ustar pravipravi#!/usr/bin/env ruby # frozen_string_literal: true require "bundler/setup" require "webauthn" # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. # (If you use this, don't forget to add pry to your Gemfile!) # require "pry" # Pry.start require "irb" IRB.start(__FILE__) webauthn-2.4.0/LICENSE.txt0000644000175000017500000000206213755257222014213 0ustar pravipraviThe MIT License (MIT) Copyright (c) 2018 Gonzalo Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. webauthn-2.4.0/webauthn.gemspec0000644000175000017500000000415413755257222015556 0ustar pravipravi# frozen_string_literal: true lib = File.expand_path('lib', __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require "webauthn/version" Gem::Specification.new do |spec| spec.name = "webauthn" spec.version = WebAuthn::VERSION spec.authors = ["Gonzalo Rodriguez", "Braulio Martinez"] spec.email = ["gonzalo@cedarcode.com", "braulio@cedarcode.com"] spec.summary = "WebAuthn ruby server library" spec.description = 'WebAuthn ruby server library ― Make your application a W3C Web Authentication conformant Relying Party and allow your users to authenticate with U2F and FIDO2 authenticators.' spec.homepage = "https://github.com/cedarcode/webauthn-ruby" spec.license = "MIT" spec.metadata = { "bug_tracker_uri" => "https://github.com/cedarcode/webauthn-ruby/issues", "changelog_uri" => "https://github.com/cedarcode/webauthn-ruby/blob/master/CHANGELOG.md", "source_code_uri" => "https://github.com/cedarcode/webauthn-ruby" } spec.files = `git ls-files -z`.split("\x0").reject do |f| f.match(%r{^(test|spec|features|assets)/}) end spec.bindir = "exe" spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] spec.required_ruby_version = ">= 2.4" spec.add_dependency "android_key_attestation", "~> 0.3.0" spec.add_dependency "awrence", "~> 1.1" spec.add_dependency "bindata", "~> 2.4" spec.add_dependency "cbor", "~> 0.5.9" spec.add_dependency "cose", "~> 1.1" spec.add_dependency "openssl", "~> 2.0" spec.add_dependency "safety_net_attestation", "~> 0.4.0" spec.add_dependency "securecompare", "~> 1.0" spec.add_dependency "tpm-key_attestation", "~> 0.10.0" spec.add_development_dependency "appraisal", "~> 2.3.0" spec.add_development_dependency "bundler", ">= 1.17", "< 3.0" spec.add_development_dependency "byebug", "~> 11.0" spec.add_development_dependency "rake", "~> 13.0" spec.add_development_dependency "rspec", "~> 3.8" spec.add_development_dependency "rubocop", "0.89" spec.add_development_dependency "rubocop-rspec", "~> 1.38.1" end webauthn-2.4.0/Rakefile0000644000175000017500000000031713755257222014036 0ustar pravipravi# frozen_string_literal: true require "bundler/gem_tasks" require "rspec/core/rake_task" require "rubocop/rake_task" RSpec::Core::RakeTask.new(:spec) RuboCop::RakeTask.new task default: [:rubocop, :spec] webauthn-2.4.0/.rspec0000644000175000017500000000002513755257222013502 0ustar pravipravi--order rand --color