Closed Satistile closed 1 month ago
Hello @Satistile, what's the base64url value of pub_key
coming out of db_calls.get_key(logged_in_user_id)
? Sharing that might help troubleshoot what's going on here.
The pub_key
variable corresponds to the response.publicKey
variable in the JSON object returned by the @simplewebauthn/browser library when calling the startRegistration()
function. This value is already encoded in base64url by the library and theoraticly I just have to decode this in my python backend. As far as I got with debugging, I can say for sure that my encoding and decoding functions for base64url are working as intended.
Without a sample value for pub_key
I can't assist in troubleshooting any further. Please feel free to reopen this issue if you are able to provide such a value.
A sample value for the pub_key
would be MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8APYzNW_OkIzf8cizuNiwqMJhk8IqIxCiVdjlxUulYH0fpTdr0W46fI89XooslN8r1V4Vu1DrUcdc1vHGnHB0g
This is bizarre, that pub_key
is the following hex:
3059301306072a8648ce3d020106082a8648ce3d03010703420004f003d8ccd5bf3a42337fc722cee362c2a309864f08a88c4289576397152e9581f47e94ddaf45b8e9f23cf57a28b2537caf557856ed43ad471d735bc71a71c1d2
When I drop that hex into https://cbor.me the site says the bytes are "the value -17", with "90 unused bytes after the end of the data item":
So this is telling me that there's something potentially misbehaving with the browser and/or authenticator, before @simplewebauthn/browser base64url-encodes the output from WebAuthn's response.getPublicKey()
...
@Satistile What OS, browser, their versions, and authenticator are you attempting to support here? Might as well let me know what version of @simplewebauthn/browser you're using too (I doubt it's the culprit but just to be safe.)
I don't actually know the version of my @simplewebauthn/browser, but I use it in my website with the recommended way without typescript (<script src="https://unpkg.com/@simplewebauthn/browser/dist/bundle/index.umd.min.js"></script>
). The site currently runs on Windows 10 pro 22h2 and I use it with Microsoft Edge V. 123.0.2420.65. I've used the Windows Hello authenticator and the authenticator build in into IOS 17.4.1. The error occured while using both authenticators. But on other websites like google.com and icloud.com aren't any problems.
I hope these informations are helpful.
Maybe it's also helpful to know, that the registration process don't throws any errors and runs without any problems.
I've thought a bit about the issue, and came to the conclusion that the error might have happened while the storing process of the public key.
Corresponding code:
def verify_registration_response_func(request):
content_type = request.headers.get('Content-Type')
if content_type == 'application/json':
request_json = request.json
# get user id from client json and prepare payload for processing
logged_in_user_id = request_json['id']
request_payload_object = json.loads(base64url_to_string(request_json['payload']))
# get current challenge from user
current_challenge = base64url_to_bytes(db_calls.get_challenge(logged_in_user_id))
# verify challenge
try:
credential = request_payload_object
verification = verify_registration_response(
credential=credential,
expected_challenge=current_challenge,
expected_rp_id="localhost",
expected_origin="http://localhost:63342",
require_user_verification=True,
)
except Exception as err:
db_calls.delete_challenge(logged_in_user_id)
return {"verified": False, "msg": str(err), "status": 400}
# remove challenge from database and add public key to db
db_calls.delete_challenge(logged_in_user_id)
db_calls.add_key(logged_in_user_id, credential["response"]["publicKey"])
return {"verified": True}
else:
return 'Content-Type not supported!'
I hope that this might help a bit
Oh this was a mistake, sorry
Hello @Satistile, is this still an issue for you?
# registration
db_calls.add_key(logged_in_user_id, credential["response"]["publicKey"])
# authentication
pub_key = base64url_to_bytes(db_calls.get_key(logged_in_user_id))
The only thing I can suggest is that credential["response"]["publicKey"]
and pub_key
are the same value whether as bytes or base64url string. If pub_key
isn't the same value then I'd say something is going wrong while storing the public key bytes...
I've checked lately if that's the case, but in the storing process seems to be no error. The values are exactly the same. My guess at this point is, that the public key that I'm storing is wrong. But the verification process with the signed challenge in the registration process seems to work flawless.
Currently, as shown in the code example I already provided (the one with the verify_registration_response_func
in it), I assumed that the public key value provided in the returned json from @simplewebauthn/browser is the public key I need later for the verification. If that's not the case it could be that there is a error in one of the librarys, or more likely, I've made a dumb mistake anywhere else in the process.
I hope, that's somewhat helpful
I'm having the same issue. Using a public key that comes from calling .response.getPublicKey()
on the result of creating a new key by calling await navigator.credentials.create({ ... })
. I've tried storing it in two different formats:
new Uint8Array(credential.response.getPublicKey()) // [48, 89, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, 72, 206, 61, 3, 1, 7, 3, 66, 0, 4, 213, 234, 207, 60, 174, 205, 100, 74, 105, 96, 227, 16, 254, 247, 215, 131, 195, 28, 29, 245, 38, 9, 61, 170, 223, 102, 211, 100, 71, 50, 33, 95, 206, 89, 85, 124, 248, 12, 138, 196, 77, 188, 60, 177, 222, 51, 232, 71, 203, 41, 203, 225, 231, 5, 185, 35, 175, 179, 142, 206, 200, 11, 86, 189]
buffer_to_b64(credential.response.getPublicKey()) // 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1erPPK7NZEppYOMQ/vfXg8McHfUmCT2q32bTZEcyIV/OWVV8+AyKxE28PLHeM+hHyynL4ecFuSOvs47OyAtWvQ=='
But calling verify_authentication_response()
always fails with the same error caused by cbor parsing returning -17:
---- snip ----
File "/home/den-antares/projects/tir-na-nog/venv-python3.12/lib64/python3.12/site-packages/webauthn/authentication/verify_authentication_response.py", line 164, in verify_authentication_response
decoded_public_key = decode_credential_public_key(credential_public_key)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/den-antares/projects/tir-na-nog/venv-python3.12/lib64/python3.12/site-packages/webauthn/helpers/decode_credential_public_key.py", line 63, in decode_credential_public_key
kty = decoded_key[COSEKey.KTY]
~~~~~~~~~~~^^^^^^^^^^^^^
TypeError: 'int' object is not subscriptable
It looks like cbor parsing relies on a C library underneath, so the -17 is probably some kind of return code, but I can't find any documentation about what it means. I've tried making a new key a few times, but I always get the same result.
Are there any working examples of creating a passkey with navigator.credentials.create()
and getting it to work with pywebauthn? The example I found just validated hard-coded keys.
I eventually figured out that pywebauthn is using a different public key format from the built in browser functions - you needs to pass the result of navigator.credentials.create()
to the server, run it through pywebauthn's verify_registration_response()
and use the public key from the result.
It took me quite a while to figure this out, and there don't seem to be any working examples of using a browser client with pywebauthn. The closest I found was https://gist.github.com/samuelcolvin/3ff019aa738aa558a185c4fb002b5751 , and that one is a few years old and doesn't work with the current version.
Hello @Densaugeo and @Satistile, I'm finally understanding what's going on now. The output of .getPublicKey()
is DER-encoded bytes comprising a SubjectPublicKeyInfo
data structure. Inside this structure is the same public key bytes that come out of verify_registration_response()
as credential_public_key
, but an RP has to know this or else they'll run into the issues you're reporting here.
I hadn't paid attention to the output of AuthenticatorAttestationResponse.getPublicKey()
till now because py_webauthn was written assuming credential_public_key
will get stored out of verify_registration_response()
to subsequently be fed into verify_authentication_response()
as its credential_public_key
argument.
I poked around right now to see if I could handle this all within py_webauthn without any changes to verify_authentication_response()
. Unfortunately there's a limitation with getPublicKey()
called out in the WebAuthn spec:
A SubjectPublicKeyInfo does not include information about the signing algorithm (for example, which hash function to use) that is included in the COSE public key. To provide this, getPublicKeyAlgorithm() returns the COSEAlgorithmIdentifier for the credential public key.
The algorithm is present in credential_public_key
returned from verify_registration_response()
, though...I can't see a way to silently detect and adapt to the output of getPublicKey()
when it's passed into verify_authentication_response()
as credential_public_key
without potentially making verify_authentication_response()
more confusing to use by adding a new "public_key_algorithm
" argument that is only to be used when an RP wants to feed in the result of getPublicKey()
as credential_public_key
.
@Densaugeo and @Satistile: is there a reason why you reached for the output from getPublicKey()
instead of storing credential_public_key
out of verify_registration_response()
? There's a solution here that involves you refactoring your code to persist and use credential_public_key
and forget about using getPublicKey()
, but I'd like to understand your use case more before I close this ticket out as WONTFIX.
I've did some refactoring and it works now. There wasn't any special use case for taking the public key straight from getPublicKey()
. I just had no clue, that verify_registration_response() has any more values, including the public key. Thus, it's not nessecary for me to change something in the library.
Thank you very much for your help!
I eventually got it working with verify_registration_response()
. If calling getPublicKey()
on the client side doesn't give all of the necessary information, it's probably best to stick with verify_registration_response()
.
I tried to use getPublicKey()
because I had trouble figuring out how to set things up - it seems like pywebauthn is designed to use the output of navigator.credentials.create()
, stringified to JSON and fed into verify_registration_response()
. This works fine in Firefox, but in Chrome passing the credential object from navigator.credentials.create()
to JSON.stringify()
returns {}
. Eventually I figured out all the different fields that have to be copied over for verify_registration_response()
to work:
const credential = await navigator.credentials.create({
publicKey: {
challenge: b64_to_u8_array(prelogin.challenge),
rp: { id: "localhost", name: "Localhost" },
user: {
id: new TextEncoder().encode('den_antares'),
name: "den_antares",
displayName: "Den Antares"
},
pubKeyCredParams: [
// Chrome logs a warning unless both of these algorithms are specified
{ type: "public-key", alg: -7 },
{ type: "public-key", alg: -257 },
],
}
})
// Works in Firefox, fails in Chrome because JSON.stringify(credential) returns '{}'
let res = await fetch('/register-key', { method: 'POST', body: JSON.stringify(credential) })
// Works in both
let res = await fetch('/register-key', { method: 'POST', body: JSON.stringify({
id: credential.id,
rawId: buffer_to_b64(credential.rawId),
response: {
attestationObject: buffer_to_b64(credential.response.attestationObject),
clientDataJSON: buffer_to_b64(credential.response.clientDataJSON),
},
type: credential.type,
}) })
My suggestion is to add an example of how to capture credentials on the client side. I wasn't able to find any current examples for how to do that, and took a lot of wrong turns trying to figure it out.
I currently try to implement the py_webauthn library in a little test project, but currently the
verify_authentication_response()
function seems to not work correctly.Here's my code (I currently work with flask):
And here's the replied JSON from it:
{"msg":"'int' object is not subscriptable","status":400,"verified":false}
The error seems to occour while the processing of the public key. And this is the point, where I don't get what's wrong. After the registration process is completed, I save the public key directly in my database. The public key is send directly from the client, where the @simplewebauthn/browser framework had processed the options and replied the public key as a base64url string.
I currently still don't know if this is my error or an error in the library. Thanks in advance for helping!
Edit: Here's the complete traceback: