Skip to content

Commit

Permalink
Support multiple relying parties (#296)
Browse files Browse the repository at this point in the history
* WIP: Relying Party model

* fixup! WIP: Relying Party model

* refactor: make `WebAuthn.configuration` a wrapper for a `RelyingParty`
instance

* fixup! refactor: make `WebAuthn.configuration` a wrapper for a `RelyingParty` instance

* refactor: delegate Configuration methods

* refactor: always pass a RelyingParty

* use relying party on when creating options

* refactor: remove unnecessary aliases

* fix: make reader private

* refactor: make parent class AuthenticatorResponse responsible for relying party

* fix: post rebase fixes

* test: get rid of all keyword 2.7 warnings

* fix: rebase trailing diff

* fix: make rubocop happy

* fix: make AttestationObject#deserialize accept relying_party param

* refactor: algorithm validation uses relying party configuration

* fix: make RP initializer use finders setter to process params correctly

* fix: cache store should recieve origin from server

* refactor: CR comments on API flexibility

* refactor: CR comments

* test: add functional comprehensive test for multiple RPs and a default configuration

* refactor: Allow #verify_authentication to be used with or without a block

* style: fix rubocop

Co-authored-by: Braulio Martinez <[email protected]>
  • Loading branch information
padulafacundo and brauliomartinezlm committed Jun 27, 2020
1 parent 924ad13 commit 2f54c92
Show file tree
Hide file tree
Showing 20 changed files with 625 additions and 134 deletions.
14 changes: 9 additions & 5 deletions lib/webauthn/attestation_object.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,22 @@ module WebAuthn
class AttestationObject
extend Forwardable

def self.deserialize(attestation_object)
from_map(CBOR.decode(attestation_object))
def self.deserialize(attestation_object, relying_party)
from_map(CBOR.decode(attestation_object), relying_party)
end

def self.from_map(map)
def self.from_map(map, relying_party)
new(
authenticator_data: WebAuthn::AuthenticatorData.deserialize(map["authData"]),
attestation_statement: WebAuthn::AttestationStatement.from(map["fmt"], map["attStmt"])
attestation_statement: WebAuthn::AttestationStatement.from(
map["fmt"],
map["attStmt"],
relying_party: relying_party
)
)
end

attr_reader :authenticator_data, :attestation_statement
attr_reader :authenticator_data, :attestation_statement, :relying_party

def initialize(authenticator_data:, attestation_statement:)
@authenticator_data = authenticator_data
Expand Down
4 changes: 2 additions & 2 deletions lib/webauthn/attestation_statement.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ class FormatNotSupportedError < Error; end
ATTESTATION_FORMAT_TPM => WebAuthn::AttestationStatement::TPM
}.freeze

def self.from(format, statement)
def self.from(format, statement, relying_party: WebAuthn.configuration.relying_party)
klass = FORMAT_TO_CLASS[format]

if klass
klass.new(statement)
klass.new(statement, relying_party)
else
raise(FormatNotSupportedError, "Unsupported attestation format '#{format}'")
end
Expand Down
17 changes: 7 additions & 10 deletions lib/webauthn/attestation_statement/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ class UnsupportedAlgorithm < Error; end
class Base
AAGUID_EXTENSION_OID = "1.3.6.1.4.1.45724.1.1.4"

def initialize(statement)
def initialize(statement, relying_party = WebAuthn.configuration.relying_party)
@statement = statement
@relying_party = relying_party
end

def valid?(_authenticator_data, _client_data_hash)
Expand All @@ -54,7 +55,7 @@ def attestation_certificate_key_id

private

attr_reader :statement
attr_reader :statement, :relying_party

def matching_aaguid?(attested_credential_data_aaguid)
extension = attestation_certificate&.extensions&.detect { |ext| ext.oid == AAGUID_EXTENSION_OID }
Expand Down Expand Up @@ -95,10 +96,10 @@ def attestation_trust_path

def trustworthy?(aaguid: nil, attestation_certificate_key_id: nil)
if ATTESTATION_TYPES_WITH_ROOT.include?(attestation_type)
configuration.acceptable_attestation_types.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
configuration.acceptable_attestation_types.include?(attestation_type)
relying_party.acceptable_attestation_types.include?(attestation_type)
end
end

