Singpass Developer Docs
Legacy Myinfo v3/v4
Legacy Myinfo v3/v4
  • Legacy Myinfo v3/v4
  • Data Catalog
  • Key Principles
  • Technical Specifications
    • Myinfo v4
      • Difference between v3 and v4
      • Technical Guidelines
      • Technical Concepts
        • OAuth 2.1 Concepts
        • Proof of Key Code Exchange (PKCE)
        • JSON Web Token (JWT)
        • Client Assertions
        • JSON Web Key Store (JWKS)
        • Demonstration of Proof-of-Possession (DPoP)
      • API Specifications
      • Tutorials
        • Tutorial 1: Myinfo Person sample Data
        • Tutorial 2: End-to-end Integration with Myinfo v4 APIs
      • Resources
        • Myinfo Connectors
        • Error Codes
      • FAQ
    • Myinfo v3
      • Technical Guidelines
      • API Specifications
      • Latest X.509 Public Key Certificate
      • Tutorials
        • Tutorial 1: Basic Person API
        • Tutorial 2: Using OAuth2
        • Tutorial 3: Implementing PKI Digital Signature
      • Resources
        • Myinfo Connectors
        • Error Codes
      • FAQ
Powered by GitBook
On this page
  • 1. Enable PKI digital signature on our Sample application
  • 1.1 Modify configuration
  • 2. Trigger MockPass Login and Consent (authorise API)
  • 3. Call the Token API
  • 3.1 Signing Your Requests
  • 3.2 Sending the Request
  • 3.3 Validating the Response
  • 4. Call the Person API
  • 4.1 Send Request
  • 4.2 Decrypt & Verify the Person Response
  • 5. Use the JSON object to Prefill Form
  • Summary

Was this helpful?

  1. Technical Specifications
  2. Myinfo v3
  3. Tutorials

Tutorial 3: Implementing PKI Digital Signature

PreviousTutorial 2: Using OAuth2NextResources

Last updated 1 month ago

Was this helpful?

Note: Make sure that you have completed Tutorial 1 and 2, and that you have understood the OAuth Process first.

Now that you have successfully used the OAuth2 process to get Myinfo Person data, the final step is to implement PKI digital signature in your server-to-server calls to Myinfo APIs.

Please refer to our for this tutorial.

You may use test personas found in the when testing Tutorial 3.


Importance of Security

Security is of utmost importance for Myinfo. To enhance security, Myinfo APIs requires your application to implement PKI digital signature when calling our server-to-server APIs, namely, the token and person APIs.

Using PKI digital signature, we can ensure the following:

  • The caller (your application) and the receiver (Our API Gateway) are both valid and trusted identities; i.e. they are who they say they are.

  • The request (from your application) and the response (from Our API Gateway) has not been changed in transit.


What is PKI Digital Signature?

Digital signatures are like electronic “fingerprints.” In the form of a coded message, the digital signature securely associates a signer with a document in a recorded transaction. Digital signatures use a standard, accepted format, called Public Key Infrastructure (PKI), to provide the highest levels of security and universal acceptance. They are a specific signature technology implementation of electronic signature (eSignature).


Public Key Infrastructure – PKI

A cryptographic system that uses two keys, a public key known to everyone and a private key, the private key has full control to the key owner, and has to keep in secured environment. A unique element to the public key system is that the public and private keys are related in such a way that only the public key can be used to encrypt messages and only the corresponding private key can be used to decrypt them. Moreover, it is virtually impossible to deduce the private key if you know the public key.


Generating a Public/Private Keypair

The first step is to generate the public/private keypair for your app, based on your CA cert. Please refer to your CA certificate vendor for instructions on how to do so. Once this is complete, you should have the files similar to the list shown below:

FILE NAME
KEY TYPE
COMMENTS

privatekey.key

private key

Make sure you save a copy of the private key in a safe place. If you lose this key you will need to generate a new key pair. You will sign your request using the private key.

