Skip to content

Commit

Permalink
feat: support hashed content CIP-30 data content which is typically g…
Browse files Browse the repository at this point in the history
…enerated by hardware wallets.
  • Loading branch information
matiwinnetou committed Sep 29, 2024
1 parent 4aa4c6c commit a71479f
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 8 deletions.
3 changes: 2 additions & 1 deletion backend-services/voting-app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ configurations {
}

repositories {
mavenLocal()
mavenCentral()
maven { url = uri("https://repo.spring.io/milestone") }
}
Expand Down Expand Up @@ -78,7 +79,7 @@ dependencies {
runtimeOnly("org.postgresql:postgresql")

implementation("org.cardanofoundation:merkle-tree-java:0.0.7")
implementation("org.cardanofoundation:cip30-data-signature-parser:0.0.11")
implementation("org.cardanofoundation:cip30-data-signature-parser:0.0.12-SNAPSHOT")

implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0")

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.cardano.foundation.voting.service.auth.web3;

import com.bloxbean.cardano.client.crypto.Blake2bUtil;
import com.bloxbean.cardano.client.util.HexUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
Expand Down Expand Up @@ -27,13 +29,13 @@
import org.zalando.problem.Problem;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Optional;

import static org.cardano.foundation.voting.domain.Role.VOTER;
import static org.cardano.foundation.voting.domain.web3.WalletType.CARDANO;
import static org.cardano.foundation.voting.resource.Headers.X_Ballot_PublicKey;
import static org.cardano.foundation.voting.resource.Headers.X_Ballot_Signature;
import static org.cardano.foundation.voting.resource.Headers.*;
import static org.cardano.foundation.voting.service.auth.LoginSystem.CARDANO_CIP93;
import static org.cardano.foundation.voting.service.auth.web3.MoreFilters.sendBackProblem;
import static org.cardano.foundation.voting.utils.MoreNumber.isNumeric;
Expand Down Expand Up @@ -77,6 +79,7 @@ protected void doFilterInternal(HttpServletRequest req,

val signatureM = Optional.ofNullable(req.getHeader(X_Ballot_Signature));
val publicKey = req.getHeader(X_Ballot_PublicKey);
val payloadM = Optional.ofNullable(req.getHeader(X_Ballot_Payload));

if (signatureM.isEmpty()) {
val problem = Problem.builder()
Expand Down Expand Up @@ -122,7 +125,37 @@ protected void doFilterInternal(HttpServletRequest req,

val walletId = maybeAddress.orElseThrow();

val cipBody = cipVerificationResult.getMessage(MessageFormat.TEXT);
var cipBody = cipVerificationResult.getMessage(MessageFormat.TEXT);
if (cipVerificationResult.isHashed() && payloadM.isEmpty()) {
val problem = Problem.builder()
.withTitle("HASHED_CONTENT_NO_PAYLOAD")
.withDetail("Payload was not sent along with the request and CIP-30 signature contains is hashed!")
.withStatus(BAD_REQUEST)
.build();

sendBackProblem(objectMapper, res, problem);
return;
}

if (cipVerificationResult.isHashed()) {
val cipBodyHash = cipVerificationResult.getMessage(MessageFormat.HEX);
val payload = payloadM.orElseThrow();

val payloadHash = HexUtil.encodeHexString(Blake2bUtil.blake2bHash224(HexUtil.decodeHexString(payload)));

if (!cipBodyHash.equals(payloadHash)) {
val problem = Problem.builder()
.withTitle("CIP_30_HASH_MISMATCH")
.withDetail("Signed hash does not match our precalculated hash!")
.withStatus(BAD_REQUEST)
.build();

sendBackProblem(objectMapper, res, problem);
return;
}

cipBody = new String(HexUtil.decodeHexString(payload)); // flip cipBody to be payload for further processing
}

val cip93EnvelopeE = jsonService.decodeGenericCIP93(cipBody);
if (cip93EnvelopeE.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@

import static org.assertj.core.api.Assertions.assertThat;
import static org.cardano.foundation.voting.domain.ChainNetwork.*;
import static org.cardano.foundation.voting.resource.Headers.X_Ballot_PublicKey;
import static org.cardano.foundation.voting.resource.Headers.X_Ballot_Signature;
import static org.cardano.foundation.voting.resource.Headers.*;
import static org.cardano.foundation.voting.service.auth.LoginSystem.CARDANO_CIP93;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
Expand All @@ -48,8 +47,7 @@ class CardanoWeb3FilterTest {
private ExpirationService expirationService;
private ChainFollowerClient chainFollowerClient;
private LoginSystemDetector loginSystemDetector;

private final ChainNetwork chainNetworkStartedOn = PREPROD;
private ChainNetwork chainNetworkStartedOn = PREPROD;

@BeforeEach
void setUp() throws IOException {
Expand Down Expand Up @@ -579,4 +577,106 @@ void doFilterInternal_shouldAuthenticate_whenAllConditionsMet() throws ServletEx
assertThat(cardanoDetails.getEnvelope()).isNotNull();
}

@Test
void doFilterInternal_shouldAuthenticate_whenAllConditionsMetWithHashedContent() throws ServletException, IOException {
chainNetworkStartedOn = MAIN;
filter = new CardanoWeb3Filter(jsonService, expirationService, objectMapper, chainFollowerClient, chainNetworkStartedOn, loginSystemDetector);

//{"action":"LOGIN","actionText":"Login","data":{"event":"CARDANO_SUMMIT_AWARDS_2024","network":"MAIN","role":"VOTER","walletId":"stake1uypayp2nyzy66tmcz6yjuth59pym0df83rjpk0758fhqrncq8vcdz","walletType":"CARDANO"},"slot":"136068942"}

val genericEnvelope = CIP93Envelope.<Map<String, Object>>builder()
.action("LOGIN")
.slot("136068942")
.data(Map.of(
"event", "CARDANO_SUMMIT_AWARDS_2024",
"walletId", "stake1uypayp2nyzy66tmcz6yjuth59pym0df83rjpk0758fhqrncq8vcdz",
"walletType", WalletType.CARDANO.name(),
"network", "MAIN",
"role", "VOTER"
)
)
.build();

when(loginSystemDetector.detect(request)).thenReturn(Optional.of(CARDANO_CIP93));
when(request.getHeader(X_Ballot_Signature)).thenReturn("84582aa201276761646472657373581de103d205532089ad2f7816892e2ef42849b7b52788e41b3fd43a6e01cfa166686173686564f5581c1c1afc33a1ed48205eadcbbda2fc8e61442af2e04673616f21b7d0385840954858f672e9ca51975655452d79a8f106011e9535a2ebfb909f7bbcce5d10d246ae62df2da3a7790edd8f93723cbdfdffc5341d08135b1a40e7a998e8b2ed06");
when(request.getHeader(X_Ballot_Payload)).thenReturn("7b22616374696f6e223a224c4f47494e222c22616374696f6e54657874223a224c6f67696e222c2264617461223a7b226576656e74223a2243415244414e4f5f53554d4d49545f4157415244535f32303234222c226e6574776f726b223a224d41494e222c22726f6c65223a22564f544552222c2277616c6c65744964223a227374616b6531757970617970326e797a793636746d637a36796a757468353970796d3064663833726a706b30373538666871726e6371387663647a222c2277616c6c657454797065223a2243415244414e4f227d2c22736c6f74223a22313336303638393432227d");
when(request.getHeader(X_Ballot_PublicKey)).thenReturn("a4010103272006215820c13745be35c2dfc3fa9523140030dda5b5346634e405662b1aae5c61389c55b3");

val cip30VerificationResult = mock(Cip30VerificationResult.class);
when(cip30VerificationResult.isValid()).thenReturn(true);
when(cip30VerificationResult.getAddress(any())).thenReturn(Optional.of("stake1uypayp2nyzy66tmcz6yjuth59pym0df83rjpk0758fhqrncq8vcdz"));

when(chainFollowerClient.getChainTip()).thenReturn(Either.right(
new ChainFollowerClient.ChainTipResponse("hash", 512, 136068942, true, MAIN))
);

when(chainFollowerClient.getEventDetails(any())).thenReturn(Either.right(Optional.of(mock(ChainFollowerClient.EventDetailsResponse.class))));

when(jsonService.decodeGenericCIP93(any())).thenReturn(Either.right(genericEnvelope));

filter.doFilterInternal(request, response, chain);

verify(chain, times(1)).doFilter(request, response);

assertThat(SecurityContextHolder.getContext().getAuthentication()).isNotNull();
assertThat(SecurityContextHolder.getContext().getAuthentication().getPrincipal()).isEqualTo("stake1uypayp2nyzy66tmcz6yjuth59pym0df83rjpk0758fhqrncq8vcdz");

val cardanoDetails = (CardanoWeb3Details) SecurityContextHolder.getContext().getAuthentication().getDetails();

assertThat(cardanoDetails.getSignedCIP30().getSignature()).isEqualTo("84582aa201276761646472657373581de103d205532089ad2f7816892e2ef42849b7b52788e41b3fd43a6e01cfa166686173686564f5581c1c1afc33a1ed48205eadcbbda2fc8e61442af2e04673616f21b7d0385840954858f672e9ca51975655452d79a8f106011e9535a2ebfb909f7bbcce5d10d246ae62df2da3a7790edd8f93723cbdfdffc5341d08135b1a40e7a998e8b2ed06");
assertThat(cardanoDetails.getSignedCIP30().getPublicKey()).isEqualTo(Optional.of("a4010103272006215820c13745be35c2dfc3fa9523140030dda5b5346634e405662b1aae5c61389c55b3"));
assertThat(cardanoDetails.getWeb3CommonDetails().getAction()).isEqualTo(Web3Action.LOGIN);
assertThat(cardanoDetails.getWeb3CommonDetails().getNetwork()).isEqualTo(MAIN);
assertThat(cardanoDetails.getEnvelope()).isNotNull();
}

// CIP30 is a data sign with hash only but hashes do not properly match
@Test
void doFilter_SignedHashFailure() throws ServletException, IOException {
chainNetworkStartedOn = MAIN;
filter = new CardanoWeb3Filter(jsonService, expirationService, objectMapper, chainFollowerClient, chainNetworkStartedOn, loginSystemDetector);

//{"action":"LOGIN","actionText":"Login","data":{"event":"CARDANO_SUMMIT_AWARDS_2024","network":"MAIN","role":"VOTER","walletId":"stake1uypayp2nyzy66tmcz6yjuth59pym0df83rjpk0758fhqrncq8vcdz","walletType":"CARDANO"},"slot":"136068943"}

val genericEnvelope = CIP93Envelope.<Map<String, Object>>builder()
.action("LOGIN")
.slot("136068943")
.data(Map.of(
"event", "CARDANO_SUMMIT_AWARDS_2024",
"walletId", "stake1uypayp2nyzy66tmcz6yjuth59pym0df83rjpk0758fhqrncq8vcdz",
"walletType", WalletType.CARDANO.name(),
"network", "MAIN",
"role", "VOTER"
)
)
.build();

when(loginSystemDetector.detect(request)).thenReturn(Optional.of(CARDANO_CIP93));
when(request.getHeader(X_Ballot_Signature)).thenReturn("84582aa201276761646472657373581de103d205532089ad2f7816892e2ef42849b7b52788e41b3fd43a6e01cfa166686173686564f5581c1c1afc33a1ed48205eadcbbda2fc8e61442af2e04673616f21b7d0385840954858f672e9ca51975655452d79a8f106011e9535a2ebfb909f7bbcce5d10d246ae62df2da3a7790edd8f93723cbdfdffc5341d08135b1a40e7a998e8b2ed06");
when(request.getHeader(X_Ballot_Payload)).thenReturn("7B22616374696F6E223A224C4F47494E222C22616374696F6E54657874223A224C6F67696E222C2264617461223A7B226576656E74223A2243415244414E4F5F53554D4D49545F4157415244535F32303234222C226E6574776F726B223A224D41494E222C22726F6C65223A22564F544552222C2277616C6C65744964223A227374616B6531757970617970326E797A793636746D637A36796A757468353970796D3064663833726A706B30373538666871726E6371387663647A222C2277616C6C657454797065223A2243415244414E4F227D2C22736C6F74223A22313336303638393433227D");
when(request.getHeader(X_Ballot_PublicKey)).thenReturn("a4010103272006215820c13745be35c2dfc3fa9523140030dda5b5346634e405662b1aae5c61389c55b3");

val cip30VerificationResult = mock(Cip30VerificationResult.class);
when(cip30VerificationResult.isValid()).thenReturn(true);
when(cip30VerificationResult.getAddress(any())).thenReturn(Optional.of("stake1uypayp2nyzy66tmcz6yjuth59pym0df83rjpk0758fhqrncq8vcdz"));

when(chainFollowerClient.getChainTip()).thenReturn(Either.right(
new ChainFollowerClient.ChainTipResponse("hash", 512, 136068943, true, MAIN))
);

when(chainFollowerClient.getEventDetails(any())).thenReturn(Either.right(Optional.of(mock(ChainFollowerClient.EventDetailsResponse.class))));

when(jsonService.decodeGenericCIP93(any())).thenReturn(Either.right(genericEnvelope));

filter.doFilterInternal(request, response, chain);

val problemCaptor = ArgumentCaptor.forClass(Problem.class);

verify(objectMapper, times(1)).writeValueAsString(problemCaptor.capture());
val capturedProblem = problemCaptor.getValue();

assertThat(capturedProblem.getTitle()).isEqualTo("CIP_30_HASH_MISMATCH");
assertThat(capturedProblem.getStatus()).isEqualTo(BAD_REQUEST);
}

}

0 comments on commit a71479f

Please sign in to comment.