A Trust Service Provider (TSP) is a component or service that provides trust-related functionalities for use in electronic signing. The responsibilities of a TSP typically include the management and validation of digital certificates, authentication and authorization of end users (signers) for the use of their private keys, and the production of signatures based on these resources. Together these capabilities help to ensure the integrity and authenticity of data.
TSP Proxy is a server-side component of the OneSpan Sign (OSS) platform which dispatches signing requests to remote External Connector (EC) services. Each EC acts as an intermediary between OSS and an arbitrary TSP. Integrators are responsible for implementing and hosting their own EC, which must conform to an API described by this document and encapsulate all of the concerns associated with the relevant TSP. TSP Proxy also provides an API which can be used by the invoked EC to retrieve a document hash and finalize the signature produced by the TSP. Once a robust production service meeting these requirements has been produced, it goes through a testing and approval process, after which its configuration is deployed to OneSpan Sign for use by TSP Proxy. In this way, OneSpan Sign can be integrated with a TSP of arbitrary design.
To successfully integrate your External Connector with the OneSpan Sign platform, you will need to focus on two main areas. The first is decrypting, verifying, and processing the request token, and the second is retrieving an access token from the TSP Proxy OAuth2 service. The former represents the signing request forwarded to the EC by TSP Proxy, and the latter is required for interacting with the TSP Proxy API.
TSP Proxy Workflow
Request Token Decryption and Verification
The request token has a nested structure. The outer layer is a standard JSON Web Encryption (JWE) token that is encrypted with AES256. The integrator receives a 256-bit secret used to decrypt the JWE. Once the EC has decrypted the request, it must carefully evaluate the correctness of a number of claims within the enclosed JSON Web Signature (JWS) token and verify its signature with an online RSA public key. These verification steps are essential and are discussed in greater detail below.
The following is a simplified example of how a request token can be decrypted and validated in Java. You can use this code as a reference to implement solutions in other programming languages.
To ensure your code runs smoothly, make sure that you have added the following dependencies to your project:
<dependencies>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>8.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
The example code:
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.AESDecrypter;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jwt.SignedJWT;
import com.onespan.external_connector.ec_mockca.model.MiniToken;
import net.minidev.json.JSONObject;
import org.springframework.web.reactive.function.client.WebClient;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.interfaces.RSAPublicKey;
import java.text.ParseException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
public class RequestTokenProcessingExample {
public static void main(String[] args) throws ParseException, JOSEException, JsonProcessingException {
String requestTokenStr = "the request token";
String sharedSecret = "a shared secret";
String targetEnv = "https://sandbox.e-signlive.com";
String jwksUrl = targetEnv + "/rs/oauth2/jwks";
String jtspUrl = targetEnv + "/rs";
String yourInitUrl = "https://your-domain.com/ec/init";
// 1. generate a symmetric key from the shared secret
SecretKey key = new SecretKeySpec(sharedSecret.getBytes(), "AES");
// 2. parse and decrypt the outer JWE layer of the request
JWEObject jweObject = JWEObject.parse(requestTokenStr);
EncryptionMethod enc = jweObject.getHeader().getEncryptionMethod();
assertEquals(EncryptionMethod.A256GCM, enc);
jweObject.decrypt(new AESDecrypter(key));
// 3. extract and process inner JWS layer of the request
SignedJWT jwt = jweObject.getPayload().toSignedJWT();
// 3.1. verify the signing algorithm
JWSAlgorithm algo = jwt.getHeader().getAlgorithm();
assertEquals(JWSAlgorithm.RS256, algo);
// 3.2. verify that the JWS has not expired
Date exp = jwt.getJWTClaimsSet().getExpirationTime();
assertTrue(exp.after(new Date()));
// 3.3. obtain the public key from the OneSpan-hosted JWKS
WebClient httpClient = WebClient.builder().build();
String response = httpClient.get()
.uri(jwksUrl)
.retrieve()
.bodyToMono(String.class)
.block();
JWKSet jwks = JWKSet.parse(response);
JWK jwk = jwks.getKeyByKeyId(jwt.getHeader().getKeyID());
RSAPublicKey publicKey = ((RSAKey)jwk).toRSAPublicKey();
// 3.4. verify the signature with the public key
JWSVerifier verifier = new RSASSAVerifier(publicKey);
assertTrue(jwt.verify(verifier));
// 3.5. verify the issuer and audience
String iss = jwt.getJWTClaimsSet().getIssuer();
assertEquals(jtspUrl, iss);
List<String> auds = jwt.getJWTClaimsSet().getAudience();
assertTrue(auds.contains(yourInitUrl));
// 4. extract the MiniToken claim from the request
JSONObject mt = (JSONObject)jwt.getJWTClaimsSet().getClaim("MiniToken");
ObjectMapper mapper = new ObjectMapper();
MiniToken minitoken = mapper.readValue(mt.toJSONString(), MiniToken.class);
Map<String, String> params = minitoken.getContents();
assertFalse(params.isEmpty());
}
}
The MiniToken class:
@Data
public class MiniToken {
private String sid;
private String lang;
private String callbackUrl;
private String otp;
private Map<String, String> contents;
}
Verification
Verification of the request token is mandatory. The following items need to be verified:
JWE Encryption Method: Expected to be A256GCM.
JWS Signing Method: Expected to be RS256.
Token Expiration: Verify that the request token has not expired.
Token Signature: Verify that the signature of the request token is valid.
Token Issuer: The base URL of the target OneSpan Sign environment followed by "/rs".
Token Audience: The entry point URL of the EC as provided to OneSpan should appear in this list.
If any of these checks fail, the request should be rejected.
Access Token
The TSP OAuth2 service only supports the Client Credential grant type. After the EC has been successfully authenticated, it will receive an access token.
Authentication
An EC needs credentials to obtain an access token for utilizing the TSP Proxy API. These credentials consists of a Client ID and Client Secret, and will be provided after you complete your registration with OneSpan successfully.
Authorization
The EC will receive a specific Scope to utilize the assigned TSP Proxy API.
To obtain the access token, perform an HTTP request similar to the one below:
curl --location --request POST 'https://{hostname}/oauth2/token' \
--header 'Authorization: Basic ${base64(clientId:clientSecret)}' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'otp=${OTP from MiniToken}' \
--data-urlencode 'scope=${assigned scope}'
This call can only be successfully performed a single time per request token, as the OTP (One Time Passcode) the request token provides will be invalidated once the access token is granted. Therefore, it is crucial for the EC to store the access token until the signing workflow is complete.
Verification
The verification against the access token is optional. The following items can be verified.
Token Expiration: Verify whether the token has expired.
Token Verification: Ensure the token is valid and trustworthy. This can be done similarly to how JWT is validated above.
Token Provider: Should be the base URL of the target environment followed by "/rs".
OAuth2 API
GET/rs/oauth2/token to get an access token.
POST/rs/oauth2/jwks to get a JWKS (JSON Web Key Set) from which the public key can be obtained.
Proxy API
GET/rs/proxy/api/hash to get a document hash
POST/rs/proxy/api/inject to inject the signed document hash (signature) into the document (pdf)
For more detailed information on the Proxy API, see https://sandbox.esignlive.com/rs/proxy.
Signing Process
The actual signing process occurs during steps 4-6 of the diagram above.
To complete a signing process, you must do the following, in order:
Obtain a document hash
Sign the document hash
Inject the signed document hash into the document
Obtaining a Document Hash
To obtain a document hash the following data needs to be provided in the request body:
SID: Session ID provided by the MiniToken;
Signature Type: PKCS1, PKCS7, or CMS;
Signer Certificate: Must be in PEM format. The signer certificate can have a header and footer or no header and footer, but it cannot have just a header, or just a footer.
For example:
{
"sid": "string",
"lang": "string",
"content": {
"signatureType": "PKCS1",
"signerCert": "string"
}
}
Then, make a POST call with the request body above to /proxy/api/hash
. The following response will be returned:
{
"sid": "string",
"lang": "string",
"result": {
"hash": "string"
}
}
The document hash is in the hash field. The hash uses RSA SHA256.
Signing a Document Hash
This step needs to be performed by a Certificate Authority, and each Certificate Authority has their own workflow. Refer to your organization's Certificate Authority process to sign the document hash.
The TSP Proxy only supports PKCS1, PKCS7, and CMS signature types.
Injecting a Signature
As soon as the document hash is signed, the signed hash can be sent to TSP Proxy for injection. To do this, the following data needs to be prepared:
Signature Type: Must be PKCS1 or PKCS7 and must be the same value as the previous call.
Signed Hash: The signed document hash.
For example:
{
"sid": "string",
"lang": "string",
"content": {
"signatureType": "PKCS1",
"signedHash": "string"
}
}
Then, make a POST call with the request body above to /proxy/api/inject
. The following response will be returned:
{
"sid": "string",
"lang": "string",
"result": {
"status": "string",
"returnUrl": "string"
}
}
If the status
" field has a value other than success, then this call needs to be re-tried.
The returnUrl
field provides a redirection URL to bring signer back to the beginning of the signing process.
Appendix
Example request token
The following is an example of a request token JWE, as it would appear when received by an EC.
eyJlbmMiOiJBMjU2R0NNIiwidGFnIjoiTHFRVHUwN2xocVpISHl6TF9qT0pBZyIsImFsZyI6IkEyNTZHQ01LVyIsIml2IjoiVHFHYjFvRFFtbkhOZXVBVyJ9.5POunw5dPvtem2MHxJsNodj_iHtqkFoL4c6cBIpy4iA.Q1d3XKUMB3WsAghA.rOE6xDKdMjYzEyQgQvcdqszGVeC-OFh74ue-MpfTlxYyZVsmUGjOo62eWOSJ_lfBDngaWycyziPeDTzUtMV63uVM-m5TanIcQC5Za3uYHGqDkgFHMyCRlZiVWDtgKCL9SDGpAkVzq6jtAKu6xeN5F-0DGZVKLMUHtKBpoJ8DaTXos275OpsD9fu_WykTJqoMAoTQy6KwjE2BurjW1eikOtoIS0CxobrpzsP_FkEeR7W0OQKC6U_VVk-5SO4Az5HWH96YtZIolYmw5TBhN0E45mkllMrgQD9ZHIWoMNOAjfS-G6SP2tmxT4Clpu0OQ3ycMtJHXqAGVIwJxJ-i_8L79H-u7j4YsVoH4_QuOK_trU7SI0M_HtrZB86qdseQUmehgrLjfa0kFGlQnIMuaRc-WLBJXT8VBdUaSJSS8xJSiesVBK6XjGR8bGNwfaMsUn86kACrQd26ftbH6d7SnYE2GLYQvSxIyUaPK5DczWTXztN1gVAuWZvDlEHZlkhbNXTrENRunnQn_Ekk4-bDC44bYerhIWk3BRNKNK7BMZL-SqppJu9pTL3ZaDMgDn4gRF6Xele1oMAPEw85XAQJIQzZwhJkpx-t6_fZiZcW1NAmcdN0jMl90FYsA9eVlJfmxIiGtYBLYxF67xUddwExUyawHYI-wDs7wfmZO2k8cVFycZFf-x68u-jIG-a_BYWBP5uGO0h8iwn8J-UUwDmTSUEE2n_2ZHF9mtakuhZV_-G9ld1ZMRPi-hVdcQUp_1lvsyVO1Efizy4mRuuoMvXsWDc-W7tM5IdPxLCT7GtgJXTeZOHdrF4zuZ4zZAOT4-zMfxDhlkXLpTCZQtI-FOFCaeGgxc4oxVrVwkt75rTY3WhRQiohyNP8LLBTcKpoiWamNo3Y6vaIFSIW9JWtUKRbU6fpWcDi-0TFB7x7x9DqXe9vHE1r73haa9Dwye_YQy_dmIfOQIK04GCbE96D8kH_LPHrNhiVlBBEkWJhe6TJ0H9K5En5oIYfhGs37uolPaW7cVxD32ZgvxEM6XVZNYs-qJSYRg3-_n5ImqYdCfzZDF7TETAfQJfE_jrTRmuNi22Fis9jPPxp9BAWUk2Hk0i5A_K4rZJEK-3N_is42dYtXc3l6A_o1lmKiX7sQ9eYB9G5VAPdD5nbo0FjHCMflWT_ymp98BPl044jiIeNKtbePQis_4vD5T6v1gbyIpURJ9YmqCm29k0G4S7HkpS9xeePsdahuv8cGRo4i9esFjr5CxXREB_nS-vyFgWAj1fD7aHbQ2ZbtpU9MAw0O0yCqUHQ8rcdUXmwktNmxa1aP5nExSTtRk6hNQP3JkgYpA5QGjGtzbqrEcJouw-K97oxz4ETOBENk25ZvI5Rv5Lqx5q5RC91xZEUnqhk1u4osXwDAYrex_YTz-o5ghtp4zBgT08iZftZ6ZEvvYPOmlHq8GNAxKcPZkGcJceIj-WExE8ugOTF9R-M3gVPmC2WmBBK5r-omlN7mCebl5CQnrvWsnOYT5VjIzh0wyhciP3zo5TUyyEKv3wL6r48NFWN3Czj0U7WZPtYJvaouci-gfnGoNhlRMxXUZvIWYqbawbsJ--FiyBDVb82xjWWqYeyfuUJ_c4yK1soY6PuJxuFagvBCdDnibvfZePcJVMWaWv8pLKna9uUNQ6hWI36-BreTQ-0t6sb0pa5TQkmbhP8ieVOciQ8e4tPV72OjU9ElTBclCY80JXaLuNr50dDIyw-7eI8l_jS.PHmauhRpPtD4-BLTmafCSw
Header after decoding
{
"enc": "A256GCM",
"tag": "LqQTu07lhqZHHyzL_jOJAg",
"alg": "A256GCMKW",
"iv": "TqGb1oDQmnHNeuAW"
}
JWS extracted from the decrypted JWE
eyJraWQiOiJjNjQzNjk5MC05YjMzLTRiZjEtYTI0Ny1kMTNlYjMwNDMyZjMiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL29zc3EzLnJuZC5lc2lnbmxpdmUuY29tL3JzIiwiYXVkIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6OTg5OS9lYy9pbml0IiwiTWluaVRva2VuIjp7InNpZCI6ImE0Y2E0Zjc3MGU4ZTZkMDg5ZjhmZjFiMmUxMmZlYTNiMTg3N2NjNzE0YzcyZGYzMmIyYWE0NmRjZTc0YmZkMzkiLCJsYW5nIjoiZW4iLCJjYWxsYmFja1VybCI6Imh0dHBzOi8vb3NzcTMucm5kLmVzaWdubGl2ZS5jb20vdHJhbnNhY3Rpb24vWDdhT1BQTldSQkQzb1l4ZGFyMjlaT1ZvRUtVPS9zaWduP2luaXRpYWxEb2N1bWVudElkPTI5Mzc0MWRiMzg4ZjQ1MGE1N2U4YTRjMWZmMzViZTg3ZjI3M2I1NDBhNDBmMGM5MCZzaWduZXJJZD1kOTNhdU1WUlNyOFImZG9jdW1lbnROYXZPcGVyYXRpb249bmV4dCIsIm90cCI6IjZiZWEzYmQzLWRmNTQtNDVlYy04ODZiLTYyMTM5ZTE2ZWZkOSIsImNvbnRlbnRzIjp7ImRvY05hbWUiOiJUZXN0X0RvY3VtZW50X0VtcHR5Iiwicm9sZUlkIjoibkt1dXgxNjNpQnNNIiwiZG9jSWQiOiIyOTM3NDFkYjM4OGY0NTBhNTdlOGE0YzFmZjM1YmU4N2YyNzNiNTQwYTQwZjBjOTAiLCJ0cmFuc0lkIjoiWDdhT1BQTldSQkQzb1l4ZGFyMjlaT1ZvRUtVPSIsInNpZ25hdHVyZVR5cGUiOiJQS0NTMSJ9fSwiZXhwIjoxNzQyMjQzODE3LCJpYXQiOjE3NDIyNDM1MTd9.aM54pCQ_RyEKIFxqzeY-KGvML7s-Z1Q7NXE-VqV1O1HuJU__KU_77bOEJuBw7bQbQPk_6sTSWrwPZ71V9QBxMT6KFu0zUGfTxMcN90x9gyYN1QMshQDhv8wUjvCk9KmIjLtMxaqGAPS0BD5xxpHt9a9B2D9aGhqAhqYMZx20GMlTjVLkyEi40IG5kU9s5UYSxbg9RgFGSV_55kK3B3vLQRiGV2qqAVSzmVCyB4lPKDQNViiOZyyweaaLx0Ri7d1AmYU5NTRV8DrtEcFbF4Uq-VxdWf6MZrFnlI9UFLdGcr89V4Q3zG2QPWtSftp5hyW0ldZvBD1Pt-JN_s-AItig6w
Header after decoding
{
"kid": "c6436990-9b33-4bf1-a247-d13eb30432f3",
"alg": "RS256"
}
Payload after decoding:
{
"iss": "https://ossq3.rnd.esignlive.com/rs",
"aud": "https://localhost:9899/ec/init",
"MiniToken": {
"sid": "a4ca4f770e8e6d089f8ff1b2e12fea3b1877cc714c72df32b2aa46dce74bfd39",
"lang": "en",
"callbackUrl": "https://ossq3.rnd.esignlive.com/transaction/X7aOPPNWRBD3oYxdar29ZOVoEKU=/sign?initialDocumentId=293741db388f450a57e8a4c1ff35be87f273b540a40f0c90&signerId=d93auMVRSr8R&documentNavOperation=next",
"otp": "6bea3bd3-df54-45ec-886b-62139e16efd9",
"contents": {
"docName": "Test_Document_Empty",
"roleId": "nKuux163iBsM",
"docId": "293741db388f450a57e8a4c1ff35be87f273b540a40f0c90",
"transId": "X7aOPPNWRBD3oYxdar29ZOVoEKU=","signatureType": "PKCS1"
}
},
"exp": 1742243817,
"iat": 1742243517
}
A typical JWKS
{
"keys": [
{
"kty": "RSA",
"e": "AQAB",
"kid": "c6436990-9b33-4bf1-a247-d13eb30432f3",
"n": "1WzJWdMmvdkah4szAo6t0VA5jEGNG6KFSMwB7ZLNAyQR2zfZ-HTYCfZGbC-BOnyodzsVoBA6MCWHak1nN098ZTrp0rWFz3WeeYRPYaJGLzmoeKzhoFyEg2WVYXzHBJfn95vYfgT1POhuCDofZnuZl5CpdOUxN6E-MOU6asZ7JtPwluqlMxeJH1CXr5hKdvy3GC3aOXwWdQw5edloGEglPmHpMGuXH667-mDPFSfGISaBn5qIqsDLXPLujim_Cgfuvm7kxAqjmxOjQPInsw6lpdOHc-EZpHLHoIzxtafPzX7TYCUmjv3Bkpe3teBJMHGp0Y_YCn97DZdLbOeyskpWdQ"
}
]
}
A typical access token
eyJraWQiOiJjNjQzNjk5MC05YjMzLTRiZjEtYTI0Ny1kMTNlYjMwNDMyZjMiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJwcm94eS1vbmVzcGFuIiwiYXVkIjoiaHR0cHM6Ly9vc3NxMy5ybmQuZXNpZ25saXZlLmNvbS9ycy9wcm94eSIsIm5iZiI6MTc0MjI0MzUyNiwic2NvcGUiOlsicHJveHkuYXBpIl0sImlzcyI6Imh0dHBzOi8vb3NzcTMucm5kLmVzaWdubGl2ZS5jb20vcnMiLCJleHAiOjE3NDIyNDUzMjYsImlhdCI6MTc0MjI0MzUyNiwianRpIjoiMDk2YTIwMTUtYTY1ZC00YTc3LWI5ZjEtOWNkYzg2NWU2MDhlIn0.0NidkTxNEdidO4jx50sD5tnwMSX6Jhy9ggIGG34fBhr33w6OHsqKFtRAfo7xAUOysCyk3Flw8ZgsUKFDmich1rYznu5Zwm6xA-3O4d_LlDm7-dDRzR_sECrBagyllwZQVXHmvuSOha63W-PBkjixxn3UwpK93pjSyXKCBSrhFCwzx63gKsd0jPaN_DP7mDJ8jBSGbfEfCCbEkCduU1-zNCSfAFWrEuKvKzdUgV8hoOn9Dzs48vTuTih2PCPQRIEyNnpn3A2mH10D3-TIHcBsAFFNg5QK7A8LTcWu5C9aCgCaF9zKjEYspUArzzxWSfMHAJJ5E5slSnmwwQIKSdFmbQ
Header after decoding
{
"kid": "cb6ffa24-bcaf-4aae-904b-a0ce979b846d",
"alg": "RS256"
}
Payload after decoding
{
"sub": "proxy-onespan",
"aud": "https://domain.com/rs/proxy",
"nbf": 1742243526,
"scope": [
"proxy.api"
],
"iss": "https://domain.com/rs",
"exp": 1742245326,
"iat": 1742243526,
"jti": "096a2015-a65d-4a77-b9f1-9cdc865e608e"
}