mycert.cer

X.509 cert (public key)

Important:Submit this file in the Deploy step of your onboarding process upon approval of your linkup request. The API uses this file for signature verification on your app’s API calls.

Note: Our sample application has a private key installed in it. This is only used for demo purposes. Your application will need its own private key.

For implementation on your own application, please make sure that your business user/partner has submitted your public key to Myinfo as part of the onboarding process.


1. Enable PKI digital signature on our Sample application

1.1 Modify configuration

Modify the configuration in the sample application you downloaded in Tutorial 2.

For Linux/MacOSFor Windows


2. Trigger MockPass Login and Consent (authorise API)

Now access the sample application via your browser at http://localhost:3001 and click on the Retrieve Myinfo button on the application and go through the MockPass login and on the consent page, click on I Agree.

Note:The process for this is exactly the same as in Tutorial 2.

The token and person API calls will follow automatically once you have finished the consent page.

We will look at the code for these 2 APIs in the following sections.


3. Call the Token API

Look at the code in the file routes/index.js in the sample application.

You should see the following code:

// function to prepare request for TOKEN API
function createTokenRequest(code) {
  var cacheCtl = "no-cache";
  var contentType = "application/x-www-form-urlencoded";
  var method = "POST";
  var request = null;
 
  // preparing the request with header and parameters
  // assemble params for Token API
  var strParams = "grant_type=authorization_code" +
    "&code=" + code +
    "&redirect_uri=" + _redirectUrl +
    "&client_id=" + _clientId +
    "&client_secret=" + _clientSecret;
  var params = querystring.parse(strParams);
 
 
  // assemble headers for Token API
  var strHeaders = "Content-Type=" + contentType + "&Cache-Control=" + cacheCtl;
  var headers = querystring.parse(strHeaders);
 
  // Sign request and add Authorization Headers
  var authHeaders = generateAuthorizationHeader(
    _tokenApiUrl,
    params,
    method,
    contentType,
    _authLevel,
    _clientId,
    _privateKeyContent,
    _clientSecret
  );
 
  if (!_.isEmpty(authHeaders)) {
    _.set(headers, "Authorization", authHeaders);
  }
 
  var request = restClient.post(_tokenApiUrl);
 
  // Set headers
  if (!_.isUndefined(headers) && !_.isEmpty(headers))
    request.set(headers);
 
  // Set Params
  if (!_.isUndefined(params) && !_.isEmpty(params))
    request.send(params);
 
  return request;
}

You will notice that the code is calling securityHelper.generateAuthorizationHeader() before sending out the request. This function is found in lib/security/security.js and generates the security headers for calling Our API Gateway. Here's the code for this function:

// generates the security headers for calling API gateway
  function generateAuthorizationHeader(url, params, method, strContentType, 
  authType, appId, keyCertContent, passphrase) {
     
    if (authType == "L2") {
      return generateRS256Header(url, params, method, strContentType, appId, 
      keyCertContent, passphrase);
    } else {
      return "";
    }
  };

3.1 Signing Your Requests

Let's have a closer look at the generateAuthorizationHeader() function in the lib/security/security.js file.

To sign your request, a few steps are required: A. Constructing the Authorisation Token B. Forming the Signature Base String C. Signing the Base String to get the Digital Signature D. Assembling the Header

