Skip to main content
Skip table of contents

Part V - CSC API

In recent years the Cloud Signing Consortium (CSC, a global group of industry, government, and academic organizations committed to driving standardization of highly secure and compliant digital signatures in the cloud) has been working on developing a REST API for remote signing. This API in particular has been designed to allow compliance with the requirements of the EU’s eIDAS regulation and similarly rigorous frameworks but it can also be used in less regulated contexts.

This part focuses on signing PDFs with iText for Java via a remote signing service offering a CSC API access. Even though there already is a version 2 of the CSC API specification, we concentrate on CSC version 1 services as this version still is in use in most operational CSC services.

For the actual REST calls we use an existing library, the CSC client by Methics Oy.

The CSC API endpoints the code has been tested with are the Methics CSC client test service and the Digidentity B.V. pre-production environment. The former represents a simple, username/password authentication based installation. The latter, on the other hand, includes OAuth2 authentication and two factor authorization for individual signatures.

The CSC client library

The Methics CSC client library is a simple Java client for CSC v1.0.4.0 API. Here, we are using its version 1.2.0. It in turn depends on Google Gson (A Java serialization/deserialization library to convert Java Objects into JSON and back) version 2.11.0 and on Square OkHttp (Square’s meticulous HTTP client for the JVM, Android, and GraalVM) version 4.12.0. To include it into your Maven project, use this dependency:

XML
<dependency>
	<groupId>fi.methics</groupId>
	<artifactId>laverca-csc-client</artifactId>
	<version>1.2.0</version>
</dependency>

As this library is not distributed to the larger repositories, you may have to build and install it locally first from the github sources.

Signing via CSC

Assuming we have an instance of CscClient (the pivotal CSC client library class) already logged in to its service, and the ID of the CSC credentials to use for signing, the following simple IExternalSignature implementation can be used to sign PDFs with:

JAVA
public class LavercaCscSignature implements IExternalSignature {
    /** The Laverca CSC client. */
    final CscClient client;

    /** The ID of the CSC credentials to use for signing. */
    final String credentialID;

    /** The certificate chain. */
    Certificate[] chain;

    /** The signature algorithm OID. */
    String algorithmOid;

    public LavercaCscSignature(CscClient client, String credentialID, String algorithm) throws GeneralSecurityException {
        this.client = client;
        this.credentialID = credentialID;

        CscCredentialsInfoResp credentialInfo = client.getCredentialInfo(credentialID);

        List<String> certificateStrings = credentialInfo.cert.certificates;
        chain = new Certificate[certificateStrings.size()];
        CertificateFactory certificateFactory = CertificateFactory.getInstance("X509");
        for (int i = 0; i < certificateStrings.size(); i++) {
            String certificateString = certificateStrings.get(i);
            byte[] certificateBytes = Base64.decode(certificateString.getBytes());
            chain[i] = certificateFactory.generateCertificate(new ByteArrayInputStream(certificateBytes));
        }

        IBouncyCastleFactory BOUNCY_CASTLE_FACTORY = BouncyCastleFactoryCreator.getFactory();
        String algorithmOid = BOUNCY_CASTLE_FACTORY.getAlgorithmOid(algorithm);
        if (algorithmOid == null)
            algorithmOid = algorithm;
        if (credentialInfo.key.algo.contains(algorithmOid))
            this.algorithmOid = algorithmOid;
    }

    @Override
    public String getDigestAlgorithmName() {
        return DigestAlgorithms.getDigest(algorithmOid);
    }

    @Override
    public String getSignatureAlgorithmName() {
        return SignatureMechanisms.getAlgorithm(algorithmOid);
    }

    @Override
    public ISignatureMechanismParams getSignatureMechanismParameters() {
        // TODO Add RSASSA-PSS support
        return null;
    }

    @Override
    public byte[] sign(byte[] message) throws GeneralSecurityException {
        MessageDigest messageDigest = new BouncyCastleDigest().getMessageDigest(getDigestAlgorithmName());
        byte[] hash = messageDigest.digest(message);
        String base64Hash = new String(Base64.encode(hash));

        CscCredentialsInfoResp credentialInfo = client.getCredentialInfo(credentialID);
        CscCredentialsAuthorizeResp authorize = null;
        if (credentialInfo.isScal2()) {
            authorize = client.authorize(credentialID, Collections.singletonList(base64Hash));
        } else {
            authorize = client.authorize(credentialID);
        }

        CscSignHashResp signhash = client.signHash(credentialID, authorize, Collections.singletonList(base64Hash), algorithmOid, null);

        return Base64.decode(signhash.signatures.get(0).getBytes());
    }

