Skip to content

Commit

Permalink
fix(jans-fido2): none assertiona and unit test for none attestation (#…
Browse files Browse the repository at this point in the history
…7199)

* feat(jans-fido2): added residentKey in attestation

Signed-off-by: Milton Ch <[email protected]>

* feat(jans-fido2): none assertion test and unit test for none attestation

Signed-off-by: Milton Ch <[email protected]>

---------

Signed-off-by: Milton Ch <[email protected]>
  • Loading branch information
Milton-Ch authored Dec 25, 2023
1 parent 5dd6786 commit a134ac9
Show file tree
Hide file tree
Showing 5 changed files with 375 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ private ObjectNode prepareAuthenticatorSelection(JsonNode params) {
// default is cross platform
AuthenticatorAttachment authenticatorAttachment = AuthenticatorAttachment.CROSS_PLATFORM;
UserVerification userVerification = UserVerification.preferred;
UserVerification residentKey = UserVerification.preferred;

Boolean requireResidentKey = false;

Expand All @@ -351,6 +352,8 @@ private ObjectNode prepareAuthenticatorSelection(JsonNode params) {
.verifyUserVerification(authenticatorSelectionNodeParameter.get("userVerification"));
requireResidentKey = commonVerifiers
.verifyRequireResidentKey(authenticatorSelectionNodeParameter.get("requireResidentKey"));
residentKey = commonVerifiers
.verifyUserVerification(authenticatorSelectionNodeParameter.get("residentKey"));
}

ObjectNode authenticatorSelectionNode = dataMapperService.createObjectNode();
Expand All @@ -364,6 +367,9 @@ private ObjectNode prepareAuthenticatorSelection(JsonNode params) {
if (userVerification != null) {
authenticatorSelectionNode.put("userVerification", userVerification.toString());
}
if (residentKey != null) {
authenticatorSelectionNode.put("residentKey", residentKey.toString());
}

return authenticatorSelectionNode;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Janssen Project software is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text.
*
* Copyright (c) 2020, Janssen Project
*/

/*
* Copyright (c) 2018 Mastercard
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is 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.
*/

package io.jans.fido2.service.processor.assertion;

import com.fasterxml.jackson.databind.JsonNode;
import io.jans.fido2.ctap.AttestationFormat;
import io.jans.fido2.exception.Fido2CompromisedDevice;
import io.jans.fido2.exception.Fido2RuntimeException;
import io.jans.fido2.model.auth.AuthData;
import io.jans.fido2.service.AuthenticatorDataParser;
import io.jans.fido2.service.Base64Service;
import io.jans.fido2.service.CoseService;
import io.jans.fido2.service.DataMapperService;
import io.jans.fido2.service.processors.AssertionFormatProcessor;
import io.jans.fido2.service.verifier.AuthenticatorDataVerifier;
import io.jans.fido2.service.verifier.CommonVerifiers;
import io.jans.fido2.service.verifier.UserVerificationVerifier;
import io.jans.orm.model.fido2.Fido2AuthenticationData;
import io.jans.orm.model.fido2.Fido2RegistrationData;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;
import org.slf4j.Logger;

import java.security.PublicKey;

/**
* Class which processes assertions of "none" fmt (attestation type)
*/
@ApplicationScoped
public class NoneAssertionFormatProcessor implements AssertionFormatProcessor {

@Inject
private Logger log;

@Inject
private CoseService coseService;

@Inject
private CommonVerifiers commonVerifiers;

@Inject
private AuthenticatorDataVerifier authenticatorDataVerifier;

@Inject
private UserVerificationVerifier userVerificationVerifier;

@Inject
private AuthenticatorDataParser authenticatorDataParser;

@Inject
private DataMapperService dataMapperService;

@Inject
private Base64Service base64Service;

@Override
public AttestationFormat getAttestationFormat() {
return AttestationFormat.none;
}

@Override
public void process(String base64AuthenticatorData, String signature, String clientDataJson, Fido2RegistrationData registration,
Fido2AuthenticationData authenticationEntity) {
log.debug("Registration: {}", registration);

AuthData authData = authenticatorDataParser.parseAssertionData(base64AuthenticatorData);
commonVerifiers.verifyRpIdHash(authData, registration.getDomain());

log.debug("User verification option: {}", authenticationEntity.getUserVerificationOption());
userVerificationVerifier.verifyUserVerificationOption(authenticationEntity.getUserVerificationOption(), authData);

byte[] clientDataHash = DigestUtils.getSha256Digest().digest(base64Service.urlDecode(clientDataJson));

try {
int counter = authenticatorDataParser.parseCounter(authData.getCounters());
commonVerifiers.verifyCounter(registration.getCounter(), counter);
registration.setCounter(counter);

JsonNode uncompressedECPointNode = dataMapperService.cborReadTree(base64Service.urlDecode(registration.getUncompressedECPoint()));
PublicKey publicKey = coseService.createUncompressedPointFromCOSEPublicKey(uncompressedECPointNode);

log.debug("Uncompressed ECpoint node: {}", uncompressedECPointNode);
log.debug("EC Public key hex: {}", Hex.encodeHexString(publicKey.getEncoded()));
log.debug("Registration algorithm: {}, default use: -7", registration.getSignatureAlgorithm());
authenticatorDataVerifier.verifyAssertionSignature(authData, clientDataHash, signature, publicKey, -7);

} catch (Fido2CompromisedDevice ex) {
log.error("Error compromised device: {}", ex.getMessage());
throw ex;
} catch (Exception ex) {
log.error("Error to check none assertion: {}", ex.getMessage());
throw new Fido2RuntimeException("Failed to check none assertion: {}", ex.getMessage(), ex);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,54 +18,57 @@

package io.jans.fido2.service.processor.attestation;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

import com.fasterxml.jackson.databind.JsonNode;
import io.jans.fido2.ctap.AttestationFormat;
import io.jans.fido2.exception.Fido2RuntimeException;
import io.jans.fido2.model.auth.AuthData;
import io.jans.fido2.model.auth.CredAndCounterData;
import io.jans.orm.model.fido2.Fido2RegistrationData;
import io.jans.fido2.service.Base64Service;
import io.jans.fido2.service.processors.AttestationFormatProcessor;
import io.jans.orm.model.fido2.Fido2RegistrationData;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.slf4j.Logger;

import com.fasterxml.jackson.databind.JsonNode;

/**
* Attestation processor for attestations of fmt = none One of the attestation
* formats called 'none'. When you getting it, that means two things:
*
* <p>
* 1. You really don't need attestation, and so you are deliberately ignoring
* it.
*
* <p>
* 2. You forgot to set attestation flag to 'direct' when making credential.
*
* <p>
* If you are getting attestation with fmt set to none, then no attestation
* is provided, and you don't have anything to verify. Simply extract user
* relevant information as specified below and save it to the database.
*
*
*/
@ApplicationScoped
public class NoneAttestationProcessor implements AttestationFormatProcessor {

@Inject
private Logger log;

@Inject
private Base64Service base64Service;

@Override
public AttestationFormat getAttestationFormat() {
return AttestationFormat.none;
}

@Override
public void process(JsonNode attStmt, AuthData authData, Fido2RegistrationData credential, byte[] clientDataHash,
CredAndCounterData credIdAndCounters) {
CredAndCounterData credIdAndCounters) {
log.debug("None/Surrogate attestation {}", attStmt);

if (attStmt.iterator().hasNext()) {
if (!attStmt.isEmpty()) {
log.error("Problem with None/Surrogate attestation");
throw new Fido2RuntimeException("Problem with None/Surrogate attestation");
}

credIdAndCounters.setAttestationType(getAttestationFormat().getFmt());
credIdAndCounters.setCredId(base64Service.urlEncodeToString(authData.getCredId()));
credIdAndCounters.setUncompressedEcPoint(base64Service.urlEncodeToString(authData.getCosePublicKey()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package io.jans.fido2.service.processor.assertion;

import com.fasterxml.jackson.databind.JsonNode;
import io.jans.fido2.exception.Fido2CompromisedDevice;
import io.jans.fido2.exception.Fido2RuntimeException;
import io.jans.fido2.model.auth.AuthData;
import io.jans.fido2.service.AuthenticatorDataParser;
import io.jans.fido2.service.Base64Service;
import io.jans.fido2.service.CoseService;
import io.jans.fido2.service.DataMapperService;
import io.jans.fido2.service.verifier.AuthenticatorDataVerifier;
import io.jans.fido2.service.verifier.CommonVerifiers;
import io.jans.fido2.service.verifier.UserVerificationVerifier;
import io.jans.orm.model.fido2.Fido2AuthenticationData;
import io.jans.orm.model.fido2.Fido2RegistrationData;
import io.jans.orm.model.fido2.UserVerification;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.slf4j.Logger;

import java.io.IOException;
import java.security.PublicKey;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class NoneAssertionFormatProcessorTest {

@InjectMocks
private NoneAssertionFormatProcessor noneAssertionFormatProcessor;

@Mock
private Logger log;

@Mock
private CoseService coseService;

@Mock
private CommonVerifiers commonVerifiers;

@Mock
private AuthenticatorDataVerifier authenticatorDataVerifier;

@Mock
private UserVerificationVerifier userVerificationVerifier;

@Mock
private AuthenticatorDataParser authenticatorDataParser;

@Mock
private DataMapperService dataMapperService;

@Mock
private Base64Service base64Service;

@Test
void getAttestationFormat_valid_none() {
String fmt = noneAssertionFormatProcessor.getAttestationFormat().getFmt();
assertNotNull(fmt);
assertEquals(fmt, "none");
}

@Test
void process_validData_success() throws IOException {
String base64AuthenticatorData = "base64AuthenticatorData_test";
String signature = "signature_test";
String clientDataJson = "clientDataJson_test";
Fido2RegistrationData registration = mock(Fido2RegistrationData.class);
Fido2AuthenticationData authenticationEntity = mock(Fido2AuthenticationData.class);

when(authenticatorDataParser.parseAssertionData(any())).thenReturn(mock(AuthData.class));
when(base64Service.urlDecode(any(String.class))).thenReturn("decode_test".getBytes());
when(dataMapperService.cborReadTree(any())).thenReturn(mock(JsonNode.class));
PublicKey publicKey = mock(PublicKey.class);
when(coseService.createUncompressedPointFromCOSEPublicKey(any())).thenReturn(publicKey);
when(publicKey.getEncoded()).thenReturn("test".getBytes());
when(authenticationEntity.getUserVerificationOption()).thenReturn(UserVerification.preferred);
when(registration.getDomain()).thenReturn("domain_test");

noneAssertionFormatProcessor.process(base64AuthenticatorData, signature, clientDataJson, registration, authenticationEntity);

verify(log).debug(eq("Registration: {}"), any(Fido2RegistrationData.class));
verify(log).debug(eq("User verification option: {}"), any(UserVerification.class));
verify(commonVerifiers).verifyRpIdHash(any(AuthData.class), any(String.class));
verify(log).debug(eq("Uncompressed ECpoint node: {}"), any(JsonNode.class));
verify(log).debug(eq("EC Public key hex: {}"), any(String.class));
verify(log).debug(eq("Registration algorithm: {}, default use: -7"), any(Integer.class));
verify(userVerificationVerifier).verifyUserVerificationOption(any(UserVerification.class), any(AuthData.class));
verify(authenticatorDataVerifier).verifyAssertionSignature(any(AuthData.class), any(byte[].class), any(String.class), any(PublicKey.class), any(Integer.class));

verify(log, never()).error(eq("Error compromised device: {}"), any(String.class));
verify(log, never()).error(eq("Error to check none assertion: {}"), any(String.class));
verifyNoMoreInteractions(log);
}

@Test
void process_ifVerifyCounterIsThrowException_fido2CompromisedDevice() throws Fido2CompromisedDevice {
String base64AuthenticatorData = "base64AuthenticatorData_test";
String signature = "signature_test";
String clientDataJson = "clientDataJson_test";
Fido2RegistrationData registration = mock(Fido2RegistrationData.class);
Fido2AuthenticationData authenticationEntity = mock(Fido2AuthenticationData.class);

when(authenticationEntity.getUserVerificationOption()).thenReturn(UserVerification.preferred);
when(registration.getDomain()).thenReturn("domain_test");
when(registration.getCounter()).thenReturn(100);

when(authenticatorDataParser.parseAssertionData(any())).thenReturn(mock(AuthData.class));
when(base64Service.urlDecode(any(String.class))).thenReturn("decode_test".getBytes());
doThrow(new Fido2CompromisedDevice("fido2CompromisedDevice_testError")).when(commonVerifiers).verifyCounter(any(Integer.class), any(Integer.class));

Fido2CompromisedDevice ex = assertThrows(Fido2CompromisedDevice.class, () -> noneAssertionFormatProcessor.process(base64AuthenticatorData, signature, clientDataJson, registration, authenticationEntity));
assertNotNull(ex);
assertEquals(ex.getMessage(), "fido2CompromisedDevice_testError");

verify(log).debug(eq("Registration: {}"), any(Fido2RegistrationData.class));
verify(log).debug(eq("User verification option: {}"), any(UserVerification.class));
verify(commonVerifiers).verifyRpIdHash(any(AuthData.class), any(String.class));
verify(authenticatorDataParser).parseCounter(any());
verify(log).error(eq("Error compromised device: {}"), any(String.class));

verify(log, never()).debug(eq("Registration algorithm: {}, default use: -7"), any(Integer.class));
verify(log, never()).error(eq("Error to check none assertion: {}"), any(String.class));
verifyNoInteractions(authenticatorDataVerifier);
verifyNoMoreInteractions(log);
}

@Test
void process_ifCborReadTreeThrowException_fido2RuntimeException() throws Fido2CompromisedDevice, IOException {
String base64AuthenticatorData = "base64AuthenticatorData_test";
String signature = "signature_test";
String clientDataJson = "clientDataJson_test";
Fido2RegistrationData registration = mock(Fido2RegistrationData.class);
Fido2AuthenticationData authenticationEntity = mock(Fido2AuthenticationData.class);

when(authenticationEntity.getUserVerificationOption()).thenReturn(UserVerification.preferred);
when(registration.getDomain()).thenReturn("domain_test");
when(registration.getCounter()).thenReturn(100);
when(registration.getUncompressedECPoint()).thenReturn("uncompressedECPoint_test");

when(authenticatorDataParser.parseAssertionData(any())).thenReturn(mock(AuthData.class));
when(base64Service.urlDecode(any(String.class))).thenReturn("decode_test".getBytes());
when(dataMapperService.cborReadTree(any(byte[].class))).thenThrow(new IOException("IOException_test"));

Fido2RuntimeException ex = assertThrows(Fido2RuntimeException.class, () -> noneAssertionFormatProcessor.process(base64AuthenticatorData, signature, clientDataJson, registration, authenticationEntity));
assertNotNull(ex);
assertEquals(ex.getMessage(), "IOException_test");

verify(log).debug(eq("Registration: {}"), any(Fido2RegistrationData.class));
verify(log).debug(eq("User verification option: {}"), any(UserVerification.class));
verify(commonVerifiers).verifyRpIdHash(any(AuthData.class), any(String.class));
verify(authenticatorDataParser).parseCounter(any());
verify(log).error(eq("Error to check none assertion: {}"), any(String.class));

verify(log, never()).error(eq("Error compromised device: {}"), any(String.class));
verifyNoInteractions(coseService, authenticatorDataVerifier);
verifyNoMoreInteractions(log);
}
}
Loading

0 comments on commit a134ac9

Please sign in to comment.