Skip to content

Commit

Permalink
Merge pull request #215 from bdewater/counter
Browse files Browse the repository at this point in the history
Add signature counter verification
  • Loading branch information
bdewater committed Jun 17, 2019
2 parents 508576b + 830fee1 commit f953cc9
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 12 deletions.
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,12 @@ begin
attestation_response.verify(expected_challenge)

# 1. Register the new user and
# 2. Keep Credential ID and Credential Public Key under storage
# 2. Keep Credential ID, Credential Public Key and Sign Count under storage
# for future authentications
# Access by invoking:
# `attestation_response.credential.id`
# `attestation_response.credential.public_key`
# `attestation_response.authenticator_data.sign_count`
rescue WebAuthn::VerificationError => e
# Handle error
end
Expand Down Expand Up @@ -171,11 +172,13 @@ Assuming you have the previously stored Credential Public Key, now in variable `
#
# E.g. in https://github.com/cedarcode/webauthn-rails-demo-app we use `Base64.strict_decode64`
# on the user-agent encoded data before calling `#verify`
selected_credential_id = "..."
authenticator_data = "..."
client_data_json = "..."
signature = "..."

assertion_response = WebAuthn::AuthenticatorAssertionResponse.new(
credential_id: selected_credential_id,
authenticator_data: authenticator_data,
client_data_json: client_data_json,
signature: signature
Expand All @@ -185,7 +188,8 @@ assertion_response = WebAuthn::AuthenticatorAssertionResponse.new(
# previously stored credential for the user that is attempting to sign in.
allowed_credential = {
id: credential_id,
public_key: credential_public_key
public_key: credential_public_key,
sign_count: sign_count,
}

begin
Expand All @@ -195,6 +199,9 @@ begin
rescue WebAuthn::VerificationError => e
# Handle error
end

# Find the selected credential in your data storage using `selected_credential_id`
# Update the stored sign count with the value from `assertion_response.authenticator_data.sign_count`
```

## Attestation Statement Formats
Expand Down
16 changes: 16 additions & 0 deletions lib/webauthn/authenticator_assertion_response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
module WebAuthn
class CredentialVerificationError < VerificationError; end
class SignatureVerificationError < VerificationError; end
class SignCountVerificationError < VerificationError; end

class AuthenticatorAssertionResponse < AuthenticatorResponse
def initialize(credential_id:, authenticator_data:, signature:, **options)
Expand All @@ -25,6 +26,7 @@ def verify(expected_challenge, expected_origin = nil, allowed_credentials:, user

verify_item(:credential, allowed_credentials)
verify_item(:signature, credential_cose_key(allowed_credentials))
verify_item(:sign_count, allowed_credentials)

true
end
Expand All @@ -43,6 +45,20 @@ def valid_signature?(credential_cose_key)
.verify(signature, authenticator_data_bytes + client_data.hash)
end

def valid_sign_count?(allowed_credentials)
matched_credential = allowed_credentials.find do |credential|
credential[:id] == credential_id
end
# TODO: make passing sign count mandatory in next major version
stored_sign_count = matched_credential.fetch(:sign_count, 0)

if authenticator_data.sign_count.nonzero? || stored_sign_count.nonzero?
authenticator_data.sign_count > stored_sign_count
else
true
end
end

def valid_credential?(allowed_credentials)
allowed_credential_ids = allowed_credentials.map { |credential| credential[:id] }

Expand Down
4 changes: 3 additions & 1 deletion lib/webauthn/fake_authenticator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ def get_assertion(
client_data_hash:,
user_present: true,
user_verified: false,
aaguid: AuthenticatorData::AAGUID
aaguid: AuthenticatorData::AAGUID,
sign_count: 0
)
credential_options = credentials[rp_id]

Expand All @@ -47,6 +48,7 @@ def get_assertion(
user_present: user_present,
user_verified: user_verified,
aaguid: aaguid,
sign_count: sign_count,
).serialize

signature = credential_key.sign("SHA256", authenticator_data + client_data_hash)
Expand Down
5 changes: 3 additions & 2 deletions lib/webauthn/fake_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def create(challenge: fake_challenge, rp_id: nil, user_present: true, user_verif
}
end

def get(challenge: fake_challenge, rp_id: nil, user_present: true, user_verified: false)
def get(challenge: fake_challenge, rp_id: nil, user_present: true, user_verified: false, sign_count: 0)
rp_id ||= URI.parse(origin).host

client_data_json = data_json_for(:get, challenge)
Expand All @@ -51,7 +51,8 @@ def get(challenge: fake_challenge, rp_id: nil, user_present: true, user_verified
rp_id: rp_id,
client_data_hash: client_data_hash,
user_present: user_present,
user_verified: user_verified
user_verified: user_verified,
sign_count: sign_count,
)

{
Expand Down
15 changes: 10 additions & 5 deletions spec/conformance/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@

RP_NAME = "webauthn-ruby #{WebAuthn::VERSION} conformance test server"

Credential = Struct.new(:id, :public_key) do
Credential = Struct.new(:id, :public_key, :sign_count) do
@credentials = {}

def self.register(username, id:, public_key:)
def self.register(username, id:, public_key:, sign_count:)
@credentials[username] ||= []
@credentials[username] << Credential.new(id, public_key)
@credentials[username] << Credential.new(id, public_key, sign_count)
end

def self.registered_for(username)
Expand Down Expand Up @@ -78,7 +78,8 @@ def descriptor
Credential.register(
cookies["username"],
id: Base64.urlsafe_encode64(attestation_response.credential.id, padding: false),
public_key: attestation_response.credential.public_key
public_key: attestation_response.credential.public_key,
sign_count: attestation_response.authenticator_data.sign_count,
)

cookies["challenge"] = nil
Expand Down Expand Up @@ -126,7 +127,7 @@ def descriptor
expected_challenge = Base64.urlsafe_decode64(cookies["challenge"])

allowed_credentials = Credential.registered_for(cookies["username"]).map do |c|
{ id: Base64.urlsafe_decode64(c.id), public_key: c.public_key }
{ id: Base64.urlsafe_decode64(c.id), public_key: c.public_key, sign_count: c.sign_count }
end

public_key_credential.verify(
Expand All @@ -135,6 +136,10 @@ def descriptor
user_verification: cookies["userVerification"] == "required"
)

used_credential = Credential.registered_for(cookies["username"]).detect do |c|
c.id == public_key_credential.id
end
used_credential.sign_count = assertion_response.authenticator_data.sign_count
cookies["challenge"] = nil
cookies["username"] = nil
cookies["userVerification"] = nil
Expand Down
58 changes: 56 additions & 2 deletions spec/webauthn/authenticator_assertion_response_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
let(:credential_id) { credential[0] }
let(:credential_public_key) { credential[1] }

let(:allowed_credentials) { [{ id: credential_id, public_key: credential_public_key }] }
let(:allowed_credentials) { [{ id: credential_id, public_key: credential_public_key, sign_count: 0 }] }

let(:origin) { fake_origin }
let(:actual_origin) { origin }
Expand Down Expand Up @@ -68,7 +68,8 @@
[
{
id: credential_id,
public_key: credential_public_key
public_key: credential_public_key,
sign_count: 0
},
{
id: SecureRandom.random_bytes(16),
Expand Down Expand Up @@ -275,6 +276,59 @@
end
end

describe "sign_count validation" do
context "if authenticator does not support counter" do
let(:allowed_credentials) { [{ id: credential_id, public_key: credential_public_key, sign_count: 0 }] }
let(:assertion) { client.get(challenge: original_challenge, sign_count: 0) }

it "verifies" do
expect(
assertion_response.verify(
original_challenge,
allowed_credentials: allowed_credentials,
)
).to be_truthy
end
end

context "when the authenticator supports counter" do
let(:allowed_credentials) { [{ id: credential_id, public_key: credential_public_key, sign_count: 5 }] }

context "and it's greater than the stored counter" do
let(:assertion) { client.get(challenge: original_challenge, sign_count: 6) }

it "verifies" do
expect(
assertion_response.verify(
original_challenge,
allowed_credentials: allowed_credentials,
)
).to be_truthy
end
end

context "and it's equal to the stored counter" do
let(:assertion) { client.get(challenge: original_challenge, sign_count: 5) }

it "doesn't verify" do
expect {
assertion_response.verify(original_challenge, allowed_credentials: allowed_credentials)
}.to raise_exception(WebAuthn::SignCountVerificationError)
end
end

context "and it's less than the stored counter" do
let(:assertion) { client.get(challenge: original_challenge, sign_count: 4) }

it "doesn't verify" do
expect {
assertion_response.verify(original_challenge, allowed_credentials: allowed_credentials)
}.to raise_exception(WebAuthn::SignCountVerificationError)
end
end
end
end

context "when Authenticator Data is invalid" do
let(:authenticator_data) { assertion[:response][:authenticator_data][0..31] }

Expand Down

0 comments on commit f953cc9

Please sign in to comment.