    public Certificate[] getChain() {
        return chain;
    }
}

Here the constructor retrieves the signer certificate and its certificate chain associated with the CSC credentials in question, and so implicitly checks that the client has access to those credentials. These certificates can be retrieved using the getChain method.

If the credentials have the sole control assurance level 2 (SCAL2), authorization to create a signature is linked to the documents to be signed: their to-be-signed hash values have to be sent along with the authorization request. Otherwise, for SCAL1, authorization can be given independent of the document to sign.

With this class a PDF can be signed with iText like this:

CODE
CscClient client = [... build and authenticate a CscClient instance ...];
CscCredentialsListResp credentials = client.listCredentials();

LavercaCscSignature signature = new LavercaCscSignature(client, credentials.credentialIDs.get(0), "SHA256withRSA");

try (   PdfReader pdfReader = new PdfReader("ToSign.pdf");
        OutputStream result = new FileOutputStream(new File("Signed.pdf"))) {
    PdfSigner pdfSigner = new PdfSigner(pdfReader, result, new StampingProperties().useAppendMode());

    IExternalDigest externalDigest = new BouncyCastleDigest();
    pdfSigner.signDetached(externalDigest, signature, signature.getChain(), null, null, null, 0, CryptoStandard.CMS);
}

And that’s it!

Specific examples

In this section we’ll use the IExternalSignature introduced above to sign PDFs using actual CSC services.

Methics CSC client test service

There is a test CSC service used in the unit tests of the Methics CSC client library. This service is simple, it works with plain, direct username/password authentication and only requires sole control assurance level 1.

You can sign a PDF using this service with iText and the IExternalSignature implementation above like this:

CODE
CscClient client = new CscClient.Builder().withBaseUrl(TestAuth.BASE_URL)
        .withTrustInsecureConnections(true)
        .withUsername(TestAuth.USERNAME)
        .withPassword(TestAuth.API_KEY)
        .build();
client.authLogin();
CscCredentialsListResp credentials = client.listCredentials();

LavercaCscSignature signature = new LavercaCscSignature(client, credentials.credentialIDs.get(0), "SHA256withRSA");

try (   PdfReader pdfReader = new PdfReader("ToSign.pdf");
        OutputStream result = new FileOutputStream(new File("Signed.pdf"))) {
    PdfSigner pdfSigner = new PdfSigner(pdfReader, result, new StampingProperties().useAppendMode());

    IExternalDigest externalDigest = new BouncyCastleDigest();
    pdfSigner.signDetached(externalDigest, signature, signature.getChain(), null, null, null, 0, CryptoStandard.CMS);
}

The service address, user name and password are contained in their TestAuth unit test to which the code here refers.

Digidentity pre-production environment

The previous example was quite simple, in particular as merely username/password authentication was required for signing. This example, though, implements a complex, real-world, eIDAS conforming use case.

This complexity in particular becomes apparent in the authentication and authorization process.

Authentication and authorization

When onboarding, Digidentity provides you with client credentials which include a client ID, a scope, and a secret. Furthermore, they ask you to install an app. This app has two functions; on one hand you use it for identifying yourself to Digidentity; and on the other hand it serves as the second factor during authorization.

Digidentity supports two approaches to initiate authorization, the “Web login“ and the “Passwordless login“. The former requires a callback URL via which Digidentity can send an authorization code back to one’s application. The latter polls Digidentity for the authorization code. As signing applications often are hidden behind firewalls in private networks and are difficult to reach by callback URL, this example uses the second approach.

Using the “Passwordless login“ to authenticate and authorize for signing, your code has to follow these steps:

  • Send a service request to retrieve the URI of a QR code; this request includes your client id and scope.

  • Retrieve and display the web page indicated by that URI; the page shows a QR code.

  • Start polling the service for an access token associated with this URI and QR code; the poll request includes your client id and secret.

  • [The user now scans the QR code which starts up the app you have been asked to install; here the user enters their PIN.]

  • After the user has correctly entered the PIN, the polling returns a success with an access token for Digidentity services.

  • Send a service request to retrieve a CSC token for the access token.

This CSC token now can be used for regular CSC API signing operations. The user, though, cannot yet pocket their smart phone or tablet, when the application sends the actual signing request, the app will ask the user to confirm.