You can see the different steps involved in the code below:

  // Signing Your Requests
  function generateRS256Header(url, params, method, strContentType, appId, keyCertContent, keyCertPassphrase) {
    var nonceValue = nonce();
    var timestamp = (new Date).getTime();
 
    // A) Construct the Authorisation Token Parameters
    var defaultAuthHeaders = {
      "app_id": appId, // App ID assigned to your application
      "nonce": nonceValue, // secure random number
      "signature_method": "RS256",
      "timestamp": timestamp // Unix epoch time
    };
 
    // B) Forming the Base String
    // Base String is a representation of the entire request (ensures message integrity)
 
    // i) Normalize request parameters
    var baseParams = sortJSON(_.merge(defaultAuthHeaders, params));
 
    var baseParamsStr = qs.stringify(baseParams);
    baseParamsStr = qs.unescape(baseParamsStr); // url safe
 
    // ii) concatenate request elements (HTTP method + url + base string parameters)
    var baseString = method.toUpperCase() + "&" + url + "&" + baseParamsStr;
 
    // C) Signing Base String to get Digital Signature
    var signWith = {
      key: fs.readFileSync(keyCertContent, "utf8")
    }; // Provides private key
 
    // Load pem file containing the x509 cert & private key & sign the base string with it to produce the Digital Signature
    var signature = crypto.createSign("RSA-SHA256")
      .update(baseString)
      .sign(signWith, "base64");
 
    // D) Assembling the Authorization Header
    var strAuthHeader = "PKI_SIGN app_id=\"" + appId + // Defaults to 1st part of incoming request hostname
      "\",timestamp=\"" + timestamp +
      "\",nonce=\"" + nonceValue +
      "\",signature_method=\"RS256\"" +
      ",signature=\"" + signature +
      "\"";
 
    return strAuthHeader;
  };

The following sections explain in detail each of the steps and how to do it for your own application.

A. Constructing the Authorisation Token

In order for the platform to recognize your app, you must construct a token and send it with the request message. This token is sent in the header of your request.

NAME
DESCRIPTION

app_id

The AppID assigned to your application. For our sample application, this is STG2-MYINFO-SELF-TEST

nonce

A random string, uniquely generated for each request. The nonce allows the server to verify that a request has never been made before and helps prevent replay attacks when requests are made over a non-secure-channel. For more information, see Generating a Nonce Value.

signature_method

A value indicating the signature method. Value = RS256

signature

The signature value. For information on how the signature value is calculated, see later sections.

timestamp

The timestamp of the request, expressed as the number of milliseconds since January 1, 1970 00:00:00 GMT. The timestamp must be a positive integer and must be greater than or equal to the timestamp used in previous requests. For more information, see Generating the Timestamp.

A.1 Generating a Nonce Value

A nonce is a random string that is uniquely generated for each request. The nonce allows the API providers to verify that a request has never been made before. A nonce is sent in the message header for both Shared Secret and PKI security mechanisms. In the Shared Secret mechanism, the same nonce value sent in the message header is also used in creating the Secret Digest.

The way you create the nonce will depend on your development environment. Most programming languages include a method for creating a nonce.

Below is an example of generating a nonce value in Java:

  // Generate the nonce value
  Import java.security.SecureRandom:
  Random rand = SecureRandom.getInstance ("SHA1PRNG");
  long nonce = rand.nextLong();

A.2 Generating the Timestamp

The timestamp of the request is sent in the message header. The timestamp must be in Unix epoch time, expressed as the number of milliseconds since January 1, 1970 00:00:00 GMT. The timestamp must be a positive integer and must be greater than or equal to the timestamp used in previous requests. In most implementations, the timestamp is taken from the host server. It's important that the timestamp in the message is accurate; if the timestamp is off, the message might be rejected.

A Java example of generating the timestamp is shown below.

  // Get the timestamp in milliseconds
  long timestamp = System.currentTimeMillis();

B. Forming the Signature Base String

The basestring is a string constructed to represent the contents of your request, which is then signed to create the digital signature of your request. This ensures that the contents of your request can be verified to ensure it has not been changed in transit.

Base String Examples: Below is an example of the Signature Base String that you will see in the onscreen logs of our demo application when calling the token API.

