Skip to content

Commit

Permalink
PRF instrumented tests
Browse files Browse the repository at this point in the history
  • Loading branch information
AdamVe committed Oct 16, 2024
1 parent 33ac890 commit d64aa66
Show file tree
Hide file tree
Showing 3 changed files with 199 additions and 15 deletions.
30 changes: 16 additions & 14 deletions fido/src/main/java/com/yubico/yubikit/fido/webauthn/Extension.java
Original file line number Diff line number Diff line change
Expand Up @@ -228,14 +228,20 @@ public Object processGetInput(Map<String, ?> inputs) {
return null;
}


Salts salts;
Map<String, Object> data = (Map<String, Object>) inputs.get("prf");
if (data != null) {
Map<String, Object> secrets = (Map<String, Object>) data.get("eval");
if (secrets == null) {
return null;
}
String firstInput = (String) secrets.get("first");
if (firstInput == null) {
return null;
}

byte[] first = SerializationUtils.deserializeBytes(
Objects.requireNonNull((String) secrets.get("first")),
Objects.requireNonNull(firstInput),
SerializationType.JSON);

byte[] second = secrets.containsKey("second")
Expand Down Expand Up @@ -271,9 +277,9 @@ public Object processGetInput(Map<String, ?> inputs) {

final ClientPin clientPin = new ClientPin(ctap, pinUvAuthProtocol);
try {
Pair<Map<Integer, ?>, byte[]> keyAgreemenbt = clientPin.getSharedSecret();
Pair<Map<Integer, ?>, byte[]> keyAgreement = clientPin.getSharedSecret();

this.sharedSecret = keyAgreemenbt.second;
this.sharedSecret = keyAgreement.second;

byte[] saltEnc = pinUvAuthProtocol.encrypt(
sharedSecret,
Expand All @@ -284,11 +290,11 @@ public Object processGetInput(Map<String, ?> inputs) {
.array());

byte[] saltAuth = pinUvAuthProtocol.authenticate(
keyAgreemenbt.second,
keyAgreement.second,
saltEnc);

final Map<Integer, Object> hmacGetSecretInput = new HashMap<>();
hmacGetSecretInput.put(1, keyAgreemenbt.first);
hmacGetSecretInput.put(1, keyAgreement.first);
hmacGetSecretInput.put(2, saltEnc);
hmacGetSecretInput.put(3, saltAuth);
hmacGetSecretInput.put(4, clientPin.getPinUvAuth().getVersion());
Expand Down Expand Up @@ -322,7 +328,9 @@ public ExtensionResult processGetOutput(
byte[] decrypted = this.pinUvAuthProtocol.decrypt(sharedSecret, value);

byte[] output1 = Arrays.copyOf(decrypted, SALT_LEN);
byte[] output2 = Arrays.copyOfRange(decrypted, SALT_LEN, 2 * SALT_LEN);
byte[] output2 = decrypted.length > SALT_LEN
? Arrays.copyOfRange(decrypted, SALT_LEN, 2 * SALT_LEN)
: new byte[0];

Logger.debug(logger, "Decrypted: {}, o1: {}, o2: {}",
StringUtils.bytesToHex(decrypted),
Expand Down Expand Up @@ -451,7 +459,7 @@ public InputWithPermission processGetInputWithPermissions(Map<String, ?> inputs)
@SuppressWarnings("unchecked")
Map<String, Object> data = inputs.containsKey("largeBlob")
? (Map<String, Object>) processGetInput((Map<String, Object>) inputs.get("largeBlob"))
: Collections.emptyMap();
: null;

int permissions = ClientPin.PIN_PERMISSION_NONE;

Expand Down Expand Up @@ -519,12 +527,6 @@ public Object processCreateInput(Map<String, ?> inputs) {

return null;
}

@Nullable
@Override
public Object processGetInput(Map<String, ?> inputs) {
return super.processGetInput(inputs);
}
}

static class CredProtectExtension extends Extension {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ public static class PinUvAuthV2Test extends FidoInstrumentedTests {
public void testCredPropsExtension() throws Throwable {
withDevice(ExtensionsTests::testCredPropsExtension);
}

@Test
public void testPrfExtension() throws Throwable {
withDevice(ExtensionsTests::testPrfExtension);
}

@Test
public void testPrfExtensionNoSupport() throws Throwable {
withDevice(ExtensionsTests::testPrfExtensionNoSupport);
}
}

@Category(PinUvAuthProtocolV1Test.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,12 @@
import com.yubico.yubikit.fido.webauthn.SerializationType;

import org.junit.Assert;
import org.junit.Assume;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -89,9 +91,174 @@ public static void testCredPropsExtension(FidoTestState state) throws Throwable
});
}

@SuppressWarnings("unchecked")
public static void testPrfExtension(FidoTestState state) throws Throwable {
final String PRF_EXT = "prf";

// non-discoverable credential
{
// no output when no input
state.withCtap2(session -> {
Assume.assumeTrue(session.getCachedInfo().getExtensions().contains("hmac-secret"));
BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session);
PublicKeyCredential cred = new Builder(webauthn).create();
Map<String, ?> result = getResult(cred.getClientExtensionResults(), PRF_EXT);
Assert.assertNull(result);
});

// { prf: { enabled: true } }
PublicKeyCredentialDescriptor credDesc = state.withCtap2(session -> {
BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session);
PublicKeyCredential cred = new Builder(webauthn)
.extensions(Collections.singletonMap(PRF_EXT, Collections.emptyMap()))
.create();
Map<String, ?> result = getResult(cred.getClientExtensionResults(), PRF_EXT);
Assert.assertNotNull(result);
Assert.assertEquals(Boolean.TRUE, result.get("enabled"));
return new PublicKeyCredentialDescriptor("public-key", cred.getRawId());
});

// assertion with { eval: { first: "value" } }
// { prf: { results: { first: String } } }
state.withCtap2(session -> {
BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session);
PublicKeyCredential cred = new Builder(webauthn)
// this is no discoverable key, we have to pass the id
.allowedCredentials(Collections.singletonList(credDesc))
.extensions(Collections.singletonMap(PRF_EXT,
Collections.singletonMap("eval",
Collections.singletonMap("first", "abba"))))
.getAssertions();
Map<String, ?> result = getResult(cred.getClientExtensionResults(), PRF_EXT);
Assert.assertNotNull(result);
Assert.assertFalse(result.containsKey("enabled"));
Assert.assertTrue(result.containsKey("results"));
Map<String, ?> results = (Map<String, ?>) result.get("results");
Assert.assertTrue(results.containsKey("first"));
Assert.assertTrue(results.get("first") instanceof String);
Assert.assertFalse(results.containsKey("second"));
});

// assertion with { eval: { first: "value", second: "value" } }
// { prf: { results: { first: String, second: String } } }
state.withCtap2(session -> {
BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session);

Map<String, Object> eval = new HashMap<>();
eval.put("first", "abba");
eval.put("second", "bebe");

PublicKeyCredential cred = new Builder(webauthn)
// this is no discoverable key, we have to pass the id
.allowedCredentials(Collections.singletonList(credDesc))
.extensions(Collections.singletonMap(PRF_EXT,
Collections.singletonMap("eval", eval)))
.getAssertions();
Map<String, ?> result = getResult(cred.getClientExtensionResults(), PRF_EXT);
Assert.assertNotNull(result);
Assert.assertFalse(result.containsKey("enabled"));
Assert.assertTrue(result.containsKey("results"));
Map<String, ?> results = (Map<String, ?>) result.get("results");
Assert.assertTrue(results.containsKey("first"));
Assert.assertTrue(results.get("first") instanceof String);
Assert.assertTrue(results.containsKey("second"));
Assert.assertTrue(results.get("second") instanceof String);
});
}

// discoverable credential
{
// no output when no input
state.withCtap2(session -> {
Assume.assumeTrue(session.getCachedInfo().getExtensions().contains("hmac-secret"));
BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session);
PublicKeyCredential cred = new Builder(webauthn).residentKey(true).create();
Map<String, ?> result = getResult(cred.getClientExtensionResults(), PRF_EXT);
Assert.assertNull(result);
deleteCredentials(webauthn, Collections.singletonList(cred.getRawId()));
});

// { prf: { enabled: true } }
PublicKeyCredentialDescriptor credDesc = state.withCtap2(session -> {
BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session);
PublicKeyCredential cred = new Builder(webauthn)
.residentKey(true)
.extensions(Collections.singletonMap(PRF_EXT, Collections.emptyMap()))
.create();
Map<String, ?> result = getResult(cred.getClientExtensionResults(), PRF_EXT);
Assert.assertNotNull(result);
Assert.assertEquals(Boolean.TRUE, result.get("enabled"));
return new PublicKeyCredentialDescriptor("public-key", cred.getRawId());
});

