Sample code

The sample code in this chapter assumes a REST application based on the Spring Boot framework.

The application follows the sequence diagram shown in the introduction chapter. It consists of six main REST entry points on the server controller:

  • the generate_challenge entry point that generates a random challenge to send to SCWS service for SCWS environment creation

  • the verify_cryptogram entry point that verifies the cryptogram returned by SCWS service for SCWS environment creation

  • the sign_env_challenge entry point that signs the challenge returned by SCWS service for SCWS environment creation (prerequisite for allowing the application to contact the SCWS)

  • the init_session entry point that simply instantiates a SCSSSession object, not yet bound to a specific client

  • the process_session entry point that takes care of the APDU command relaying mechanism

  • the exec_session entry point that proceed with the operation that must be actually made on the token, from the server side (this entry point contains the application-specific logic).

In order to make SCWS environment creation work, it is required to save in the project directory the ‘IdoptePublicKeys’ file (provided by Idopte) containing the list of public keys. This file should be parsed in the same way as in the verify_cryptogram implementation example. The index of the right public key to use in the file is returned by SCWS.findService function on cient-side.

Note that this implementation requires the Bouncy Castle API to work (used for PEM parsing).

Caution

Note that cryptogram verification and challenge signature are done in a unique function, which does not insure compatibility with middleware/SCWS API versions prior to 6.23.25.0 and 2.11, respectively. If you wish to insure compatibility with previous versions and do not need to use high level SCWS entry points (requestCertificates/requestPrivateKey and SignEncrypt entry points), you should use the old method for SCWS environment creation (one-sided authentication instead of mutual). You can find sample codes here: Old server-side code samples. If you do not need to insure compatibility and you wish to use the newest SCWS functions, you should use sample codes given below.

@RestController
public class ProcController implements ExternalAuthenticateHandler, MutualAuthenticateHandler {

        /* This is the private key for SCWS environment creation
        * (should correspond to the public key for which the webappcert has been generated)
        * Note: the webappcert is in the javascript code */
        private static final String privateKeyPEM =
                        "-----BEGIN RSA PRIVATE KEY-----\r\n" +
                        "MIIBOgIBAAJBAMPMNNpbZZddeT/GTjU0PWuuN9VEGpxXJTAkmZY02o8238fQ2ynt\r\n" +
                        "N40FVl08YksWBO/74XEjU30mAjuaz/FB2kkCAwEAAQJBALoMlsROSLCWD5q8EqCX\r\n" +
                        "rS1e9IrgFfEtFZczkAWc33lo3FnFeFTXSMVCloNCBWU35od4zTOhdRPAWpQ1Mzxi\r\n" +
                        "aCkCIQD9qjKjNvbDXjUcCNqdiJxPDlPGpa78yzyCCUA/+TNwVwIhAMWZoqZO3eWq\r\n" +
                        "SCBTLelVQsg6CwJh9W7vlezvWxUni+ZfAiAopBAg3jmC66EOsMx12OFSOTVq6jiy\r\n" +
                        "/8zd+KV2mnKHWQIgVpZiLZo1piQeAvwwDCUuZGr61Ap08C3QdsjUEssHhOUCIBee\r\n" +
                        "72JZuJeABcv7lHhAWzsiCddVAkdnZKUo6ubaxw3u\r\n" +
                        "-----END RSA PRIVATE KEY-----";

        /**
        * Entry point for generating a random challenge, for the initial environment creation.
        * (called by the client before SCWS.findService).
        * The function stores the generated challenge on server-side in a session variable.
        * @return the random challenge to send to local SCWS service through SCWS.findService.
        */
        @GetMapping("/generate_challenge")
        public String generateChallenge(HttpServletRequest request) {
                SecureRandom random = new SecureRandom();
                byte[] challenge = new byte[16];
                random.nextBytes(challenge);

                String challengeHex = Hex.toHexString(challenge);
                HttpSession session = request.getSession();
                session.setAttribute("challenge", challengeHex);

                return challengeHex;
        }

        /**
        * Entry point for verifying the cryptogram and generating the signature, for the initial environment creation.
        * (called by the client between SCWS.findService and SCWS.createEnvironment).
        * The function retrieves the original challenge to verify the cryptogram with using the session variable.
        * @param keyID an Integer (coming from the client-side SCWS) corresponding to the index of the public key to use for verification, as it is stored in the Idopte public keys file.
        * @param cryptogram the cryptogram to verify (coming from the client-side SCWS).
        * @param rnd the challenge to sign (coming from the client-side SCWS).
        * @return the cryptogram that will authorize environment creation.
        */
        @GetMapping("/verify_cryptogram_sign_env_challenge")
        public String verifyCryptogramSignChallenge(Integer keyID, String cryptogram, String rnd, HttpServletRequest request) {
                try {
                        // Parse IdoptePublicKeys file and retrieve the public key to use for verification
                        String pubKeysStr = new String(Files.readAllBytes(Paths.get("IdoptePublicKeys")));

                        Pattern pattern = Pattern.compile("-----BEGIN PUBLIC KEY-----(.|\\n|\\r)*?-----END PUBLIC KEY-----");
                        Matcher matcher = pattern.matcher(pubKeysStr);
                        List<String> matches = new ArrayList<>();
                        while (matcher.find()) {
                                matches.add(matcher.group());
                        }
                        if (keyID >= matches.size()) {
                                throw new Exception("Invalid key ID");
                        }
                        String publicIdoKeyPEM = matches.get(keyID);
                        if (publicIdoKeyPEM.contains("!REVOKED!")) {
                                throw new Exception("Public key revoked");
                        }

                        // Prepare public key
                        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
                        PemReader reader = new PemReader(new StringReader(publicIdoKeyPEM));
                        byte[] pubKey = reader.readPemObject().getContent();
                        X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(pubKey);
                        PublicKey publicKey = keyFactory.generatePublic(publicKeySpec);

                        // Retrieve challenge generated in generateChallenge stored in session variable
                        HttpSession session = request.getSession();
                        String challengeHex = session.getAttribute("challenge").toString();

                        // Verify the cryptogram
                        Signature verifier = Signature.getInstance("NONEwithRSA");
                        verifier.initVerify(publicKey);
                        verifier.update(Hex.decode(challengeHex));
                        boolean verification = verifier.verify(Hex.decode(cryptogram));
                        if (!verification) {
                                throw new Exception("Cryptogram verification failed");
                        }
                        else {
                                PrivateKey privateKey;
                                try (PEMParser pemParser = new PEMParser(new StringReader(privateKeyPEM))) {
                                        JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
                                        Object object = pemParser.readObject();
                                        KeyPair kp = converter.getKeyPair((PEMKeyPair)object);
                                        privateKey = kp.getPrivate();
                                }
                                Signature signer = Signature.getInstance("NONEwithRSA","BC");
                                signer.initSign(privateKey);
                                signer.update(Hex.decode(rnd));
                                byte[] signature = signer.sign();
                                return Hex.toHexString(signature);
                        }
                }
                catch (Exception e) {
                        e.printStackTrace();
                        throw new RuntimeException(e.getMessage(), e);
                }
        }

        /**
        * Create a {@link SCSSSession} object.
        * @return the handle of created {@link SCSSSession} object.
        * @throws SCMException
        * @throws IOException
        */
        @GetMapping("/init_session")
        public String startTest() throws SCMException, IOException {
                final SCSSSession s = new SCSSSession();
                return s.getHandle();
        }

        /**
        * Process session and allow communication between ServerSide SmartCard and ClientSide SmartCard middlewares.
        * @param id the handle of the {@link SCSSSession} object to communicate with.
        * @param body the body of HTTP request
        * @return the result of processed session.
        * @throws SCMException
        * @throws IOException
        */
        @PostMapping("/process_session/{id}")
        public String processSession(@PathVariable String id, @RequestBody String body) throws SCMException, IOException {
                return SCSSSession.process(id, body);
        }

        /**
        * Execute session. Once {@link SCSSSession} object is created and session is managed with {@link SCSSSession#process}, call this to use methods and attributes from
        * {@link Token} object.
        * @param id the handle of {@link SCSSSession} object.
        * @return some result
        * @throws SCMException
        * @throws IOException
        */
        @GetMapping("/exec_session/{id}")
        public String execSession(@PathVariable String id) throws SCMException, IOException {
                SCSSSession s = SCSSSession.getSession(id);

                try {
                        s.init();

                        /* -------------------------------------------------------------------- */
                        // here lies the server-side application logic.
                        // Operations are made by calling Token methods on the `s` object.
                        /* -------------------------------------------------------------------- */

                }
                catch (Exception e){
                        return "Operation failed";
                }
                finally {
                        s.close();
                }
                return "Operation OK";
        }

}

On the client, most of the logic is typically dedicated to establishing the connection with the SCWS and obtaining a Token instance from the reader (this process is done in the init() function below). The interesting bit is located at the end of the sample, in the test() function: this opens the server-side session.

window.webappcert = "4SyF8E6?J4{SVYI)psu*Xb0t#E=1oJdCH}B[Y8){oDbd%GP-L&ZI*KtJl@P?<:t$Tvv[NPS9>6Mqkpnmg^O1Zkn[Zo#czsNjEe:ASWVef!l-I$=e02=*YAmCkmvm1iifG])b[EFTe2>-:Hmk.KPTX9S0CQPVV41h3>S0>QyDEq/Bb?u#Zc^i7v]JDE@jgy@*bbc=59z!woxg6nRpqTxix&o8>5J8D6lzoLkuw=Q8MreUlGfHfBt(&q8!8Eh>BBL.6A)S.&IBkyKcR%$V@Zb$.!0aJjM1<iEMzoN)2!j^GtkU:d>A/Rx=u*@Q7W6$kk[zM";

