Skip to content

Commit 34a7553

Browse files
author
Jahziah Wagner
authored
Merge pull request #9 from jahwag/feat/signed-jwt
feat: response of /wallet-unit-attestion should be a signed jwt
2 parents d494156 + 13533b5 commit 34a7553

File tree

12 files changed

+329
-64
lines changed

12 files changed

+329
-64
lines changed

src/main/java/se/digg/wallet/provider/Application.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66

77
import org.springframework.boot.SpringApplication;
88
import org.springframework.boot.autoconfigure.SpringBootApplication;
9+
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
910

1011
@SpringBootApplication
12+
@ConfigurationPropertiesScan
1113
public class Application {
1214
public static void main(String[] args) {
1315
SpringApplication.run(Application.class, args);
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// SPDX-FileCopyrightText: 2025 Digg - Agency for Digital Government
2+
//
3+
// SPDX-License-Identifier: EUPL-1.2
4+
5+
package se.digg.wallet.provider.application.config;
6+
7+
import java.security.KeyStore;
8+
import java.security.PrivateKey;
9+
import java.security.cert.Certificate;
10+
import java.security.interfaces.ECPrivateKey;
11+
import java.security.interfaces.ECPublicKey;
12+
import org.springframework.boot.context.properties.ConfigurationProperties;
13+
import org.springframework.core.io.Resource;
14+
15+
@ConfigurationProperties(prefix = "wua.keystore")
16+
public record WuaKeystoreProperties(Resource location, String password, String alias, String type) {
17+
18+
public ECPrivateKey getSigningKey() {
19+
try {
20+
KeyStore keyStore = KeyStore.getInstance(type());
21+
keyStore.load(location().getInputStream(), password().toCharArray());
22+
PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias(), password().toCharArray());
23+
24+
return (ECPrivateKey) privateKey;
25+
} catch (Exception e) {
26+
throw new RuntimeException("Failed to load signing key from filesystem", e);
27+
}
28+
}
29+
30+
public ECPublicKey getPublicKey() {
31+
try {
32+
KeyStore keyStore = KeyStore.getInstance(type());
33+
keyStore.load(location().getInputStream(), password().toCharArray());
34+
Certificate cert = keyStore.getCertificate(alias());
35+
return (ECPublicKey) cert.getPublicKey();
36+
} catch (Exception e) {
37+
throw new RuntimeException("Failed to load public key from filesystem", e);
38+
}
39+
}
40+
}

src/main/java/se/digg/wallet/provider/application/controller/WalletUnitAttestationController.java

Lines changed: 10 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,55 +4,30 @@
44

55
package se.digg.wallet.provider.application.controller;
66

7-
import com.nimbusds.jose.jwk.ECKey;
7+
import com.nimbusds.jwt.SignedJWT;
88
import org.springframework.http.ResponseEntity;
99
import org.springframework.web.bind.annotation.PostMapping;
1010
import org.springframework.web.bind.annotation.RequestBody;
1111
import org.springframework.web.bind.annotation.RequestMapping;
1212
import org.springframework.web.bind.annotation.RestController;
1313
import se.digg.wallet.provider.application.model.WalletUnitAttestationDto;
14+
import se.digg.wallet.provider.application.service.WalletUnitAttestationService;
1415

1516
@RequestMapping("/wallet-unit-attestation")
1617
@RestController
1718
public class WalletUnitAttestationController {
1819

19-
private final String createWuaResponse =
20-
"""
21-
{
22-
"iss": "Digg",
23-
"iat": 1516247022,
24-
"exp": 1541493724,
25-
"eudi_wallet_info": {
26-
"general_info": {
27-
"wallet_provider_name": "Digg",
28-
"wallet_solution_id": "Diggidigg-id",
29-
"wallet_solution_version": "0.0.1",
30-
"wallet_solution_certification_information": "UNCERTIFIED"
31-
},
32-
"wscd_info": {
33-
"wscd_type": "REMOTE",
34-
"wscd_certification_information": "UNCERTIFIED",
35-
"wscd_attack_resistance": 2
36-
}
37-
},
38-
"status": {
39-
"status_list": {
40-
"idx": 412,
41-
"uri": "https://revocation_url/statuslists/1"
42-
}
43-
},
44-
"attested_keys": [
45-
%s
46-
]
47-
}
48-
""";
20+
private final WalletUnitAttestationService attestationService;
21+
22+
public WalletUnitAttestationController(WalletUnitAttestationService attestationService) {
23+
this.attestationService = attestationService;
24+
}
4925

5026
@PostMapping
5127
public ResponseEntity<String> postWalletUnitAttestation(
5228
@RequestBody WalletUnitAttestationDto walletUnitAttestationDto) throws Exception {
53-
54-
ECKey key = ECKey.parse(walletUnitAttestationDto.jwk());
55-
56-
return ResponseEntity.ok(String.format(createWuaResponse, key.toString()));
29+
SignedJWT signedJwt =
30+
attestationService.createWalletUnitAttestation(walletUnitAttestationDto.jwk());
31+
return ResponseEntity.ok(signedJwt.serialize());
5732
}
5833
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// SPDX-FileCopyrightText: 2025 diggsweden/wallet-backend-reference
2+
//
3+
// SPDX-License-Identifier: EUPL-1.2
4+
5+
package se.digg.wallet.provider.application.service;
6+
7+
import com.nimbusds.jose.JOSEException;
8+
import com.nimbusds.jose.JOSEObjectType;
9+
import com.nimbusds.jose.JWSAlgorithm;
10+
import com.nimbusds.jose.JWSHeader;
11+
import com.nimbusds.jose.JWSSigner;
12+
import com.nimbusds.jose.crypto.ECDSASigner;
13+
import com.nimbusds.jose.jwk.ECKey;
14+
import com.nimbusds.jwt.JWTClaimsSet;
15+
import com.nimbusds.jwt.SignedJWT;
16+
import java.security.interfaces.ECPrivateKey;
17+
import java.time.Duration;
18+
import java.time.Instant;
19+
import java.util.Date;
20+
import java.util.HashMap;
21+
import java.util.List;
22+
import java.util.Map;
23+
import org.springframework.stereotype.Service;
24+
import se.digg.wallet.provider.application.config.WuaKeystoreProperties;
25+
26+
@Service
27+
public class WalletUnitAttestationService {
28+
29+
private final WuaKeystoreProperties keystoreProperties;
30+
31+
public WalletUnitAttestationService(WuaKeystoreProperties keystoreProperties) {
32+
this.keystoreProperties = keystoreProperties;
33+
}
34+
35+
public SignedJWT createWalletUnitAttestation(String walletPublicKeyJwk) throws Exception {
36+
ECKey attestedKey = ECKey.parse(walletPublicKeyJwk);
37+
38+
Map<String, Object> eudiWalletInfo =
39+
Map.of(
40+
"general_info",
41+
Map.of(
42+
"wallet_provider_name", "Digg",
43+
"wallet_solution_id", "Diggidigg-id",
44+
"wallet_solution_version", "0.0.1",
45+
"wallet_solution_certification_information", "UNCERTIFIED"),
46+
"wscd_info", Map.of("wscd_certification_information", "UNCERTIFIED"));
47+
48+
Map<String, Object> status =
49+
Map.of("status_list", Map.of("idx", 412, "uri", "https://revocation_url/statuslists/1"));
50+
51+
List<Map<String, Object>> attestedKeys = List.of(attestedKey.toJSONObject());
52+
53+
Map<String, Object> claims = new HashMap<>();
54+
claims.put("eudi_wallet_info", eudiWalletInfo);
55+
claims.put("status", status);
56+
claims.put("attested_keys", attestedKeys);
57+
58+
return createSignedJwt(
59+
keystoreProperties.getSigningKey(),
60+
keystoreProperties.alias(),
61+
"Digg",
62+
Duration.ofHours(24),
63+
claims);
64+
}
65+
66+
private SignedJWT createSignedJwt(
67+
ECPrivateKey signingKey,
68+
String keyId,
69+
String issuer,
70+
Duration validity,
71+
Map<String, Object> claims)
72+
throws JOSEException {
73+
Instant now = Instant.now();
74+
75+
JWTClaimsSet.Builder claimsBuilder =
76+
new JWTClaimsSet.Builder()
77+
.issuer(issuer)
78+
.issueTime(Date.from(now))
79+
.expirationTime(Date.from(now.plus(validity)));
80+
81+
if (claims != null) {
82+
claims.forEach(claimsBuilder::claim);
83+
}
84+
85+
JWTClaimsSet claimsSet = claimsBuilder.build();
86+
87+
JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.ES256)
88+
.keyID(keyId)
89+
.type(new JOSEObjectType("keyattestation+jwt"))
90+
.build();
91+
92+
SignedJWT signedJwt = new SignedJWT(header, claimsSet);
93+
94+
JWSSigner signer = new ECDSASigner(signingKey);
95+
signedJwt.sign(signer);
96+
97+
return signedJwt;
98+
}
99+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# SPDX-FileCopyrightText: 2025 Digg - Agency for Digital Government
2+
# SPDX-License-Identifier: EUPL-1.2
3+
4+
wua:
5+
keystore:
6+
location: file:src/test/resources/dev/wua-signing.p12
7+
password: Test1234
8+
alias: wua-signing
9+
type: PKCS12

src/main/resources/application.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,10 @@ management:
3434
enabled: true
3535
diskspace:
3636
enabled: true
37+
38+
wua:
39+
keystore:
40+
location: ${WUA_KEYSTORE_PATH}
41+
password: ${WUA_KEYSTORE_PASSWORD}
42+
alias: ${WUA_KEYSTORE_ALIAS}
43+
type: PKCS12
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// SPDX-FileCopyrightText: 2025 Digg - Agency for Digital Government
2+
//
3+
// SPDX-License-Identifier: EUPL-1.2
4+
5+
package se.digg.wallet.provider.application.config;
6+
7+
import static org.junit.jupiter.api.Assertions.assertEquals;
8+
import static org.junit.jupiter.api.Assertions.assertNotNull;
9+
10+
import java.security.interfaces.ECPrivateKey;
11+
import java.security.interfaces.ECPublicKey;
12+
import org.junit.jupiter.api.Test;
13+
import org.springframework.beans.factory.annotation.Autowired;
14+
import org.springframework.boot.test.context.SpringBootTest;
15+
16+
@SpringBootTest
17+
class WuaKeystorePropertiesTest {
18+
19+
@Autowired
20+
private WuaKeystoreProperties properties;
21+
22+
@Test
23+
void assertThatGetSigningKey_givenValidKeyStore_shouldReturnSigningKey() {
24+
ECPrivateKey key = properties.getSigningKey();
25+
26+
assertNotNull(key);
27+
assertEquals("EC", key.getAlgorithm());
28+
}
29+
30+
@Test
31+
void assertThatGetSigningKey_givenValidKeyStore_shouldReturnPublicKey() {
32+
ECPublicKey key = properties.getPublicKey();
33+
34+
assertNotNull(key);
35+
assertEquals("EC", key.getAlgorithm());
36+
}
37+
}

src/test/java/se/digg/wallet/provider/application/controller/WalletUnitAttestationControllerTest.java

Lines changed: 25 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,58 +4,54 @@
44

55
package se.digg.wallet.provider.application.controller;
66

7+
import static org.mockito.ArgumentMatchers.anyString;
8+
import static org.mockito.Mockito.when;
79
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
8-
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.log;
9-
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
10+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
1011
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
1112

1213
import com.fasterxml.jackson.core.JsonProcessingException;
1314
import com.fasterxml.jackson.databind.ObjectMapper;
1415
import com.fasterxml.jackson.databind.ObjectWriter;
15-
import com.nimbusds.jose.jwk.Curve;
16-
import com.nimbusds.jose.jwk.ECKey;
17-
import com.nimbusds.jose.jwk.JWK;
18-
import java.security.KeyPair;
19-
import java.security.KeyPairGenerator;
20-
import java.security.interfaces.ECPrivateKey;
21-
import java.security.interfaces.ECPublicKey;
16+
import com.nimbusds.jwt.SignedJWT;
2217
import java.util.UUID;
2318
import org.junit.jupiter.api.Test;
2419
import org.springframework.beans.factory.annotation.Autowired;
2520
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
2621
import org.springframework.http.MediaType;
22+
import org.springframework.test.context.bean.override.mockito.MockitoBean;
2723
import org.springframework.test.web.servlet.MockMvc;
2824
import se.digg.wallet.provider.application.model.WalletUnitAttestationDto;
25+
import se.digg.wallet.provider.application.service.WalletUnitAttestationService;
2926

3027
@WebMvcTest(WalletUnitAttestationController.class)
3128
class WalletUnitAttestationControllerTest {
32-
@Autowired
33-
MockMvc mockMvc;
3429

35-
ObjectMapper mapper = new ObjectMapper();
30+
private final ObjectMapper mapper = new ObjectMapper();
31+
@Autowired
32+
private MockMvc mockMvc;
33+
@MockitoBean
34+
private WalletUnitAttestationService service;
3635

3736
@Test
38-
void assertThatPostWalletUnitAttestation_givenValidPublicKey_shouldReturnOk() throws Exception {
39-
40-
KeyPairGenerator gen = KeyPairGenerator.getInstance("EC");
41-
gen.initialize(Curve.P_256.toECParameterSpec());
42-
KeyPair keyPair = gen.generateKeyPair();
43-
44-
// Convert to JWK format
45-
JWK jwk =
46-
new ECKey.Builder(Curve.P_256, (ECPublicKey) keyPair.getPublic())
47-
.privateKey((ECPrivateKey) keyPair.getPrivate())
48-
.build();
37+
void assertThatPostWalletUnitAttestation_givenWalletIdAndValidPublicKey_shouldReturnOk()
38+
throws Exception {
39+
String expectedJwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJEaWdnIn0.test";
40+
when(service.createWalletUnitAttestation(anyString())).thenReturn(SignedJWT.parse(expectedJwt));
4941

50-
WalletUnitAttestationDto input =
51-
new WalletUnitAttestationDto(UUID.randomUUID(), jwk.toString());
42+
String jwk =
43+
"{\"kty\":\"EC\",\"use\":\"sig\",\"crv\":\"P-256\","
44+
+ "\"x\":\"18wHLeIgW9wVN6VD1Txgpqy2LszYkMf6J8njVAibvhM\","
45+
+ "\"y\":\"-V4dS4UaLMgP_4fY4j8ir7cl1TXlFdAgcx55o7TkcSA\"}";
46+
WalletUnitAttestationDto input = new WalletUnitAttestationDto(UUID.randomUUID(), jwk);
5247

53-
String path = "/wallet-unit-attestation";
5448
mockMvc
55-
.perform(post(path).contentType(MediaType.APPLICATION_JSON).content(asJson(input)))
56-
.andDo(log())
49+
.perform(
50+
post("/wallet-unit-attestation")
51+
.contentType(MediaType.APPLICATION_JSON)
52+
.content(asJson(input)))
5753
.andExpect(status().isOk())
58-
.andExpect(jsonPath("$.attested_keys[0].x").value(jwk.toECKey().getX().toString()));
54+
.andExpect(content().string(expectedJwt));
5955
}
6056

6157
private String asJson(WalletUnitAttestationDto input) throws JsonProcessingException {

0 commit comments

Comments
 (0)