Files
notifications-admin/app/models/webauthn_credential.py
Ben Thorner 8502827afb Handle errors when registration fails
Previously we would raise a 500 error in a variety of cases:

- If a second key was being registered simultaneously (e.g. in a
separate tab), which means the registration state could be missing
after the first registration completes. That smells like an attack.

- If the server-side verification failed e.g. origin verification,
challenge verification, etc. The library seems to use 'ValueError'
for all such errors [1] (after auditing its 'raise' statements, and
excluding AttestationError [2], since we're not doing that).

- If a key is used that attempts to sign with an unsupported
algorithm. This would normally raise a NotImplemented error as part
of verifying attestation [3], but we don't do that, so we need to
verify the algorithm is supported by the library manually.

This adds error handling to return a 400 response and error message
in these cases, since the error is not unexpected (i.e. not a 500).
A 400 seems more appropriate than a 403, since in many cases it's
not clear if the request data is valid.

I've used CBOR for the transport encoding, to match the successful
request / response encoding. Note that the ordering of then/catch
matters in JS - we don't want to catch our own throws!

[1]: 142587b3e6/fido2/server.py (L255)
[2]: c42d9628a4/fido2/attestation/base.py (L39)
[3]: c42d9628a4/fido2/cose.py (L92)
2021-05-17 12:18:24 +01:00

63 lines
1.8 KiB
Python

import base64
from fido2 import cbor
from fido2.client import ClientData
from fido2.cose import UnsupportedKey
from fido2.ctap2 import AttestationObject, AttestedCredentialData
from flask import current_app
from app.models import JSONModel
class RegistrationError(Exception):
pass
class WebAuthnCredential(JSONModel):
ALLOWED_PROPERTIES = {
'id',
'name',
'credential_data', # contains public key and credential ID for auth
'registration_response', # sent to API for later auditing (not used)
'created_at',
'updated_at'
}
@classmethod
def from_registration(cls, state, response):
server = current_app.webauthn_server
try:
auth_data = server.register_complete(
state,
ClientData(response["clientDataJSON"]),
AttestationObject(response["attestationObject"]),
)
except ValueError as e:
raise RegistrationError(e)
if isinstance(auth_data.credential_data.public_key, UnsupportedKey):
raise RegistrationError("Encryption algorithm not supported")
return cls({
'name': 'Unnamed key',
'credential_data': base64.b64encode(
cbor.encode(auth_data.credential_data),
).decode('utf-8'),
'registration_response': base64.b64encode(
cbor.encode(response),
).decode('utf-8')
})
def to_credential_data(self):
return AttestedCredentialData(
cbor.decode(base64.b64decode(self.credential_data.encode()))
)
def serialize(self):
return {
'name': self.name,
'credential_data': self.credential_data,
'registration_response': self.registration_response,
}