window.connections = [];
window.connecting = false;

/* boilerplate function for performing a ajax request */
function req(method, url, contents) {
        return new Promise(function(resolve, reject) {
                var req = new XMLHttpRequest();
                req.onreadystatechange = function () {
                        if (this.readyState === 4) {
                                if (this.status === 200)
                                        resolve(req.responseText);
                                else
                                        reject(new Error("App server request failed"));
                        }
                };
                req.open(method, url, true);
                req.send(contents);
        });
}

function reqGet(url) {
        return req("GET", url);
}

function reqPost(url, contents) {
        return req("POST", url, contents);
}

/* connects to the SCWS, then tries to find smart cards in readers and connect to them */
function init() {
        reqGet("generate_challenge").then(function(generatedChallenge){
                /* connecting to SCWS */
                console.log("Connecting to SCWS...");
                SCWS.findService(window.webappcert, generatedChallenge).then(function(findServiceData) {
                        console.log("Connection to SCWS succeeded");
                        return reqGet("verify_cryptogram?keyID=" + findServiceData.keyID + "&cryptogram=" + findServiceData.cryptogram).then(function(res) {
                                return reqGet("sign_env_challenge?rnd=" + findServiceData.challenge).then(function(signature){
                                        return SCWS.createEnvironment(signature);
                                });
                        });
                })
        }).then(function() {
                console.log("SCWS environment created successfully");
                /* check all readers already connected */
                var readers = SCWS.readers;
                if (readers.length == 0) {
                        console.log("No readers found");
                }
                else {
                        for (var i = 0; i < readers.length; i++)
                                checkReader(readers[i]);
                }
                document.getElementById("initbtn").style.display = "none";
                updateStatus();
                /* register event callbacks for checking readers changes */
                SCWS.onreaderadded = function(reader) {
                        console.log("Reader inserted: '" + reader.name + "'");
                        checkReader(reader);
                }
                SCWS.onreaderremoved = function(reader) {
                        console.log("Reader removed: '" + reader.name + "'");
                        checkReader(reader);
                }
                SCWS.onreaderstatechanged = function(reader) {
                        console.log("Reader state changed: '" + reader.name + "'");
                        checkReader(reader);
                }
        }).catch(function(err) {
                console.log("ERROR: " + err.message);
        });
}

function updateStatus()
{
        // update GUI elements to show connection state
}

/**
* Called when a reader needs to be scanned for objects.
*/
function checkReader(reader) {
        /* trying to find reader from existing connections */
        for (var connidx = 0; connidx < window.connections.length; connidx++) {
                if (window.connections[connidx].reader == reader)
                        break;
        }
        if (connidx == window.connections.length) {
                if (reader.status == "ok" && reader.cardPresent) {
                        /* reader not found in existing connections, and reader contains card: connecting */
                        console.log("Connecting to reader '" + reader.name + "'...");
                        var connection = { reader: reader };
                        window.connections.push(connection);
                        window.connecting = true;
                        updateStatus();
                        reader.connect().then(function(token) {
                                connection.token = token;
                                connection.items = [];
                                console.log("Getting objects from reader '" + reader.name + "'...");
                                return token.getObjects();
                        }).then(function(items) {
                                connection.items = items;
                                window.connecting = false;
                                updateStatus();
                        }).catch(function(err) {
                                console.log("ERROR: " + err.message);
                                window.connecting = false;
                                updateStatus();
                        });
                }
        }
        else {
                var conn = connections[connidx];
                if (reader.status != "ok" || !reader.cardPresent) {
                        /* reader found in existing connections, but contains no card: removing */
                        if (conn.token) {
                                console.log("Disconnecting from reader '" + reader.name + "'.");
                                conn.token.disconnect();
                        }
                        window.connections.splice(connidx, 1);
                        updateStatus();
                }
        }
}

function showError(err) {
        document.getElementById("errmsg").textContent = err.message || err;
}

/* Function that actually opens the server session */
function test() {
        /* first, create the SCSSSession instance on the server */
        reqGet("init_session").then(function(sessHandle) {
                /* process the session (session handle is used in the URL) */
                window.connections[0].token.processServerSession("process_session/" + sessHandle).then(function() {
                        console.log("OK");
                        /* at that point, the session is fully completed */
                }, function() {
                        console.log("error");
                });
                /* concurrently with the processServerSession call, the REST entry point that actually performs the operation is called, also giving the session handle */
                reqGet("exec_session/" + sessHandle).then(function(result) {
                        console.log(result);
                        /* although the REST operation is completed, the session may not be yet finished at that point */
                });
        });
}