Skip to content

Commit 62f6dce

Browse files
committed
Merge branch 'RM-4609_MID_JWSSigner' into 'SID'
RM-4032: Support for signing auth-tokens with Mobile-ID, "decrypt" with Mobile-ID See merge request cdoc2/cdoc2-java-ref-impl!96
2 parents e3a8ecc + 3348c7a commit 62f6dce

31 files changed

+771
-229
lines changed

cdoc2-lib/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
<dependency>
3333
<groupId>ee.cyber.cdoc2</groupId>
3434
<artifactId>cdoc2-auth-token</artifactId>
35-
<version>0.2.0-SNAPSHOT</version>
35+
<version>0.3.1-SNAPSHOT</version>
3636
</dependency>
3737

3838
<dependency>

cdoc2-lib/src/main/java/ee/cyber/cdoc2/client/mobileid/MobileIdClient.java

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package ee.cyber.cdoc2.client.mobileid;
22

3-
import ee.sk.mid.MidAuthenticationHashToSign;
4-
import ee.sk.mid.MidAuthenticationIdentity;
3+
import ee.sk.mid.MidAuthentication;
54
import ee.sk.mid.MidClient;
5+
import ee.sk.mid.MidHashToSign;
66
import ee.sk.mid.rest.dao.request.MidAuthenticationRequest;
77

88
import java.io.IOException;
@@ -45,18 +45,19 @@ public MobileIdClient(MobileIdClientConfiguration conf) {
4545
}
4646

