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
| Term | Description |
|---|---|
| signature | Message signature, used to verify whether the request is from IDaaS to prevent attacker forgery. The signature algorithm is HMAC-SHA256 + Base64. |
| AESKey | Key for the AES algorithm. Encryption algorithms support AES/GCM/NoPadding and AES/ECB/PKCS5Padding; AES/GCM/NoPadding is recommended. |
| msg | Plaintext message body in JSON format. |
| encrypt_msg | Ciphertext 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:
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:
javaString 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)));Compare the calculated signature cal_signature with the request parameter signature; if equal, verification passes.
The enterprise application returns the response message format as required.
Plaintext Encryption Process
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:
javaString dataStr = RandomStringUtils.random(16, true, false) + "&" + data;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:
javaCipher 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:
javaCipher 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
Base64 decode the ciphertext.
javabyte[] encryptStr = Base64.getDecoder().decode(data);Decrypt using AESKey.
AES/ECB/PKCS5Padding:
javaCipher 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:
javaCipher 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));
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.
javaString dataStr = new String(bytes, CHARSET).substring(17);AES/GCM/NoPadding:
javaString 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:
javapackage 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:
javaimport 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; } }