Expand All @@ -122,7 +123,7 @@ def attestation_root_certificates_store(aaguid: nil, attestation_certificate_key

def root_certificates(aaguid: nil, attestation_certificate_key_id: nil)
root_certificates =
configuration.attestation_root_certificates_finders.reduce([]) do |certs, finder|
relying_party.attestation_root_certificates_finders.reduce([]) do |certs, finder|
if certs.empty?
finder.find(
attestation_format: format,
Expand Down Expand Up @@ -169,14 +170,10 @@ def verification_data(authenticator_data, client_data_hash)
def cose_algorithm
@cose_algorithm ||=
COSE::Algorithm.find(algorithm).tap do |alg|
alg && configuration.algorithms.include?(alg.name) ||
alg && relying_party.algorithms.include?(alg.name) ||
raise(UnsupportedAlgorithm, "Unsupported algorithm #{algorithm}")
end
end

def configuration
WebAuthn.configuration
end
end
end
end
7 changes: 4 additions & 3 deletions lib/webauthn/authenticator_assertion_response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ class SignatureVerificationError < VerificationError; end
class SignCountVerificationError < VerificationError; end

class AuthenticatorAssertionResponse < AuthenticatorResponse
def self.from_client(response)
encoder = WebAuthn.configuration.encoder
def self.from_client(response, relying_party: WebAuthn.configuration.relying_party)
encoder = relying_party.encoder

user_handle =
if response["userHandle"]
Expand All @@ -22,7 +22,8 @@ def self.from_client(response)
authenticator_data: encoder.decode(response["authenticatorData"]),
client_data_json: encoder.decode(response["clientDataJSON"]),
signature: encoder.decode(response["signature"]),
user_handle: user_handle
user_handle: user_handle,
relying_party: relying_party
)
end

Expand Down
17 changes: 10 additions & 7 deletions lib/webauthn/authenticator_attestation_response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ class AttestedCredentialVerificationError < VerificationError; end
class AuthenticatorAttestationResponse < AuthenticatorResponse
extend Forwardable

def self.from_client(response)
encoder = WebAuthn.configuration.encoder
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"])
client_data_json: encoder.decode(response["clientDataJSON"]),
relying_party: relying_party
)
end

Expand All @@ -33,21 +34,22 @@ 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 WebAuthn.configuration.verify_attestation_statement
if relying_party.verify_attestation_statement
verify_item(:attestation_statement)
end

true
end

def attestation_object
@attestation_object ||= WebAuthn::AttestationObject.deserialize(attestation_object_bytes)
@attestation_object ||= WebAuthn::AttestationObject.deserialize(attestation_object_bytes, relying_party)
end

