Passkey: add support for discoverable credentials

Apart from server-side credentials passkey should also register
discoverable credentials.
ipa user-add-passkey --register now supports an additional option,
--cred-type server-side|discoverable
that is propagated to passkey_child command.

Signed-off-by: Florence Blanc-Renaud <flo@redhat.com>
Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
This commit is contained in:
Florence Blanc-Renaud 2022-12-15 16:53:07 +01:00
parent 56e179748b
commit 6f0da62f5a
4 changed files with 52 additions and 7 deletions

View File

@ -60,6 +60,8 @@ During the registration process, it is possible to specify
the authentication will force to execute the user verification check even if
the passkey settings do not set this flag. If credentials are registered without
the flag, the global passkey settings apply.
- credential type: `server-side` or `discoverable`
Discoverable credentials do not require to first identify the user.
When the passkey credential is registered, a relaying party (RP) is set to be
the IPA domain (e.g. ipa.test). While using a domain-wide relaying party

View File

@ -34,6 +34,12 @@ class baseuser_add_passkey(MethodOverride):
doc=_('COSE type to use for registration'),
values=('es256', 'rs256', 'eddsa'),
),
StrEnum(
'credtype?',
cli_name="cred_type",
doc=_('Credential type'),
values=('server-side', 'discoverable'),
),
)
def get_args(self):
@ -69,6 +75,7 @@ class baseuser_add_passkey(MethodOverride):
options.pop('register')
cosetype = options.pop('cosetype', None)
require_verif = options.pop('require_user_verification', None)
credtype = options.pop('credtype', None)
cmd = [paths.PASSKEY_CHILD, "--register",
"--domain", self.api.env.domain,
"--username", args[0]]
@ -78,6 +85,9 @@ class baseuser_add_passkey(MethodOverride):
if require_verif is not None:
cmd.append("--user-verification")
cmd.append(str(require_verif).lower())
if credtype:
cmd.append("--cred-type")
cmd.append(credtype)
logger.debug("Executing command: %s", cmd)
subp = subprocess.Popen(cmd, stdout=subprocess.PIPE)

View File

@ -167,14 +167,15 @@ def validate_passkey(ugettext, key):
The expected format is passkey:<key id>,<pubkey>
"""
pattern = re.compile(r'passkey:(?P<id>.*),(?P<pkey>.*)')
pattern = re.compile(
r'^passkey:(?P<id>[^,]*),(?P<pkey>[^,]*),?(?P<userid>.*)$')
result = re.match(pattern, key)
if result is None:
return '"%s" is not a valid passkey mapping' % key
# Validate the id part
try:
base64.b64decode(result.group('id'))
base64.b64decode(result.group('id'), validate=True)
except Exception:
return '"%s" is not a valid passkey mapping, invalid id' % key
@ -187,6 +188,13 @@ def validate_passkey(ugettext, key):
backend=default_backend())
except ValueError:
return '"%s" is not a valid passkey mapping, invalid key' % key
# Validate the (optional) userid
try:
userid = result.group('userid')
if userid:
base64.b64decode(userid, validate=True)
except Exception:
return '"%s" is not a valid passkey mapping, invalid userid' % key
return None

View File

@ -55,6 +55,12 @@ PASSKEY_KEY = ("passkey:"
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgryfr3YR"
"M9OVdWHEDrbvcSyT5D0b/8Ks+fMp8MM0BXV/FOo436ZP"
"jUqSU+2LOXVGdKkJU1XBiwl+n/X+vGD1vw==")
PASSKEY_DISCOVERABLEKEY = (
"passkey:"
"pP2z07ygq36HkNabd79ki9H6rfYEIVdluSHjY1YykUbVECXJ3ZDZ3n1EZ9G8HhMv,"
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEpP2z07ygq36HkNabd1H9Knqqghjv"
"vhlW0+FcNzOoXP+49tC/Ee2TbjC3x2dIzJEBFi7iDPSc+OCM+WmD1AfPLQ==,"
"P6GjSqAo+RoQRJhGFA3lKcvtpKTGETjCdtVIyLX0KcY=")
@pytest.fixture
@ -64,15 +70,17 @@ def passkeyuser(request):
class TestAddRemovePasskey(XMLRPC_test):
def test_add_passkey(self, passkeyuser):
@pytest.mark.parametrize("key", [PASSKEY_KEY, PASSKEY_DISCOVERABLEKEY])
def test_add_passkey(self, passkeyuser,key):
passkeyuser.ensure_exists()
passkeyuser.add_passkey(ipapasskey=PASSKEY_KEY)
passkeyuser.add_passkey(ipapasskey=key)
passkeyuser.ensure_missing()
def test_remove_passkey(self, passkeyuser):
@pytest.mark.parametrize("key", [PASSKEY_KEY, PASSKEY_DISCOVERABLEKEY])
def test_remove_passkey(self, passkeyuser, key):
passkeyuser.ensure_exists()
passkeyuser.add_passkey(ipapasskey=PASSKEY_KEY)
passkeyuser.remove_passkey(ipapasskey=PASSKEY_KEY)
passkeyuser.add_passkey(ipapasskey=key)
passkeyuser.remove_passkey(ipapasskey=key)
@pytest.mark.parametrize("key", ['wrongval', 'passkey:123', 'passkey,123'])
def test_add_passkey_invalid(self, passkeyuser, key):
@ -112,6 +120,23 @@ class TestAddRemovePasskey(XMLRPC_test):
error=msg.format(key))):
cmd(key)
def test_add_passkey_invaliduserid(self, passkeyuser):
passkeyuser.ensure_exists()
key = ("passkey:"
"E8Zay6UJm6PG/GcQnej2WMyUrWqijejBCqPWFX6THPrxab01Z59bUguti"
"pn5MIk8/zMU6RBlp7jSbkNJsZtomw==,"
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgryfr3YRM9OVdWHEDrbvc"
"SyT5D0b/8Ks+fMp8MM0BXV/FOo436ZPjUqSU+2LOXVGdKkJU1XBiwl+n/X"
"+vGD1vw==,"
"wrongid")
msg = '"{}" is not a valid passkey mapping, invalid userid'
cmd = passkeyuser.make_command('user_add_passkey',
passkeyuser.name)
with raises_exact(errors.ValidationError(
name='passkey',
error=msg.format(key))):
cmd(key)
STAGEPASSKEY_USER = 'stagepasskeyuser'