baseString:
POST&https://test.api.myinfo.gov.sg/com/v3/token&app_id=STG2-MYINFO-SELF-TEST&
client_id=STG2-MYINFO-SELF-TEST&client_secret=44d953c796cccebcec9bdc826852857a
b412fbe2&code=13a2f7db-1665-4c4e-9baa-b8ec464b9aad&grant_type=authorization_co
de&nonce=150589435358500&redirect_uri=http://localhost:3001/callback&signature
_method=RS256&timestamp=1505894353585

Likewise, the Base String for the person API looks like this:

baseString:
GET&https://test.api.myinfo.gov.sg/com/v3/person/9E9B2260-47B8-455B-89B5-C48F4
DB98322/&app_id=STG2-MYINFO-SELF-TEST&attributes=name,sex,race,nationality,dob
,email,mobileno,regadd,housingtype,hdbtype,marital,edulevel,assessableincome,h
anyupinyinname,aliasname,hanyupinyinaliasname,marriedname,cpfcontributions,cpf
balances&client_id=STG2-MYINFO-SELF-TEST&nonce=150589435395700&signature_metho
d=RS256&timestamp=1505894353957

The process of constructing the Signature Base String consists of three steps:

Step 1: Normalize request parameters

Collect the request parameters, sort them, and concatenate them into a normalized string. Parameters to include:

  • All parameters in the HTTP Authorization header except the signature parameters which must be excluded.

  • Parameters in the HTTP POST request body (with a content-type of application/x-www-form-urlencoded).

  • HTTP GET parameters added to the URLs in the query part.

Normalize the parameters into a single string following the steps below:

  • Sort by parameter name, using lexicographical byte value ordering. If two or more parameters share the same name, sort by value. For example:

      a=1, c=hi%20there, f=25, f=50, f=a, z=p, z=t
  • When parameters are sorted, concatenate them into a single string. For each parameter, separate the name from the corresponding value by an equals (=) character (ASCII code 61), even if the value is empty. Each name-value pair is separated by an ampersand (&) character (ASCII code 38).

    • For example: a=1&c=hi%20there&f=25&f=50&f=a&z=p&z=t

Step 2: Construct request URL

The Signature Base String includes the absolute URL for the request, tying the signature to a specific endpoint. The URL used in the Signature Base String:

  • Must include the scheme, authority, and path.

  • Must exclude the query and fragment.

URL scheme and authority must be lowercase and must include the port number (except port 80 or 443).

Note: HTTP default port 80 and HTTPS default port 443 must be excluded.

For example, this person GET request:

https://test.api.myinfo.gov.sg:443/com/v3/person/
9E9B2260-47B8-455B-89B5-C48F4DB98322/?attributes=name,sex,race,
nationality,dob,email,mobileno,regadd,housingtype,hdbtype,marital,
edulevel,assessableincome,hanyupinyinname,aliasname,
hanyupinyinaliasname,marriedname,cpfcontributions,
cpfbalances&client_id=STG2-MYINFO-SELF-TEST

Would be included in the Signature Base String as:

https://test.api.myinfo.gov.sg/com/v3/person/9E9B2260-47B8-455B-89B5-C48F4DB98322/
Step 3: Concatenate request elements

The following items must be concatenated in order into a single Signature Base string. Each item is encoded and separated by an ampersand (&) character (ASCII code 38), even if empty.

  • The HTTP request method used to send the request. Value must be uppercase, for example: POST, GET, PUT.

  • The request URL.

  • The normalized request parameters string. Note that the signature parameter, scheme are excluded.

C. Signing the Base String to get the Digital Signature

Once the Signature Base String is constructed, the next step is signing:

  1. Use the SHA-2 algorithm to generate the hash of the Signature Base String.

  2. Sign the hashed value using the private key of the app.

  3. Base64-encode the signature value.

Note: Base64 encoding should not include the CRLF (carriage return/line feed) every 72 characters which is part of strict Base64 encoding. Instead, the whole Base64 encoded string should be without line breaks.

  1. Set the string as the value for the signature parameter.

Example code in Java below:

