Skip to content

Commit a23b3e9

Browse files
committed
Add csc module, add tests
DEVSIX-8682
1 parent d208a9c commit a23b3e9

File tree

11 files changed

+890
-9
lines changed

11 files changed

+890
-9
lines changed

csc/pom.xml

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
3+
<modelVersion>4.0.0</modelVersion>
4+
<parent>
5+
<groupId>com.itextpdf.signingexamples</groupId>
6+
<artifactId>signing-examples-parent</artifactId>
7+
<version>9.0.0-SNAPSHOT</version>
8+
</parent>
9+
<artifactId>signing-examples-csc</artifactId>
10+
<packaging>bundle</packaging>
11+
<name>${project.groupId}/${project.artifactId}</name>
12+
<properties>
13+
<laverca-csc-client-version>1.2.0</laverca-csc-client-version>
14+
<maven.compiler.source>1.8</maven.compiler.source>
15+
<maven.compiler.target>1.8</maven.compiler.target>
16+
</properties>
17+
<dependencyManagement>
18+
<dependencies>
19+
<dependency>
20+
<groupId>fi.methics</groupId>
21+
<artifactId>laverca-csc-client</artifactId>
22+
<version>${laverca-csc-client-version}</version>
23+
</dependency>
24+
</dependencies>
25+
</dependencyManagement>
26+
<dependencies>
27+
<dependency>
28+
<groupId>com.itextpdf</groupId>
29+
<artifactId>forms</artifactId>
30+
</dependency>
31+
<dependency>
32+
<groupId>com.itextpdf</groupId>
33+
<artifactId>io</artifactId>
34+
</dependency>
35+
<dependency>
36+
<groupId>com.itextpdf</groupId>
37+
<artifactId>kernel</artifactId>
38+
</dependency>
39+
<dependency>
40+
<groupId>com.itextpdf</groupId>
41+
<artifactId>sign</artifactId>
42+
</dependency>
43+
<dependency>
44+
<groupId>fi.methics</groupId>
45+
<artifactId>laverca-csc-client</artifactId>
46+
</dependency>
47+
<dependency>
48+
<groupId>org.slf4j</groupId>
49+
<artifactId>slf4j-api</artifactId>
50+
</dependency>
51+
<dependency>
52+
<groupId>com.itextpdf</groupId>
53+
<artifactId>bouncy-castle-adapter</artifactId>
54+
<scope>test</scope>
55+
</dependency>
56+
<dependency>
57+
<groupId>org.junit.jupiter</groupId>
58+
<artifactId>junit-jupiter-api</artifactId>
59+
<scope>test</scope>
60+
</dependency>
61+
<dependency>
62+
<groupId>org.junit.jupiter</groupId>
63+
<artifactId>junit-jupiter-engine</artifactId>
64+
<scope>test</scope>
65+
</dependency>
66+
</dependencies>
67+
</project>
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package com.itextpdf.signingexamples.csc;
2+
3+
import java.io.ByteArrayInputStream;
4+
import java.security.GeneralSecurityException;
5+
import java.security.MessageDigest;
6+
import java.security.cert.Certificate;
7+
import java.security.cert.CertificateFactory;
8+
import java.util.Collections;
9+
import java.util.List;
10+
11+
import com.itextpdf.bouncycastleconnector.BouncyCastleFactoryCreator;
12+
import com.itextpdf.commons.bouncycastle.IBouncyCastleFactory;
13+
import com.itextpdf.kernel.crypto.DigestAlgorithms;
14+
import com.itextpdf.kernel.xmp.impl.Base64;
15+
import com.itextpdf.signatures.BouncyCastleDigest;
16+
import com.itextpdf.signatures.IExternalSignature;
17+
import com.itextpdf.signatures.ISignatureMechanismParams;
18+
import com.itextpdf.signatures.SignatureMechanisms;
19+
20+
import fi.methics.laverca.csc.CscClient;
21+
import fi.methics.laverca.csc.json.credentials.CscCredentialsAuthorizeResp;
22+
import fi.methics.laverca.csc.json.credentials.CscCredentialsInfoResp;
23+
import fi.methics.laverca.csc.json.signatures.CscSignHashResp;
24+
25+
/**
26+
* @author mkl
27+
*/
28+
public class LavercaCscSignature implements IExternalSignature {
29+
/** The Laverca CSC client. */
30+
final CscClient client;
31+
32+
/** The Laverca CSC client. */
33+
final String credentialID;
34+
35+
/** The certificate chain. */
36+
Certificate[] chain;
37+
38+
/** The signature algorithm OID. */
39+
String algorithmOid;
40+
41+
public LavercaCscSignature(CscClient client, String credentialID, String algorithm) throws GeneralSecurityException {
42+
this.client = client;
43+
this.credentialID = credentialID;
44+
45+
CscCredentialsInfoResp credentialInfo = client.getCredentialInfo(credentialID);
46+
47+
List<String> certificateStrings = credentialInfo.cert.certificates;
48+
chain = new Certificate[certificateStrings.size()];
49+
CertificateFactory certificateFactory = CertificateFactory.getInstance("X509");
50+
for (int i = 0; i < certificateStrings.size(); i++) {
51+
String certificateString = certificateStrings.get(i);
52+
byte[] certificateBytes = Base64.decode(certificateString.getBytes());
53+
chain[i] = certificateFactory.generateCertificate(new ByteArrayInputStream(certificateBytes));
54+
}
55+
56+
IBouncyCastleFactory BOUNCY_CASTLE_FACTORY = BouncyCastleFactoryCreator.getFactory();
57+
String algorithmOid = BOUNCY_CASTLE_FACTORY.getAlgorithmOid(algorithm);
58+
if (algorithmOid == null)
59+
algorithmOid = algorithm;
60+
if (credentialInfo.key.algo.contains(algorithmOid))
61+
this.algorithmOid = algorithmOid;
62+
}
63+
64+
@Override
65+
public String getDigestAlgorithmName() {
66+
return DigestAlgorithms.getDigest(algorithmOid);
67+
}
68+
69+
@Override
70+
public String getSignatureAlgorithmName() {
71+
return SignatureMechanisms.getAlgorithm(algorithmOid);
72+
}
73+
74+
@Override
75+
public ISignatureMechanismParams getSignatureMechanismParameters() {
76+
// TODO Add RSASSA-PSS support
77+
return null;
78+
}
79+
80+
@Override
81+
public byte[] sign(byte[] message) throws GeneralSecurityException {
82+
MessageDigest messageDigest = new BouncyCastleDigest().getMessageDigest(getDigestAlgorithmName());
83+
byte[] hash = messageDigest.digest(message);
84+
String base64Hash = new String(Base64.encode(hash));
85+
86+
CscCredentialsInfoResp credentialInfo = client.getCredentialInfo(credentialID);
87+
CscCredentialsAuthorizeResp authorize = null;
88+
if (credentialInfo.isScal2()) {
89+
authorize = client.authorize(credentialID, Collections.singletonList(base64Hash));
90+
} else {
91+
authorize = client.authorize(credentialID);
92+
}
93+
94+
CscSignHashResp signhash = client.signHash(credentialID, authorize, Collections.singletonList(base64Hash), algorithmOid, null);
95+
96+
return Base64.decode(signhash.signatures.get(0).getBytes());
97+
}
98+
99+
public Certificate[] getChain() {
100+
return chain;
101+
}
102+
}
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
package com.itextpdf.signingexamples.csc.digidentity;
2+
3+
import java.io.IOException;
4+
import java.lang.reflect.Field;
5+
6+
import com.google.gson.annotations.SerializedName;
7+
8+
import fi.methics.laverca.csc.CscClient;
9+
import fi.methics.laverca.csc.json.CscErrorResp;
10+
import fi.methics.laverca.csc.json.GsonMessage;
11+
import okhttp3.Credentials;
12+
import okhttp3.MultipartBody;
13+
import okhttp3.OkHttpClient;
14+
import okhttp3.Request;
15+
import okhttp3.RequestBody;
16+
import okhttp3.Response;
17+
18+
/**
19+
* This class implements Authorization for Digidentity Signing via
20+
* their CSC API implementation. It employs their QR code based method
21+
* to not depend on the availability of callback routing.
22+
*/
23+
public class Authorization {
24+
//
25+
// Constructors
26+
//
27+
public Authorization() {
28+
this(new OkHttpClient());
29+
}
30+
31+
public Authorization(OkHttpClient okHttpClient) {
32+
this.okHttpClient = okHttpClient;
33+
}
34+
35+
//
36+
// Credentials
37+
//
38+
public Authorization withScope(String scope) {
39+
this.scope = scope;
40+
return this;
41+
}
42+
43+
public Authorization withClient(String client) {
44+
this.client = client;
45+
return this;
46+
}
47+
48+
public Authorization withSecret(String secret) {
49+
this.secret = secret;
50+
return this;
51+
}
52+
53+
//
54+
// QR code URL retrieval
55+
//
56+
public String retrieveQrCodeUri() throws IOException {
57+
return retrieveQrCodeUri(OAUTH2_AUTHORIZE_URL);
58+
}
59+
60+
public String retrieveQrCodeUri(String oAuth2AuthorizeUrl) throws IOException {
61+
Request request = new Request.Builder()
62+
.url(oAuth2AuthorizeUrl + "?client_id=" + client + "&scope=" + scope + "&response_type=code")
63+
.method("GET", null)
64+
.build();
65+
Response response = okHttpClient.newCall(request).execute();
66+
67+
QrCodeUriResp qrCodeUriResp = QrCodeUriResp.fromResponse(response, QrCodeUriResp.class);
68+
69+
if (qrCodeUriResp.data == null)
70+
throw new IOException("OAUTH2 AUTHORIZE Response: Missing or malformed data element");
71+
if (qrCodeUriResp.data.id == null)
72+
throw new IOException("OAUTH2 AUTHORIZE Response: Missing or malformed data.id element");
73+
if (!"passwordless_login_session".equals(qrCodeUriResp.data.type))
74+
throw new IOException("OAUTH2 AUTHORIZE Response: Missing or unexpected data.type element: " + qrCodeUriResp.data.type);
75+
if (qrCodeUriResp.data.attributes == null)
76+
throw new IOException("OAUTH2 AUTHORIZE Response: Missing or malformed data.attributes element");
77+
if (!"qr_code".equals(qrCodeUriResp.data.attributes.type))
78+
throw new IOException("OAUTH2 AUTHORIZE Response: Missing or unexpected data.attributes.type element: " + qrCodeUriResp.data.attributes.type);
79+
if (qrCodeUriResp.data.attributes.qr_code_uri == null)
80+
throw new IOException("OAUTH2 AUTHORIZE Response: Missing or malformed data.attributes.qr_code_uri element");
81+
82+
authorizationCode = qrCodeUriResp.data.id;
83+
return qrCodeUriResp.data.attributes.qr_code_uri;
84+
}
85+
86+
//
87+
// Polling for the Digidentity authorization token
88+
//
89+
public void pollAuthorization(long pollInterval) throws IOException {
90+
pollAuthorization(OAUTH2_TOKEN_URL, pollInterval);
91+
}
92+
93+
public void pollAuthorization(String oauth2TokenUrl, long pollInterval) throws IOException {
94+
if (pollInterval < 0)
95+
pollInterval = 5000;
96+
97+
for (;;) {
98+
try {
99+
Thread.sleep(pollInterval);
100+
} catch (InterruptedException e) {
101+
Thread.currentThread().interrupt();
102+
}
103+
104+
RequestBody body = new MultipartBody.Builder().setType(MultipartBody.FORM)
105+
.addFormDataPart("code", authorizationCode)
106+
.addFormDataPart("grant_type", "authorization_code")
107+
.build();
108+
Request request = new Request.Builder()
109+
.url(oauth2TokenUrl)
110+
.method("POST", body)
111+
.addHeader("Authorization", Credentials.basic(client, secret))
112+
.build();
113+
Response response = okHttpClient.newCall(request).execute();
114+
115+
if (response.code() == 200) {
116+
TokenResp digidentityToken = TokenResp.fromResponse(response, TokenResp.class);
117+
accessToken = digidentityToken.access_token;
118+
refreshToken = digidentityToken.refresh_token;
119+
break;
120+
}
121+
if (response.code() == 400) {
122+
CscErrorResp errorResp = CscErrorResp.fromResponse(response);
123+
if ("session_not_found".equals(errorResp.error_description)) {
124+
throw new IOException("OAUTH2 TOKEN Response: Session timeout OR non-existent session");
125+
}
126+
if (!"login_pending".equals(errorResp.error_description)) {
127+
throw new IOException("OAUTH2 TOKEN Response: Unexpected error: " + errorResp.error_description);
128+
}
129+
} else {
130+
throw new IOException("OAUTH2 TOKEN Response: Unexpected response: " + response);
131+
}
132+
}
133+
}
134+
135+
//
136+
// retrieve CSC token
137+
//
138+
public String retrieveCscToken() throws IOException {
139+
return retrieveCscToken(DIGIDENTITY_API_BASE_URL);
140+
}
141+
142+
public String retrieveCscToken(String digidentityApiBaseUrl) throws IOException {
143+
RequestBody body = new MultipartBody.Builder().setType(MultipartBody.FORM)
144+
.addFormDataPart("access_token", accessToken)
145+
.build();
146+
Request request = new Request.Builder()
147+
.url(digidentityApiBaseUrl + "/api/esign/tokens")
148+
.method("POST", body)
149+
.build();
150+
Response response = okHttpClient.newCall(request).execute();
151+
152+
TokenResp tokenResp = TokenResp.fromResponse(response, TokenResp.class);
153+
if (tokenResp.access_token == null)
154+
throw new IOException("API ESIGN TOKENS Response: Missing token");
155+
156+
return tokenResp.access_token;
157+
}
158+
159+
static void injectCscToken(CscClient cscClient, String accessToken) throws IllegalAccessException, NoSuchFieldException {
160+
Field accessTokenField = CscClient.class.getDeclaredField("access_token");
161+
accessTokenField.setAccessible(true);
162+
accessTokenField.set(cscClient, accessToken);
163+
}
164+
165+
//
166+
// variables and constants
167+
//
168+
public final static String OAUTH2_AUTHORIZE_URL = "https://auth.digidentity-preproduction.eu/oauth2/authorize.json";
169+
public final static String OAUTH2_TOKEN_URL = "https://auth.digidentity-preproduction.eu/oauth2/token.json";
170+
public final static String DIGIDENTITY_API_BASE_URL = "https://esign.digidentity-preproduction.eu";
171+
public final static String CSC_API_BASE_URL = "https://esign.digidentity-preproduction.eu";
172+
173+
final OkHttpClient okHttpClient;
174+
175+
String scope;
176+
String client;
177+
String secret;
178+
179+
String authorizationCode;
180+
String accessToken;
181+
String refreshToken;
182+
183+
//
184+
// classes for wrapping JSON data objects
185+
//
186+
static class TokenResp extends GsonMessage {
187+
@SerializedName("access_token")
188+
public String access_token;
189+
190+
@SerializedName("refresh_token")
191+
public String refresh_token;
192+
193+
@SerializedName("scope")
194+
public String scope;
195+
196+
@SerializedName("token_type")
197+
public String token_type;
198+
199+
@SerializedName("expires_in")
200+
public int expires_in;
201+
}
202+
203+
static class QrCodeUriResp extends GsonMessage {
204+
@SerializedName("data")
205+
public QrCodeUriRespData data;
206+
}
207+
208+
static class QrCodeUriRespData extends GsonMessage {
209+
@SerializedName("id")
210+
public String id;
211+
212+
@SerializedName("type")
213+
public String type;
214+
215+
@SerializedName("attributes")
216+
public QrCodeUriRespDataAttributes attributes;
217+
}
218+
219+
static class QrCodeUriRespDataAttributes extends GsonMessage {
220+
@SerializedName("type")
221+
public String type;
222+
223+
@SerializedName("qr_code_uri")
224+
public String qr_code_uri;
225+
}
226+
}

0 commit comments

Comments
 (0)