mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-02-23 11:51:05 -05:00
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)
63 lines
1.8 KiB
Python
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,
|
|
}
|