webauthn-3.0.0/0000755000004100000410000000000014416763746013401 5ustar www-datawww-datawebauthn-3.0.0/docs/0000755000004100000410000000000014416763746014331 5ustar www-datawww-datawebauthn-3.0.0/docs/advanced_configuration.md0000644000004100000410000002015514416763746021352 0ustar www-datawww-data# Advanced Configuration ## Global vs Instance Based Configuration Which approach suits best your needs will depend on the architecture of your application and how do your users need to register and authenticate to it. If you have a multi-tenant application, or any application segmenation, where your users register and authenticate to each of these tenants or segments individuallly using different hostnames, or with different security needs, you need to go through [Instance Based Configuration](#instance-based-configuration). However, if your application is served for just one hostname, or else if your users authenticate to only one subdmain (e.g. your application serves www.example.com and admin.example.com but all you users authenticate through auth.example.com) you can still rely on one [Global Configuration](../README.md#configuration). If you are still not sure, or want to keep your options open, be aware that [Instance Based Configuration](#instance-based-configuration) is also a valid way of defining a single instance configuration and how you share such configuration across your application, it's up to you. ## Instance Based Configuration Intead of the [Global Configuration](../README.md#configuration) you place in `config/initializers/webauthn.rb`, you can now have an on-demand instance of `WebAuthn::RelyingParty` with the same configuration options, that you can build anywhere in you application, in the following way: ```ruby relying_party = WebAuthn::RelyingParty.new( # This value needs to match `window.location.origin` evaluated by # the User Agent during registration and authentication ceremonies. origin: "https://admin.example.com", # Relying Party name for display purposes name: "Admin Site for 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 # 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 "admin.example.com", but you can set it to # the suffix "example.com" # # 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. # # encoding: :base64url # Possible values: "ES256", "ES384", "ES512", "PS256", "PS384", "PS512", "RS256", "RS384", "RS512", "RS1" # Default: ["ES256", "PS256", "RS256"] # # algorithms: ["ES384"] ) ``` ## Instance Based API **DISCLAIMER: This API was released on version 3.0.0.alpha1 and is still under evaluation. Although it has been throughly tested and it is fully functional it might be changed until the final release of version 3.0.0.** The explanation for each ceremony can be found in depth in [Credential Registration](../README.md#credential-registration) and [Credential Authentication](../README.md#credential-authentication) but if you choose this instance based approach to define your WebAuthn configurations and assuming `relying_party` is the result of an instance you get through `WebAuthn::RelytingParty.new(...)` the code in those explanations needs to be updated to: ### Credential Registration #### 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 = relying_party.options_for_registration( 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]: begin webauthn_credential = relying_party.verify_registration( params[:publicKeyCredential], params[:create_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 #### Initiation phase ```ruby options = relying_party.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 ```ruby begin # Assuming you're using @github/webauthn-json package to send the `PublicKeyCredential` object back # in params[:publicKeyCredential]: webauthn_credential, stored_credential = relying_party.verify_authentication( params[:publicKeyCredential], session[:authentication_challenge] ) do # the returned object needs to respond to #public_key and #sign_count user.credentials.find_by(webauthn_id: webauthn_credential.id) end # 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 ``` ## Moving from Global to Instance Based Configuration Adding a configuration for a new instance does not mean you need to get rid of your Global configuration. They can co-exist in your application and be both available for the different usages you might have. `WebAuthn.configuration.relying_party` will always return the global one while `WebAuthn::RelyingParty.new`, executed anywhere in your codebase, will allow you to create a different instance as you see the need. They will not collide and instead operate in isolation without any shared state. The gem API described in the current [Usage](../README.md#usage) section for the [Global Configuration](../README.md#configuration) approach will still valid but the [Instance Based API](#instance-based-api) also works with the global `relying_party` that is maintain globally at `WebAuthn.configuration.relying_party`. webauthn-3.0.0/docs/u2f_migration.md0000644000004100000410000001214414416763746017422 0ustar www-datawww-data# 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#initiation-phase-1), 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` You can request the use of the `appid` extension by setting the AppID in the configuration, like this: ```ruby WebAuthn.configure do |config| config.legacy_u2f_appid = "https://login.example.com" end ``` By doing this, the `appid` extension will be automatically requested when generating the options for get: ```ruby options = WebAuthn::Credential.options_for_get ``` 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 }`. During authentication verification phase, if you followed the [verification phase documentation](https://github.com/cedarcode/webauthn-ruby#verification-phase-1) and have set the AppID in the config, the method `PublicKeyCredentialWithAssertion#verify` will be smart enough to determine if it should use the AppID or the RP ID to verify the WebAuthn credential, depending on the output of the `appid` client extension: > 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. webauthn-3.0.0/.rspec0000644000004100000410000000002514416763746014513 0ustar www-datawww-data--order rand --color webauthn-3.0.0/README.md0000644000004100000410000004763414416763746014676 0ustar www-datawww-data__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.com/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's [public key credential](https://www.w3.org/TR/webauthn/#public-key-credential) (also called a "passkey"), 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 native [Android](https://developers.google.com/identity/fido/android/native-apps) or [macOS/iOS/iPadOS](https://developer.apple.com/documentation/authenticationservices/public-private_key_authentication) 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 pasword-less login in a __Rails__ app in [webauthn-rails-demo-app](https://github.com/cedarcode/webauthn-rails-demo-app). If you want to see an example on how to use this gem as a second factor authenticator in a __Rails__ application instead, you can check it in [webauthn-2fa-rails-demo](https://github.com/cedarcode/webauthn-2fa-rails-demo). 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 If you have a multi-tenant application or just need to configure WebAuthn differently for separate parts of your application (e.g. if your users authenticate to different subdomains in the same application), we strongly recommend you look at this [Advanced Configuration](docs/advanced_configuration.md) section instead of this. 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 Formats | Attestation Statement Format | Supported? | | -------- | :--------: | | packed (self attestation) | Yes | | packed (x5c attestation) | Yes | | tpm (x5c attestation) | Yes | | android-key | Yes | | android-safetynet | Yes | | apple | 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-3.0.0/bin/0000755000004100000410000000000014416763746014151 5ustar www-datawww-datawebauthn-3.0.0/bin/console0000755000004100000410000000056514416763746015547 0ustar www-datawww-data#!/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-3.0.0/bin/setup0000755000004100000410000000020314416763746015232 0ustar www-datawww-data#!/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-3.0.0/CHANGELOG.md0000644000004100000410000004033114416763746015213 0ustar www-datawww-data# Changelog ## [v3.0.0] - 2023-02-15 ### Added - Add the capability of handling appid extension #319 [@santiagorodriguez96] - Add support for credential backup flags #378 [@santiagorodriguez96] - Update dependencies to make gem compatible with OpenSSL 3.1 ([@bdewater],[@santiagorodriguez96]) ## [v3.0.0.alpha2] - 2022-09-12 ### Added - Rebased support for multiple relying parties from v3.0.0.alpha1 on top of v2.5.2, the previous alpha version was based on v2.3.0 ([@bdewater]) ### BREAKING CHANGES - Bumped minimum required Ruby version to 2.5 ([@bdewater]) ## [v3.0.0.alpha1] - 2020-06-27 ### Added - Ability to define multiple relying parties with the introduction of the `WebAuthn::RelyingParty` class ([@padulafacundo], [@brauliomartinezlm]) ## [v2.5.2] - 2022-07-13 ### Added - Updated dependencies to make the gem compatible with openssl-3 [@ClearlyClaire] ## [v2.5.1] - 2022-03-20 ### Added - Updated openssl support to be ~>2.2 [@bdewater] ### Removed - Removed dependency [secure_compare dependency] (https://rubygems.org/gems/secure_compare/versions/0.0.1) and use OpenSSL#secure_compare instead [@bdewater] ## [v2.5.0] - 2021-03-14 ### Added - Support 'apple' attestation statement format ([#343](https://github.com/cedarcode/webauthn-ruby/pull/343) / [@juanarias93], [@santiagorodriguez96]) - Allow specifying an array of ids as `allow_credentials:` for `FakeClient#get` method ([#335](https://github.com/cedarcode/webauthn-ruby/pull/335) / [@kingjan1999]) ### Removed - No longer accept "removed from the WebAuthn spec" options `rp: { icon: }` and `user: { icon: }` for `WebAuthn::Credential.options_for_create` method ([#326](https://github.com/cedarcode/webauthn-ruby/pull/326) / [@santiagorodriguez96]) ## [v2.4.1] - 2021-02-15 ### Fixed - Fix verification of new credential if no attestation provided and 'None' type is not among configured `acceptable_attestation_types`. I.e. reject it instead of letting it go through. ## [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 [v3.0.0]: https://github.com/cedarcode/webauthn-ruby/compare/2-stable...v3.0.0/ [v3.0.0.alpha2]: https://github.com/cedarcode/webauthn-ruby/compare/2-stable...v3.0.0.alpha2/ [v3.0.0.alpha1]: https://github.com/cedarcode/webauthn-ruby/compare/v2.3.0...v3.0.0.alpha1 [v2.5.2]: https://github.com/cedarcode/webauthn-ruby/compare/v2.5.1...v2.5.2/ [v2.5.1]: https://github.com/cedarcode/webauthn-ruby/compare/v2.5.0...v2.5.1/ [v2.5.0]: https://github.com/cedarcode/webauthn-ruby/compare/v2.4.1...v2.5.0/ [v2.4.1]: https://github.com/cedarcode/webauthn-ruby/compare/v2.4.0...v2.4.1/ [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/ [@brauliomartinezlm]: https://github.com/brauliomartinezlm [@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 [@juanarias93]: https://github.com/juanarias93 [@kingjan1999]: https://github.com/@kingjan1999 [@jdongelmans]: https://github.com/jdongelmans [@petergoldstein]: https://github.com/petergoldstein [@ClearlyClaire]: https://github.com/ClearlyClaire webauthn-3.0.0/.rubocop.yml0000644000004100000410000001026214416763746015654 0ustar www-datawww-datarequire: - rubocop-rspec - rubocop-rake inherit_mode: merge: - AllowedNames AllCops: TargetRubyVersion: 2.5 DisabledByDefault: true NewCops: disable Exclude: - "gemfiles/**/*" - "vendor/**/*" Bundler: Enabled: true Gemspec: Enabled: true Layout: Enabled: true Layout/ClassStructure: Enabled: true Layout/EmptyLineBetweenDefs: AllowAdjacentOneLineDefs: 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 Naming/VariableNumber: Enabled: false 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-3.0.0/.gitignore0000644000004100000410000000031014416763746015363 0ustar www-datawww-data/.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-3.0.0/webauthn.gemspec0000644000004100000410000000410214416763746016560 0ustar www-datawww-data# 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.5" 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.2" spec.add_dependency "safety_net_attestation", "~> 0.4.0" spec.add_dependency "tpm-key_attestation", "~> 0.12.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", "~> 1.9.1" spec.add_development_dependency "rubocop-rake", "~> 0.5.1" spec.add_development_dependency "rubocop-rspec", "~> 2.2.0" end webauthn-3.0.0/Rakefile0000644000004100000410000000031714416763746015047 0ustar www-datawww-data# 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-3.0.0/lib/0000755000004100000410000000000014416763746014147 5ustar www-datawww-datawebauthn-3.0.0/lib/webauthn.rb0000644000004100000410000000055714416763746016320 0ustar www-datawww-data# 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-3.0.0/lib/webauthn/0000755000004100000410000000000014416763746015764 5ustar www-datawww-datawebauthn-3.0.0/lib/webauthn/version.rb0000644000004100000410000000010714416763746017774 0ustar www-datawww-data# frozen_string_literal: true module WebAuthn VERSION = "3.0.0" end webauthn-3.0.0/lib/webauthn/credential_rp_entity.rb0000644000004100000410000000021514416763746022516 0ustar www-datawww-data# frozen_string_literal: true require "webauthn/credential_entity" module WebAuthn class CredentialRPEntity < CredentialEntity end end webauthn-3.0.0/lib/webauthn/attestation_object.rb0000644000004100000410000000255014416763746022200 0ustar www-datawww-data# 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, relying_party) from_map(CBOR.decode(attestation_object), relying_party) end def self.from_map(map, relying_party) new( authenticator_data: WebAuthn::AuthenticatorData.deserialize(map["authData"]), attestation_statement: WebAuthn::AttestationStatement.from( map["fmt"], map["attStmt"], relying_party: relying_party ) ) end attr_reader :authenticator_data, :attestation_statement, :relying_party 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-3.0.0/lib/webauthn/relying_party.rb0000644000004100000410000000721014416763746021201 0ustar www-datawww-data# frozen_string_literal: true require "openssl" require "webauthn/credential" require "webauthn/encoder" require "webauthn/error" module WebAuthn class RootCertificateFinderNotSupportedError < Error; end class RelyingParty def self.if_pss_supported(algorithm) OpenSSL::PKey::RSA.instance_methods.include?(:verify_pss) ? algorithm : nil end DEFAULT_ALGORITHMS = ["ES256", "PS256", "RS256"].compact.freeze def initialize( algorithms: DEFAULT_ALGORITHMS.dup, encoding: WebAuthn::Encoder::STANDARD_ENCODING, origin: nil, id: nil, name: nil, verify_attestation_statement: true, credential_options_timeout: 120000, silent_authentication: false, acceptable_attestation_types: ['None', 'Self', 'Basic', 'AttCA', 'Basic_or_AttCA', 'AnonCA'], attestation_root_certificates_finders: [], legacy_u2f_appid: nil ) @algorithms = algorithms @encoding = encoding @origin = origin @id = id @name = name @verify_attestation_statement = verify_attestation_statement @credential_options_timeout = credential_options_timeout @silent_authentication = silent_authentication @acceptable_attestation_types = acceptable_attestation_types @legacy_u2f_appid = legacy_u2f_appid self.attestation_root_certificates_finders = attestation_root_certificates_finders end attr_accessor :algorithms, :encoding, :origin, :id, :name, :verify_attestation_statement, :credential_options_timeout, :silent_authentication, :acceptable_attestation_types, :legacy_u2f_appid attr_reader :attestation_root_certificates_finders # 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 def options_for_registration(**keyword_arguments) WebAuthn::Credential.options_for_create( **keyword_arguments, relying_party: self ) end def verify_registration(raw_credential, challenge, user_verification: nil) webauthn_credential = WebAuthn::Credential.from_create(raw_credential, relying_party: self) if webauthn_credential.verify(challenge, user_verification: user_verification) webauthn_credential end end def options_for_authentication(**keyword_arguments) WebAuthn::Credential.options_for_get( **keyword_arguments, relying_party: self ) end def verify_authentication( raw_credential, challenge, user_verification: nil, public_key: nil, sign_count: nil ) webauthn_credential = WebAuthn::Credential.from_get(raw_credential, relying_party: self) stored_credential = yield(webauthn_credential) if block_given? if webauthn_credential.verify( challenge, public_key: public_key || stored_credential.public_key, sign_count: sign_count || stored_credential.sign_count, user_verification: user_verification ) block_given? ? [webauthn_credential, stored_credential] : webauthn_credential end end end end webauthn-3.0.0/lib/webauthn/fake_authenticator/0000755000004100000410000000000014416763746021624 5ustar www-datawww-datawebauthn-3.0.0/lib/webauthn/fake_authenticator/attestation_object.rb0000644000004100000410000000410514416763746026036 0ustar www-datawww-data# 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, backup_eligibility: false, backup_state: 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 @backup_eligibility = backup_eligibility @backup_state = backup_state @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, :backup_eligibility, :backup_state, :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, backup_eligibility: backup_eligibility, backup_state: backup_state, sign_count: 0, extensions: extensions ) end end end end end webauthn-3.0.0/lib/webauthn/fake_authenticator/authenticator_data.rb0000644000004100000410000000705614416763746026024 0ustar www-datawww-data# 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.generate("prime256v1").public_key }, sign_count: 0, user_present: true, user_verified: !user_present, backup_eligibility: false, backup_state: false, 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 @backup_eligibility = backup_eligibility @backup_state = backup_state @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, :backup_eligibility, :backup_state def flags [ [ bit(:user_present), reserved_for_future_use_bit, bit(:user_verified), bit(:backup_eligibility), bit(:backup_state), 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, backup_eligibility: backup_eligibility, backup_state: backup_state } 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-3.0.0/lib/webauthn/u2f_migrator.rb0000644000004100000410000000506014416763746020712 0ustar www-datawww-data# 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( id: hash[:id], public_key: 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-3.0.0/lib/webauthn/credential_entity.rb0000644000004100000410000000023614416763746022020 0ustar www-datawww-data# frozen_string_literal: true module WebAuthn class CredentialEntity attr_reader :name def initialize(name:) @name = name end end end webauthn-3.0.0/lib/webauthn/credential_creation_options.rb0000644000004100000410000000477014416763746024072 0ustar www-datawww-data# 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-3.0.0/lib/webauthn/encoder.rb0000644000004100000410000000167014416763746017734 0ustar www-datawww-data# 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-3.0.0/lib/webauthn/public_key_credential_with_assertion.rb0000644000004100000410000000206014416763746025751 0ustar www-datawww-data# 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, rp_id: appid_extension_output ? appid : nil ) true end def user_handle if raw_user_handle encoder.encode(raw_user_handle) end end def raw_user_handle response.user_handle end private def appid_extension_output return if client_extension_outputs.nil? client_extension_outputs['appid'] end def appid URI.parse(relying_party.legacy_u2f_appid || raise("Unspecified legacy U2F AppID")).to_s end end end webauthn-3.0.0/lib/webauthn/authenticator_response.rb0000644000004100000410000000621514416763746023105 0ustar www-datawww-data# frozen_string_literal: true require "webauthn/authenticator_data" require "webauthn/client_data" require "webauthn/error" 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:, relying_party: WebAuthn.configuration.relying_party) @client_data_json = client_data_json @relying_party = relying_party end def verify(expected_challenge, expected_origin = nil, user_verification: nil, rp_id: nil) expected_origin ||= relying_party.origin || raise("Unspecified expected origin") rp_id ||= relying_party.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 !relying_party.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, :relying_party 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) OpenSSL.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-3.0.0/lib/webauthn/authenticator_assertion_response.rb0000644000004100000410000000425514416763746025176 0ustar www-datawww-data# 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, relying_party: WebAuthn.configuration.relying_party) encoder = relying_party.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, relying_party: relying_party ) 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-3.0.0/lib/webauthn/attestation_statement.rb0000644000004100000410000000325214416763746022736 0ustar www-datawww-data# frozen_string_literal: true require "webauthn/attestation_statement/android_key" require "webauthn/attestation_statement/android_safetynet" require "webauthn/attestation_statement/apple" 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" ATTESTATION_FORMAT_APPLE = "apple" 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, ATTESTATION_FORMAT_APPLE => WebAuthn::AttestationStatement::Apple }.freeze def self.from(format, statement, relying_party: WebAuthn.configuration.relying_party) klass = FORMAT_TO_CLASS[format] if klass klass.new(statement, relying_party) else raise(FormatNotSupportedError, "Unsupported attestation format '#{format}'") end end end end webauthn-3.0.0/lib/webauthn/public_key_credential_with_attestation.rb0000644000004100000410000000123214416763746026301 0ustar www-datawww-data# 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-3.0.0/lib/webauthn/credential_request_options.rb0000644000004100000410000000200314416763746023741 0ustar www-datawww-data# 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-3.0.0/lib/webauthn/error.rb0000644000004100000410000000012614416763746017441 0ustar www-datawww-data# frozen_string_literal: true module WebAuthn class Error < StandardError; end end webauthn-3.0.0/lib/webauthn/public_key_credential.rb0000644000004100000410000000344214416763746022634 0ustar www-datawww-data# 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, relying_party: WebAuthn.configuration.relying_party) new( type: credential["type"], id: credential["id"], raw_id: relying_party.encoder.decode(credential["rawId"]), client_extension_outputs: credential["clientExtensionResults"], response: response_class.from_client(credential["response"], relying_party: relying_party), relying_party: relying_party ) end def initialize( type:, id:, raw_id:, response:, client_extension_outputs: {}, relying_party: WebAuthn.configuration.relying_party ) @type = type @id = id @raw_id = raw_id @client_extension_outputs = client_extension_outputs @response = response @relying_party = relying_party 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 def backup_eligible? authenticator_data&.credential_backup_eligible? end def backed_up? authenticator_data&.credential_backed_up? end private attr_reader :relying_party 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 relying_party.encoder end end end webauthn-3.0.0/lib/webauthn/fake_authenticator.rb0000644000004100000410000000622314416763746022154 0ustar www-datawww-data# 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, backup_eligibility: false, backup_state: 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, backup_eligibility: backup_eligibility, backup_state: backup_state, 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, backup_eligibility: false, backup_state: false, aaguid: AuthenticatorData::AAGUID, sign_count: nil, extensions: nil, allow_credentials: nil ) credential_options = credentials[rp_id] if credential_options allow_credentials ||= credential_options.keys credential_id = (credential_options.keys & allow_credentials).first unless credential_id raise "No matching credentials (allowed=#{allow_credentials}) " \ "found for RP #{rp_id} among credentials=#{credential_options}" end credential = credential_options[credential_id] 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, backup_eligibility: backup_eligibility, backup_state: backup_state, 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.generate("prime256v1"), 0] end def hashed(target) OpenSSL::Digest::SHA256.digest(target) end end end webauthn-3.0.0/lib/webauthn/configuration.rb0000644000004100000410000000266414416763746021170 0ustar www-datawww-data# frozen_string_literal: true require 'forwardable' require 'webauthn/relying_party' module WebAuthn def self.configuration @configuration ||= Configuration.new end def self.configure yield(configuration) end class Configuration extend Forwardable def_delegators :@relying_party, :algorithms, :algorithms=, :encoding, :encoding=, :origin, :origin=, :verify_attestation_statement, :verify_attestation_statement=, :credential_options_timeout, :credential_options_timeout=, :silent_authentication, :silent_authentication=, :acceptable_attestation_types, :acceptable_attestation_types=, :attestation_root_certificates_finders, :attestation_root_certificates_finders=, :encoder, :encoder=, :legacy_u2f_appid, :legacy_u2f_appid= attr_reader :relying_party def initialize @relying_party = RelyingParty.new end def rp_name relying_party.name end def rp_name=(name) relying_party.name = name end def rp_id relying_party.id end def rp_id=(id) relying_party.id = id end end end webauthn-3.0.0/lib/webauthn/attestation_statement/0000755000004100000410000000000014416763746022407 5ustar www-datawww-datawebauthn-3.0.0/lib/webauthn/attestation_statement/tpm.rb0000644000004100000410000000466514416763746023547 0ustar www-datawww-data# 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], trusted_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::TRUSTED_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-3.0.0/lib/webauthn/attestation_statement/android_key.rb0000644000004100000410000000410714416763746025226 0ustar www-datawww-data# 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 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-3.0.0/lib/webauthn/attestation_statement/fido_u2f.rb0000644000004100000410000000450114416763746024431 0ustar www-datawww-data# 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-3.0.0/lib/webauthn/attestation_statement/base.rb0000644000004100000410000001171514416763746023653 0ustar www-datawww-data# 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_TYPE_ANONCA = "AnonCA" ATTESTATION_TYPES_WITH_ROOT = [ ATTESTATION_TYPE_BASIC, ATTESTATION_TYPE_BASIC_OR_ATTCA, ATTESTATION_TYPE_ATTCA, ATTESTATION_TYPE_ANONCA ].freeze class Base AAGUID_EXTENSION_OID = "1.3.6.1.4.1.45724.1.1.4" def initialize(statement, relying_party = WebAuthn.configuration.relying_party) @statement = statement @relying_party = relying_party 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 attestation_certificate_key_id attestation_certificate.subject_key_identifier&.unpack("H*")&.[](0) end private attr_reader :statement, :relying_party def matching_aaguid?(attested_credential_data_aaguid) extension = attestation_certificate&.find_extension(AAGUID_EXTENSION_OID) if extension aaguid_value = OpenSSL::ASN1.decode(extension.value_der).value aaguid_value == attested_credential_data_aaguid else true end end def matching_public_key?(authenticator_data) attestation_certificate.public_key.to_der == authenticator_data.credential.public_key_object.to_der 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) relying_party.acceptable_attestation_types.include?(attestation_type) && valid_certificate_chain?(aaguid: aaguid, attestation_certificate_key_id: attestation_certificate_key_id) else relying_party.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 = relying_party.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 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 && relying_party.algorithms.include?(alg.name) || raise(UnsupportedAlgorithm, "Unsupported algorithm #{algorithm}") end end end end end webauthn-3.0.0/lib/webauthn/attestation_statement/packed.rb0000644000004100000410000000451514416763746024170 0ustar www-datawww-data# 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.find_extension('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-3.0.0/lib/webauthn/attestation_statement/fido_u2f/0000755000004100000410000000000014416763746024104 5ustar www-datawww-datawebauthn-3.0.0/lib/webauthn/attestation_statement/fido_u2f/public_key.rb0000644000004100000410000000212114416763746026553 0ustar www-datawww-data# 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-3.0.0/lib/webauthn/attestation_statement/none.rb0000644000004100000410000000072514416763746023677 0ustar www-datawww-data# frozen_string_literal: true require "webauthn/attestation_statement/base" module WebAuthn module AttestationStatement class None < Base def valid?(*_args) if statement == {} && trustworthy? [WebAuthn::AttestationStatement::ATTESTATION_TYPE_NONE, nil] else false end end private def attestation_type WebAuthn::AttestationStatement::ATTESTATION_TYPE_NONE end end end end webauthn-3.0.0/lib/webauthn/attestation_statement/android_safetynet.rb0000644000004100000410000000370514416763746026443 0ustar www-datawww-data# 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 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 certificates 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-3.0.0/lib/webauthn/attestation_statement/apple.rb0000644000004100000410000000436114416763746024041 0ustar www-datawww-data# frozen_string_literal: true require "openssl" require "webauthn/attestation_statement/base" module WebAuthn module AttestationStatement class Apple < Base # Source: https://www.apple.com/certificateauthority/private/ ROOT_CERTIFICATE = OpenSSL::X509::Certificate.new(<<~PEM) -----BEGIN CERTIFICATE----- MIICEjCCAZmgAwIBAgIQaB0BbHo84wIlpQGUKEdXcTAKBggqhkjOPQQDAzBLMR8w HQYDVQQDDBZBcHBsZSBXZWJBdXRobiBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJ bmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMB4XDTIwMDMxODE4MjEzMloXDTQ1MDMx NTAwMDAwMFowSzEfMB0GA1UEAwwWQXBwbGUgV2ViQXV0aG4gUm9vdCBDQTETMBEG A1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTB2MBAGByqGSM49 AgEGBSuBBAAiA2IABCJCQ2pTVhzjl4Wo6IhHtMSAzO2cv+H9DQKev3//fG59G11k xu9eI0/7o6V5uShBpe1u6l6mS19S1FEh6yGljnZAJ+2GNP1mi/YK2kSXIuTHjxA/ pcoRf7XkOtO4o1qlcaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUJtdk 2cV4wlpn0afeaxLQG2PxxtcwDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA MGQCMFrZ+9DsJ1PW9hfNdBywZDsWDbWFp28it1d/5w2RPkRX3Bbn/UbDTNLx7Jr3 jAGGiQIwHFj+dJZYUJR786osByBelJYsVZd2GbHQu209b5RCmGQ21gpSAk9QZW4B 1bWeT0vT -----END CERTIFICATE----- PEM NONCE_EXTENSION_OID = "1.2.840.113635.100.8.2" def valid?(authenticator_data, client_data_hash) valid_nonce?(authenticator_data, client_data_hash) && matching_public_key?(authenticator_data) && trustworthy? && [attestation_type, attestation_trust_path] end private def valid_nonce?(authenticator_data, client_data_hash) extension = cred_cert&.find_extension(NONCE_EXTENSION_OID) if extension sequence = OpenSSL::ASN1.decode(extension.value_der) sequence.tag == OpenSSL::ASN1::SEQUENCE && sequence.value.size == 1 && sequence.value[0].value[0].value == OpenSSL::Digest::SHA256.digest(authenticator_data.data + client_data_hash) end end def attestation_type WebAuthn::AttestationStatement::ATTESTATION_TYPE_ANONCA end def cred_cert attestation_certificate end def default_root_certificates [ROOT_CERTIFICATE] end end end end webauthn-3.0.0/lib/webauthn/credential_options.rb0000644000004100000410000000047514416763746022204 0ustar www-datawww-data# 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-3.0.0/lib/webauthn/authenticator_attestation_response.rb0000644000004100000410000000437114416763746025525 0ustar www-datawww-data# 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, relying_party: WebAuthn.configuration.relying_party) encoder = relying_party.encoder new( attestation_object: encoder.decode(response["attestationObject"]), client_data_json: encoder.decode(response["clientDataJSON"]), relying_party: relying_party ) end attr_reader :attestation_type, :attestation_trust_path def initialize(attestation_object:, **options) super(**options) @attestation_object_bytes = attestation_object @relying_party = relying_party end def verify(expected_challenge, expected_origin = nil, user_verification: nil, rp_id: nil) super verify_item(:attested_credential) if relying_party.verify_attestation_statement verify_item(:attestation_statement) end true end def attestation_object @attestation_object ||= WebAuthn::AttestationObject.deserialize(attestation_object_bytes, relying_party) 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, :relying_party def type WebAuthn::TYPES[:create] end def valid_attested_credential? attestation_object.valid_attested_credential? && relying_party.algorithms.include?(authenticator_data.credential.algorithm) end def valid_attestation_statement? @attestation_type, @attestation_trust_path = attestation_object.valid_attestation_statement?(client_data.hash) end end end webauthn-3.0.0/lib/webauthn/credential_user_entity.rb0000644000004100000410000000052714416763746023061 0ustar www-datawww-data# 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-3.0.0/lib/webauthn/authenticator_data/0000755000004100000410000000000014416763746021627 5ustar www-datawww-datawebauthn-3.0.0/lib/webauthn/authenticator_data/attested_credential_data.rb0000644000004100000410000000356614416763746027166 0ustar www-datawww-data# 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 Credential = Struct.new(:id, :public_key, :algorithm, keyword_init: true) 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: id, public_key: public_key, algorithm: algorithm) end end def length if valid? AAGUID_LENGTH + ID_LENGTH_LENGTH + id_length + public_key_length end end private def algorithm COSE::Algorithm.find(cose_key.alg).name end def valid_credential_public_key? !!cose_key.alg end def cose_key @cose_key ||= COSE::Key.deserialize(public_key) 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-3.0.0/lib/webauthn/public_key.rb0000644000004100000410000000370414416763746020443 0ustar www-datawww-data# 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-3.0.0/lib/webauthn/public_key_credential/0000755000004100000410000000000014416763746022304 5ustar www-datawww-datawebauthn-3.0.0/lib/webauthn/public_key_credential/user_entity.rb0000644000004100000410000000073214416763746025205 0ustar www-datawww-data# 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-3.0.0/lib/webauthn/public_key_credential/options.rb0000644000004100000410000000303314416763746024323 0ustar www-datawww-data# frozen_string_literal: true require "awrence" require "securerandom" module WebAuthn class PublicKeyCredential class Options CHALLENGE_LENGTH = 32 attr_reader :timeout, :extensions, :relying_party def initialize(timeout: nil, extensions: nil, relying_party: WebAuthn.configuration.relying_party) @relying_party = relying_party @timeout = timeout || default_timeout @extensions = default_extensions.merge(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 relying_party.encoder end def raw_challenge @raw_challenge ||= SecureRandom.random_bytes(CHALLENGE_LENGTH) end def default_timeout relying_party.credential_options_timeout end def default_extensions {} end def as_public_key_descriptors(ids) Array(ids).map { |id| { type: TYPE_PUBLIC_KEY, id: id } } end end end end webauthn-3.0.0/lib/webauthn/public_key_credential/request_options.rb0000644000004100000410000000213114416763746026071 0ustar www-datawww-data# 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 || relying_party.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 default_extensions extensions = super || {} if relying_party.legacy_u2f_appid extensions.merge!(appid: relying_party.legacy_u2f_appid) end extensions end def allow_credentials_from_allow if allow as_public_key_descriptors(allow) end end end end end webauthn-3.0.0/lib/webauthn/public_key_credential/entity.rb0000644000004100000410000000122514416763746024145 0ustar www-datawww-data# frozen_string_literal: true require "awrence" module WebAuthn class PublicKeyCredential class Entity attr_reader :name def initialize(name:) @name = name 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] end end end end webauthn-3.0.0/lib/webauthn/public_key_credential/rp_entity.rb0000644000004100000410000000057614416763746024656 0ustar www-datawww-data# 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-3.0.0/lib/webauthn/public_key_credential/creation_options.rb0000644000004100000410000000422114416763746026207 0ustar www-datawww-data# 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] ||= relying_party.name rp[:id] ||= relying_party.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 || relying_party.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-3.0.0/lib/webauthn/authenticator_data.rb0000644000004100000410000000615514416763746022163 0ustar www-datawww-data# 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_2 bit1 :backup_state bit1 :backup_eligibility 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 credential_backup_eligible? flags.backup_eligibility == 1 end def credential_backed_up? flags.backup_state == 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-3.0.0/lib/webauthn/credential.rb0000644000004100000410000000177214416763746020432 0ustar www-datawww-data# 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" require "webauthn/relying_party" 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, relying_party: WebAuthn.configuration.relying_party) WebAuthn::PublicKeyCredentialWithAttestation.from_client(credential, relying_party: relying_party) end def self.from_get(credential, relying_party: WebAuthn.configuration.relying_party) WebAuthn::PublicKeyCredentialWithAssertion.from_client(credential, relying_party: relying_party) end end end webauthn-3.0.0/lib/webauthn/client_data.rb0000644000004100000410000000222414416763746020560 0ustar www-datawww-data# 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-3.0.0/lib/webauthn/fake_client.rb0000644000004100000410000001047114416763746020560 0ustar www-datawww-data# 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, :encoding 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, backup_eligibility: false, backup_state: 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, backup_eligibility: backup_eligibility, backup_state: backup_state, 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, backup_eligibility: false, backup_state: true, sign_count: nil, extensions: nil, user_handle: nil, allow_credentials: nil) rp_id ||= URI.parse(origin).host client_data_json = data_json_for(:get, encoder.decode(challenge)) client_data_hash = hashed(client_data_json) if allow_credentials allow_credentials = allow_credentials.map { |credential| encoder.decode(credential) } end assertion = authenticator.get_assertion( rp_id: rp_id, client_data_hash: client_data_hash, user_present: user_present, user_verified: user_verified, backup_eligibility: backup_eligibility, backup_state: backup_state, sign_count: sign_count, extensions: extensions, allow_credentials: allow_credentials ) { "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 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-3.0.0/lib/cose/0000755000004100000410000000000014416763746015100 5ustar www-datawww-datawebauthn-3.0.0/lib/cose/rsapkcs1_algorithm.rb0000644000004100000410000000257414416763746021232 0ustar www-datawww-data# 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-3.0.0/CONTRIBUTING.md0000644000004100000410000000410214416763746015627 0ustar www-datawww-data## 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-3.0.0/Gemfile0000644000004100000410000000030314416763746014670 0ustar www-datawww-data# 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-3.0.0/.github/0000755000004100000410000000000014416763746014741 5ustar www-datawww-datawebauthn-3.0.0/.github/workflows/0000755000004100000410000000000014416763746016776 5ustar www-datawww-datawebauthn-3.0.0/.github/workflows/git.yml0000644000004100000410000000126114416763746020304 0ustar www-datawww-data# Syntax reference: # https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions name: Git Checks on: pull_request: types: [opened, synchronize] jobs: # Fixup commits are OK in pull requests, but should generally be squashed # before merging to master, e.g. using `git rebase -i --autosquash master`. # See https://github.com/marketplace/actions/block-autosquash-commits block-fixup: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Block autosquash commits uses: xt0rted/block-autosquash-commits-action@v2 with: repo-token: ${{ secrets.GITHUB_TOKEN }} webauthn-3.0.0/.github/workflows/build.yml0000644000004100000410000000150614416763746020622 0ustar www-datawww-data# This workflow uses actions that are not certified by GitHub. # They are provided by a third-party and are governed by # separate terms of service, privacy policy, and support # documentation. # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby name: build on: push jobs: test: runs-on: ubuntu-20.04 strategy: fail-fast: false matrix: ruby: - '3.2' - '3.1' - '3.0' - '2.7' - '2.6' - '2.5' - truffleruby steps: - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - run: bundle exec rake webauthn-3.0.0/SECURITY.md0000644000004100000410000000100014416763746015161 0ustar www-datawww-data# Security Policy ## Supported Versions | Version | Supported | | ------- | ------------------ | | 2.5.z | :white_check_mark: | | 2.4.z | :white_check_mark: | | 2.3.z | :white_check_mark: | | 2.2.z | :x: | | 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-3.0.0/LICENSE.txt0000644000004100000410000000206214416763746015224 0ustar www-datawww-dataThe 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.