String baseString = "Constructed base string";
Signature sig = Signature.getInstance("RSA-SHA256");
sig.initSign(privateKey); // Get private key from keystore
sig.update(baseString.getBytes());
byte[] signedData = sig.sign();
String finalStr = Base64.getEncoder().encodeToString(signedData);

Example code in NodeJS below:

var signature = crypto.createSign('RSA-SHA256')
.update(baseString)
.sign(signWith, 'base64');

D. Assembling the Header

Now that you have gotten the digital signature, you are ready to assemble the final header for your request. Make sure you include the following parameters in the header:

  • app_id

  • nonce

  • signature_method

  • signature

  • timestamp

Sample header with authorization parameters

Below is an example of an Authorization header for the sample application. Make sure you list the parameters in the sequence shown below.

Authorization: PKI_SIGN
timestamp="1505900210349",
nonce="150590021034800",
app_id="STG2-MYINFO-SELF-TEST",
signature_method="RS256",
signature="EEm+HEcNQajb5FkVd82zjojk+daYZXxSGPCO
R2GHZeoyjZY1PK+aFMzHfWu7eJZYMa5WaEwWxdOdq5hjNbl
8kHD7bMaOks7FgEPdjE++TNomfv7
SMktDnIvZmPYAxhjb/C9POU2KT6tSlZT/Si/qMgD1cryaPw
SeMoM59UZa1GzY
mqlkveba7rma58uGwb3wZFH0n57UnouR6LYX
DOOLkqi8uMZBuvRUvSJRXETAj2N0hT+4QJiN96Ct6IEQh/w
oZh0o74K5Ol9Pp
DSM08qC7Lj6N/k694J+hbBQVVviGn7/6mDkfbwdMDuoKs4t
7NpqmAnwT+xaQS
IZcexfrAVQYA=="

3.2 Sending the Request

Once the header is assembled, you are ready to send out your request. If you have done the steps correctly, Our API Gateway should recognize your request to be valid and send back a valid response, similar to the one you saw in Tutorial 2.

Note: Verify that the request URL remains as e.g. https://api.myinfo.gov.sg/com/v3/*

3.3 Validating the Response

Validate the JWT response sent back to you in the way that we went through in Tutorial 2.

Note: It is very important for your application to validate the signature of the JWT to ensure the response has not been changed in transit.


4. Call the Person API

4.1 Send Request

Now that you have the JWT access token, you can call the person API in the same way as the token API.

Note: Remember to construct the header and digital signature of your request in the same way as the token API.

4.2 Decrypt & Verify the Person Response

The response payload for the Person API (for test and production environments) is first signed, then encrypted:

Encryption protects the data at rest while a signed payload means, if necessary, you will be able to pass this signed payload to a 3rd party where they can verify the payload's integrity with our public certificate.

In order to read the payload, you have to perform the following steps in order:

  1. Decrypt the payload with your application's private key.

  2. Validate the decrypted payload signature with our public key.

After doing the above steps, your application will be able to extract the payload in JSON format.

STEP 1: Decryption

  • Encryption is done using your application's public key that you provided during onboarding. Decryption of the payload should be using the private key of that key-pair.

  • Current encryption algorithms used:

    • RSA-OAEP (for content key wrapping)

    • AES256GCM (for content encrytion)

What is the JSON Web Encryption Compact Serialization format?

The format consist of five parts separated by dots (.), which are:

PART
DESCRIPTION

Header

