diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/operation/AttestationService.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/operation/AttestationService.java index d260a11eac2..104d9a1dd36 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/operation/AttestationService.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/operation/AttestationService.java @@ -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; @@ -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(); @@ -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; } diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/processor/assertion/NoneAssertionFormatProcessor.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/processor/assertion/NoneAssertionFormatProcessor.java new file mode 100644 index 00000000000..6cfa4a84a66 --- /dev/null +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/processor/assertion/NoneAssertionFormatProcessor.java @@ -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); + } + } +} diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/processor/attestation/NoneAttestationProcessor.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/processor/attestation/NoneAttestationProcessor.java index fb4b9b99237..7f3cd797005 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/processor/attestation/NoneAttestationProcessor.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/processor/attestation/NoneAttestationProcessor.java @@ -18,33 +18,30 @@ 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: - * + *

* 1. You really don't need attestation, and so you are deliberately ignoring * it. - * + *

* 2. You forgot to set attestation flag to 'direct' when making credential. - * + *

* 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 { @@ -52,6 +49,9 @@ public class NoneAttestationProcessor implements AttestationFormatProcessor { @Inject private Logger log; + @Inject + private Base64Service base64Service; + @Override public AttestationFormat getAttestationFormat() { return AttestationFormat.none; @@ -59,13 +59,16 @@ public AttestationFormat getAttestationFormat() { @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())); } } diff --git a/jans-fido2/server/src/test/java/io/jans/fido2/service/processor/assertion/NoneAssertionFormatProcessorTest.java b/jans-fido2/server/src/test/java/io/jans/fido2/service/processor/assertion/NoneAssertionFormatProcessorTest.java new file mode 100644 index 00000000000..d017f74020a --- /dev/null +++ b/jans-fido2/server/src/test/java/io/jans/fido2/service/processor/assertion/NoneAssertionFormatProcessorTest.java @@ -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); + } +} diff --git a/jans-fido2/server/src/test/java/io/jans/fido2/service/processor/attestation/NoneAttestationProcessorTest.java b/jans-fido2/server/src/test/java/io/jans/fido2/service/processor/attestation/NoneAttestationProcessorTest.java new file mode 100644 index 00000000000..ea9089ff7c2 --- /dev/null +++ b/jans-fido2/server/src/test/java/io/jans/fido2/service/processor/attestation/NoneAttestationProcessorTest.java @@ -0,0 +1,77 @@ +package io.jans.fido2.service.processor.attestation; + +import com.fasterxml.jackson.databind.JsonNode; +import io.jans.fido2.exception.Fido2RuntimeException; +import io.jans.fido2.model.auth.AuthData; +import io.jans.fido2.model.auth.CredAndCounterData; +import io.jans.fido2.service.Base64Service; +import io.jans.orm.model.fido2.Fido2RegistrationData; +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 static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class NoneAttestationProcessorTest { + + @InjectMocks + private NoneAttestationProcessor noneAttestationProcessor; + + @Mock + private Logger log; + + @Mock + private Base64Service base64Service; + + @Test + void getAttestationFormat_valid_none() { + String fmt = noneAttestationProcessor.getAttestationFormat().getFmt(); + assertNotNull(fmt); + assertEquals(fmt, "none"); + } + + @Test + void process_validData_success() { + JsonNode attStmt = mock(JsonNode.class); + AuthData authData = mock(AuthData.class); + Fido2RegistrationData credential = mock(Fido2RegistrationData.class); + byte[] clientDataHash = "clientDataHash_test".getBytes(); + CredAndCounterData credIdAndCounters = mock(CredAndCounterData.class); + + when(attStmt.isEmpty()).thenReturn(true); + when(authData.getCredId()).thenReturn("credId_test".getBytes()); + when(authData.getCosePublicKey()).thenReturn("cosePublicKey_test".getBytes()); + + noneAttestationProcessor.process(attStmt, authData, credential, clientDataHash, credIdAndCounters); + + verify(log).debug(eq("None/Surrogate attestation {}"), any(JsonNode.class)); + verify(base64Service, times(2)).urlEncodeToString(any(byte[].class)); + + verify(log, never()).error(eq("Problem with None/Surrogate attestation")); + } + + @Test + void process_ifAttStmtIsEmptyFalse_fido2RuntimeException() { + JsonNode attStmt = mock(JsonNode.class); + AuthData authData = mock(AuthData.class); + Fido2RegistrationData credential = mock(Fido2RegistrationData.class); + byte[] clientDataHash = "clientDataHash_test".getBytes(); + CredAndCounterData credIdAndCounters = mock(CredAndCounterData.class); + + when(attStmt.isEmpty()).thenReturn(false); + + Fido2RuntimeException ex = assertThrows(Fido2RuntimeException.class, () -> noneAttestationProcessor.process(attStmt, authData, credential, clientDataHash, credIdAndCounters)); + assertNotNull(ex); + assertEquals(ex.getMessage(), "Problem with None/Surrogate attestation"); + + verify(log).debug(eq("None/Surrogate attestation {}"), any(JsonNode.class)); + verify(log).error(eq("Problem with None/Surrogate attestation")); + + verifyNoInteractions(base64Service); + } +}