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

SLS-38 - Enable DER signature validation #43

Merged
merged 7 commits into from
Apr 27, 2023
Merged
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
5 changes: 4 additions & 1 deletion core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,22 @@ configurations {
dependencies {
implementation 'javax.inject:javax.inject:1'
implementation 'com.typesafe:config:1.4.2'
implementation 'javax.inject:javax.inject:1'
implementation 'com.nimbusds:nimbus-jose-jwt:9.31'
implementation 'ch.qos.logback:logback-classic:1.4.6'
implementation 'ch.qos.logback:logback-core:1.4.6'
implementation 'org.codehaus.janino:janino:3.1.9'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.14.2'
implementation 'org.apache.wss4j:wss4j-ws-security-common:2.4.1'

implementation 'javax.servlet:javax.servlet-api:3.0.1'

// Use JUnit Jupiter for testing.
testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2'
testImplementation 'org.mockito:mockito-core:5.2.0'
testImplementation 'org.mockito:mockito-junit-jupiter:5.2.0'
testImplementation 'org.assertj:assertj-core:3.24.2'
testImplementation 'org.springframework:spring-test:5.3.26'

}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@
@Builder
public class LollipopConsumerRequest {
private String requestBody;
private Map<String, String> requestParams;
private Map<String, String[]> requestParams;
private Map<String, String> headerParams;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/* (C)2023 */
package it.pagopa.tech.lollipop.consumer.utils;

import static it.pagopa.tech.lollipop.consumer.command.impl.LollipopConsumerCommandImpl.VERIFICATION_SUCCESS_CODE;

import it.pagopa.tech.lollipop.consumer.model.CommandResult;
import it.pagopa.tech.lollipop.consumer.model.LollipopConsumerRequest;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class LollipopConsumerConverter {

private LollipopConsumerConverter() {
throw new IllegalStateException("Utility class");
}

/**
* Utility method to be used to generate a LollipopConsumerRequest from a HttpServletRequest
*
* @param httpServletRequest http request to be converted into a lollipop request
* @return instance of {@link LollipopConsumerRequest} produced from the httpServletRequest
* @throws IOException exception return if body extraction fails
*/
public static LollipopConsumerRequest convertToLollipopRequest(
HttpServletRequest httpServletRequest) throws IOException {

byte[] requestBody = null;

String method = httpServletRequest.getMethod();

if (method != null && (!method.equals("GET") && !method.equals("DELETE"))) {
InputStream requestInputStream = httpServletRequest.getInputStream();
requestBody = requestInputStream.readAllBytes();
}

return LollipopConsumerRequest.builder()
.requestBody(requestBody != null ? new String(requestBody) : null)
.headerParams(
Collections.list(httpServletRequest.getHeaderNames()).stream()
.collect(Collectors.toMap(h -> h, httpServletRequest::getHeader)))
.requestParams(httpServletRequest.getParameterMap())
.build();
}

/**
* Utility method used to convert the commandResult in a HttpServletResponse
*
* @param commandResult results of the LollipopConsumerCommand's doExecute
* @return instance of HttpServletResponse with the commandResult status code and response
* @throws IOException when failed to send error
*/
public static HttpServletResponse interceptResult(
CommandResult commandResult, HttpServletResponse httpResponse) throws IOException {

if (!commandResult.getResultCode().equals(VERIFICATION_SUCCESS_CODE)) {
httpResponse.sendError(401, commandResult.getResultMessage());
}

return httpResponse;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/* (C)2023 */
package it.pagopa.tech.lollipop.consumer.utils;

import static it.pagopa.tech.lollipop.consumer.command.impl.LollipopConsumerCommandImpl.VERIFICATION_SUCCESS_CODE;

import it.pagopa.tech.lollipop.consumer.model.CommandResult;
import it.pagopa.tech.lollipop.consumer.model.LollipopConsumerRequest;
import java.io.IOException;
import java.util.Enumeration;
import javax.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;

class LollipopConsumerConverterTest {

String REQUEST_BODY_STRING = "{\"message\":\"a valid message payload\"}";
String COMMAND_RESPONSE_SUCCESS = "SAML assertion validated successfully";
String COMMAND_RESPONSE_FAILED = "Validation of SAML assertion failed, authentication failed";
byte[] REQUEST_BODY = REQUEST_BODY_STRING.getBytes();

static Enumeration<String> REQUEST_HEADERS;

static MockHttpServletRequest mockRequest;

@BeforeAll
static void setUp() {
mockRequest = new MockHttpServletRequest();
mockRequest.addHeader("content-digest", "sha-256=:test:");
mockRequest.setParameter("testParam", "value1", "value2");
}

@Test
void convertGetHttpRequest() throws IOException {
mockRequest.setMethod("GET");

LollipopConsumerRequest request =
LollipopConsumerConverter.convertToLollipopRequest(mockRequest);
Assertions.assertNotNull(request.getHeaderParams());
Assertions.assertNotNull(request.getRequestParams());
}

@Test
void convertPostHttpRequest() throws IOException {
mockRequest.setMethod("POST");
mockRequest.setContent(REQUEST_BODY);

LollipopConsumerRequest request =
LollipopConsumerConverter.convertToLollipopRequest(mockRequest);
Assertions.assertNotNull(request.getRequestBody());
Assertions.assertNotNull(request.getHeaderParams());
Assertions.assertNotNull(request.getRequestParams());
}

@Test
void convertSuccessResponse() throws IOException {
CommandResult result =
new CommandResult(VERIFICATION_SUCCESS_CODE, COMMAND_RESPONSE_SUCCESS);
int MOCK_RESPONSE_STATUS = 200;
MockHttpServletResponse mockResponse = new MockHttpServletResponse();
mockResponse.setStatus(MOCK_RESPONSE_STATUS);

HttpServletResponse response =
LollipopConsumerConverter.interceptResult(result, mockResponse);

Assertions.assertEquals(MOCK_RESPONSE_STATUS, response.getStatus());
}

@Test
void convertUnauthorizedResponse() throws IOException {
CommandResult result = new CommandResult("FAILED", COMMAND_RESPONSE_FAILED);
MockHttpServletResponse mockResponse = new MockHttpServletResponse();

HttpServletResponse response =
LollipopConsumerConverter.interceptResult(result, mockResponse);

Assertions.assertEquals(401, response.getStatus());
Assertions.assertSame(
COMMAND_RESPONSE_FAILED, ((MockHttpServletResponse) response).getErrorMessage());
}
}
21 changes: 21 additions & 0 deletions gradle/verification-metadata.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2846,6 +2846,14 @@
<sha256 value="943e12b100627804638fa285805a0ab788a680266531e650921ebfe4621a8bfa" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="javax.servlet" name="javax.servlet-api" version="3.0.1">
<artifact name="javax.servlet-api-3.0.1.jar">
<sha256 value="377d8bde87ac6bc7f83f27df8e02456d5870bb78c832dac656ceacc28b016e56" origin="Generated by Gradle"/>
</artifact>
<artifact name="javax.servlet-api-3.0.1.pom">
<sha256 value="a6b7d67a7035beab0f6c75467e9c2a2185d73056f42eab7200280da6a8f51b29" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="javax.servlet" name="javax.servlet-api" version="4.0.1">
<artifact name="javax.servlet-api-4.0.1.jar">
<md5 value="b80414033bf3397de334b95e892a2f44" origin="Generated by Gradle"/>
Expand Down Expand Up @@ -5625,6 +5633,14 @@
<sha256 value="85b1af0535665ce8f23a2b9f1f581cc1e7877bb1289304140827731216c78ffd" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.slf4j" name="slf4j-api" version="2.0.5">
<artifact name="slf4j-api-2.0.5.jar">
<sha256 value="f4a2974509291acc49fda4a79b0d59e15e2b524095d6421c66391b92387af4c9" origin="Generated by Gradle"/>
</artifact>
<artifact name="slf4j-api-2.0.5.pom">
<sha256 value="9ef2da63149f5d9eccf7ad3c953cdfc124eb6d147bf78e38600f59f605c5d46e" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.slf4j" name="slf4j-api" version="2.0.6">
<artifact name="slf4j-api-2.0.6.jar">
<md5 value="0dd65c386e8c5f4e6e014de3f7a7ae60" origin="Generated by Gradle"/>
Expand Down Expand Up @@ -5684,6 +5700,11 @@
<sha256 value="dc9dc87f53a8af721ee49dbb521bd06cda06134214fdad9400d872a75426cffd" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.slf4j" name="slf4j-parent" version="2.0.5">
<artifact name="slf4j-parent-2.0.5.pom">
<sha256 value="170b11b04815005c3b4cc6df79c91843e0b950f85b58ffbe8d483ed21913b98b" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.slf4j" name="slf4j-parent" version="2.0.6">
<artifact name="slf4j-parent-2.0.6.pom">
<md5 value="3f4c6974f8934696b5566425a85f22b4" origin="Generated by Gradle"/>
Expand Down
1 change: 1 addition & 0 deletions http-verifier/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2'
testImplementation 'org.assertj:assertj-core:3.24.2'
implementation 'com.nimbusds:nimbus-jose-jwt:9.31'
implementation 'org.slf4j:slf4j-api:2.0.5'
implementation project(':core')
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
package it.pagopa.tech.lollipop.consumer.http_verifier.visma;

import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.crypto.impl.ECDSA;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.KeyType;
import it.pagopa.tech.lollipop.consumer.config.LollipopConsumerRequestConfig;
Expand All @@ -16,14 +18,17 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.visma.autopay.http.digest.DigestException;
import net.visma.autopay.http.signature.*;
import net.visma.autopay.http.structured.StructuredBytes;

/**
* Implementation of the @HttpMessageVerifier using Visma-AutoPay http-signature of the
* http-signature draft
*/
@AllArgsConstructor
@Slf4j
public class VismaHttpMessageVerifier implements HttpMessageVerifier {

String defaultEncoding;
Expand Down Expand Up @@ -109,25 +114,30 @@ public boolean verifyHttpSignature(
}
}

var signatureContext =
SignatureContext.builder()
.headers(parameters)
.header("Signature-Input", signatureInputToProcess)
.header("Signature", signatureToProcess)
.build();

/* Attempt to recover a valid key from the provided jwt */
PublicKey publicKey = null;
try {
JWK jwk = JWK.parse(new String(Base64.getDecoder().decode(lollipopKey)));
KeyType keyType = jwk.getKeyType();
publicKey = getPublicKey(jwk, keyType);
if (KeyType.EC.equals(keyType)) {
publicKey = jwk.toECKey().toECPublicKey();
signatureToProcess = transcodeSignature(signatureToProcess);
} else if (KeyType.RSA.equals(keyType)) {
publicKey = jwk.toRSAKey().toRSAPublicKey();
}
} catch (ParseException | JOSEException e) {
throw new LollipopSignatureException(
LollipopSignatureException.ErrorCode.INVALID_SIGNATURE_ALG,
"Missing Signature Algorithm");
}

var signatureContext =
SignatureContext.builder()
.headers(parameters)
.header("Signature-Input", signatureInputToProcess)
.header("Signature", signatureToProcess)
.build();

/* Populate Visma Sign Validator*/
SignatureAlgorithm finalSignatureAlgorithm = signatureAlgorithm;
PublicKey finalPublicKey = publicKey;
Expand Down Expand Up @@ -156,6 +166,25 @@ public boolean verifyHttpSignature(
return true;
}

private String transcodeSignature(String signatureToProcess) {
try {
String[] signatureParts = signatureToProcess.split("=", 2);
String signatureValue =
StructuredBytes.of(
ECDSA.transcodeSignatureToConcat(
Base64.getMimeDecoder()
.decode(signatureParts[1].getBytes()),
ECDSA.getSignatureByteArrayLength(JWSAlgorithm.ES256)))
.toString();
ECDSA.ensureLegalSignature(
Base64.getMimeDecoder().decode(signatureValue.getBytes()), JWSAlgorithm.ES256);
signatureToProcess = signatureParts[0].concat("=").concat(signatureValue);
} catch (Exception e) {
log.debug("Could not convert EC signature to valid format");
}
return signatureToProcess;
}

private static void isSignatureAlgorithmNotNull(SignatureAlgorithm signatureAlgorithm)
throws LollipopSignatureException {
if (signatureAlgorithm == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -328,4 +328,45 @@ void invalidLollipopMultipleSignatureWithLessInput() {
.INVALID_SIGNATURE_NUMBER);
});
}

@Test
void validLollipopSignatureCheckSingleEcdaSha256WithDer() {

String signatureInput =
"sig1=(\"x-pagopa-lollipop-original-method\""
+ " \"x-pagopa-lollipop-original-url\");created=1681473980;nonce=\"aNonce\";alg=\"ecdsa-p256-sha256\";keyid=\"sha256-HiNolL87UYKQfaKISwIzyWY4swKPUzpaOWJCxaHy89M\"";
var signature =
"sig1=:MEUCIFiZHxuLhk2Jlt46E5kbB8hCx7fN7QeeAj2gaSK3Y+WzAiEAtggj3Jwu8RbTGdNmsDix2zymh0gKwKxoPlolL7j6VTg=:";

Map<String, String> requestHeaders =
new HashMap<>(
Map.of(
"x-pagopa-lollipop-assertion-ref",
"sha256-HiNolL87UYKQfaKISwIzyWY4swKPUzpaOWJCxaHy89M",
"x-pagopa-lollipop-assertion-type",
"SAML",
"x-pagopa-lollipop-user-id",
"aFiscalCode",
"x-pagopa-lollipop-public-key",
"eyJrdHkiOiJFQyIsInkiOiJNdkVCMENsUHFnTlhrNVhIYm9xN1hZUnE2TnJTQkFTVmZhT2wzWnAxQmJzPSIsImNydiI6IlAtMjU2IiwieCI6InF6YTQzdGtLTnIrYWlTZFdNL0Q1cTdxMElmV3lZVUFIVEhSNng3dFByZEU9In0",
"x-pagopa-lollipop-auth-jwt",
"aValidJWT",
"x-pagopa-lollipop-original-method",
"POST",
"x-pagopa-lollipop-original-url",
"https://api-app.io.pagopa.it/first-lollipop/sign",
"content-digest",
"sha-256=:cpyRqJ1VhoVC+MSs9fq4/4wXs4c46EyEFriskys43Zw=:",
"Signature-Input",
signatureInput,
"Signature",
signature));

// execute & verify
assertThatNoException()
.isThrownBy(
() ->
vismaDigestVerifier.verifyHttpSignature(
signature, signatureInput, requestHeaders));
}
}
Loading