Contains the encryption algorithms used to produce the 1. cipher text (encrypted data) and 2. encrypted key (symmetric key encrypted with application's RSA public key)

Encrypted Key

The encrypted symmetric key that was used to encrypt the data.

Initialization Vector

Secure random value used together with the symmetric key to encrypt the data.

Cipher Text

The encrypted data.

Tag

Value used to ensure integrity of the encrypted data and header.

Therefore, a JWE typically looks like the following: aaaaa.bbbbb.ccccc.ddddd.eeeee

How can I decrypt the JWE?

You will need to use a library that performs JWE decryption in order to decrypt the cipher text. Such libraries are readily available on the internet.

For example, in our sample application, which is implemented with Node.js, we use the jose library, and we call the jose.jwe.decrypt method. If the decryption fails then the library will throw an error which the application needs to handle.

Firstly, the Encrypted Key is decrypted using your application's RSA private key with the "alg" algorithm specified in the Header to produce the symmetric key. Next, the JWE decryption library is used to decrypt the Cipher Text using the symmetric key, Initialization Vector, Tag and ascii-encoded Header, with the "enc" algorithm in the Header.

Look at the code in the file lib/security/security.js and search for the following code snippet:

// Decrypt JWE using private key
security.decryptJWE = function decryptJWE(header, encryptedKey, iv, cipherText, tag, privateKey) {
  console.log("Decrypting JWE".green + " (Format: " + "header".red + "." +
  "encryptedKey".cyan + "." + "iv".green + "." +
  "cipherText".magenta + "." + "tag".yellow + ")");
  console.log(header.red + "." + encryptedKey.cyan + "." + iv.green + "." + cipherText.magenta + "." + tag.yellow);
  return new Promise((resolve, reject) => {
     
    var keystore = jose.JWK.createKeyStore();
     
    console.log((new Buffer(header,"base64")).toString("ascii"));
     
    var data = {
      "type": "compact",
      "ciphertext": cipherText,
      "protected": header,
      "encrypted_key": encryptedKey,
      "tag": tag,
      "iv": iv,
      "header": JSON.parse(jose.util.base64url.decode(header).toString())
    };
    keystore.add(fs.readFileSync(privateKey, "utf8"), "pem")
      .then(function(jweKey) {
        // {result} is a jose.JWK.Key
        jose.JWE.createDecrypt(jweKey)
          .decrypt(data)
          .then(function(result) {
            resolve(JSON.parse(result.payload.toString()));
          })
          .catch(function(error) {
            reject(error);
          });
      });
       
  })
  .catch (error => {
    console.error("Error with decrypting JWE: %s".red, error);
    throw "Error with decrypting JWE";
  })
}

This calls the library to decrypt the JWE and return the decrypted JSON object, which is the signed person's data.

STEP 2: Verification of Signature

  • signature algorithm used is RS256.

You may download Myinfo X.509 Public Key Certificate used for verification, after application is created during onboarding in the application details page upon login.

Sample Code in NodeJS

      // Sample Code for Verifying & Decoding JWS or JWT
      function verifyJWS(jws, publicCert) {
        // verify payload
        // ignore notbefore check because it gives errors sometimes if the call is too fast.
        try {
          var jwspayload = jwt.verify(jws, fs.readFileSync(publicCert, "utf8"), {
            algorithms: ["RS256"],
            ignoreNotBefore: true
          });
          return jwspayload;
        }
        catch(error) {
          throw("Error with verifying and decoding JWS");
        }
      }

5. Use the JSON object to Prefill Form

Once you have decrypted the JWE and verified the JWS, you will get the Person JSON data format which we saw in Tutorial 1 and 2. Use this data to prefill your application form.


Summary

You've successfully used the OAuth2 process and PKI digital signature to get the Person data from Myinfo sandbox environment.

You are now ready to use the same method to integrate your own application to our Staging and Production APIs

Note: For Staging and Production APIs, please ensure you have separate pairs of (private key+public cert) for each environment. Your business user/partner should submit these as part of the onboarding process.

For more information on Unix epoch time, and examples of implementation of the timestamp in different programming languages, see ._ _

Signing is done using format

Encryption is done using format

The decrypted payload is signed according to format, similar to the access token.

API specifications
Personas Page
Epoch Converter
JWS (JSON Web Signature)
JWE (JSON Web Encryption) Compact Serialization
JWS (JSON Web Signature)