Update webauthn requirement from ==0.4.* to ==2.0.* (#3880)

* Get rid of unmaintained dependency python-u2flib-server

* Update webauthn requirement from ==0.4.* to ==2.0.*

* Fix tests

* Update src/pretix/control/views/auth.py

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Update src/pretix/control/views/auth.py

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Update src/pretix/control/views/user.py

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Update src/pretix/control/views/user.py

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Update src/pretix/control/views/user.py

Co-authored-by: Richard Schreiber <schreiber@rami.io>

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
This commit is contained in:
Raphael Michel
2024-02-14 13:27:24 +01:00
committed by GitHub
co-authored by Richard Schreiber
parent 7b5ce5e198
commit 57738f19bf
8 changed files with 196 additions and 206 deletions
+1 -1
View File
@@ -102,7 +102,7 @@ dependencies = [
"tqdm==4.*", "tqdm==4.*",
"vat_moss_forked==2020.3.20.0.11.0", "vat_moss_forked==2020.3.20.0.11.0",
"vobject==0.9.*", "vobject==0.9.*",
"webauthn==0.4.*", "webauthn==2.0.*",
"zeep==4.2.*" "zeep==4.2.*"
] ]
+15 -25
View File
@@ -37,9 +37,7 @@ import json
import operator import operator
from datetime import timedelta from datetime import timedelta
from functools import reduce from functools import reduce
from urllib.parse import urlparse
import webauthn
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import ( from django.contrib.auth.models import (
AbstractBaseUser, BaseUserManager, PermissionsMixin, AbstractBaseUser, BaseUserManager, PermissionsMixin,
@@ -53,11 +51,12 @@ from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_otp.models import Device from django_otp.models import Device
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
from webauthn.helpers.structs import PublicKeyCredentialDescriptor
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.helpers.urls import build_absolute_uri from pretix.helpers.urls import build_absolute_uri
from ...helpers.u2f import pub_key_from_der, websafe_decode, websafe_encode from ...helpers.u2f import pub_key_from_der, websafe_decode
from .base import LoggingMixin from .base import LoggingMixin
@@ -606,7 +605,12 @@ class U2FDevice(Device):
json_data = models.TextField() json_data = models.TextField()
@property @property
def webauthnuser(self): def webauthndevice(self):
d = json.loads(self.json_data)
return PublicKeyCredentialDescriptor(websafe_decode(d['keyHandle']))
@property
def webauthnpubkey(self):
d = json.loads(self.json_data) d = json.loads(self.json_data)
# We manually need to convert the pubkey from DER format (used in our # We manually need to convert the pubkey from DER format (used in our
# former U2F implementation) to the format required by webauthn. This # former U2F implementation) to the format required by webauthn. This
@@ -618,16 +622,7 @@ class U2FDevice(Device):
pub_key.public_numbers().x, pub_key.public_numbers().y pub_key.public_numbers().x, pub_key.public_numbers().y
) )
) )
return webauthn.WebAuthnUser( return pub_key
d['keyHandle'],
self.user.email,
str(self.user),
settings.SITE_URL,
d['keyHandle'],
websafe_encode(pub_key),
1,
urlparse(settings.SITE_URL).netloc
)
class WebAuthnDevice(Device): class WebAuthnDevice(Device):
@@ -639,14 +634,9 @@ class WebAuthnDevice(Device):
sign_count = models.IntegerField(default=0) sign_count = models.IntegerField(default=0)
@property @property
def webauthnuser(self): def webauthndevice(self):
return webauthn.WebAuthnUser( return PublicKeyCredentialDescriptor(websafe_decode(self.credential_id))
self.ukey,
self.user.email, @property
str(self.user), def webauthnpubkey(self):
settings.SITE_URL, return websafe_decode(self.pub_key)
self.credential_id,
self.pub_key,
self.sign_count,
urlparse(settings.SITE_URL).netloc
)
+52 -31
View File
@@ -32,11 +32,11 @@
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is # Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License. # License for the specific language governing permissions and limitations under the License.
import base64
import json import json
import logging import logging
import time import time
from urllib.parse import quote from urllib.parse import quote, urlparse
import webauthn import webauthn
from django.conf import settings from django.conf import settings
@@ -54,6 +54,7 @@ from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import TemplateView from django.views.generic import TemplateView
from django_otp import match_token from django_otp import match_token
from webauthn.helpers import generate_challenge
from pretix.base.auth import get_auth_backends from pretix.base.auth import get_auth_backends
from pretix.base.forms.auth import ( from pretix.base.forms.auth import (
@@ -62,7 +63,6 @@ from pretix.base.forms.auth import (
from pretix.base.models import TeamInvite, U2FDevice, User, WebAuthnDevice from pretix.base.models import TeamInvite, U2FDevice, User, WebAuthnDevice
from pretix.base.services.mail import SendMailException from pretix.base.services.mail import SendMailException
from pretix.helpers.http import redirect_to_url from pretix.helpers.http import redirect_to_url
from pretix.helpers.webauthn import generate_challenge
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -389,6 +389,10 @@ def get_u2f_appid(request):
return settings.SITE_URL return settings.SITE_URL
def get_webauthn_rp_id(request):
return urlparse(settings.SITE_URL).hostname
class Login2FAView(TemplateView): class Login2FAView(TemplateView):
template_name = 'pretixcontrol/auth/login_2fa.html' template_name = 'pretixcontrol/auth/login_2fa.html'
@@ -427,25 +431,41 @@ class Login2FAView(TemplateView):
devices = U2FDevice.objects.filter(user=self.user) devices = U2FDevice.objects.filter(user=self.user)
for d in devices: for d in devices:
credential_current_sign_count = d.sign_count if isinstance(d, WebAuthnDevice) else 0
try: try:
wu = d.webauthnuser webauthn_assertion_response = webauthn.verify_authentication_response(
credential=resp,
if isinstance(d, U2FDevice): expected_challenge=base64.b64decode(challenge),
# RP_ID needs to be appId for U2F devices, but we can't expected_rp_id=get_webauthn_rp_id(self.request),
# set it that way in U2FDevice.webauthnuser, since that expected_origin=settings.SITE_URL,
# breaks the frontend part. credential_public_key=d.webauthnpubkey,
wu.rp_id = settings.SITE_URL credential_current_sign_count=credential_current_sign_count,
webauthn_assertion_response = webauthn.WebAuthnAssertionResponse(
wu,
resp,
challenge,
settings.SITE_URL,
uv_required=False # User Verification
) )
sign_count = webauthn_assertion_response.verify() sign_count = webauthn_assertion_response.new_sign_count
if sign_count < credential_current_sign_count:
raise Exception("Possible replay attack, sign count not higher")
except Exception: except Exception:
logger.exception('U2F login failed') if isinstance(d, U2FDevice):
# https://www.w3.org/TR/webauthn/#sctn-appid-extension says
# "When verifying the assertion, expect that the rpIdHash MAY be the hash of the AppID instead of the RP ID."
try:
webauthn_assertion_response = webauthn.verify_authentication_response(
credential=resp,
expected_challenge=base64.b64decode(challenge),
expected_rp_id=get_u2f_appid(self.request),
expected_origin=settings.SITE_URL,
credential_public_key=d.webauthnpubkey,
credential_current_sign_count=credential_current_sign_count,
)
if webauthn_assertion_response.new_sign_count < 1:
raise Exception("Possible replay attack, sign count set")
except Exception:
logger.exception('U2F login failed')
else:
valid = True
break
else:
logger.exception('Webauthn login failed')
else: else:
if isinstance(d, WebAuthnDevice): if isinstance(d, WebAuthnDevice):
d.sign_count = sign_count d.sign_count = sign_count
@@ -471,23 +491,24 @@ class Login2FAView(TemplateView):
ctx = super().get_context_data() ctx = super().get_context_data()
if 'webauthn_challenge' in self.request.session: if 'webauthn_challenge' in self.request.session:
del self.request.session['webauthn_challenge'] del self.request.session['webauthn_challenge']
challenge = generate_challenge(32) challenge = generate_challenge()
self.request.session['webauthn_challenge'] = challenge self.request.session['webauthn_challenge'] = base64.b64encode(challenge).decode()
devices = [ devices = [
device.webauthnuser for device in WebAuthnDevice.objects.filter(confirmed=True, user=self.user) device.webauthndevice for device in WebAuthnDevice.objects.filter(confirmed=True, user=self.user)
] + [ ] + [
device.webauthnuser for device in U2FDevice.objects.filter(confirmed=True, user=self.user) device.webauthndevice for device in U2FDevice.objects.filter(confirmed=True, user=self.user)
] ]
if devices: if devices:
webauthn_assertion_options = webauthn.WebAuthnAssertionOptions( auth_options = webauthn.generate_authentication_options(
devices, rp_id=get_webauthn_rp_id(self.request),
challenge challenge=challenge,
allow_credentials=devices,
) )
ad = webauthn_assertion_options.assertion_dict
ad['extensions'] = { # Backwards compatibility to U2F
'appid': get_u2f_appid(self.request) j = json.loads(webauthn.options_to_json(auth_options))
} j["extensions"] = {"appid": get_u2f_appid(self.request)}
ctx['jsondata'] = json.dumps(ad) ctx['jsondata'] = json.dumps(j)
return ctx return ctx
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
+77 -77
View File
@@ -35,10 +35,9 @@
import base64 import base64
import json import json
import logging import logging
import os
import time import time
from collections import defaultdict from collections import defaultdict
from urllib.parse import quote, urlparse from urllib.parse import quote
import webauthn import webauthn
from django.conf import settings from django.conf import settings
@@ -57,6 +56,7 @@ from django.views.generic import FormView, ListView, TemplateView, UpdateView
from django_otp.plugins.otp_static.models import StaticDevice from django_otp.plugins.otp_static.models import StaticDevice
from django_otp.plugins.otp_totp.models import TOTPDevice from django_otp.plugins.otp_totp.models import TOTPDevice
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
from webauthn.helpers import generate_challenge, generate_user_handle
from pretix.base.auth import get_auth_backends from pretix.base.auth import get_auth_backends
from pretix.base.forms.auth import ReauthForm from pretix.base.forms.auth import ReauthForm
@@ -70,9 +70,9 @@ from pretix.control.forms.users import StaffSessionForm
from pretix.control.permissions import ( from pretix.control.permissions import (
AdministratorPermissionRequiredMixin, StaffMemberRequiredMixin, AdministratorPermissionRequiredMixin, StaffMemberRequiredMixin,
) )
from pretix.control.views.auth import get_u2f_appid from pretix.control.views.auth import get_u2f_appid, get_webauthn_rp_id
from pretix.helpers.http import redirect_to_url from pretix.helpers.http import redirect_to_url
from pretix.helpers.webauthn import generate_challenge, generate_ukey from pretix.helpers.u2f import websafe_encode
REAL_DEVICE_TYPES = (TOTPDevice, WebAuthnDevice, U2FDevice) REAL_DEVICE_TYPES = (TOTPDevice, WebAuthnDevice, U2FDevice)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -105,25 +105,41 @@ class ReauthView(TemplateView):
devices = U2FDevice.objects.filter(user=self.request.user) devices = U2FDevice.objects.filter(user=self.request.user)
for d in devices: for d in devices:
credential_current_sign_count = d.sign_count if isinstance(d, WebAuthnDevice) else 0
try: try:
wu = d.webauthnuser webauthn_assertion_response = webauthn.verify_authentication_response(
credential=resp,
if isinstance(d, U2FDevice): expected_challenge=base64.b64decode(challenge),
# RP_ID needs to be appId for U2F devices, but we can't expected_rp_id=get_webauthn_rp_id(self.request),
# set it that way in U2FDevice.webauthnuser, since that expected_origin=settings.SITE_URL,
# breaks the frontend part. credential_public_key=d.webauthnpubkey,
wu.rp_id = settings.SITE_URL credential_current_sign_count=credential_current_sign_count,
webauthn_assertion_response = webauthn.WebAuthnAssertionResponse(
wu,
resp,
challenge,
settings.SITE_URL,
uv_required=False # User Verification
) )
sign_count = webauthn_assertion_response.verify() sign_count = webauthn_assertion_response.new_sign_count
if sign_count < credential_current_sign_count:
raise Exception("Possible replay attack, sign count not higher")
except Exception: except Exception:
logger.exception('U2F login failed') if isinstance(d, U2FDevice):
# https://www.w3.org/TR/webauthn/#sctn-appid-extension says
# "When verifying the assertion, expect that the rpIdHash MAY be the hash of the AppID instead of the RP ID."
try:
webauthn_assertion_response = webauthn.verify_authentication_response(
credential=resp,
expected_challenge=base64.b64decode(challenge),
expected_rp_id=get_u2f_appid(self.request),
expected_origin=settings.SITE_URL,
credential_public_key=d.webauthnpubkey,
credential_current_sign_count=credential_current_sign_count,
)
if webauthn_assertion_response.new_sign_count < 1:
raise Exception("Possible replay attack, sign count set")
except Exception:
logger.exception('U2F login failed')
else:
valid = True
break
else:
logger.exception('Webauthn login failed')
else: else:
if isinstance(d, WebAuthnDevice): if isinstance(d, WebAuthnDevice):
d.sign_count = sign_count d.sign_count = sign_count
@@ -162,23 +178,24 @@ class ReauthView(TemplateView):
ctx = super().get_context_data() ctx = super().get_context_data()
if 'webauthn_challenge' in self.request.session: if 'webauthn_challenge' in self.request.session:
del self.request.session['webauthn_challenge'] del self.request.session['webauthn_challenge']
challenge = generate_challenge(32) challenge = generate_challenge()
self.request.session['webauthn_challenge'] = challenge self.request.session['webauthn_challenge'] = base64.b64encode(challenge).decode()
devices = [ devices = [
device.webauthnuser for device in WebAuthnDevice.objects.filter(confirmed=True, user=self.request.user) device.webauthndevice for device in WebAuthnDevice.objects.filter(confirmed=True, user=self.request.user)
] + [ ] + [
device.webauthnuser for device in U2FDevice.objects.filter(confirmed=True, user=self.request.user) device.webauthndevice for device in U2FDevice.objects.filter(confirmed=True, user=self.request.user)
] ]
if devices: if devices:
webauthn_assertion_options = webauthn.WebAuthnAssertionOptions( auth_options = webauthn.generate_authentication_options(
devices, rp_id=get_webauthn_rp_id(self.request),
challenge challenge=challenge,
allow_credentials=devices,
) )
ad = webauthn_assertion_options.assertion_dict
ad['extensions'] = { # Backwards compatibility to U2F
'appid': get_u2f_appid(self.request) j = json.loads(webauthn.options_to_json(auth_options))
} j["extensions"] = {"appid": get_u2f_appid(self.request)}
ctx['jsondata'] = json.dumps(ad) ctx['jsondata'] = json.dumps(j)
ctx['form'] = self.form ctx['form'] = self.form
return ctx return ctx
@@ -387,23 +404,26 @@ class User2FADeviceConfirmWebAuthnView(RecentAuthenticationRequiredMixin, Templa
if 'webauthn_challenge' in self.request.session: if 'webauthn_challenge' in self.request.session:
del self.request.session['webauthn_challenge'] del self.request.session['webauthn_challenge']
challenge = generate_challenge(32) challenge = generate_challenge()
ukey = generate_ukey() ukey = generate_user_handle()
self.request.session['webauthn_challenge'] = challenge self.request.session['webauthn_challenge'] = base64.b64encode(challenge).decode()
self.request.session['webauthn_register_ukey'] = ukey self.request.session['webauthn_register_ukey'] = base64.b64encode(ukey).decode()
make_credential_options = webauthn.WebAuthnMakeCredentialOptions( devices = [
challenge, device.webauthndevice for device in WebAuthnDevice.objects.filter(confirmed=True, user=self.request.user)
urlparse(settings.SITE_URL).netloc, ] + [
urlparse(settings.SITE_URL).netloc, device.webauthndevice for device in U2FDevice.objects.filter(confirmed=True, user=self.request.user)
ukey, ]
self.request.user.email, make_credential_options = webauthn.generate_registration_options(
str(self.request.user), rp_id=get_webauthn_rp_id(self.request),
settings.SITE_URL, rp_name=get_webauthn_rp_id(self.request),
attestation="none" user_id=ukey,
user_name=self.request.user.email,
challenge=challenge,
exclude_credentials=devices,
) )
ctx['jsondata'] = json.dumps(make_credential_options.registration_dict) ctx['jsondata'] = webauthn.options_to_json(make_credential_options)
return ctx return ctx
@@ -412,30 +432,13 @@ class User2FADeviceConfirmWebAuthnView(RecentAuthenticationRequiredMixin, Templa
challenge = self.request.session['webauthn_challenge'] challenge = self.request.session['webauthn_challenge']
ukey = self.request.session['webauthn_register_ukey'] ukey = self.request.session['webauthn_register_ukey']
resp = json.loads(self.request.POST.get("token")) resp = json.loads(self.request.POST.get("token"))
trust_anchor_dir = os.path.normpath(os.path.join(
os.path.dirname(os.path.abspath(__file__)),
'../../static/webauthn_trusted_attestation_roots' # currently does not exist
))
# We currently do not check attestation certificates, since there's no real risk
# and we do not have any policies specifying what devices can be used. (Also, we
# didn't get it to work.)
# Read more: https://fidoalliance.org/fido-technotes-the-truth-about-attestation/
trusted_attestation_cert_required = False
self_attestation_permitted = True
none_attestation_permitted = True
webauthn_registration_response = webauthn.WebAuthnRegistrationResponse( registration_verification = webauthn.verify_registration_response(
urlparse(settings.SITE_URL).netloc, credential=resp,
settings.SITE_URL, expected_challenge=base64.b64decode(challenge),
resp, expected_rp_id=get_webauthn_rp_id(self.request),
challenge, expected_origin=settings.SITE_URL,
trust_anchor_dir,
trusted_attestation_cert_required,
self_attestation_permitted,
none_attestation_permitted,
uv_required=False
) )
webauthn_credential = webauthn_registration_response.verify()
# Check that the credentialId is not yet registered to any other user. # Check that the credentialId is not yet registered to any other user.
# If registration is requested for a credential that is already registered # If registration is requested for a credential that is already registered
@@ -443,7 +446,7 @@ class User2FADeviceConfirmWebAuthnView(RecentAuthenticationRequiredMixin, Templa
# ceremony, or it MAY decide to accept the registration, e.g. while deleting # ceremony, or it MAY decide to accept the registration, e.g. while deleting
# the older registration. # the older registration.
credential_id_exists = WebAuthnDevice.objects.filter( credential_id_exists = WebAuthnDevice.objects.filter(
credential_id=webauthn_credential.credential_id credential_id=registration_verification.credential_id
).first() ).first()
if credential_id_exists: if credential_id_exists:
messages.error(request, _('This security device is already registered.')) messages.error(request, _('This security device is already registered.'))
@@ -451,14 +454,11 @@ class User2FADeviceConfirmWebAuthnView(RecentAuthenticationRequiredMixin, Templa
'device': self.device.pk 'device': self.device.pk
})) }))
webauthn_credential.credential_id = str(webauthn_credential.credential_id, "utf-8") self.device.credential_id = websafe_encode(registration_verification.credential_id)
webauthn_credential.public_key = str(webauthn_credential.public_key, "utf-8") self.device.ukey = websafe_encode(ukey)
self.device.pub_key = websafe_encode(registration_verification.credential_public_key)
self.device.credential_id = webauthn_credential.credential_id self.device.sign_count = registration_verification.sign_count
self.device.ukey = ukey self.device.rp_id = get_webauthn_rp_id(request)
self.device.pub_key = webauthn_credential.public_key
self.device.sign_count = webauthn_credential.sign_count
self.device.rp_id = urlparse(settings.SITE_URL).netloc
self.device.icon_url = settings.SITE_URL self.device.icon_url = settings.SITE_URL
self.device.confirmed = True self.device.confirmed = True
self.device.save() self.device.save()
-45
View File
@@ -1,45 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import random
import string
def generate_challenge(challenge_len):
return ''.join([
random.SystemRandom().choice(string.ascii_letters + string.digits)
for i in range(challenge_len)
])
def generate_ukey():
"""
Its value's id member is required, and contains an identifier
for the account, specified by the Relying Party. This is not meant
to be displayed to the user, but is used by the Relying Party to
control the number of credentials - an authenticator will never
contain more than one credential for a given Relying Party under
the same id.
A unique identifier for the entity. For a relying party entity,
sets the RP ID. For a user account entity, this will be an
arbitrary string specified by the relying party.
"""
return generate_challenge(20)
@@ -35,16 +35,23 @@ async function fetch_json(url, options) {
* @param {Object} credentialCreateOptionsFromServer * @param {Object} credentialCreateOptionsFromServer
*/ */
const transformCredentialCreateOptions = function (credentialCreateOptionsFromServer) { const transformCredentialCreateOptions = function (credentialCreateOptionsFromServer) {
let {challenge, user} = credentialCreateOptionsFromServer; let {challenge, user, excludeCredentials} = credentialCreateOptionsFromServer;
user.id = Uint8Array.from( user.id = user.id.replace(/\_/g, "/").replace(/\-/g, "+");
atob(credentialCreateOptionsFromServer.user.id), c => c.charCodeAt(0)); user.id = Uint8Array.from(atob(user.id), c => c.charCodeAt(0));
challenge = Uint8Array.from( challenge = challenge.replace(/\_/g, "/").replace(/\-/g, "+");
atob(credentialCreateOptionsFromServer.challenge), c => c.charCodeAt(0)); challenge = Uint8Array.from(atob(challenge), c => c.charCodeAt(0));
excludeCredentials = excludeCredentials.map(credentialDescriptor => {
let {id} = credentialDescriptor;
id = id.replace(/\_/g, "/").replace(/\-/g, "+");
id = Uint8Array.from(atob(id), c => c.charCodeAt(0));
return Object.assign({}, credentialDescriptor, {id});
});
const transformedCredentialCreateOptions = Object.assign( const transformedCredentialCreateOptions = Object.assign(
{}, credentialCreateOptionsFromServer, {}, credentialCreateOptionsFromServer,
{challenge, user}); {challenge, user, excludeCredentials});
return transformedCredentialCreateOptions; return transformedCredentialCreateOptions;
}; };
@@ -56,22 +63,25 @@ const transformCredentialCreateOptions = function (credentialCreateOptionsFromSe
* @param {PublicKeyCredential} newAssertion * @param {PublicKeyCredential} newAssertion
*/ */
const transformNewAssertionForServer = (newAssertion) => { const transformNewAssertionForServer = (newAssertion) => {
const attObj = new Uint8Array( const attObj = new Uint8Array(newAssertion.response.attestationObject);
newAssertion.response.attestationObject); const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
const clientDataJSON = new Uint8Array( const rawId = new Uint8Array(newAssertion.rawId);
newAssertion.response.clientDataJSON); const transports = newAssertion.response.getTransports();
const rawId = new Uint8Array( const authenticatorAttachment = newAssertion.authenticatorAttachment;
newAssertion.rawId);
const registrationClientExtensions = newAssertion.getClientExtensionResults(); const registrationClientExtensions = newAssertion.getClientExtensionResults();
return { return {
id: newAssertion.id, id: newAssertion.id,
rawId: b64enc(rawId), rawId: b64enc(rawId),
response: {
attestationObject: b64enc(attObj),
clientDataJSON: b64enc(clientDataJSON),
transports: transports,
},
type: newAssertion.type, type: newAssertion.type,
attObj: b64enc(attObj), clientExtensionResults: JSON.stringify(registrationClientExtensions),
clientData: b64enc(clientDataJSON), authenticatorAttachment: authenticatorAttachment,
registrationClientExtensions: JSON.stringify(registrationClientExtensions)
}; };
}; };
@@ -79,8 +89,8 @@ const transformNewAssertionForServer = (newAssertion) => {
const transformCredentialRequestOptions = (credentialRequestOptionsFromServer) => { const transformCredentialRequestOptions = (credentialRequestOptionsFromServer) => {
let {challenge, allowCredentials} = credentialRequestOptionsFromServer; let {challenge, allowCredentials} = credentialRequestOptionsFromServer;
challenge = Uint8Array.from( challenge = challenge.replace(/\_/g, "/").replace(/\-/g, "+");
atob(challenge), c => c.charCodeAt(0)); challenge = Uint8Array.from(atob(challenge), c => c.charCodeAt(0));
allowCredentials = allowCredentials.map(credentialDescriptor => { allowCredentials = allowCredentials.map(credentialDescriptor => {
let {id} = credentialDescriptor; let {id} = credentialDescriptor;
@@ -106,16 +116,22 @@ const transformAssertionForServer = (newAssertion) => {
const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON); const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
const rawId = new Uint8Array(newAssertion.rawId); const rawId = new Uint8Array(newAssertion.rawId);
const sig = new Uint8Array(newAssertion.response.signature); const sig = new Uint8Array(newAssertion.response.signature);
const userHandle = new Uint8Array(newAssertion.response.userHandle);
const assertionClientExtensions = newAssertion.getClientExtensionResults(); const assertionClientExtensions = newAssertion.getClientExtensionResults();
const authenticatorAttachment = newAssertion.authenticatorAttachment;
return { return {
id: newAssertion.id, id: newAssertion.id,
rawId: b64enc(rawId), rawId: b64enc(rawId),
type: newAssertion.type, type: newAssertion.type,
authData: b64RawEnc(authData), response: {
clientData: b64RawEnc(clientDataJSON), authenticatorData: b64RawEnc(authData),
signature: hexEncode(sig), clientDataJSON: b64RawEnc(clientDataJSON),
assertionClientExtensions: JSON.stringify(assertionClientExtensions) signature: b64RawEnc(sig),
userHandle: b64RawEnc(userHandle),
},
authenticatorAttachment: authenticatorAttachment,
clientExtensionResults: JSON.stringify(assertionClientExtensions)
}; };
}; };
+8 -2
View File
@@ -45,6 +45,9 @@ from django.test import TestCase, override_settings
from django.utils.timezone import now from django.utils.timezone import now
from django_otp.oath import TOTP from django_otp.oath import TOTP
from django_otp.plugins.otp_totp.models import TOTPDevice from django_otp.plugins.otp_totp.models import TOTPDevice
from webauthn.authentication.verify_authentication_response import (
VerifiedAuthentication,
)
from pretix.base.models import U2FDevice, User from pretix.base.models import U2FDevice, User
from pretix.helpers import security from pretix.helpers import security
@@ -382,7 +385,7 @@ class Login2FAFormTest(TestCase):
raise Exception("Failed") raise Exception("Failed")
m = self.monkeypatch m = self.monkeypatch
m.setattr("webauthn.WebAuthnAssertionResponse.verify", fail) m.setattr("webauthn.verify_authentication_response", fail)
U2FDevice.objects.create( U2FDevice.objects.create(
user=self.user, name='test', user=self.user, name='test',
json_data='{"appId": "https://local.pretix.eu", "keyHandle": ' json_data='{"appId": "https://local.pretix.eu", "keyHandle": '
@@ -403,7 +406,10 @@ class Login2FAFormTest(TestCase):
def test_u2f_valid(self): def test_u2f_valid(self):
m = self.monkeypatch m = self.monkeypatch
m.setattr("webauthn.WebAuthnAssertionResponse.verify", lambda *args, **kwargs: 1) m.setattr("webauthn.verify_authentication_response",
lambda *args, **kwargs: VerifiedAuthentication(
b'', 1, 'single_device', True,
))
U2FDevice.objects.create( U2FDevice.objects.create(
user=self.user, name='test', user=self.user, name='test',
+6 -4
View File
@@ -40,7 +40,9 @@ from django_otp.oath import TOTP
from django_otp.plugins.otp_static.models import StaticDevice from django_otp.plugins.otp_static.models import StaticDevice
from django_otp.plugins.otp_totp.models import TOTPDevice from django_otp.plugins.otp_totp.models import TOTPDevice
from tests.base import SoupTest, extract_form_fields from tests.base import SoupTest, extract_form_fields
from webauthn import WebAuthnCredential from webauthn.registration.verify_registration_response import (
VerifiedRegistration,
)
from pretix.base.models import ( from pretix.base.models import (
Event, Organizer, U2FDevice, User, WebAuthnDevice, Event, Organizer, U2FDevice, User, WebAuthnDevice,
@@ -356,9 +358,9 @@ class UserSettings2FATest(SoupTest):
}, follow=True) }, follow=True)
m = self.monkeypatch m = self.monkeypatch
m.setattr("webauthn.WebAuthnRegistrationResponse.verify", m.setattr("webauthn.verify_registration_response",
lambda *args, **kwargs: WebAuthnCredential( lambda *args, **kwargs: VerifiedRegistration(
'', '', b'asd', b'foo', 1 b'', b'', 1, '', 'foo', 'public-key', True, b'', 'single_device', True
)) ))
d = WebAuthnDevice.objects.first() d = WebAuthnDevice.objects.first()