diff --git a/jans-bom/pom.xml b/jans-bom/pom.xml
index a506833ba66..8fb081c9814 100644
--- a/jans-bom/pom.xml
+++ b/jans-bom/pom.xml
@@ -832,7 +832,44 @@
4.13.2
test
+
+ org.jboss.weld
+ weld-junit5
+ 4.0.0.Final
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ 5.9.2
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.9.2
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-params
+ 5.9.2
+ test
+
+
+ org.mockito
+ mockito-core
+ 5.1.1
+ test
+
+
+ org.mockito
+ mockito-inline
+ 5.1.1
+ test
+
+
net.openhft
diff --git a/jans-core/service/src/main/java/io/jans/service/net/NetworkService.java b/jans-core/service/src/main/java/io/jans/service/net/NetworkService.java
index e1dd51424dd..29a0e165c7f 100644
--- a/jans-core/service/src/main/java/io/jans/service/net/NetworkService.java
+++ b/jans-core/service/src/main/java/io/jans/service/net/NetworkService.java
@@ -6,18 +6,18 @@
package io.jans.service.net;
-import io.jans.net.InetAddressUtility;
-import io.jans.util.StringHelper;
+import java.io.Serializable;
+import java.net.URI;
+import java.net.URISyntaxException;
+
import org.slf4j.Logger;
+import io.jans.net.InetAddressUtility;
+import io.jans.util.StringHelper;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.faces.context.FacesContext;
import jakarta.inject.Inject;
-import jakarta.inject.Named;
import jakarta.servlet.http.HttpServletRequest;
-import java.io.Serializable;
-import java.net.URI;
-import java.net.URISyntaxException;
/**
* Network service
@@ -25,7 +25,6 @@
* @author Yuriy Movchan Date: 04/28/2016
*/
@ApplicationScoped
-@Named
public class NetworkService implements Serializable {
private static final long serialVersionUID = -1393318600428448743L;
diff --git a/jans-fido2/pom.xml b/jans-fido2/pom.xml
index da48914c817..38b6d8ffbe5 100644
--- a/jans-fido2/pom.xml
+++ b/jans-fido2/pom.xml
@@ -127,6 +127,11 @@
+
+ org.apache.maven.plugins
+ maven-war-plugin
+ 2.3
+
org.apache.maven.plugins
maven-clean-plugin
@@ -175,6 +180,18 @@
jacoco-maven-plugin
${jacoco.version}
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.0.0-M9
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.9.2
+
+
+
diff --git a/jans-fido2/server/pom.xml b/jans-fido2/server/pom.xml
index 8116f3c4595..7bbc27517d6 100644
--- a/jans-fido2/server/pom.xml
+++ b/jans-fido2/server/pom.xml
@@ -36,6 +36,8 @@
**/*.json
**/*.xml
+ **/keys/**/*.*
+ **/*.properties
@@ -90,6 +92,18 @@
false
+
@@ -149,6 +163,33 @@
kerby-asn1
2.0.1
+
+
+
+ org.jboss.weld
+ weld-junit5
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-params
+ test
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+ org.mockito
+ mockito-inline
+ test
+
diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/sg/converter/AttestationSuperGluuController.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/sg/converter/AttestationSuperGluuController.java
index de869f5032a..f99d7a263c6 100644
--- a/jans-fido2/server/src/main/java/io/jans/fido2/service/sg/converter/AttestationSuperGluuController.java
+++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/sg/converter/AttestationSuperGluuController.java
@@ -103,7 +103,24 @@ public class AttestationSuperGluuController {
* "appId":"https://yurem-emerging-pig.gluu.info/identity/authcode.htm","version":"U2F_V2"}]}
*/
public JsonNode startRegistration(String userName, String appId, String sessionId, String enrollmentCode) {
- boolean oneStep = StringHelper.isEmpty(userName);
+ ObjectNode params = buildFido2AttestationStartResponse(userName, appId, sessionId);
+
+ ObjectNode result = attestationService.options(params);
+
+ // Build start registration response
+ ObjectNode superGluuResult = dataMapperService.createObjectNode();
+ ArrayNode registerRequests = superGluuResult.putArray("registerRequests");
+
+ result.put("appId", appId);
+ registerRequests.add(result);
+
+ result.put("version", "U2F_V2");
+
+ return superGluuResult;
+ }
+
+ public ObjectNode buildFido2AttestationStartResponse(String userName, String appId, String sessionId) {
+ boolean oneStep = StringHelper.isEmpty(userName);
boolean valid = userSessionIdService.isValidSessionId(sessionId, userName);
if (!valid) {
@@ -130,20 +147,8 @@ public JsonNode startRegistration(String userName, String appId, String sessionI
params.put("attestation", "direct");
log.debug("Prepared U2F_V2 attestation options request: {}", params.toString());
-
- ObjectNode result = attestationService.options(params);
-
- // Build start registration response
- ObjectNode superGluuResult = dataMapperService.createObjectNode();
- ArrayNode registerRequests = superGluuResult.putArray("registerRequests");
-
- result.put("appId", appId);
- registerRequests.add(result);
-
- result.put("version", "U2F_V2");
-
- return superGluuResult;
- }
+ return params;
+ }
/* Example for one_step:
* - request:
@@ -189,14 +194,31 @@ public JsonNode startRegistration(String userName, String appId, String sessionI
*
*/
public JsonNode finishRegistration(String userName, String registerResponseString) {
- RegisterResponse registerResponse;
+ RegisterResponse registerResponse = parseRegisterResponse(registerResponseString);
+
+ ObjectNode params = buildFido2AttestationVerifyResponse(userName, registerResponse);
+
+ ObjectNode result = attestationService.verify(params);
+
+ result.put("status", "success");
+ result.put("challenge", registerResponse.getClientData().getChallenge());
+
+ return result;
+ }
+
+ public RegisterResponse parseRegisterResponse(String registerResponseString) {
+ RegisterResponse registerResponse;
try {
registerResponse = dataMapperService.readValue(registerResponseString, RegisterResponse.class);
} catch (IOException ex) {
throw new Fido2RpRuntimeException("Failed to parse options attestation request", ex);
}
- if (!ArrayUtils.contains(RawRegistrationService.SUPPORTED_REGISTER_TYPES, registerResponse.getClientData().getTyp())) {
+ return registerResponse;
+ }
+
+ public ObjectNode buildFido2AttestationVerifyResponse(String userName, RegisterResponse registerResponse) {
+ if (!ArrayUtils.contains(RawRegistrationService.SUPPORTED_REGISTER_TYPES, registerResponse.getClientData().getTyp())) {
throw new Fido2RuntimeException("Invalid options attestation request type");
}
@@ -250,14 +272,8 @@ public JsonNode finishRegistration(String userName, String registerResponseStrin
}
log.debug("Prepared U2F_V2 attestation verify request: {}", params.toString());
-
- ObjectNode result = attestationService.verify(params);
-
- result.put("status", "success");
- result.put("challenge", registerResponse.getClientData().getChallenge());
-
- return result;
- }
+ return params;
+ }
private byte[] generateAuthData(ClientData clientData, RawRegisterResponse rawRegisterResponse) throws IOException {
byte[] rpIdHash = digestService.hashSha256(clientData.getOrigin());
diff --git a/jans-fido2/server/src/test/java/io/jans/fido2/service/KeySignatureVerifierTest.java b/jans-fido2/server/src/test/java/io/jans/fido2/service/KeySignatureVerifierTest.java
new file mode 100644
index 00000000000..3a42477e8bf
--- /dev/null
+++ b/jans-fido2/server/src/test/java/io/jans/fido2/service/KeySignatureVerifierTest.java
@@ -0,0 +1,78 @@
+/*
+ * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text.
+ *
+ * Copyright (c) 2023, Janssen Project
+ */
+
+package io.jans.fido2.service;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+
+import org.apache.commons.io.FileUtils;
+import org.jboss.weld.junit5.auto.AddPackages;
+import org.jboss.weld.junit5.auto.WeldJunit5AutoExtension;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import io.jans.as.model.util.SecurityProviderUtility;
+import jakarta.inject.Inject;
+
+/**
+ * @author Yuriy Movchan
+ * @version 0.1, 17/02/2023
+ */
+@ExtendWith(WeldJunit5AutoExtension.class)
+@AddPackages(io.jans.service.util.Resources.class)
+public class KeySignatureVerifierTest {
+
+ @Inject
+ Base64Service base64Service;
+
+ @BeforeAll
+ public static void beforeAll() {
+ SecurityProviderUtility.installBCProvider();
+ }
+
+ /*
+ * openssl ecparam -name secp256r1 -genkey -noout -out private.key
+ * openssl ec -in private.key -pubout -out public.pem
+ * echo -n "test" > data.txt
+ *
+ * openssl dgst -sha256 -sign private.key data.txt | openssl enc -base64 > signature.txt
+ */
+ @Test
+ public void testSHA256withECDSASignature() throws IOException, NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException, InvalidKeyException, SignatureException {
+ String key = FileUtils.readFileToString(new File("./target/test-classes/keys/secp256r1/public.pem"), StandardCharsets.UTF_8);
+ String publicKeyPEM = key.replace("-----BEGIN PUBLIC KEY-----", "").replaceAll(System.lineSeparator(), "").replace("-----END PUBLIC KEY-----", "");
+
+ KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC");
+ X509EncodedKeySpec keySpec = new X509EncodedKeySpec(base64Service.decode(publicKeyPEM));
+ PublicKey publicKey = keyFactory.generatePublic(keySpec);
+
+ byte[] signature = base64Service.decode(FileUtils.readFileToString(new File("./target/test-classes/keys/secp256r1/signature.txt"),
+ StandardCharsets.UTF_8).replaceAll(System.lineSeparator(), ""));
+ byte[] signedBytes = FileUtils.readFileToString(new File("./target/test-classes/keys/secp256r1/data.txt"), StandardCharsets.UTF_8).getBytes(StandardCharsets.UTF_8);
+
+ Signature ecdsaSignature = Signature.getInstance("SHA256withECDSA", "BC");
+ ecdsaSignature.initVerify(publicKey);
+ ecdsaSignature.update(signedBytes);
+
+ boolean isValid = ecdsaSignature.verify(signature);
+ assertTrue(isValid);
+ }
+
+}
diff --git a/jans-fido2/server/src/test/java/io/jans/fido2/service/sg/AttestationSignatureAndroidTest.java b/jans-fido2/server/src/test/java/io/jans/fido2/service/sg/AttestationSignatureAndroidTest.java
new file mode 100644
index 00000000000..92d021ecf8f
--- /dev/null
+++ b/jans-fido2/server/src/test/java/io/jans/fido2/service/sg/AttestationSignatureAndroidTest.java
@@ -0,0 +1,209 @@
+/*
+ * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text.
+ *
+ * Copyright (c) 2023, Janssen Project
+ */
+
+package io.jans.fido2.service.sg;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyString;
+
+import java.util.Arrays;
+
+import org.jboss.weld.junit5.ExplicitParamInjection;
+import org.jboss.weld.junit5.auto.AddBeanClasses;
+import org.jboss.weld.junit5.auto.EnableAutoWeld;
+import org.jboss.weld.junit5.auto.ExcludeBean;
+import org.jboss.weld.junit5.auto.WeldJunit5AutoExtension;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import io.jans.as.common.model.common.User;
+import io.jans.as.model.config.BaseDnConfiguration;
+import io.jans.as.model.config.StaticConfiguration;
+import io.jans.as.model.fido.u2f.protocol.RegisterResponse;
+import io.jans.as.model.util.SecurityProviderUtility;
+import io.jans.fido2.model.conf.AppConfiguration;
+import io.jans.fido2.model.conf.Fido2Configuration;
+import io.jans.fido2.service.ChallengeGenerator;
+import io.jans.fido2.service.operation.AttestationService;
+import io.jans.fido2.service.persist.RegistrationPersistenceService;
+import io.jans.fido2.service.persist.UserSessionIdService;
+import io.jans.fido2.service.processor.attestation.U2FSuperGluuAttestationProcessor;
+import io.jans.fido2.service.sg.converter.AttestationSuperGluuController;
+import io.jans.fido2.service.shared.UserService;
+import io.jans.fido2.service.verifier.CommonVerifiers;
+import io.jans.fido2.sg.SuperGluuMode;
+import io.jans.junit.extension.CustomExtension;
+import io.jans.junit.extension.Name;
+import io.jans.orm.model.fido2.Fido2RegistrationEntry;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.inject.Produces;
+import jakarta.inject.Inject;
+
+/**
+ * @author Yuriy Movchan
+ * @version 0.1, 17/02/2023
+ */
+@EnableAutoWeld
+@ExtendWith(WeldJunit5AutoExtension.class)
+@TestMethodOrder(OrderAnnotation.class)
+@AddBeanClasses(io.jans.service.util.Resources.class)
+@AddBeanClasses(io.jans.service.net.NetworkService.class)
+@ExplicitParamInjection
+public class AttestationSignatureAndroidTest {
+
+ private String issuer;
+ private String challenge;
+
+ // Static to store value between tests executions
+ static Fido2RegistrationEntry registrationEntry;
+
+ AutoCloseable closeable;
+
+ @Inject
+ AttestationSuperGluuController attestationSuperGluuController;
+
+ @Inject
+ U2FSuperGluuAttestationProcessor attestationProcessor;
+
+ @Inject
+ AttestationService attestationService;
+
+ @Mock
+ UserService userService = Mockito.mock(UserService.class);
+
+ @InjectMocks
+ RegistrationPersistenceService registrationPersistenceService = Mockito.mock(RegistrationPersistenceService.class);
+
+ @BeforeAll
+ public static void beforeAll() {
+ SecurityProviderUtility.installBCProvider();
+ }
+
+ @BeforeEach
+ void initService() {
+ closeable = MockitoAnnotations.openMocks(this);
+ }
+
+ @AfterEach
+ void closeService() throws Exception {
+ closeable.close();
+ }
+
+ @ApplicationScoped
+ @Produces
+ StaticConfiguration produceStaticConfiguration() {
+ StaticConfiguration staticConfiguration = Mockito.mock(StaticConfiguration.class);
+
+ BaseDnConfiguration baseDnConfiguration = new BaseDnConfiguration();
+ Mockito.when(staticConfiguration.getBaseDn()).thenReturn(baseDnConfiguration);
+
+ return staticConfiguration;
+ }
+
+ @ApplicationScoped
+ @Produces
+ AppConfiguration produceAppConfiguration() {
+ AppConfiguration appConfiguration = Mockito.mock(AppConfiguration.class);
+
+ Fido2Configuration fido2Configuration = new Fido2Configuration();
+ Mockito.when(appConfiguration.getFido2Configuration()).thenReturn(fido2Configuration);
+ Mockito.when(appConfiguration.getIssuer()).thenReturn(issuer);
+
+ return appConfiguration;
+ }
+
+ @ApplicationScoped
+ @Produces
+ @ExcludeBean
+ RegistrationPersistenceService produceRegistrationPersistenceService() {
+ Mockito.when(registrationPersistenceService.buildFido2RegistrationEntry(any(), anyBoolean())).thenCallRealMethod();
+ Mockito.when(registrationPersistenceService.findByChallenge(anyString(), anyBoolean())).thenReturn(Arrays.asList(registrationEntry));
+
+ Mockito.when(userService.getUser(anyString(), any())).thenReturn(new User());
+
+ return registrationPersistenceService;
+ }
+
+ @ApplicationScoped
+ @Produces
+ @ExcludeBean
+ ChallengeGenerator produceChallengeGenerator() {
+ return Mockito.when(Mockito.mock(ChallengeGenerator.class).getChallenge())
+ .thenReturn(challenge).getMock();
+ }
+
+ @ApplicationScoped
+ @Produces
+ @ExcludeBean
+ UserSessionIdService produceUserSessionIdService() {
+ return Mockito.when(Mockito.mock(UserSessionIdService.class).isValidSessionId(anyString(), anyString()))
+ .thenReturn(true).getMock();
+ }
+
+ @Test
+ @Order(1)
+ @ExtendWith(CustomExtension.class)
+ public void testStartAttestationSignature(@Name("attestation.android.two-step.issuer") String issuer, @Name("attestation.android.two-step.challenge") String challenge,
+ @Name("attestation.android.two-step.userName") String userName, @Name("attestation.android.two-step.applicationId") String applicationId,
+ @Name("attestation.android.two-step.sessionId") String sessionId, @Name("attestation.android.two-step.enrollmentCode") String enrollmentCode) {
+
+ this.issuer = issuer;
+ this.challenge = challenge;
+
+ JsonNode request = attestationSuperGluuController.buildFido2AttestationStartResponse(userName, applicationId, sessionId);
+ assertEquals(request.get(CommonVerifiers.SUPER_GLUU_REQUEST).asBoolean(), true);
+ assertEquals(request.get(CommonVerifiers.SUPER_GLUU_MODE).asText(), SuperGluuMode.TWO_STEP.getMode());
+ assertEquals(request.get(CommonVerifiers.SUPER_GLUU_APP_ID).asText(), applicationId);
+
+ ObjectNode response = attestationService.options(request);
+
+ // Get saved entry for finish attestation test
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Fido2RegistrationEntry.class);
+ Mockito.verify(registrationPersistenceService).save(captor.capture());
+ registrationEntry = captor.getValue();
+
+ assertNotNull(registrationEntry);
+ assertNotNull(response);
+ assertEquals(response.get("challenge").asText(), challenge);
+ }
+
+ @Test
+ @Order(2)
+ @ExtendWith(CustomExtension.class)
+ public void testFinishAttestationSignature(@Name("attestation.android.two-step.userName") String userName,
+ @Name("attestation.android.two-step.finish.request") String registerFinishResponse) {
+ // Parse register response
+ RegisterResponse registerResponse = attestationSuperGluuController.parseRegisterResponse(registerFinishResponse);
+
+ JsonNode request = attestationSuperGluuController.buildFido2AttestationVerifyResponse(userName, registerResponse);
+ assertEquals(request.get(CommonVerifiers.SUPER_GLUU_REQUEST).asBoolean(), true);
+ assertEquals(request.get(CommonVerifiers.SUPER_GLUU_MODE).asText(), SuperGluuMode.TWO_STEP.getMode());
+
+ ObjectNode response = attestationService.verify(request);
+
+ assertNotNull(response);
+ assertEquals(response.get("status").asText(), "ok");
+ assertEquals(response.get("createdCredentials").get("id").asText(), "lGWf7urVmKzN_4vklat2W8jqJoWCTIYfrjkLFDkef2Zmdl7k13FXCFHdMMw0G_YyluFAHwx5oDf-7bcbAlG0Wg");
+ }
+
+}
diff --git a/jans-fido2/server/src/test/java/io/jans/junit/extension/CustomExtension.java b/jans-fido2/server/src/test/java/io/jans/junit/extension/CustomExtension.java
new file mode 100644
index 00000000000..02181057e0b
--- /dev/null
+++ b/jans-fido2/server/src/test/java/io/jans/junit/extension/CustomExtension.java
@@ -0,0 +1,89 @@
+/*
+ * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text.
+ *
+ * Copyright (c) 2023, Janssen Project
+ */
+
+package io.jans.junit.extension;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Properties;
+
+import org.apache.commons.io.IOUtils;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ParameterContext;
+import org.junit.jupiter.api.extension.ParameterResolutionException;
+import org.junit.jupiter.api.extension.ParameterResolver;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.jans.util.StringHelper;
+
+/**
+ * @author Yuriy Movchan
+ * @version 0.1, 17/02/2023
+ */
+public class CustomExtension implements ParameterResolver {
+
+ Logger logger = LoggerFactory.getLogger(CustomExtension.class);
+
+ private Map parameters;
+
+ public CustomExtension() throws IOException {
+ logger.info("Loading test properties...");
+
+ String propertiesFile = "target/test-classes/test.properties";
+
+ // Load test parameters
+ FileInputStream conf = new FileInputStream(propertiesFile);
+ Properties prop;
+ try {
+ prop = new Properties();
+ prop.load(conf);
+ } finally {
+ IOUtils.closeQuietly(conf);
+ }
+
+ parameters = new HashMap();
+ for (Entry