This authentication and authorization prelude is not implemented in the Methics CSC client library. Thus, we have packed the code required for it into the Authorization helper class, see below.

Signing

With the Authorization helper class, you can sign a PDF using the Digidentity signing service with iText and the IExternalSignature implementation above like this:

JAVA
Authorization authorization = new Authorization()
        .withScope(SCOPE)
        .withClient(CLIENT)
        .withSecret(SECRET);
String qrCodeUri = authorization.retrieveQrCodeUri();
Desktop.getDesktop().browse(new URI(qrCodeUri));
authorization.pollAuthorization(2000);
String cscToken = authorization.retrieveCscToken();
CscClient client = new CscClient.Builder().withBaseUrl(Authorization.CSC_API_BASE_URL)
        .withTrustInsecureConnections(true)
        .build();
Authorization.injectCscToken(client, cscToken);;
CscCredentialsListResp credentials = client.listCredentials();

LavercaCscSignature signature = new LavercaCscSignature(client, credentials.credentialIDs.get(0), "SHA256withRSA");

try (   PdfReader pdfReader = new PdfReader("ToSign.pdf");
        OutputStream result = new FileOutputStream(new File("Signed.pdf"))) {
    PdfSigner pdfSigner = new PdfSigner(pdfReader, result, new StampingProperties().useAppendMode());

    IExternalDigest externalDigest = new BouncyCastleDigest();
    pdfSigner.signDetached(externalDigest, signature, signature.getChain(), null, null, null, 0, CryptoStandard.CMS);
}

Here we use Desktop.getDesktop().browse to display the web page with the QR code. Depending on your use case you may want to use different means, e.g. some web control of the UI toolkit of your choice.

The helper class

This is the helper class providing the functionality required for the authentication and authorization with the Digidentity services.

JAVA
public class Authorization {
    //
    // Constructors
    //
    public Authorization() {
        this(new OkHttpClient());
    }

    public Authorization(OkHttpClient okHttpClient) {
        this.okHttpClient = okHttpClient;
    }

    //
    // Credentials
    //
    public Authorization withScope(String scope) {
        this.scope = scope;
        return this;
    }

    public Authorization withClient(String client) {
        this.client = client;
        return this;
    }

    public Authorization withSecret(String secret) {
        this.secret = secret;
        return this;
    }

    //
    // QR code URL retrieval
    //
    public String retrieveQrCodeUri() throws IOException {
        return retrieveQrCodeUri(OAUTH2_AUTHORIZE_URL);
    }

    public String retrieveQrCodeUri(String oAuth2AuthorizeUrl) throws IOException {
        Request request = new Request.Builder()
                .url(oAuth2AuthorizeUrl + "?client_id=" + client + "&scope=" + scope + "&response_type=code")
                .method("GET", null)
                .build();
        Response response = okHttpClient.newCall(request).execute();

        QrCodeUriResp qrCodeUriResp = QrCodeUriResp.fromResponse(response, QrCodeUriResp.class);

        if (qrCodeUriResp.data == null)
            throw new IOException("OAUTH2 AUTHORIZE Response: Missing or malformed data element");
        if (qrCodeUriResp.data.id == null)
            throw new IOException("OAUTH2 AUTHORIZE Response: Missing or malformed data.id element");
        if (!"passwordless_login_session".equals(qrCodeUriResp.data.type))
            throw new IOException("OAUTH2 AUTHORIZE Response: Missing or unexpected data.type element: " + qrCodeUriResp.data.type);
        if (qrCodeUriResp.data.attributes == null)
            throw new IOException("OAUTH2 AUTHORIZE Response: Missing or malformed data.attributes element");
        if (!"qr_code".equals(qrCodeUriResp.data.attributes.type))
            throw new IOException("OAUTH2 AUTHORIZE Response: Missing or unexpected data.attributes.type element: " + qrCodeUriResp.data.attributes.type);
        if (qrCodeUriResp.data.attributes.qr_code_uri == null)
            throw new IOException("OAUTH2 AUTHORIZE Response: Missing or malformed data.attributes.qr_code_uri element");

        authorizationCode = qrCodeUriResp.data.id;
        return qrCodeUriResp.data.attributes.qr_code_uri;
    }

    //
    // Polling for the Digidentity authorization token
    //
    public void pollAuthorization(long pollInterval) throws IOException {
        pollAuthorization(OAUTH2_TOKEN_URL, pollInterval);
    }