// assertion with { eval: { first: "value" } }
// { prf: { results: { first: String } } }
state.withCtap2(session -> {
BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session);
PublicKeyCredential cred = new Builder(webauthn)
.extensions(Collections.singletonMap(PRF_EXT,
Collections.singletonMap("eval",
Collections.singletonMap("first", "abba"))))
.getAssertions();
Map<String, ?> result = getResult(cred.getClientExtensionResults(), PRF_EXT);
Assert.assertNotNull(result);
Assert.assertFalse(result.containsKey("enabled"));
Assert.assertTrue(result.containsKey("results"));
Map<String, ?> results = (Map<String, ?>) result.get("results");
Assert.assertTrue(results.containsKey("first"));
Assert.assertTrue(results.get("first") instanceof String);
Assert.assertFalse(results.containsKey("second"));
});

// assertion with { eval: { first: "value", second: "value" } }
// { prf: { results: { first: String, second: String } } }
state.withCtap2(session -> {
BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session);

Map<String, Object> eval = new HashMap<>();
eval.put("first", "abba");
eval.put("second", "bebe");

PublicKeyCredential cred = new Builder(webauthn)
.extensions(Collections.singletonMap(PRF_EXT,
Collections.singletonMap("eval", eval)))
.getAssertions();
Map<String, ?> result = getResult(cred.getClientExtensionResults(), PRF_EXT);
Assert.assertNotNull(result);
Assert.assertFalse(result.containsKey("enabled"));
Assert.assertTrue(result.containsKey("results"));
Map<String, ?> results = (Map<String, ?>) result.get("results");
Assert.assertTrue(results.containsKey("first"));
Assert.assertTrue(results.get("first") instanceof String);
Assert.assertTrue(results.containsKey("second"));
Assert.assertTrue(results.get("second") instanceof String);

deleteCredentials(webauthn, Collections.singletonList(credDesc.getId()));
});
}
}

