Polish up Passkey interface
- Fix indentation - Split awkward initializer into separate methods - Rename credentials to passkeys - Simplify authenticator registry loading - Flatten nested errors under ActionPack::WebAuthn - Set passkey current params only requests that use it - Inline anemic methods - Replace custom validation with ActiveModel::Validation - Rename identity to holder in Passkey - Rename credentials to passkeys in JS - Extract framework library out of controllers - Pass params hashes down to ActionPack - Attempt to simplify public interface - Push data decoding down to the classes representing the data - Introduce has_passkeys - Add CBOR bigint support - Rename ActionPack::WebAuthn::Passkey to ActionPack::Passkey - Add create_passkey_button helper - Rename public-key to creation-options - Add sign_in_with_passkey_button helper - Dispatch events for the whole Passkey lifecycle - Add ED25519 support - Prevent crash on missing meta tag - Validate resident key options - Don't clobber existing ActionPack config options - Validate cryptographic params - Move CurrentWebAuthnRequest into ActionPack::Passkey - Use ActiveModel::Attributes for Options objects - Implement expiring challanges - Add lifecycle events - Extract param helpers into Request - Add passkey_creation_options and passkey_request_options helpers - Add create_passkey_challenge to make it easier to override the create method if needed - Prefix all view helpers with passkey_ - Auto-include ActionPack::Passkey::Holder - Make the passkey challange url configurable - Add a reminder about Passkeys to the magic link email
This commit is contained in:
@@ -19,22 +19,25 @@
|
||||
# [ES256]
|
||||
# ECDSA with P-256 curve and SHA-256. The most common algorithm for WebAuthn.
|
||||
#
|
||||
# [EdDSA]
|
||||
# EdDSA with Ed25519 curve. Increasingly supported by modern authenticators.
|
||||
#
|
||||
# [RS256]
|
||||
# RSASSA-PKCS1-v1_5 with SHA-256. Used by some security keys and platforms.
|
||||
#
|
||||
# == Attributes
|
||||
#
|
||||
# [+key_type+]
|
||||
# The COSE key type (2 for EC2, 3 for RSA).
|
||||
# The COSE key type (1 for OKP, 2 for EC2, 3 for RSA).
|
||||
#
|
||||
# [+algorithm+]
|
||||
# The COSE algorithm identifier (-7 for ES256, -257 for RS256).
|
||||
# The COSE algorithm identifier (-7 for ES256, -8 for EdDSA, -257 for RS256).
|
||||
#
|
||||
# [+parameters+]
|
||||
# The full COSE key parameters map, including curve and coordinate data.
|
||||
class ActionPack::WebAuthn::CoseKey
|
||||
# Raised when the key type, algorithm, or curve is not supported.
|
||||
class UnsupportedKeyTypeError < StandardError; end
|
||||
P256_COORDINATE_LENGTH = 32
|
||||
MINIMUM_RSA_KEY_BITS = 2048
|
||||
|
||||
# COSE key labels
|
||||
KEY_TYPE_LABEL = 1
|
||||
@@ -44,18 +47,25 @@ class ActionPack::WebAuthn::CoseKey
|
||||
EC2_Y_LABEL = -3
|
||||
RSA_N_LABEL = -1
|
||||
RSA_E_LABEL = -2
|
||||
OKP_CURVE_LABEL = -1
|
||||
OKP_X_LABEL = -2
|
||||
|
||||
# COSE key types
|
||||
OKP = 1
|
||||
EC2 = 2
|
||||
RSA = 3
|
||||
|
||||
# COSE algorithms
|
||||
ES256 = -7
|
||||
EDDSA = -8
|
||||
RS256 = -257
|
||||
|
||||
# COSE EC2 curves
|
||||
P256 = 1
|
||||
|
||||
# COSE OKP curves
|
||||
ED25519 = 6
|
||||
|
||||
# OpenSSL types
|
||||
UNCOMPRESSED_POINT_MARKER = 0x04
|
||||
|
||||
@@ -84,26 +94,30 @@ class ActionPack::WebAuthn::CoseKey
|
||||
|
||||
# Converts the COSE key to an OpenSSL public key object.
|
||||
#
|
||||
# Returns an +OpenSSL::PKey::EC+ for EC2 keys or +OpenSSL::PKey::RSA+ for
|
||||
# RSA keys, suitable for use with +OpenSSL::PKey#verify+.
|
||||
# Returns an +OpenSSL::PKey::EC+ for EC2 keys, +OpenSSL::PKey::RSA+ for
|
||||
# RSA keys, or an Ed25519 key for OKP keys, suitable for use with
|
||||
# +OpenSSL::PKey#verify+.
|
||||
#
|
||||
# Raises +UnsupportedKeyTypeError+ if the key type, algorithm, or curve
|
||||
# is not supported.
|
||||
def to_openssl_key
|
||||
case [ key_type, algorithm ]
|
||||
when [ EC2, ES256 ] then build_ec2_es256_key
|
||||
when [ OKP, EDDSA ] then build_okp_eddsa_key
|
||||
when [ RSA, RS256 ] then build_rsa_rs256_key
|
||||
else raise UnsupportedKeyTypeError, "Unsupported COSE key type/algorithm: #{key_type}/#{algorithm}"
|
||||
else raise ActionPack::WebAuthn::UnsupportedKeyTypeError, "Unsupported COSE key type/algorithm: #{key_type}/#{algorithm}"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def build_ec2_es256_key
|
||||
curve = parameters[EC2_CURVE_LABEL]
|
||||
raise UnsupportedKeyTypeError, "Unsupported EC curve: #{curve}" unless curve == P256
|
||||
raise ActionPack::WebAuthn::UnsupportedKeyTypeError, "Unsupported EC curve: #{curve}" unless curve == P256
|
||||
|
||||
x = parameters[EC2_X_LABEL]
|
||||
y = parameters[EC2_Y_LABEL]
|
||||
raise ActionPack::WebAuthn::InvalidKeyError, "Missing EC2 key coordinates" if x.nil? || y.nil?
|
||||
raise ActionPack::WebAuthn::InvalidKeyError, "Invalid EC2 coordinate length" unless x.bytesize == P256_COORDINATE_LENGTH && y.bytesize == P256_COORDINATE_LENGTH
|
||||
|
||||
# Uncompressed point format: 0x04 || x || y
|
||||
public_key_bytes = [ UNCOMPRESSED_POINT_MARKER, *x.bytes, *y.bytes ].pack("C*")
|
||||
@@ -117,11 +131,37 @@ class ActionPack::WebAuthn::CoseKey
|
||||
])
|
||||
|
||||
OpenSSL::PKey::EC.new(asn1.to_der)
|
||||
rescue OpenSSL::PKey::PKeyError => error
|
||||
raise ActionPack::WebAuthn::InvalidKeyError, "Invalid EC2 key: #{error.message}"
|
||||
end
|
||||
|
||||
def build_okp_eddsa_key
|
||||
curve = parameters[OKP_CURVE_LABEL]
|
||||
raise ActionPack::WebAuthn::UnsupportedKeyTypeError, "Unsupported OKP curve: #{curve}" unless curve == ED25519
|
||||
|
||||
x = parameters[OKP_X_LABEL]
|
||||
raise ActionPack::WebAuthn::InvalidKeyError, "Missing OKP key coordinate" if x.nil?
|
||||
|
||||
asn1 = OpenSSL::ASN1::Sequence([
|
||||
OpenSSL::ASN1::Sequence([
|
||||
OpenSSL::ASN1::ObjectId("ED25519")
|
||||
]),
|
||||
OpenSSL::ASN1::BitString(x)
|
||||
])
|
||||
|
||||
OpenSSL::PKey.read(asn1.to_der)
|
||||
rescue OpenSSL::PKey::PKeyError => error
|
||||
raise ActionPack::WebAuthn::InvalidKeyError, "Invalid OKP key: #{error.message}"
|
||||
end
|
||||
|
||||
def build_rsa_rs256_key
|
||||
n = OpenSSL::BN.new(parameters[RSA_N_LABEL], 2)
|
||||
e = OpenSSL::BN.new(parameters[RSA_E_LABEL], 2)
|
||||
n_bytes = parameters[RSA_N_LABEL]
|
||||
e_bytes = parameters[RSA_E_LABEL]
|
||||
raise ActionPack::WebAuthn::InvalidKeyError, "Missing RSA key parameters" if n_bytes.nil? || e_bytes.nil?
|
||||
raise ActionPack::WebAuthn::InvalidKeyError, "RSA key must be at least #{MINIMUM_RSA_KEY_BITS} bits" if n_bytes.bytesize * 8 < MINIMUM_RSA_KEY_BITS
|
||||
|
||||
n = OpenSSL::BN.new(n_bytes, 2)
|
||||
e = OpenSSL::BN.new(e_bytes, 2)
|
||||
|
||||
asn1 = OpenSSL::ASN1::Sequence([
|
||||
OpenSSL::ASN1::Sequence([
|
||||
@@ -137,5 +177,7 @@ class ActionPack::WebAuthn::CoseKey
|
||||
])
|
||||
|
||||
OpenSSL::PKey::RSA.new(asn1.to_der)
|
||||
rescue OpenSSL::PKey::PKeyError => error
|
||||
raise ActionPack::WebAuthn::InvalidKeyError, "Invalid RSA key: #{error.message}"
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user