    public void pollAuthorization(String oauth2TokenUrl, long pollInterval) throws IOException {
        if (pollInterval < 0)
            pollInterval = 5000;

        for (;;) {
            try {
                Thread.sleep(pollInterval);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }

            RequestBody body = new MultipartBody.Builder().setType(MultipartBody.FORM)
                    .addFormDataPart("code", authorizationCode)
                    .addFormDataPart("grant_type", "authorization_code")
                    .build();
            Request request = new Request.Builder()
                    .url(oauth2TokenUrl)
                    .method("POST", body)
                    .addHeader("Authorization", Credentials.basic(client, secret))
                    .build();
            Response response = okHttpClient.newCall(request).execute();

            if (response.code() == 200) {
                TokenResp digidentityToken = TokenResp.fromResponse(response, TokenResp.class);
                accessToken = digidentityToken.access_token;
                refreshToken = digidentityToken.refresh_token;
                break;
            }
            if (response.code() == 400) {
                CscErrorResp errorResp = CscErrorResp.fromResponse(response);
                if ("session_not_found".equals(errorResp.error_description)) {
                    throw new IOException("OAUTH2 TOKEN Response: Session timeout OR non-existent session");
                }
                if (!"login_pending".equals(errorResp.error_description)) {
                    throw new IOException("OAUTH2 TOKEN Response: Unexpected error: " + errorResp.error_description);
                }
            } else {
                throw new IOException("OAUTH2 TOKEN Response: Unexpected response: " + response);
            }
        }
    }

    //
    // retrieve CSC token
    //
    public String retrieveCscToken() throws IOException {
        return retrieveCscToken(DIGIDENTITY_API_BASE_URL);
    }

    public String retrieveCscToken(String digidentityApiBaseUrl) throws IOException {
        RequestBody body = new MultipartBody.Builder().setType(MultipartBody.FORM)
                .addFormDataPart("access_token", accessToken)
                .build();
        Request request = new Request.Builder()
                .url(digidentityApiBaseUrl + "/api/esign/tokens")
                .method("POST", body)
                .build();
        Response response = okHttpClient.newCall(request).execute();

        TokenResp tokenResp = TokenResp.fromResponse(response, TokenResp.class);
        if (tokenResp.access_token == null)
            throw new IOException("API ESIGN TOKENS Response: Missing token");

        return tokenResp.access_token;
    }

    static void injectCscToken(CscClient cscClient, String accessToken) throws IllegalAccessException, NoSuchFieldException {
        Field accessTokenField = CscClient.class.getDeclaredField("access_token");
        accessTokenField.setAccessible(true);
        accessTokenField.set(cscClient, accessToken);
    }

    //
    // variables and constants
    //
    public final static String OAUTH2_AUTHORIZE_URL = "https://auth.digidentity-preproduction.eu/oauth2/authorize.json";
    public final static String OAUTH2_TOKEN_URL = "https://auth.digidentity-preproduction.eu/oauth2/token.json";
    public final static String DIGIDENTITY_API_BASE_URL = "https://esign.digidentity-preproduction.eu";
    public final static String CSC_API_BASE_URL = "https://esign.digidentity-preproduction.eu";

    final OkHttpClient okHttpClient;

    String scope;
    String client;
    String secret;

    String authorizationCode;
    String accessToken;
    String refreshToken;

    //
    // classes for wrapping JSON data objects
    //
    static class TokenResp extends GsonMessage {
        @SerializedName("access_token")
        public String access_token;

        @SerializedName("refresh_token")
        public String refresh_token;

        @SerializedName("scope")
        public String scope;
 
        @SerializedName("token_type")
        public String token_type;

        @SerializedName("expires_in")
        public int expires_in;
    }

    static class QrCodeUriResp extends GsonMessage {
        @SerializedName("data")
        public QrCodeUriRespData data;
    }

    static class QrCodeUriRespData extends GsonMessage {
        @SerializedName("id")
        public String id;

        @SerializedName("type")
        public String type;

        @SerializedName("attributes")
        public QrCodeUriRespDataAttributes attributes;
    }

    static class QrCodeUriRespDataAttributes extends GsonMessage {
        @SerializedName("type")
        public String type;

        @SerializedName("qr_code_uri")
        public String qr_code_uri;
    }
}

Injecting the CSC token into the CscClient instance here requires Java reflection, see the method injectCscToken. A feature request in that regard has been filed with the CSC client project, and a pull request for it is soon to be created. Thus, soon this reflection based code will not be necessary anymore.

JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.