Created
January 3, 2025 05:47
-
-
Save GeorgeDewar/c3d48a34e92da3e01ac09889e2e8346b to your computer and use it in GitHub Desktop.
Authenticating to a service using an exported Passkey in Ruby
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require "json" | |
require "digest" | |
require "openssl" | |
require "base64" | |
module Fido2Client | |
Passkey = Data.define(:credentialId, :keyAlgorithm, :keyCurve, :keyValue, :userHandle) | |
Assertion = Data.define(:authenticator_data, :client_data_json, :credential_id, :user_handle, :signature) | |
class Client | |
def initialize | |
@origin = "https://app.simplicity.kiwi" | |
@rp_id = "simplicity.kiwi" | |
end | |
def get_assertion(passkey, challenge) | |
collected_client_data = { | |
type: "webauthn.get", | |
challenge: challenge, | |
origin: @origin, | |
crossOrigin: false, | |
} | |
client_data_json = JSON.dump(collected_client_data) | |
client_data_hash = Digest::SHA256.digest(client_data_json) | |
# Assertion | |
auth_data = generate_auth_data | |
private_key = parse_private_key(passkey.keyAlgorithm, passkey.keyCurve, passkey.keyValue) | |
signature = generate_signature(auth_data, client_data_hash, private_key) | |
Assertion.new( | |
authenticator_data: Base64.urlsafe_encode64(auth_data.pack("c*"), padding: false), | |
client_data_json: Base64.urlsafe_encode64(client_data_json, padding: false), | |
credential_id: Base64.urlsafe_encode64(guid_to_raw_format(passkey.credentialId), padding: false), | |
user_handle: passkey.userHandle, | |
signature: Base64.urlsafe_encode64(signature, padding: false), | |
) | |
end | |
private | |
def box(tag, lines) | |
lines.unshift "-----BEGIN #{tag}-----" | |
lines.push "-----END #{tag}-----" | |
lines.join("\n") | |
end | |
def der_to_pem(tag, der) | |
box tag, Base64.strict_encode64(der).scan(/.{1,64}/) | |
end | |
def parse_private_key(key_algorithm, key_curve, key_value) | |
raise "Unsupported key algorithm: #{key_algorithm}" unless key_algorithm == "ECDSA" | |
raise "Unsupported key curve: #{key_curve}" unless key_curve == "P-256" | |
# Decode the Base64 key value | |
key_value_bin = Base64.urlsafe_decode64(key_value) | |
pem = der_to_pem("PRIVATE KEY", key_value_bin) | |
OpenSSL::PKey::EC.new(pem) | |
end | |
def generate_signature(auth_data, client_data_hash, private_key) | |
sig_base = [*auth_data, *client_data_hash.bytes] | |
digest = OpenSSL::Digest.new("SHA256") | |
private_key.sign(digest, sig_base.pack("c*")) | |
end | |
def generate_auth_data | |
auth_data = [] | |
rp_id_hash = Digest::SHA256.digest(@rp_id) | |
auth_data += rp_id_hash.bytes | |
# Flags asserted: Backup eligibility, Backup State, User Verification, User Presence | |
flags = 0x1D | |
auth_data.push(flags) | |
# Counter | |
auth_data += [0, 0, 0, 0] | |
auth_data | |
end | |
def guid_to_raw_format(guid) | |
raise TypeError, "GUID parameter is invalid" unless guid.match?(/\A[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\z/) | |
# Remove the hyphens and pack the string into raw binary | |
[guid.delete("-")].pack("H*") | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment