Signature Verification and Signing Instructions
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 guaranteeing data interaction in a secure environment.
# Signature Verification/Encryption and Decryption Terminology
| Term | Description |
|---|---|
| signature | Message signature, used to verify whether the request originates from IDaaS, to prevent attackers from forging it. The signature algorithm is HMAC-SHA256 + Base64. |
| AESKey | The key for the AES algorithm. Supported encryption algorithms are AES/GCM/NoPadding and AES/ECB/PKCS5Padding. AES/GCM/NoPadding is recommended. |
| msg | Plaintext message body, formatted as JSON. |
| encrypt _msg | Ciphertext obtained after encrypting the plaintext message msg and performing Base64 encoding. |
# Signature Verification
To allow enterprise applications to confirm that event push notifications originate from IDaaS, when IDaaS pushes events to the enterprise application's callback service, the request body includes a request signature identified by the parameter signature. The enterprise application needs to verify the correctness of this parameter before decryption. The verification steps are as follows:
- Calculate the signature: It consists of five parts: the signature key, Nonce value, timestamp, event type, and encrypted message body, connected with
&. The HMAC-SHA256 + Base64 algorithm is used for encryption. The following is a Java language signature example:
String message = nonce + "&" + timestamp + "&" + eventType + "&" + encryptData;
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(签名秘钥.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(secretKey);
String newSignature = Base64.getEncoder().encodeToString(mac.doFinal(message.getBytes(StandardCharsets.UTF_8)));
2
3
4
5
Compare the calculated signature
cal_signaturewith the request parametersignature. If they are equal, the verification passes.The enterprise application returns the response message format as required.
# Plaintext Encryption Process
- Concatenate the plaintext string. The plaintext string is composed of a 16-byte random string and the plaintext
msg, connected with&. The following is a Java language example:
String dataStr = RandomStringUtils.random(16, true, false) + "&" + data;
- Encrypt the concatenated plaintext string using the AESkey, then perform Base64 encoding to obtain the ciphertext
encrypt_msg. The following are Java language examples demonstrating two different encryption algorithms:
AES/ECB/PKCS5Padding:
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); SecretKeySpec secretKey = new SecretKeySpec(加密密钥.getBytes(StandardCharsets.UTF_8), "AES"); cipher.init(1, secretKey); byte[] bytes = dataStr.getBytes(StandardCharsets.UTF_8); String ecnryptStr = Base64.getEncoder().encodeToString(cipher.doFinal(bytes));1
2
3
4
5AES/GCM/NoPadding:
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); SecretKeySpec secretKey = new SecretKeySpec(加密密钥.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));1
2
3
4
5
6
7
8
9
# Ciphertext Decryption Process
- Decode the ciphertext using BASE64.
byte[] encryptStr = Base64.getDecoder().decode(data);
- Decrypt using the AESKey.
AES/ECB/PKCS5Padding:
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);1
2
3
4AES/GCM/NoPadding:
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));1
2
3
4
5
6
- Obtain the plaintext content.
AES/ECB/PKCS5Padding: Need to remove the first 16 random bytes from the rand_msg header, the remaining part is the plaintext content msg
String dataStr = new String(bytes, CHARSET).substring(17);1AES/GCM/NoPadding:
String dataStr = new String(bytes, CHARSET);1
# Data Signing & Encryption/Decryption Example
- The following is a Java language example for data signing & encryption/decryption, demonstrating two algorithms respectively:
- AES/ECB/PKCS5Padding:
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 the Authorization header information String authorization = httpRequest.getHeader("Authorization"); assertAuthorizationMessage(authorization); //Note: To determine if the received event callback message is a duplicate, it can be verified through the first 16 characters of the decrypted data. (i.e., the string before & is used to check if the message is a duplicate, the string after & is the object attribute data) JSONObject messageBody = JSON.parseObject(body); //Verify if the message body source is legitimate 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 logic to be implemented by the client JSONObject returnData = new JSONObject(1); returnData.put("id", eventData.getString("username")); return returnData; }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
private JSONObject processCreateOrganization(JSONObject eventData) {
//TODO Business logic to be implemented
JSONObject returnData = new JSONObject(1);
returnData.put("id", eventData.getString("code"));
return returnData;
}
private JSONObject processUpdateUser(JSONObject eventData) {
//TODO Business logic to be implemented
JSONObject returnData = new JSONObject(1);
returnData.put("id", eventData.getString("id"));
return returnData;
}
private JSONObject processUpdateOrganization(JSONObject eventData) {
//TODO Business logic to be implemented
JSONObject returnData = new JSONObject(1);
returnData.put("id", eventData.getString("id"));
return returnData;
}
private void processDeleteUser(JSONObject eventData) {
//TODO Business logic to be implemented
}
private void processDeleteOrganization(JSONObject eventData) {
//TODO Business logic to be implemented
}
private JSONObject processCheckUrl() {
//TODO Business logic to be implemented
JSONObject returnData = new JSONObject(1);
String randomStr = UUID.randomUUID().toString().replaceAll("-", "");
returnData.put("randomStr", randomStr);
return returnData;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
+ 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 the Authorization header information
String authorization = httpRequest.getHeader("Authorization");
assertAuthorizationMessage(authorization);
//Note: If it is necessary to determine whether the received event callback message is duplicated, it can be verified through the first 16 characters of the decrypted data. (i.e., the string before & is used to verify if the message is duplicated, and the string after & is the object property data)
JSONObject messageBody = JSON.parseObject(body);
// Verify whether the message body source is legitimate
assertMessageSourceValidity(messageBody);
// Parse the encrypted message body
String encryptMessage = messageBody.getString("data");
String plaintMessage = parseEncryptedMessage(encryptMessage);
// Process the 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 logic to be implemented
JSONObject returnData = new JSONObject(1);
returnData.put("id", eventData.getString("username"));
return returnData;
}
private JSONObject processCreateOrganization(JSONObject eventData) {
//TODO Business logic to be implemented
JSONObject returnData = new JSONObject(1);
returnData.put("id", eventData.getString("code"));
return returnData;
}
private JSONObject processUpdateUser(JSONObject eventData) {
//TODO Business logic to be implemented
JSONObject returnData = new JSONObject(1);
returnData.put("id", eventData.getString("id"));
return returnData;
}
private JSONObject processUpdateOrganization(JSONObject eventData) {
//TODO Business logic to be implemented
JSONObject returnData = new JSONObject(1);
returnData.put("id", eventData.getString("id"));
return returnData;
}
private void processDeleteUser(JSONObject eventData) {
//TODO Business logic to be implemented
}
private void processDeleteOrganization(JSONObject eventData) {
//TODO Business logic to be implemented
}
private JSONObject processCheckUrl() {
//TODO Business logic to be implemented
JSONObject returnData = new JSONObject(1);
String randomStr = UUID.randomUUID().toString().replaceAll("-", "");
returnData.put("randomStr", randomStr);
return returnData;
}
}
```
