Skip to content

Instantly share code, notes, and snippets.

@GeorgeDewar
Created January 3, 2025 05:47
Show Gist options
  • Save GeorgeDewar/c3d48a34e92da3e01ac09889e2e8346b to your computer and use it in GitHub Desktop.
Save GeorgeDewar/c3d48a34e92da3e01ac09889e2e8346b to your computer and use it in GitHub Desktop.
Authenticating to a service using an exported Passkey in Ruby
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