Webauthn - Server side libraries
Arnaud Dagnelies
Posted on March 13, 2022
Since manual validation/verification of "attestations", which contain the authenticity proof of the payload, let's check the ecosystem and look if good libraries are available. As a reminder, the attestation can be encoded into various formats, each containing contain some form of cryptographic signature. Therefore, it is possible that a library support some of these formats and not others.
When looking for promising libraries on gihub, the following four appear to be the most popular:
Language | Stars | License | Link |
---|---|---|---|
GO | 750+ | BSD-3 | https://github.com/duo-labs/webauthn |
Python | 380+ | BSD-3 | https://github.com/duo-labs/py_webauthn |
Java | 200+ | Apache-2 | https://github.com/webauthn4j/webauthn4j |
PHP | 120+ | MIT | https://github.com/lbuchs/WebAuthn |
Of course, there are mony more libraries. This is just a little pick to illustrate that the ecosystem is already there, although young, and that a variety of programming languages have libraries available.
Python example
This will not be a full example with a webserver, but rather a snippet to test the decoding and validation using the previously mentioned library.
Let us start with some JSON similar to the one produced in the previous tutorial of this series. Namely, the same format of the normal response of navigator.credentials.create(...)
with all byte buffers directly encoded as base64url.
{
"id": "ZoIKP1JQvKdrYj1bTUPJ2eTUsbLeFkv-X5xJQNr4k6s",
"rawId": "ZoIKP1JQvKdrYj1bTUPJ2eTUsbLeFkv-X5xJQNr4k6s",
"response": {
"attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVkBZ0mWDeWIDoxodDQXD2R2YFuP5K65ooYyx5lc87qDHZdjRQAAAAAAAAAAAAAAAAAAAAAAAAAAACBmggo_UlC8p2tiPVtNQ8nZ5NSxst4WS_5fnElA2viTq6QBAwM5AQAgWQEA31dtHqc70D_h7XHQ6V_nBs3Tscu91kBL7FOw56_VFiaKYRH6Z4KLr4J0S12hFJ_3fBxpKfxyMfK66ZMeAVbOl_wemY4S5Xs4yHSWy21Xm_dgWhLJjZ9R1tjfV49kDPHB_ssdvP7wo3_NmoUPYMgK-edgZ_ehttp_I6hUUCnVaTvn_m76b2j9yEPReSwl-wlGsabYG6INUhTuhSOqG-UpVVQdNJVV7GmIPHCA2cQpJBDZBohT4MBGme_feUgm4sgqVCWzKk6CzIKIz5AIVnspLbu05SulAVnSTB3NxTwCLNJR_9v9oSkvphiNbmQBVQH1tV_psyi9HM1Jtj9VJVKMeyFDAQAB",
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiQ2VUV29nbWcwY2NodWlZdUZydjhEWFhkTVpTSVFSVlpKT2dhX3hheVZWRWNCajBDdzN5NzN5aEQ0RmtHU2UtUnJQNmhQSkpBSW0zTFZpZW40aFhFTGciLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJjcm9zc09yaWdpbiI6ZmFsc2V9"
},
...
}
The exact content might differ depending on where/how the registration was performed, but the above content is what matters. Let's parse it first.
import webauthn
parsed_credential = webauthn.helpers.structs.RegistrationCredential.parse_raw(json)
Then, validate it using the following method.
registration = webauthn.verify_registration_response(
credential=parsed_credential,
expected_challenge=webauthn.base64url_to_bytes("CeTWogmg0cchuiYuFrv8DXXdMZSIQRVZJOga_xayVVEcBj0Cw3y73yhD4FkGSe-RrP6hPJJAIm3LVien4hXELg"),
expected_origin="http://localhost:5000",
expected_rp_id="localhost"
)
That is the bare minimum. First, this validates the content, including the signature, but also provides the public key, used later to validate authentication requests.
print('Credential ID:', webauthn.helpers.bytes_to_base64url(registration.credential_id))
print('Public Key:', webauthn.helpers.bytes_to_base64url(registration.credential_public_key))
The authentication works similarly. Here as well, we will take the input of navigator.credentials.get(...)
with all byte buffers directly encoded as base64url.
parsed_credential = webauthn.helpers.structs.AuthenticationCredential.parse_raw('''
{
"id": "ZoIKP1JQvKdrYj1bTUPJ2eTUsbLeFkv-X5xJQNr4k6s",
"rawId": "ZoIKP1JQvKdrYj1bTUPJ2eTUsbLeFkv-X5xJQNr4k6s",
"response": {
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAQ",
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiaVBtQWkxUHAxWEw2b0FncTNQV1p0WlBuWmExekZVRG9HYmFRMF9LdlZHMWxGMnMzUnRfM280dVN6Y2N5MHRtY1RJcFRUVDRCVTFULUk0bWFhdm5kalEiLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJjcm9zc09yaWdpbiI6ZmFsc2V9",
"signature": "iOHKX3erU5_OYP_r_9HLZ-CexCE4bQRrxM8WmuoKTDdhAnZSeTP0sjECjvjfeS8MJzN1ArmvV0H0C3yy_FdRFfcpUPZzdZ7bBcmPh1XPdxRwY747OrIzcTLTFQUPdn1U-izCZtP_78VGw9pCpdMsv4CUzZdJbEcRtQuRS03qUjqDaovoJhOqEBmxJn9Wu8tBi_Qx7A33RbYjlfyLm_EDqimzDZhyietyop6XUcpKarKqVH0M6mMrM5zTjp8xf3W7odFCadXEJg-ERZqFM0-9Uup6kJNLbr6C5J4NDYmSm3HCSA6lp2iEiMPKU8Ii7QZ61kybXLxsX4w4Dm3fOLjmDw"
}
}
''')
authentication_verification = webauthn.verify_authentication_response(
credential=parsed_credential,
expected_challenge=webauthn.base64url_to_bytes("iPmAi1Pp1XL6oAgq3PWZtZPnZa1zFUDoGbaQ0_KvVG1lF2s3Rt_3o4uSzccy0tmcTIpTTT4BU1T-I4maavndjQ"),
expected_rp_id="localhost",
expected_origin="http://localhost:5000",
credential_public_key=registration.credential_public_key,
credential_current_sign_count=0,
require_user_verification=True,
)
Before closing the topic I would like to take a step back and look at the big picture again. Although the user ID and name is required during the creation of credentials, it is not present in the response at all, nor used in the authentication process afterwards ...this asymmetry is kind of bizarre IMHO. Perhaps the authenticator uses it somehow for display purposes for things like "sub accounts" ...it's not very transparent.
This specification is also "in flux", and there are for example plans to add comfort functions to provide json obects directly. See https://github.com/w3c/webauthn/pull/1703 . Nevertheless, the complexity of the whole remains.
In the next series, we will discuss how this complex functionality could be offered more conviniently.
Posted on March 13, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.