Class Relations
- class: ::nx::EnsembleObject
![[i]](/resources/acs-subsite/ZoomIn16.gif)
::nx::EnsembleObject create ::webauthn::WebAuthn::slot::__auth
Methods (to be applied on the object)
assertion_verify (scripted)
set return_url [dict get $st return_url]
set expectedRpId [dict get $st rpId]
ns_log notice DEBUG: auth/assertion_verify st '$st' req '$req'
if {![dict exists $req id]
|| ![dict exists $req response clientDataJSON]
|| ![dict exists $req response authenticatorData]
|| ![dict exists $req response signature]} {
throw {validation fields-missing} "missing required fields"
}
set credential_id [dict get $req id] ;
if {![::xo::dc 0or1row get_cred {
select user_id, public_key, sign_count as old_sign_count
from webauthn_credentials
where credential_id = :credential_id
}]} {
if {[dict exists $st user_id]} {
throw {validation no-passkey} "No passkey registered for this account (or it was removed)."
}
throw {validation credential-unknown} "unknown credential"
}
if {[dict exists $st user_id] && $user_id != [dict get $st user_id]} {
ns_log notice "webauthn: mismatch" selected_user [dict get $st user_id] credential_user $user_id credential_id $credential_id
throw {validation credential-user-mismatch} "Passkey does not match the selected account"
}
set clientData_json [:assert_clientdata_json -clientData_raw [dict get $req response clientDataJSON] -expected_type "webauthn.get" -expected_challenge [dict get $st challenge] -expected_origin [dict get $st origin]]
set authData_b64u [dict get $req response authenticatorData]
set sig_b64u [dict get $req response signature]
set authData [ns_base64urldecode -binary -- $authData_b64u]
set sig [ns_base64urldecode -binary -- $sig_b64u]
if {[string length $authData] < 37} {
throw {validation authenicator-invalid} "authenticatorData too short"
}
set rpIdHash [string range $authData 0 31]
:assert_rpidhash -rpIdHash $rpIdHash -rpId $expectedRpId
binary scan [string range $authData 32 32] cu flags
if {($flags & 0x01) == 0} {
throw {validation user-data-missing} "user not present"
}
binary scan [string range $authData 33 36] Iu new_sign_count
set clientHash [ns_crypto::md string -digest sha256 -binary -encoding binary -- $clientData_json]
set signedData "${authData}${clientHash}"
if {![dict exists $public_key cose_b64u]} {
throw {validation key-invalid} "stored public key missing cose_b64u"
}
set coseKey_bin [ns_base64urldecode -binary -- [dict get $public_key cose_b64u]]
set cose [ns_cbor decode -binary -encoding binary $coseKey_bin]
if {![dict exists $cose 3] || [dict get $cose 3] != -7} {
throw {validation alg-unsupported} "unsupported COSE alg (expected -7 ES256)"
}
if {![dict exists $cose 1] || [dict get $cose 1] != 2} {
throw {validation keytype-unsupported} "unsupported COSE kty (expected 2 EC2)"
}
if {![dict exists $cose -1] || [dict get $cose -1] != 1} {
throw {validation curve-unsupported} "unsupported COSE crv (expected 1 P-256)"
}
set x [dict get $cose -2]
set y [dict get $cose -3]
if {[string length $x] != 32 || [string length $y] != 32} {
throw {validation key-invalid} "unexpected EC coordinate length"
}
if {[string length $sig] == 64} {
throw {validation signature-format} "unexpected raw 64-byte signature; expected DER"
}
set pubpem [ns_crypto::eckey fromcoords -curve prime256v1 -x $x -y $y -binary -format pem]
set ok [ns_crypto::md string -digest sha256 -binary -encoding binary -verify $pubpem -signature $sig -- $signedData]
ns_log notice "DEBUG SIGNATURE OK? $ok"
if {!$ok} {
throw {validation signature-invalid} "signature verification failed"
}
ns_log notice DEBUG: update credential_id $credential_id old_sign_count $old_sign_count new_sign_count $new_sign_count
db_dml update_last_used {
update webauthn_credentials
set last_used_at = now(),
sign_count = :new_sign_count
where credential_id = :credential_id
}
return $user_idissue_options (scripted)
set state [::xo::oauth::nonce]
set challenge [:new_challenge 32]
set key [:state_key auth $state]
${:store} set $key [dict create challenge $challenge rpId ${:rp_id} return_url $return_url origin [:origin] ts [ns_time] ]
return [dict create state $state options [dict create challenge $challenge timeout 60000 rpId ${:rp_id} userVerification preferred ] ]