Skip to content

Instantly share code, notes, and snippets.

@bdewater
Last active May 30, 2019 17:23
Show Gist options
  • Save bdewater/e3b7b213302845ffac8906eef971f0af to your computer and use it in GitHub Desktop.
Save bdewater/e3b7b213302845ffac8906eef971f0af to your computer and use it in GitHub Desktop.
Migrating from U2F to WebAuthn
# Migrating from https://github.com/castle/ruby-u2f to https://github.com/cedarcode/webauthn-ruby
# Also see FIDO CTAP2 specs on backwards compatibility with CTAP1/U2F authenticators:
# https://fidoalliance.org/specs/fido-v2.0-rd-20180702/fido-client-to-authenticator-protocol-v2.0-rd-20180702.html#u2f-authenticatorMakeCredential-interoperability
# Chrome team encouraging to migrate: https://groups.google.com/a/chromium.org/forum/#!msg/security-dev/BGWA1d7a6rI/W2avestmBAAJ
require 'u2f'
require 'webauthn'
require 'webauthn/fake_client'
require 'webauthn/attestation_statement/fido_u2f'
domain = URI("https://login.example.com")
# Use a Fake U2F device to create a registration like an application would store in the database
u2f_device = U2F::FakeU2F.new(domain.to_s)
U2F_PRIVATE_KEY = u2f_device.send(:origin_key)
u2f_challenge = U2F.urlsafe_encode64(SecureRandom.random_bytes(32))
u2f_register_challenge = u2f_device.register_response(u2f_challenge)
u2f_register_response = U2F::RegisterResponse.load_from_json(u2f_register_challenge)
u2f_registration = U2F::U2F.new(domain.to_s).register!(u2f_challenge, u2f_register_response)
# <U2F::Registration:0x00007fb815b4ebf8
# @certificate=
# "MIIBCzCBsgIBATAKBggqhkjOPQQDAjASMRAwDgYDVQQDDAdVMkZUZXN0MB4XDTE5MDUyOTE4MDQwOVoXDTIwMDUyODE4MDQwOVowEjEQMA4GA1UEAwwHVTJGVGVzdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABIn18j1TysGWNO7Rl84dD1nPTysGAnnS39r3J3DrYW8SWMVf1bYn/cjvb4pfsUhvxVK+CNkAkKoTTmot4As9PGQwCgYIKoZIzj0EAwIDSAAwRQIgNR23ORnxC3zaJrsQQiv3Ule/xFPFYG3Zgfb9nrwFgAwCIQCuLWxCUOu8SVlPYrDc/ivZ32PpnjjVRfTN6sybj/DeyQ==",
# @key_handle="Y58TQlujqbWrUnMW7JPqGXrENd5YyZJ_aIh9-H84WtA",
# @public_key="BHljYElOPoJrhYqflKPvjqXTH8F5R9sG3hutKbZ74b0n9ALOfslwDNPNzoSL+YPcyPdrtQII4TzN+9O5K4jXEfk=">
# A class that quacks like an WebAuthn::AuthenticatorAttestationResponse object, mimicking a real WebAuthn registration
class U2fCredentialMigrator
def initialize(domain:, u2f_certificate:, u2f_key_handle:, u2f_public_key:, u2f_counter:)
@domain = domain
@u2f_certificate = u2f_certificate
@u2f_key_handle = u2f_key_handle
@u2f_public_key = u2f_public_key
@u2f_counter = u2f_counter
end
def authenticator_data
@authenticator_data ||= WebAuthn::FakeAuthenticator::AuthenticatorData.new(
rp_id_hash: OpenSSL::Digest::SHA256.digest(@domain.host),
credential: {
id: credential_id,
public_key: credential_cose_key
},
sign_count: @u2f_count,
user_present: true,
user_verified: false,
aaguid: WebAuthn::AttestationStatement::FidoU2f::VALID_ATTESTED_AAGUID,
# extensions: nil
)
end
def credential
@credential ||= begin
hash = authenticator_data.send(:credential)
WebAuthn::AuthenticatorData::AttestedCredentialData::Credential.new(hash[:id], hash[:public_key].serialize)
end
end
def attestation_type
WebAuthn::AttestationStatement::ATTESTATION_TYPE_BASIC_OR_ATTCA
end
def attestation_trust_path
@attestation_trust_path ||= [OpenSSL::X509::Certificate.new(Base64.strict_decode64(@u2f_certificate))]
end
private
# Let credentialId be a credentialIdLength byte array initialized with CTAP1/U2F response key handle bytes.
def credential_id
Base64.urlsafe_decode64(@u2f_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(@u2f_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
# Use U2fMigratedAuthenticatorAssertion like a real response, following from the gem readme:
# > Keep Credential ID and Credential Public Key under storage for future authentications
# > Access by invoking:
# > `attestation_response.credential.id`
# > `attestation_response.credential.public_key`
MIGRATION_RESPONSE = U2fCredentialMigrator.new(
domain: domain,
u2f_certificate: u2f_registration.certificate,
u2f_key_handle: u2f_registration.key_handle,
u2f_public_key: u2f_registration.public_key,
u2f_counter: u2f_registration.counter
)
MIGRATION_RESPONSE.credential
# <struct WebAuthn::AuthenticatorData::AttestedCredentialData::Credential
# id="c\x9F\x13B[\xA3\xA9\xB5\xABRs\x16\xEC\x93\xEA\x19z\xC45\xDEX\xC9\x92\x7Fh\x88}\xF8\x7F8Z\xD0",
# public_key=
# #<COSE::Key::EC2:0x00007fb815a08b90
# @alg=-7,
# @base_iv=nil,
# @crv=1,
# @d=nil,
# @key_ops=nil,
# @kid=nil,
# @x="yc`IN>\x82k\x85\x8A\x9F\x94\xA3\xEF\x8E\xA5\xD3\x1F\xC1yG\xDB\x06\xDE\e\xAD)\xB6{\xE1\xBD'",
# @y="\xF4\x02\xCE~\xC9p\f\xD3\xCD\xCE\x84\x8B\xF9\x83\xDC\xC8\xF7k\xB5\x02\b\xE1<\xCD\xFB\xD3\xB9+\x88\xD7\x11\xF9">>
#
# Quick 'n dirty monkey patch to inject our U2F data into the FakeAuthenticator
class U2FMigratedFakeAuthenticator < WebAuthn::FakeAuthenticator
private
def new_credential
[MIGRATION_RESPONSE.credential.id, U2F_PRIVATE_KEY]
end
end
# Register our migrated authenticator
fake_authenticator = U2FMigratedFakeAuthenticator.new
fake_client = WebAuthn::FakeClient.new(domain.to_s, authenticator: fake_authenticator)
fake_client.create(user_present: true, user_verified: false)
# Authenticate with the migrated authenticator
webauthn_challenge = SecureRandom.random_bytes(32)
fake_response = fake_client.get(challenge: webauthn_challenge, user_present: true, user_verified: false)
assertion_response = WebAuthn::AuthenticatorAssertionResponse.new(
credential_id: MIGRATION_RESPONSE.credential.id,
authenticator_data: fake_response[:response][:authenticator_data],
client_data_json: fake_response[:response][:client_data_json],
signature: fake_response[:response][:signature]
)
allowed_credential = { id: MIGRATION_RESPONSE.credential.id, public_key: MIGRATION_RESPONSE.credential.public_key }
assertion_response.verify(webauthn_challenge, domain.to_s, allowed_credentials: [allowed_credential])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment