Skip to content

Signature Verification Description

When synchronizing data to enterprise applications, the enterprise application needs to identify and confirm the synchronization event to ensure the security and reliability of the event source, thereby ensuring data interaction in a secure environment.

Signature Verification/Encryption Terminology

TermDescription
signatureMessage signature, used to verify whether the request is from IDaaS to prevent attacker forgery. The signature algorithm is HMAC-SHA256 + Base64.
AESKeyKey for the AES algorithm. Encryption algorithms support AES/GCM/NoPadding and AES/ECB/PKCS5Padding; AES/GCM/NoPadding is recommended.
msgPlaintext message body in JSON format.
encrypt_msgCiphertext obtained by encrypting the plaintext message msg and Base64 encoding it.

Signature Verification

To allow the enterprise application to confirm that event pushes come from IDaaS, when IDaaS pushes events to the enterprise application callback service, the request body contains a request signature identified by the parameter signature. The enterprise application needs to verify the correctness of this parameter before decrypting. The verification steps are as follows:

  1. Calculate signature: composed of five parts: signing key, nonce value, timestamp, event type, and encrypted message body, connected with &. Encrypted using HMAC-SHA256 + Base64. The following is a Java signature example:

    java
    String message = nonce + "&" + timestamp + "&" + eventType + "&" + encryptData;
    Mac mac = Mac.getInstance("HmacSHA256");
    SecretKeySpec secretKey = new SecretKeySpec(signingKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
    mac.init(secretKey);
    String newSignature = Base64.getEncoder().encodeToString(mac.doFinal(message.getBytes(StandardCharsets.UTF_8)));
  2. Compare the calculated signature cal_signature with the request parameter signature; if equal, verification passes.

  3. The enterprise application returns the response message format as required.

Plaintext Encryption Process

  1. Concatenate the plaintext string. The plaintext string consists of a 16-byte random string concatenated with the plaintext msg, connected with &. The following is a Java example:

    java
    String dataStr = RandomStringUtils.random(16, true, false) + "&" + data;
  2. Encrypt the concatenated plaintext string with AESKey, then Base64 encode it to obtain the ciphertext encrypt_msg. The following is a Java example demonstrating two different encryption algorithms:

    • AES/ECB/PKCS5Padding:

      java
      Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
      SecretKeySpec secretKey = new SecretKeySpec(encryptionKey.getBytes(StandardCharsets.UTF_8), "AES");
      cipher.init(1, secretKey);
      byte[] bytes = dataStr.getBytes(StandardCharsets.UTF_8);
      String ecnryptStr = Base64.getEncoder().encodeToString(cipher.doFinal(bytes));
    • AES/GCM/NoPadding:

      java
      Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
      SecretKeySpec secretKey = new SecretKeySpec(encryptionKey.getBytes(StandardCharsets.UTF_8), "AES");
      cipher.init(1, secretKey);
      byte[] bytes = dataStr.getBytes(StandardCharsets.UTF_8);
      // Generate a string of length 24
      String repIvStr = generate(24);
      byte[] iv =  Base64.getDecoder().decode(repIvStr);
      cipher.init(1, secretKey , new GCMParameterSpec(128, iv));
      String ecnryptStr = Base64.getEncoder().encodeToString(cipher.doFinal(bytes));

Ciphertext Decryption Process

  1. Base64 decode the ciphertext.

    java
    byte[] encryptStr = Base64.getDecoder().decode(data);
  2. Decrypt using AESKey.

    • AES/ECB/PKCS5Padding:

      java
      Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
      SecretKeySpec secretKey = new SecretKeySpec(encryptionKey.getBytes(StandardCharsets.UTF_8), "AES");
      cipher.init(2, secretKey);
      byte[] bytes = cipher.doFinal(encryptStr);
    • AES/GCM/NoPadding:

      java
      Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
      SecretKeySpec secretKey = new SecretKeySpec(encryptionKey.getBytes(CHARSET), ENCRYPT_KEY_ALGORITHM);
      byte[] iv = Base64.getDecoder().decode(data.substring(0, 24));
      data = data.substring(24);
      cipher.init(2, secretKey, new GCMParameterSpec(AES_KEY_SIZE, iv));
      byte[] bytes = cipher.doFinal(Base64.getDecoder().decode(data));
  3. Obtain plaintext content.

    • AES/ECB/PKCS5Padding: Remove the 16 random bytes at the head of rand_msg; the remaining part is the plaintext content msg.

      java
      String dataStr = new String(bytes, CHARSET).substring(17);
    • AES/GCM/NoPadding:

      java
      String dataStr = new String(bytes, CHARSET);

Data Signature & Encryption/Decryption Example

  • The following are Java examples of data signature & encryption/decryption, demonstrating two algorithms:

    • AES/ECB/PKCS5Padding:
    java
    
    package BOOT-INF.classes.com.example.demo.controller;
      import com.alibaba.fastjson.JSON;
      import com.alibaba.fastjson.JSONObject;
      import org.apache.commons.lang3.RandomStringUtils;
      import org.apache.commons.lang3.StringUtils;
      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;
      import org.springframework.stereotype.Controller;
      import org.springframework.web.bind.annotation.RequestBody;
      import org.springframework.web.bind.annotation.RequestMapping;
      import org.springframework.web.bind.annotation.ResponseBody;
    
      import javax.crypto.Cipher;
      import javax.crypto.Mac;
      import javax.crypto.spec.SecretKeySpec;
      import javax.servlet.http.HttpServletRequest;
      import java.nio.charset.Charset;
      import java.nio.charset.StandardCharsets;
      import java.util.Base64;
      import java.util.UUID;
    
      @RequestMapping
      @Controller
      public class DemoController {
      private static final Logger log = LoggerFactory.getLogger(DemoController.class);
    
          private static final String SEPARATOR = "&";
    
          private static final String SIGN_KEY = "wGt9VxV2qqLbgRrs";
    
          private static final String ENCRYPTION_KEY = "ZJIXSHUdo8WK7FQo";
    
          private static final String TOKEN = "4JVImwu3GdM3zNCE4JVImwu3GdM3zNCE";
    
          private static final String HMAC_SHA256 = "HmacSHA256";
    
          private static final String ENCRYPT_ALGORITHM = "AES/ECB/PKCS5Padding";
    
          private static final String ENCRYPT_KEY_ALGORITHM = "AES";
    
          private static final Charset CHARSET = StandardCharsets.UTF_8;
    
          @ResponseBody
          @RequestMapping({"/callback"})
          public JSONObject demo(HttpServletRequest httpRequest, @RequestBody String body) {
              try {
                  // Verify authorization information
                  // Get Authorization header information
                  String authorization = httpRequest.getHeader("Authorization");
                  assertAuthorizationMessage(authorization);
    
                  // Note: To determine whether a received event callback message is duplicated, verify using the first 16 characters of the decrypted data. (That is, the string before & is used to check whether the message is duplicated, and the string after & is the object attribute data.)
    
                  JSONObject messageBody = JSON.parseObject(body);
    
                  // Verify whether the message source is valid
                  assertMessageSourceValidity(messageBody);
    
                  // Parse the encrypted message body
                  String encryptMessage = messageBody.getString("data");
                  String plaintMessage = parseEncryptedMessage(encryptMessage);
    
                  // Process data
                  JSONObject eventData = JSON.parseObject(plaintMessage);
                  String eventType = messageBody.getString("eventType");
                  JSONObject processResultData = processEventData(eventData, eventType);
    
                  String processResultDataStr = RandomStringUtils.random(16, true, false)+ "&" + processResultData.toJSONString(); ;
                  String encryptResultData =encryptResultData(processResultDataStr);
                  JSONObject result = new JSONObject();
                  result.put("code", "200");
                  result.put("message", "success");
                  result.put("data", encryptResultData);
                  return result;
              } catch (Exception e) {
                  JSONObject result = new JSONObject();
                  result.put("code", "400");
                  String message = e.getMessage();
                  result.put("message", StringUtils.isNotEmpty(message) ? message : e);
                  return result;
              }
          }
    
          private String encryptResultData(String dataStr) {
              try {
                  Cipher cipher = Cipher.getInstance(ENCRYPT_ALGORITHM);
                  SecretKeySpec secretKey = new SecretKeySpec(ENCRYPTION_KEY.getBytes(CHARSET), ENCRYPT_KEY_ALGORITHM);
                  cipher.init(1, secretKey);
                  byte[] bytes = dataStr.getBytes(CHARSET);
                  return Base64.getEncoder().encodeToString(cipher.doFinal(bytes));
              } catch (Exception e) {
                  throw new RuntimeException("error encrypt Result Data", e);
              }
          }
    
          private void assertAuthorizationMessage(String authorization) {
              if (!"Bearer ".concat(TOKEN).equals(authorization)) {
                  throw new RuntimeException("error authorization.");
              }
          }
    
          private void assertMessageSourceValidity(JSONObject messageBody) {
              try {
                  String nonce = messageBody.getString("nonce");
                  String timestamp = messageBody.getString("timestamp");
                  String eventType = messageBody.getString("eventType");
                  String encryptData = messageBody.getString("data");
                  String signature = messageBody.getString("signature");
    
                  String message = nonce + "&" + timestamp + "&" + eventType + "&" + encryptData;
                  Mac mac = Mac.getInstance(HMAC_SHA256);
                  SecretKeySpec secretKey = new SecretKeySpec(SIGN_KEY.getBytes(StandardCharsets.UTF_8), HMAC_SHA256);
                  mac.init(secretKey);
                  byte[] bytes = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
                  String cal_signature = Base64.getEncoder().encodeToString(bytes);
    
                  if (!cal_signature.equals(signature)) {
                      throw new RuntimeException("signature validation failed.");
                  }
              } catch (Exception e) {
                  throw new RuntimeException("error validating message signature", e);
              }
          }
    
          private String parseEncryptedMessage(String encryptMessage) {
              try {
                  Cipher cipher = Cipher.getInstance(ENCRYPT_ALGORITHM);
                  SecretKeySpec secretKey = new SecretKeySpec(ENCRYPTION_KEY.getBytes(CHARSET), ENCRYPT_KEY_ALGORITHM);
                  cipher.init(2, secretKey);
                  byte[] bytes = cipher.doFinal(Base64.getDecoder().decode(encryptMessage));
                  return new String(bytes, CHARSET).substring(17);
              } catch (Exception e) {
                  throw new RuntimeException("error parsing encrypted message", e);
              }
          }
    
          private JSONObject processEventData(JSONObject eventData, String eventType) {
              if (eventType.equals("CREATE_USER")) {
                  return processCreateUser(eventData);
              } else if (eventType.equals("CREATE_ORGANIZATION")) {
                  return processCreateOrganization(eventData);
              } else if (eventType.equals("UPDATE_USER")) {
                  return processUpdateUser(eventData);
              } else if (eventType.equals("UPDATE_ORGANIZATION")) {
                  return processUpdateOrganization(eventData);
              } else if (eventType.equals("DELETE_ORGANIZATION")) {
                  // No data to return
                  processDeleteUser(eventData);
                  return new JSONObject();
              } else if (eventType.equals("DELETE_USER")) {
                  // No data to return
                  processDeleteOrganization(eventData);
                  return new JSONObject();
              } else if (eventType.equals("CHECK_URL")) {
                  return processCheckUrl();
              } else {
                  throw new RuntimeException("the event type is not supported");
              }
          }
    
          private JSONObject processCreateUser(JSONObject eventData) {
              // TODO: business-specific processing logic
              JSONObject returnData = new JSONObject(1);
              returnData.put("id", eventData.getString("username"));
              return returnData;
          }
    
          private JSONObject processCreateOrganization(JSONObject eventData) {
              // TODO: business-specific processing logic
              JSONObject returnData = new JSONObject(1);
              returnData.put("id", eventData.getString("code"));
              return returnData;
          }
    
          private JSONObject processUpdateUser(JSONObject eventData) {
              // TODO: business-specific processing logic
              JSONObject returnData = new JSONObject(1);
              returnData.put("id", eventData.getString("id"));
              return returnData;
          }
    
          private JSONObject processUpdateOrganization(JSONObject eventData) {
              // TODO: business-specific processing logic
              JSONObject returnData = new JSONObject(1);
              returnData.put("id", eventData.getString("id"));
              return returnData;
          }
    
          private void processDeleteUser(JSONObject eventData) {
              // TODO: business-specific processing logic
          }
    
          private void processDeleteOrganization(JSONObject eventData) {
              // TODO: business-specific processing logic
          }
    
          private JSONObject processCheckUrl() {
              // TODO: business-specific processing logic
              JSONObject returnData = new JSONObject(1);
              String randomStr = UUID.randomUUID().toString().replaceAll("-", "");
              returnData.put("randomStr", randomStr);
              return returnData;
          }
      }
    • AES/GCM/NoPadding:
    java
    import com.alibaba.fastjson.JSON;
    import com.alibaba.fastjson.JSONObject;
    import org.apache.commons.lang3.RandomStringUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    
    import javax.crypto.Cipher;
    import javax.crypto.Mac;
    import javax.crypto.spec.GCMParameterSpec;
    import javax.crypto.spec.SecretKeySpec;
    import javax.servlet.http.HttpServletRequest;
    import java.nio.charset.Charset;
    import java.nio.charset.StandardCharsets;
    import java.util.Base64;
    import java.util.UUID;
    
    @RequestMapping
    @Controller
    public class DemoController {
    private static final Logger log = LoggerFactory.getLogger(com.example.demo.controller.DemoController.class);
    
        private static final String SEPARATOR = "&";
    
        private static final String SIGN_KEY = "wGt9VxV2qqLbgRrs";
    
        private static final String ENCRYPTION_KEY = "ZJIXSHUdo8WK7FQo";
    
        private static final String TOKEN = "4JVImwu3GdM3zNCE";
    
        private static final String HMAC_SHA256 = "HmacSHA256";
    
        private static final String ENCRYPT_ALGORITHM = "AES/GCM/NoPadding";
    
        private static final String ENCRYPT_KEY_ALGORITHM = "AES";
    
        private static final Charset CHARSET = StandardCharsets.UTF_8;
    
        @ResponseBody
        @RequestMapping({"/callback"})
        public JSONObject demo(HttpServletRequest httpRequest, @RequestBody String body) {
            try {
                // Verify authorization information
                // Get Authorization header information
                String authorization = httpRequest.getHeader("Authorization");
                assertAuthorizationMessage(authorization);
    
                // Note: To determine whether a received event callback message is duplicated, verify using the first 16 characters of the decrypted data. (That is, the string before & is used to check whether the message is duplicated, and the string after & is the object attribute data.)
    
                JSONObject messageBody = JSON.parseObject(body);
    
                // Verify whether the message source is valid
                assertMessageSourceValidity(messageBody);
    
                // Parse the encrypted message body
                String encryptMessage = messageBody.getString("data");
                String plaintMessage = parseEncryptedMessage(encryptMessage);
    
                // Process data
                JSONObject eventData = JSON.parseObject(plaintMessage);
                String eventType = messageBody.getString("eventType");
                JSONObject processResultData = processEventData(eventData, eventType);
    
                String encryptResultData = encryptResultData(processResultData);
                JSONObject result = new JSONObject();
                result.put("code", "200");
                result.put("message", "success");
                result.put("data", encryptResultData);
                return result;
            } catch (Exception e) {
                JSONObject result = new JSONObject();
                result.put("code", "400");
                String message = e.getMessage();
                result.put("message", StringUtils.isNotEmpty(message) ? message : e);
                return result;
            }
        }
    
        private String encryptResultData(JSONObject processResultData) {
            try {
                String dataStr = processResultData.toJSONString();
                Cipher cipher = Cipher.getInstance(ENCRYPT_ALGORITHM);
                SecretKeySpec secretKey = new SecretKeySpec(ENCRYPTION_KEY.getBytes(CHARSET), ENCRYPT_KEY_ALGORITHM);
                String ivRandom = RandomStringUtils.random(24, true, true);
                byte[] iv = Base64.getDecoder().decode(ivRandom);
                cipher.init(1, secretKey, new GCMParameterSpec(128, iv));
                byte[] bytes = dataStr.getBytes(CHARSET);
                dataStr = Base64.getEncoder().encodeToString(cipher.doFinal(bytes));
                return ivRandom + dataStr;
            } catch (Exception e) {
                throw new RuntimeException("error encrypt Result Data", e);
            }
        }
    
        private void assertAuthorizationMessage(String authorization) {
            if (!"Bearer ".concat(TOKEN).equals(authorization)) {
                throw new RuntimeException("error authorization.");
            }
        }
    
        private void assertMessageSourceValidity(JSONObject messageBody) {
            try {
                String nonce = messageBody.getString("nonce");
                String timestamp = messageBody.getString("timestamp");
                String eventType = messageBody.getString("eventType");
                String encryptData = messageBody.getString("data");
                String signature = messageBody.getString("signature");
    
                String message = nonce + "&" + timestamp + "&" + eventType + "&" + encryptData;
                Mac mac = Mac.getInstance(HMAC_SHA256);
                SecretKeySpec secretKey = new SecretKeySpec(SIGN_KEY.getBytes(StandardCharsets.UTF_8), HMAC_SHA256);
                mac.init(secretKey);
                byte[] bytes = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
                String cal_signature = Base64.getEncoder().encodeToString(bytes);
    
                if (!cal_signature.equals(signature)) {
                    throw new RuntimeException("signature validation failed.");
                }
            } catch (Exception e) {
                throw new RuntimeException("error validating message signature", e);
            }
        }
    
        private String parseEncryptedMessage(String encryptMessage) {
            try {
                Cipher cipher = Cipher.getInstance(ENCRYPT_ALGORITHM);
                SecretKeySpec secretKey = new SecretKeySpec(ENCRYPTION_KEY.getBytes(CHARSET), ENCRYPT_KEY_ALGORITHM);
                byte[] iv = Base64.getDecoder().decode(encryptMessage.substring(0, 24));
                String encryptMsg = encryptMessage.substring(24);
                cipher.init(2, secretKey, new GCMParameterSpec(128, iv));
                byte[] bytes = cipher.doFinal(Base64.getDecoder().decode(encryptMsg));
                return new String(bytes, CHARSET);
            } catch (Exception e) {
                throw new RuntimeException("error parsing encrypted message", e);
            }
        }
    
        private JSONObject processEventData(JSONObject eventData, String eventType) {
            if (eventType.equals("CREATE_USER")) {
                return processCreateUser(eventData);
            } else if (eventType.equals("CREATE_ORGANIZATION")) {
                return processCreateOrganization(eventData);
            } else if (eventType.equals("UPDATE_USER")) {
                return processUpdateUser(eventData);
            } else if (eventType.equals("UPDATE_ORGANIZATION")) {
                return processUpdateOrganization(eventData);
            } else if (eventType.equals("DELETE_ORGANIZATION")) {
                // No data to return
                processDeleteUser(eventData);
                return new JSONObject();
            } else if (eventType.equals("DELETE_USER")) {
                // No data to return
                processDeleteOrganization(eventData);
                return new JSONObject();
            } else if (eventType.equals("CHECK_URL")) {
                return processCheckUrl();
            } else {
                throw new RuntimeException("the event type is not supported");
            }
        }
    
        private JSONObject processCreateUser(JSONObject eventData) {
            // TODO: business-specific processing logic
            JSONObject returnData = new JSONObject(1);
            returnData.put("id", eventData.getString("username"));
            return returnData;
        }
    
        private JSONObject processCreateOrganization(JSONObject eventData) {
            // TODO: business-specific processing logic
            JSONObject returnData = new JSONObject(1);
            returnData.put("id", eventData.getString("code"));
            return returnData;
        }
    
        private JSONObject processUpdateUser(JSONObject eventData) {
            // TODO: business-specific processing logic
            JSONObject returnData = new JSONObject(1);
            returnData.put("id", eventData.getString("id"));
            return returnData;
        }
    
        private JSONObject processUpdateOrganization(JSONObject eventData) {
            // TODO: business-specific processing logic
            JSONObject returnData = new JSONObject(1);
            returnData.put("id", eventData.getString("id"));
            return returnData;
        }
    
        private void processDeleteUser(JSONObject eventData) {
            // TODO: business-specific processing logic
        }
    
        private void processDeleteOrganization(JSONObject eventData) {
            // TODO: business-specific processing logic
        }
    
        private JSONObject processCheckUrl() {
            // TODO: business-specific processing logic
            JSONObject returnData = new JSONObject(1);
            String randomStr = UUID.randomUUID().toString().replaceAll("-", "");
            returnData.put("randomStr", randomStr);
            return returnData;
        }
    }

BambooCloud IDaaS Open Platform