Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FIDO2] Implement updateUserInformation sub command of authenticatorCredentialManagement #159

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,30 @@ public void deleteCredential(PublicKeyCredentialDescriptor credential) throws IO
throw ClientError.wrapCtapException(e);
}
}

/**
* Update user information associated to a credential. Only name and displayName can be changed.
*
* @param credential A {@link PublicKeyCredentialDescriptor} which can be gotten from
* {@link #getCredentials(String)}.
* @param user A {@link PublicKeyCredentialUserEntity} containing updated data.
* @throws IOException A communication error in the transport layer.
* @throws CommandException A communication in the protocol layer.
* @throws ClientError A higher level error.
* @throws UnsupportedOperationException If the authenticator does not support updating user
* information.
*/
public void updateUserInformation(
PublicKeyCredentialDescriptor credential,
PublicKeyCredentialUserEntity user)
throws IOException, CommandException, ClientError, UnsupportedOperationException {
try {
credentialManagement.updateUserInformation(
credential.toMap(SerializationType.CBOR),
user.toMap(SerializationType.CBOR)
);
} catch (CtapException e) {
throw ClientError.wrapCtapException(e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
Expand Down Expand Up @@ -84,11 +85,16 @@ public CredentialManagement(
}

public static boolean isSupported(Ctap2Session.InfoData info) {
final Map<String, ?> options = info.getOptions();
if (Boolean.TRUE.equals(options.get("credMgmt"))) {
return true;
} else return info.getVersions().contains("FIDO_2_1_PRE") &&
Boolean.TRUE.equals(options.get("credentialMgmtPreview"));
return supportsCredMgmt(info) || supportsCredentialMgmtPreview(info);
}

private static boolean supportsCredMgmt(Ctap2Session.InfoData info) {
return Boolean.TRUE.equals(info.getOptions().get("credMgmt"));
}

private static boolean supportsCredentialMgmtPreview(Ctap2Session.InfoData info) {
return info.getVersions().contains("FIDO_2_1_PRE") &&
Boolean.TRUE.equals(info.getOptions().get("credentialMgmtPreview"));
}

private Map<Integer, ?> call(
Expand Down Expand Up @@ -202,6 +208,36 @@ public void deleteCredential(Map<String, ?> credentialId) throws IOException, Co
call(CMD_DELETE_CREDENTIAL, Collections.singletonMap(PARAM_CREDENTIAL_ID, credentialId), true);
}

/**
* @return true if updating user information is supported
*/
public boolean isUpdateUserInformationSupported() {
return supportsCredMgmt(ctap.getCachedInfo());
}

/**
* Update user information associated to a credential.
* Only supported on authenticators with version FIDO_2_1 and greater.
*
* @param credentialId A Map representing a PublicKeyCredentialDescriptor identifying a credential to delete.
* @param userEntity A Map representing a PublicKeyCredentialUserEntity containing the updated information.
* @throws IOException A communication error in the transport layer.
* @throws CommandException A communication in the protocol layer.
* @throws UnsupportedOperationException In case the functionality is not supported.
*/
public void updateUserInformation(Map<String, ?> credentialId, Map<String, ?> userEntity)
throws IOException, CommandException {

if (!isUpdateUserInformationSupported()) {
throw new UnsupportedOperationException("Update user information not supported");
}

Map<Integer, Object> parameters = new HashMap<>();
parameters.put((int) PARAM_CREDENTIAL_ID, credentialId);
parameters.put((int) PARAM_USER, userEntity);
call(CMD_UPDATE_USER_INFORMATION, parameters, true);
}

/**
* CTAP2 Credential Management Metadata object.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ public void testReadMetadata() throws Throwable {
public void testManagement() throws Throwable {
withCtap2Session(Ctap2CredentialManagementTests::testManagement);
}

@Test
@Category(SmokeTest.class)
public void testUpdateUserInformation() throws Throwable {
withCtap2Session(Ctap2CredentialManagementTests::testUpdateUserInformation);
}
}

@Category(PinUvAuthProtocolV1Test.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,25 @@ public static void testClientCredentialManagement(FidoTestState state) throws Th
assertThat(Objects.requireNonNull(credentials.get(key))
.getId(), equalTo(TestData.USER_ID));

try {
PublicKeyCredentialUserEntity updatedUser = new PublicKeyCredentialUserEntity(
"New name", credentials.get(key).getId(), "New display name"
);
credentialManager.updateUserInformation(key, updatedUser);

// verify new information
Map<PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity> updatedCreds =
credentialManager.getCredentials(TestData.RP_ID);
assertThat(updatedCreds.size(), equalTo(1));
PublicKeyCredentialDescriptor updatedKey = updatedCreds.keySet().iterator().next();
PublicKeyCredentialUserEntity updatedUserEntity = Objects.requireNonNull(updatedCreds.get(updatedKey));
assertThat(updatedUserEntity.getId(), equalTo(TestData.USER_ID));
assertThat(updatedUserEntity.getName(), equalTo("New name"));
assertThat(updatedUserEntity.getDisplayName(), equalTo("New display name"));
} catch (UnsupportedOperationException unsupportedOperationException) {
// ignored
}

credentialManager.deleteCredential(key);
assertThat(credentialManager.getCredentialCount(), equalTo(0));
assertTrue(credentialManager.getCredentials(TestData.RP_ID).isEmpty());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import com.yubico.yubikit.fido.ctap.ClientPin;
import com.yubico.yubikit.fido.ctap.CredentialManagement;
import com.yubico.yubikit.fido.ctap.Ctap2Session;
import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialUserEntity;
import com.yubico.yubikit.fido.webauthn.SerializationType;

import java.io.IOException;
Expand Down Expand Up @@ -79,48 +80,95 @@ public static void testReadMetadata(Ctap2Session session, FidoTestState state) t
public static void testManagement(Ctap2Session session, FidoTestState state) throws Throwable {

CredentialManagement credentialManagement = setupCredentialManagement(session, state);
assertThat(credentialManagement.enumerateRps(), empty());

final SerializationType cborType = SerializationType.CBOR;
byte[] pinToken = new ClientPin(session, credentialManagement.getPinUvAuth()).getPinToken(
TestData.PIN,
ClientPin.PIN_PERMISSION_MC,
TestData.RP.getId());

byte[] pinAuth = credentialManagement.getPinUvAuth().authenticate(pinToken, TestData.CLIENT_DATA_HASH);
makeTestCredential(state, session, pinAuth);

// this sets correct permission for handling credential management commands
credentialManagement = setupCredentialManagement(session, state);
CredentialManagement.CredentialData credData = getFirstTestCredential(credentialManagement);

Map<String, ?> userData = credData.getUser();
assertThat(userData.get("id"), equalTo(TestData.USER_ID));
assertThat(userData.get("name"), equalTo(TestData.USER_NAME));
assertThat(userData.get("displayName"), equalTo(TestData.USER_DISPLAY_NAME));

deleteAllCredentials(credentialManagement);
}

public static void testUpdateUserInformation(Ctap2Session session, FidoTestState state) throws Throwable {

CredentialManagement credentialManagement = setupCredentialManagement(session, state);

assumeTrue("Update user information is supported",
credentialManagement.isUpdateUserInformationSupported());

assertThat(credentialManagement.enumerateRps(), empty());

Map<String, Object> options = new HashMap<>();
options.put("rk", true);
byte[] pinToken = new ClientPin(session, credentialManagement.getPinUvAuth()).getPinToken(
TestData.PIN,
ClientPin.PIN_PERMISSION_MC,
TestData.RP.getId());

byte[] pinToken = new ClientPin(session, credentialManagement.getPinUvAuth())
.getPinToken(TestData.PIN, ClientPin.PIN_PERMISSION_MC, TestData.RP.getId());
byte[] pinAuth = credentialManagement.getPinUvAuth().authenticate(pinToken, TestData.CLIENT_DATA_HASH);
makeTestCredential(state, session, pinAuth);

// this sets correct permission for handling credential management commands
credentialManagement = setupCredentialManagement(session, state);
CredentialManagement.CredentialData credData = getFirstTestCredential(credentialManagement);

// change user name and display name
PublicKeyCredentialUserEntity updated = new PublicKeyCredentialUserEntity(
"UPDATED NAME",
(byte[]) credData.getUser().get("id"),
"UPDATED DISPLAY NAME");

// function under test
credentialManagement.updateUserInformation(credData.getCredentialId(), updated.toMap(SerializationType.CBOR));

// verify that information has been changed
CredentialManagement.CredentialData updatedCredData = getFirstTestCredential(credentialManagement);
Map<String, ?> updatedUserData = updatedCredData.getUser();

assertThat(updatedUserData.get("id"), equalTo(TestData.USER_ID));
assertThat(updatedUserData.get("name"), equalTo("UPDATED NAME"));
assertThat(updatedUserData.get("displayName"), equalTo("UPDATED DISPLAY NAME"));

deleteAllCredentials(credentialManagement);
}

// helper methods
private static void makeTestCredential(FidoTestState state, Ctap2Session session, byte[] pinAuth) throws IOException, CommandException {
final SerializationType cborType = SerializationType.CBOR;
session.makeCredential(
TestData.CLIENT_DATA_HASH,
TestData.RP.toMap(cborType),
TestData.USER.toMap(cborType),
Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256.toMap(cborType)),
null,
null,
options,
Collections.singletonMap("rk", true),
pinAuth,
state.getPinUvAuthProtocol().getVersion(),
null,
null
);
}


// this sets correct permission for handling credential management commands
credentialManagement = setupCredentialManagement(session, state);

private static CredentialManagement.CredentialData getFirstTestCredential(CredentialManagement credentialManagement) throws IOException, CommandException {
List<CredentialManagement.RpData> rps = credentialManagement.enumerateRps();
assertThat(rps.size(), equalTo(1));
CredentialManagement.RpData rpData = rps.get(0);
assertThat(rpData.getRp().get("id"), equalTo(TestData.RP_ID));

List<CredentialManagement.CredentialData> creds = credentialManagement.enumerateCredentials(rpData.getRpIdHash());
assertThat(creds.size(), equalTo(1));
CredentialManagement.CredentialData credData = creds.get(0);
Map<String, ?> userData = credData.getUser();
assertThat(userData.get("id"), equalTo(TestData.USER_ID));
assertThat(userData.get("name"), equalTo(TestData.USER_NAME));
assertThat(userData.get("displayName"), equalTo(TestData.USER_DISPLAY_NAME));

deleteAllCredentials(credentialManagement);
return creds.get(0);
}

}