def_delegators(
Expand All @@ -63,14 +65,15 @@ def attestation_object

private

attr_reader :attestation_object_bytes
attr_reader :attestation_object_bytes, :relying_party

def type
WebAuthn::TYPES[:create]
end

def valid_attested_credential?
attestation_object.valid_attested_credential?
attestation_object.valid_attested_credential? &&
relying_party.algorithms.include?(authenticator_data.credential.algorithm)
end

def valid_attestation_statement?
Expand Down
14 changes: 10 additions & 4 deletions lib/webauthn/authenticator_data/attested_credential_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class AttestedCredentialData < BinData::Record

# TODO: use keyword_init when we dropped Ruby 2.4 support
Credential =
Struct.new(:id, :public_key) do
Struct.new(:id, :public_key, :algorithm) do
def public_key_object
COSE::Key.deserialize(public_key).to_pkey
end
Expand All @@ -47,7 +47,7 @@ def aaguid
def credential
@credential ||=
if valid?
Credential.new(id, public_key)
Credential.new(id, public_key, algorithm)
end
end

Expand All @@ -59,10 +59,16 @@ def length

private

def algorithm
COSE::Algorithm.find(cose_key.alg).name
end

def valid_credential_public_key?
cose_key = COSE::Key.deserialize(public_key)
!!cose_key.alg
end

!!cose_key.alg && WebAuthn.configuration.algorithms.include?(COSE::Algorithm.find(cose_key.alg).name)
def cose_key
@cose_key ||= COSE::Key.deserialize(public_key)
end

def public_key
Expand Down
11 changes: 6 additions & 5 deletions lib/webauthn/authenticator_response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ class UserPresenceVerificationError < VerificationError; end
class UserVerifiedVerificationError < VerificationError; end

class AuthenticatorResponse
def initialize(client_data_json:)
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 ||= WebAuthn.configuration.origin || raise("Unspecified expected origin")
rp_id ||= WebAuthn.configuration.rp_id
expected_origin ||= relying_party.origin || raise("Unspecified expected origin")
rp_id ||= relying_party.id

verify_item(:type)
verify_item(:token_binding)
Expand All @@ -35,7 +36,7 @@ def verify(expected_challenge, expected_origin = nil, user_verification: nil, rp
verify_item(:authenticator_data)
verify_item(:rp_id, rp_id || rp_id_from_origin(expected_origin))

if !WebAuthn.configuration.silent_authentication
if !relying_party.silent_authentication
verify_item(:user_presence)
end

Expand All @@ -58,7 +59,7 @@ def client_data

private

attr_reader :client_data_json
attr_reader :client_data_json, :relying_party

def verify_item(item, *args)
if send("valid_#{item}?", *args)
Expand Down
78 changes: 36 additions & 42 deletions lib/webauthn/configuration.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# frozen_string_literal: true

require "openssl"
require "webauthn/encoder"
require "webauthn/error"
require 'forwardable'
require 'webauthn/relying_party'

module WebAuthn
def self.configuration
Expand All @@ -13,54 +12,49 @@ def self.configure
yield(configuration)
end

class RootCertificateFinderNotSupportedError < Error; end

class Configuration
def self.if_pss_supported(algorithm)
OpenSSL::PKey::RSA.instance_methods.include?(:verify_pss) ? algorithm : nil
end

DEFAULT_ALGORITHMS = ["ES256", if_pss_supported("PS256"), "RS256"].compact.freeze

attr_accessor :algorithms
attr_accessor :encoding
attr_accessor :origin
attr_accessor :rp_id
attr_accessor :rp_name
attr_accessor :verify_attestation_statement
attr_accessor :credential_options_timeout
attr_accessor :silent_authentication
attr_accessor :acceptable_attestation_types
attr_reader :attestation_root_certificates_finders
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=

attr_reader :relying_party

def initialize
@algorithms = DEFAULT_ALGORITHMS.dup
@encoding = WebAuthn::Encoder::STANDARD_ENCODING
@verify_attestation_statement = true
@credential_options_timeout = 120000
@silent_authentication = false
@acceptable_attestation_types = ['None', 'Self', 'Basic', 'AttCA', 'Basic_or_AttCA']
@attestation_root_certificates_finders = []
@relying_party = RelyingParty.new
end

# This is the user-data encoder.
# Used to decode user input and to encode data provided to the user.
def encoder
@encoder ||= WebAuthn::Encoder.new(encoding)
def rp_name
relying_party.name
end

def attestation_root_certificates_finders=(finders)
if !finders.respond_to?(:each)
finders = [finders]
end
def rp_name=(name)
relying_party.name = name
end

finders.each do |finder|
unless finder.respond_to?(:find)
raise RootCertificateFinderNotSupportedError, "Finder must implement `find` method"
end
end
def rp_id
relying_party.id
end

@attestation_root_certificates_finders = finders
def rp_id=(id)
relying_party.id = id
end
end
end
9 changes: 5 additions & 4 deletions lib/webauthn/credential.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
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
Expand All @@ -15,12 +16,12 @@ def self.options_for_get(**keyword_arguments)
WebAuthn::PublicKeyCredential::RequestOptions.new(**keyword_arguments)
end

def self.from_create(credential)
WebAuthn::PublicKeyCredentialWithAttestation.from_client(credential)
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)
WebAuthn::PublicKeyCredentialWithAssertion.from_client(credential)
def self.from_get(credential, relying_party: WebAuthn.configuration.relying_party)
WebAuthn::PublicKeyCredentialWithAssertion.from_client(credential, relying_party: relying_party)
end
end
end
4 changes: 2 additions & 2 deletions lib/webauthn/fake_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ module WebAuthn
class FakeClient
TYPES = { create: "webauthn.create", get: "webauthn.get" }.freeze

attr_reader :origin, :token_binding
attr_reader :origin, :token_binding, :encoding

def initialize(
origin = fake_origin,
Expand Down Expand Up @@ -104,7 +104,7 @@ def get(challenge: fake_challenge,

private

attr_reader :authenticator, :encoding
attr_reader :authenticator

def data_json_for(method, challenge)
data = {
Expand Down
Loading

0 comments on commit 2f54c92

Please sign in to comment.