// this test is active only on devices without hmac-secret
public static void testPrfExtensionNoSupport(FidoTestState state) throws Throwable {
final String PRF_EXT = "prf";

// { prf: { enabled: false } }
state.withCtap2(session -> {
Assume.assumeFalse(session.getCachedInfo().getExtensions().contains("hmac-secret"));
BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session);
PublicKeyCredential cred = new Builder(webauthn)
.extensions(Collections.singletonMap(PRF_EXT, Collections.emptyMap()))
.create();
Map<String, ?> result = getResult(cred.getClientExtensionResults(), PRF_EXT);
Assert.assertNotNull(result);
Assert.assertEquals(Boolean.FALSE, result.get("enabled"));
});
}

static class Builder {
final BasicWebAuthnClient client;

List<PublicKeyCredentialDescriptor> allowedCredentials = null;
boolean residentKey = false;

@Nullable
Expand All @@ -113,6 +280,11 @@ Builder extensions(@Nullable Map<String, ?> extensions) {
return this;
}

Builder allowedCredentials(@Nullable List<PublicKeyCredentialDescriptor> allowedCredentials) {
this.allowedCredentials = allowedCredentials;
return this;
}

PublicKeyCredential create() throws IOException, CommandException, ClientError {
PublicKeyCredentialCreationOptions options = getCreateOptions(
new PublicKeyCredentialUserEntity(
Expand All @@ -139,7 +311,7 @@ PublicKeyCredential getAssertions() throws IOException, CommandException, Client
TestData.CHALLENGE,
(long) 90000,
TestData.RP_ID,
null,
allowedCredentials,
null,
extensions
);
Expand Down

0 comments on commit d64aa66

Please sign in to comment.