4747
/**
48-
* Authentication request to Mobile ID client.
48+
* Authentication request to Mobile ID client. Returns raw MidAuthentication that contains MidSignature and signing
49+
* Certificate
4950
* @param userData user request data
5051
* @param authenticationHash Base64 encoded hash function output to be signed
51-
* @return MidAuthenticationIdentity object
52+
* @return MidAuthentication object that contains MidSignature and Certificate
5253
*/
53-
public MidAuthenticationIdentity startAuthentication(
54+
public MidAuthentication startAuthentication(
5455
MobileIdUserData userData,
55-
MidAuthenticationHashToSign authenticationHash
56+
MidHashToSign authenticationHash
5657
) throws CdocMobileIdClientException {
5758

5859
// ToDo display verification code and text to the user in RM-4086
59-
String verificationCode = authenticationHash.calculateVerificationCode();
60+
//String verificationCode = authenticationHash.calculateVerificationCode();
6061

6162
MidAuthenticationRequest request = MidAuthenticationRequest.newBuilder()
6263
.withPhoneNumber(userData.phoneNumber())
@@ -89,7 +90,7 @@ private MidClient configureMobileIdClient() throws ConfigurationLoadingException
8990
/**
9091
* Read trusted certificates for Mobile ID client secure TLS transport
9192
*/
92-
private KeyStore readTrustedCertificates() throws ConfigurationLoadingException {
93+
public KeyStore readTrustedCertificates() throws ConfigurationLoadingException {
9394
try (InputStream is = Resources.getResourceAsStream(
9495
mobileIdClientConfig.getTrustStore(), this.getClass().getClassLoader())
9596
) {

cdoc2-lib/src/main/java/ee/cyber/cdoc2/client/mobileid/MobileIdClientWrapper.java

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
package ee.cyber.cdoc2.client.mobileid;
22

33
import ee.sk.mid.MidAuthentication;
4-
import ee.sk.mid.MidAuthenticationHashToSign;
5-
import ee.sk.mid.MidAuthenticationIdentity;
64
import ee.sk.mid.MidAuthenticationResponseValidator;
75
import ee.sk.mid.MidAuthenticationResult;
86
import ee.sk.mid.MidClient;
7+
import ee.sk.mid.MidHashToSign;
98
import ee.sk.mid.exception.MidDeliveryException;
109
import ee.sk.mid.exception.MidInternalErrorException;
1110
import ee.sk.mid.exception.MidInvalidUserConfigurationException;
@@ -25,7 +24,6 @@
2524

2625
import ee.cyber.cdoc2.exceptions.CdocMobileIdClientException;
2726

28-
2927
/**
3028
* Mobile-ID Client wrapper
3129
*/
@@ -47,12 +45,12 @@ public MobileIdClientWrapper(MidClient midClient) {
4745
* Authentication request to {@code /authentication}
4846
* @param request MID authentication request
4947
* @param authenticationHash Base64 encoded hash function output to be signed
50-
* @return MidAuthenticationIdentity object
48+
* @return MidAuthentication object that contains MidSignature and Certificate
5149
* @throws CdocMobileIdClientException if authentication fails
5250
*/
53-
public MidAuthenticationIdentity authenticate(
51+
public MidAuthentication authenticate(
5452
MidAuthenticationRequest request,
55-
MidAuthenticationHashToSign authenticationHash
53+
MidHashToSign authenticationHash
5654
) throws CdocMobileIdClientException {
5755

5856
try {
@@ -63,15 +61,18 @@ public MidAuthenticationIdentity authenticate(
6361
.getSessionStatusPoller()
6462
.fetchFinalAuthenticationSessionStatus(authResponse.getSessionID());
6563

66-
MidAuthentication authentication = midClient.createMobileIdAuthentication(
67-
sessionStatus, authenticationHash
68-
);
64+
MidAuthentication midAuthentication
65+
= midClient.createMobileIdAuthentication(sessionStatus, authenticationHash);
6966

70-
if (authentication.getResult().equals("OK")) {
71-
return validateAuthenticationAndReturnIdentity(authentication);
67+
//Other responses beside "OK" https://github.com/SK-EID/MID?tab=readme-ov-file#338-session-end-result-codes
68+
if (midAuthentication.getResult().equals("OK")) {
69+
validateAuthenticationAndReturnIdentity(midAuthentication); // throws CdocMobileIdClientException
70+
return midAuthentication;
7271
}
7372

74-
throw new CdocMobileIdClientException("Mobile ID authentication session has failed");
73+
throw new CdocMobileIdClientException("Mobile ID authentication session has failed with "
74+
+ midAuthentication.getResult());
75+
7576
} catch (MidUserCancellationException
7677
| MidNotMidClientException
7778
| MidSessionTimeoutException
@@ -86,14 +87,14 @@ public MidAuthenticationIdentity authenticate(
8687
}
8788
}
8889

89-
public MidAuthenticationIdentity validateAuthenticationAndReturnIdentity(
90+
public void validateAuthenticationAndReturnIdentity(
9091
MidAuthentication authentication
9192
) throws CdocMobileIdClientException {
9293

9394
MidAuthenticationResult authResult = responseValidator.validate(authentication);
9495
List<String> authErrors = authResult.getErrors();
9596
if (authResult.isValid() && authErrors.isEmpty()) {
96-
return authResult.getAuthenticationIdentity();
97+
return;
9798
}
9899

99100
throw new CdocMobileIdClientException(

cdoc2-lib/src/main/java/ee/cyber/cdoc2/container/Envelope.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ private static List<ArchiveEntry> processContainer(
329329
InputStream cdocInputStream,
330330
DecryptionKeyMaterial keyMaterial,
331331
TarEntryProcessingDelegate tarProcessingDelegate,
332-
@Nullable Services services
332+
Services services
333333
) throws GeneralSecurityException, IOException, CDocException {
334334

335335
CountingInputStream containerIs = new CountingInputStream(cdocInputStream);

cdoc2-lib/src/main/java/ee/cyber/cdoc2/crypto/AuthenticationIdentifier.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,4 +180,12 @@ public static SemanticsIdentifier createSemanticsIdentifier(String idCode) {
180180
);
181181
}
182182

183+
/**
184+
* Smart-ID and Mobile-ID interaction parameters.
185+
* @param document document to be decrypted, will be displayed to user when user PIN is required
186+
*/
187+
public record SidMidInteractionParams(
188+
String document
189+
) {
190+
}
183191
}

cdoc2-lib/src/main/java/ee/cyber/cdoc2/crypto/KekTools.java

Lines changed: 82 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package ee.cyber.cdoc2.crypto;
22

3+
import ee.cyber.cdoc2.auth.EtsiIdentifier;
34
import ee.cyber.cdoc2.client.KeyShareClientFactory;
45
import ee.cyber.cdoc2.client.KeySharesClient;
6+
import ee.cyber.cdoc2.client.mobileid.MobileIdClient;
57
import ee.cyber.cdoc2.client.model.KeyShare;
68
import ee.cyber.cdoc2.client.smartid.SmartIdClient;
79
import ee.cyber.cdoc2.container.CDocParseException;
@@ -31,19 +33,22 @@
3133
import java.security.KeyPair;
3234
import java.security.interfaces.ECPublicKey;
3335
import java.security.interfaces.RSAPrivateKey;
34-
import java.util.ArrayList;
36+
import java.util.LinkedList;
3537
import java.util.List;
3638
import java.util.NoSuchElementException;
39+
import java.util.Objects;
3740
import java.util.Optional;
3841
import javax.crypto.SecretKey;
3942

4043
import ee.cyber.cdoc2.services.Services;
41-
import ee.cyber.cdoc2.util.SIDAuthTokenCreator;
44+
import ee.cyber.cdoc2.crypto.jwt.IdentityJWSSigner;
45+
import ee.cyber.cdoc2.crypto.jwt.MIDAuthJWSSigner;
46+
import ee.cyber.cdoc2.crypto.jwt.SIDAuthJWSSigner;
47+
import ee.cyber.cdoc2.crypto.jwt.SidMidAuthTokenCreator;
48+
import ee.sk.smartid.rest.dao.SemanticsIdentifier;
4249
import org.slf4j.Logger;
4350
import org.slf4j.LoggerFactory;
4451

45-
import static ee.cyber.cdoc2.crypto.AuthenticationIdentifier.ETSI_IDENTIFIER_PREFIX;
46-
4752

4853
/**
4954
* Functions for deriving KEK in different scenarios
@@ -258,80 +263,115 @@ public static byte[] deriveKekFromShares(
258263
KeyShareClientFactory keyShareClientFactory,
259264
Services services //XXX: for now its generic services, in future might be more specific type to use SID/MID
260265
) throws GeneralSecurityException, CDocException {
266+
267+
Objects.requireNonNull(services);
261268
validateKeyOrigin(
262269
EncryptionKeyOrigin.KEY_SHARE,
263270
keyMaterial.getKeyOrigin(),
264271
"Expected key shares for KeySharesRecipient"
265272
);
266273

267-
if (services == null || !services.hasService(SmartIdClient.class)) {
268-
throw new CDocException("SmartIdClient not configured");
269-
}
274+
try {
275+
List<byte[]> listOfShares =
276+
fetchKeyShares(keyMaterial, keySharesRecipient, keyShareClientFactory, services);
270277

271-
List<KeyShareUri> shares = keySharesRecipient.getKeyShares();
272-
List<byte[]> listOfShares = new ArrayList<>();
278+
return Crypto.combineKek(
279+
listOfShares,
280+
keyShareClientFactory.getKeySharesConfiguration().getKeySharesServersMinNum()
281+
);
273282

274-
try {
275-
addKeyShares(listOfShares, keyMaterial, keySharesRecipient, keyShareClientFactory, services);
276283
} catch (AuthSignatureCreationException asce) {
277284
throw new GeneralSecurityException(asce);
278285
}
279286

280-
return Crypto.combineKek(
281-
listOfShares,
282-
keyShareClientFactory.getKeySharesConfiguration().getKeySharesServersMinNum()
283-
);
284287
}
285288

286-
private static void addKeyShares(
287-
List<byte[]> listOfShares,
289+
private static List<byte[]> fetchKeyShares(
288290
KeyShareDecryptionKeyMaterial decryptKeyMaterial,
289291
KeySharesRecipient keySharesRecipient,
290292
KeyShareClientFactory keyShareClientFactory,
291293
Services services
292-
) throws GeneralSecurityException, AuthSignatureCreationException {
293-
294-
//FIXME: write proper implementation with RM-4309, RM-4032, RM-4073
295-
String semanticsIdentifier = String.valueOf(keySharesRecipient.getRecipientId()) // etsi/PNOEE-48010010101
296-
.substring(ETSI_IDENTIFIER_PREFIX.length()); // PNOEE-48010010101
294+
) throws GeneralSecurityException, AuthSignatureCreationException, CDocException {
297295

296+
List<byte[]> listOfShares = new LinkedList<>();
298297
List<KeyShareUri> shares = keySharesRecipient.getKeyShares();
299298

300-
AuthenticationIdentifier.AuthenticationType authType
301-
= decryptKeyMaterial.authIdentifier().getAuthType();
299+
SidMidAuthTokenCreator tokenCreator =
300+
signShareAccessTokens(shares, decryptKeyMaterial, keyShareClientFactory, services);
301+
302+
for (KeyShareUri share : shares) {
303+
listOfShares.add(getKeyShare(share, keyShareClientFactory, tokenCreator));
304+
}
305+
return listOfShares;
306+
}
307+
308+
/**
309+
* Ask nonce for each share, sign share with nonce using auth means
310+
* @param shares
311+
* @param decryptKeyMaterial
312+
* @param keyShareClientFactory
313+
* @param services
314+
* @return
315+
* @throws CDocException
316+
* @throws AuthSignatureCreationException
317+
*/
318+
private static SidMidAuthTokenCreator signShareAccessTokens(
319+
List<KeyShareUri> shares,
320+
KeyShareDecryptionKeyMaterial decryptKeyMaterial,
321+
KeyShareClientFactory keyShareClientFactory,
322+
Services services
323+
324+
) throws CDocException, AuthSignatureCreationException {
325+
326+
AuthenticationIdentifier.AuthenticationType authType =
327+
decryptKeyMaterial.authIdentifier().getAuthType();
328+
SemanticsIdentifier semanticsIdentifier =
329+
decryptKeyMaterial.authIdentifier().getSemanticsIdentifier();
330+
EtsiIdentifier etsiIdentifier = new EtsiIdentifier(decryptKeyMaterial.authIdentifier().getEtsiIdentifier());
302331

303332
switch (authType) {
304333
case SID -> {
305-
SIDAuthTokenCreator tokenCreator = new SIDAuthTokenCreator(
306-
semanticsIdentifier,
307-
shares,
308-
keyShareClientFactory,
309-
services.get(SmartIdClient.class));
310-
for (KeyShareUri share : shares) {
311-
listOfShares.add(extractSharesFromSidRecipient(keyShareClientFactory, tokenCreator, share));
334+
if (!services.hasService(SmartIdClient.class)) {
335+
throw new CDocException("SmartIdClient not configured");
312336
}
337+
SmartIdClient sidClient = services.get(SmartIdClient.class);
338+
return new SidMidAuthTokenCreator(
339+
new SIDAuthJWSSigner(sidClient, semanticsIdentifier),
340+
shares,
341+
keyShareClientFactory);
313342
}
314343
case MID -> {
315-
// ToDo connect authentication here. RM-4609
316-
// MobileIdUserData userData = new MobileIdUserData(
317-
// decryptKeyMaterial.authIdentifier().getMobileNumber(),
318-
// decryptKeyMaterial.authIdentifier().getIdCode()
319-
// );
320-
// MobileIdClient.startAuthentication(userData);
321-
for (KeyShareUri share : shares) {
322-
listOfShares.add(extractSharesFromMidRecipient(keyShareClientFactory, share));
344+
if (!services.hasService(MobileIdClient.class)) {
345+
throw new CDocException("MobileIdClient not configured");
323346
}
347+
MobileIdClient midClient = services.get(MobileIdClient.class);
348+
String mobileNumber = decryptKeyMaterial.authIdentifier().getMobileNumber();
349+
IdentityJWSSigner jwsSigner = new MIDAuthJWSSigner(midClient, etsiIdentifier, mobileNumber);
350+
351+
// constructor gets nonce for each share from shares-server and signs shareUris and their nonces
352+
// with jwsSigner
353+
return new SidMidAuthTokenCreator(
354+
jwsSigner,
355+
shares,
356+
keyShareClientFactory);
324357
}
325358
default -> throw new IllegalStateException(
326359
"Unexpected authentication type: " + authType
327360
);
328361
}
329362
}
330363

331-
private static byte[] extractSharesFromSidRecipient(
364+
/**
365+
* @param keyShareClientFactory
366+
* @param tokenCreator signed authentication token
367+
* @param share share to fetch
368+
* @return
369+
* @throws GeneralSecurityException
370+
*/
371+
private static byte[] getKeyShare(
372+
KeyShareUri share,
332373
KeyShareClientFactory keyShareClientFactory,
333-
SIDAuthTokenCreator tokenCreator,
334-
KeyShareUri share
374+
SidMidAuthTokenCreator tokenCreator
335375
) throws GeneralSecurityException {
336376
KeySharesClient client
337377
= keyShareClientFactory.getClientForServerUrl(share.serverBaseUrl());
@@ -341,20 +381,6 @@ private static byte[] extractSharesFromSidRecipient(
341381
return getKeyShare(share, client, authTicket, authenticatorCertPEM);
342382
}
343383

344-
// ToDo add authentication token here. RM-4609
345-
private static byte[] extractSharesFromMidRecipient(
346-
KeyShareClientFactory keyShareClientFactory,
347-
// MIDAuthTokenCreator tokenCreator
348-
KeyShareUri share
349-
) throws GeneralSecurityException {
350-
KeySharesClient client
351-
= keyShareClientFactory.getClientForServerUrl(share.serverBaseUrl());
352-
// String authTicket = tokenCreator.getTokenForShareID(share.shareId());
353-
// String authenticatorCertPEM = tokenCreator.getAuthenticatorCertPEM();
354-
355-
return getKeyShare(share, client, null, null);
356-
}
357-
358384
private static byte[] getKeyShare(
359385
KeyShareUri share,
360386
KeySharesClient client,
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package ee.cyber.cdoc2.crypto.jwt;
2+
3+
import com.nimbusds.jose.JWSHeader;
4+
import com.nimbusds.jose.JWSSigner;
5+
import ee.cyber.cdoc2.auth.EtsiIdentifier;
6+
import jakarta.annotation.Nullable;
7+
8+
import java.security.cert.X509Certificate;
9+
10+
/**
11+
* JWSSigner that provides methods for getting signer identity and public certificate. Implementations are for
12+
* Mobile-ID and Smart-ID. Instead of providing private key,
13+
* it's initialized by providing signer identity (and mobile number for Mobile-ID) and signature is created using
14+
* remote REST API. Singer certificate is available only after successful signing.
15+
*/
16+
public interface IdentityJWSSigner extends JWSSigner {
17+
18+
/**
19+
* Get signer identity, currently supported format is "etsi/SemanticsIdentifier" e.g. "etsi/PNOEE-48010010101"
20+
* @return signer identity who is signing the JWT using JWSSigner
21+
*/
22+
EtsiIdentifier getSignerIdentifier(); // XXX: return more generic type?
23+
24+
/**
25+
* After {@link #sign(JWSHeader, byte[])} has succeeded, signer public certificate can be queried
26+
* @return signer certificate if {@code sign()} has succeeded, otherwise will be {@code null}
27+
*/
28+
@Nullable X509Certificate getSignerCertificate();
29+
}

0 commit comments

Comments
 (0)