PDF Signing By p12 With JS

DocSignController.java

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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
@GetMapping("/doc-sign/sign")
public ResponseEntity<Resource> getHashForSign() throws IOException {
log.debug("start to generate merged PDF in java web start program");
synchronized (eventPublisher) {
eventPublisher.publishEvent(new OnDocMergeEvent(getUserId(), getXxxFormMaster().getXxxFormMasterId(), getXxxFormMaster().getFormType()));
}
Integer xxxFormMasterId = getXxxFormMaster().getXxxFormMasterId();
String userId = getUserId();

List<SuppDoc> suppDocs = suppDocMapper.selectSuppDocByXxxFormMasterIdAndDocTypeAndUserId(xxxFormMasterId, userId, Constants.SUPP_DOC_TYPE_SYS_GEN_MERGE);
if (suppDocs == null || suppDocs.isEmpty()) {
suppDocs = suppDocMapper.selectSuppDocByXxxFormMasterIdAndDocTypeAndUserId(xxxFormMasterId, userId, Constants.SUPP_DOC_TYPE_SYS_SIGN);
if (suppDocs != null && !suppDocs.isEmpty()) {
log.info("signed doc exists !!");
return ResponseEntity.status(HttpStatus.NOT_ACCEPTABLE).body(null);
}
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
}

SuppDoc suppDoc = suppDocs.get(0);
Integer suppDocId = suppDoc.getSuppDocId();

Resource pdfFile = storageService.loadAsResource(xxxFormMasterId, suppDocId);

File tempPdfFile = File.createTempFile("tmp", ".pdf");
log.debug("Prepare pdf file for sign: {}", tempPdfFile.getPath());

log.info("get hash for sign, userId {} form master id {}", userId, xxxFormMasterId);

InputStream is = null;
try {
is = emptySignature(pdfFile.getInputStream(), tempPdfFile);
} catch (DocumentException | GeneralSecurityException e) {
log.error("error occurrs on calculate signature", e);
}

byte[] encryptedEmptySig = encrypt(StreamUtils.copyToByteArray(is));
//byte[] emptySig = StreamUtils.copyToByteArray(is);
String emptySig = Base64.getEncoder().encodeToString(encryptedEmptySig);

ByteArrayResource resource = new ByteArrayResource(emptySig.getBytes(StandardCharsets.ISO_8859_1));

HttpHeaders headers = new HttpHeaders();
headers.add("Cache-Control", "no-cache, no-store, must-revalidate");
headers.add("Pragma", "no-cache");
headers.add("Expires", "0");

request.getSession(false).setAttribute(TEMP_SIGN_PATH, tempPdfFile.getPath());

return ResponseEntity.ok()
.headers(headers)
.contentLength(resource.contentLength())
.contentType(MediaType.TEXT_PLAIN)
.body(resource);
}

private byte[] encrypt(byte[] emptySig) {
/*try {
log.debug("js debugging for signing with forge")
log.debug("HexBin.encode(emptySig) " + HexBin.encode(emptySig).substring(0, 500));
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
byte hash[] = messageDigest.digest(emptySig);
System.out.println("server emptySig sha256 hash = " + Hex.toHexString(hash));
} catch (NoSuchAlgorithmException e1) {
}*/

String type = "X.509";

String encodedCertificate = request.getParameter("certificate");
try {
CertificateFactory certificateFactory = CertificateFactory.getInstance(type);
Certificate certificate = certificateFactory.generateCertificate(
new ByteArrayInputStream(HashUtils.base64Decode(encodedCertificate)));
PublicKey publicKey = certificate.getPublicKey();

Cipher rsaCipher;
try {
rsaCipher = Cipher.getInstance(RSA_ALGORITHM);
rsaCipher.init(Cipher.ENCRYPT_MODE, publicKey);

// reference from https://stackoverflow.com/questions/24338108/java-encrypt-string-with-existing-public-key-file
// Now create a new 256 bit Rijndael key to encrypt the file itself.
// This will be the session key.
/*KeyGenerator rijndaelKeyGenerator = KeyGenerator.getInstance("Rijndael");
rijndaelKeyGenerator.init(256);
log.debug("Generating session key...");
Key rijndaelKey = rijndaelKeyGenerator.generateKey();
log.debug("Done generating key.");

byte[] encodedKeyBytes = rsaCipher.doFinal(rijndaelKey.getEncoded());
ByteArrayOutputStream output = new ByteArrayOutputStream();
output.write(encodedKeyBytes.length);
output.write(encodedKeyBytes);

// Now we need an Initialization Vector for the symmetric cipher in CBC mode
SecureRandom random = new SecureRandom();
byte[] iv = new byte[16];
random.nextBytes(iv);

output.write(iv);

IvParameterSpec spec = new IvParameterSpec(iv);
// Create the cipher for encrypting the file itself.
Cipher symmetricCipher = Cipher.getInstance(RIJNDAEL_ALGORITHM);
symmetricCipher.init(Cipher.ENCRYPT_MODE, rijndaelKey, spec);
*/

ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream output = new DataOutputStream(baos);

KeyGenerator generator = KeyGenerator.getInstance("AES");
generator.init(128);
log.debug("Generating session key...");
SecretKey aesKey = generator.generateKey();
log.debug("Done generating key.");

log.debug("aesKey.getEncoded() = " + Base64.getEncoder().encodeToString(aesKey.getEncoded()));
byte[] encodedKeyBytes = rsaCipher.doFinal(aesKey.getEncoded());

// output = encrypted AES key length + encrypted AES key + empty signature encrypted by AES key
log.debug("encrypted AES key length = " + encodedKeyBytes.length);
output.writeInt(encodedKeyBytes.length);
output.write(encodedKeyBytes);

Cipher symmetricCipher = Cipher.getInstance(AES_ALGORITHM);
symmetricCipher.init(Cipher.ENCRYPT_MODE, aesKey);

output.write(symmetricCipher.doFinal(emptySig));
output.flush();

log.debug("length of encrypted data = " + baos.toByteArray().length);
return baos.toByteArray();
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
| IllegalBlockSizeException | BadPaddingException | IOException e) {
log.error("encrypt empty signature", e);
}
} catch (CertificateException e) {
log.error("encrypt", e);
}

return null;
}

private InputStream emptySignature(InputStream src, File dest)
throws IOException, DocumentException, GeneralSecurityException {
PdfReader reader = new PdfReader(src);
FileOutputStream os = new FileOutputStream(dest);
PdfStamper stamper = PdfStamper.createSignature(reader, os, '\0');
PdfSignatureAppearance appearance = stamper.getSignatureAppearance();

//appearance.setVisibleSignature(new Rectangle(0, 0, 0, 0), 1, FIELD_NAME);
log.info("create empty signature");
String position = (String) WebUtils.getSession().getAttribute(WebAttributeNames.SESSION_PSCT_DOC_SIGNATURE_POSITION);
String[] p = position.split("\\|");
appearance.setVisibleSignature(new Rectangle(Float.valueOf(p[0]), Float.valueOf(p[1]), Float.valueOf(p[2]), Float.valueOf(p[3])), Integer.valueOf(p[4]), Constants.SIG_FIELD_NAME);

// set signature text
Font font = new Font(FontFamily.TIMES_ROMAN, 10, Font.BOLD);
appearance.setLayer2Font(font);
appearance.setRunDirection(PdfWriter.RUN_DIRECTION_RTL);

appearance.setLayer2Text("Signed By Digital Certificate");

ClassPathResource pinImage = new ClassPathResource("static/images/Cert-sign.png");
appearance.setImage(Image.getInstance(pinImage.getURL()));

//appearance.setCertificate(chain[0]);
ExternalSignatureContainer external = new ExternalBlankSignatureContainer(PdfName.ADOBE_PPKLITE,
PdfName.ADBE_PKCS7_DETACHED);
MakeSignature.signExternalContainer(appearance, external, 8192);
os.close();
return appearance.getRangeStream();
}

@PostMapping(value = "/doc-sign/sign", produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public CertSignResult attachSignedHash() {
CertSignResult certSignResult = new CertSignResult();
certSignResult.setStatus(CertSignResult.STATUS_NOT_SIGNED);

try {
if (!WebUtils.validateCert(pkiClient)) {
return certSignResult;
}
} catch (Exception e) {
log.error("signHash", e);
return certSignResult;
}

String tempPath = (String) request.getSession(false).getAttribute(TEMP_SIGN_PATH);

Integer xxxFormMasterId = getXxxFormMaster().getXxxFormMasterId();
String userId = getUserId();

log.info("sign hash, userId {} form master id {}", userId, xxxFormMasterId);

// tryToDeleteSignedDoc();

File tempSignedFile = null;

try {
tempSignedFile = File.createTempFile("signed-" + xxxFormMasterId + "-", ".pdf");

String hash = request.getParameter("signed");
//log.debug("hash {}", hash);
// Save the hash to file pkcs7-hash.pem
// View p7 file content by: openssl asn1parse -inform pem -in pkcs7-hash.pem

byte[] signedhash = Base64.getDecoder().decode(hash);

createSignature(tempPath, tempSignedFile, signedhash);
} catch (DocumentException | GeneralSecurityException | IOException e) {
log.error("Fail to sign document " + e);
return certSignResult;
}

SimpleSuppDoc doc = new SimpleSuppDoc();
doc.setXxxFormMasterId(xxxFormMasterId);
doc.setLocalFile(tempSignedFile);
doc.setSuppDocType(Constants.SUPP_DOC_TYPE_SYS_SIGN);

try {
Integer suppDocId = storageService.store(doc, userId);

if (doc.isUploadSucess()) {
insertSigningLog(userId, xxxFormMasterId, suppDocId);

// no need to keep the merged doc
storageService.deleteDocByUserIdAndType(xxxFormMasterId, userId, Constants.SUPP_DOC_TYPE_SYS_GEN_MERGE);

certSignResult.setStatus(CertSignResult.STATUS_SIGNED);
request.getSession(false).setAttribute(WebAttributeNames.SESSION_PSCT_DOC_SIGN_STATUS, certSignResult);

createZipFileForSignedDoc(userId, tempSignedFile);
return certSignResult;
}
} catch (IOException e) {
log.error("sign pdf file", e);
} finally {
if (tempSignedFile != null)
tempSignedFile.delete();
}

return certSignResult;
}

private void createSignature(String src, File dest, byte[] signedHash)
throws IOException, DocumentException, GeneralSecurityException {
PdfReader reader = new PdfReader(src);
FileOutputStream os = new FileOutputStream(dest);
ExternalSignatureContainer external = new MyExternalSignatureContainer(signedHash);
MakeSignature.signDeferred(reader, Constants.SIG_FIELD_NAME, os, external);
os.close();
}

class MyExternalSignatureContainer implements ExternalSignatureContainer {
protected byte[] sig;

public MyExternalSignatureContainer(byte[] sig) {
this.sig = sig;
}

@Override
public void modifySigningDictionary(PdfDictionary signDic) {
}

@Override
public byte[] sign(InputStream arg0) throws GeneralSecurityException {
return sig;
}
}

JS for PDF signing

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
function signAndSubmit() {
console.log("signAndSubmit")

var idType = $(":radio[name='idType']:checked").val();
var hkid = $("#hkid").val();
var hkidCheckDigit = $("#hkidCheckDigit").val();
var passport = $("#passport").val()
var file = document.getElementById("filename").files[0];

if (!checkInput(idType, hkid, hkidCheckDigit, passport, file))
return false;

var password = $("#pin").val();
console.log("password " + password)

//Reading certificate from a 'file' form field
var reader = new FileReader();
reader.onload = function (e) {
var contents = e.target.result;

var pkcs12 = loadFile(password, contents);
if(!pkcs12){
showError(ErrorCode.INVALID_PIN);
return false;
}

extractInfo(pkcs12, idType, hkid, hkidCheckDigit, passport).then(function (info) {
var data = {
signature: info.signature,
// certLogin: "1",
certificate: info.certificate,
hkid: hkid + ":" + hkidCheckDigit,
passport: passport,
idType: idType,
};

$.ajax({
type: 'GET',
url: resetStatusUrl + "?keep-challenge=1",
dataType: 'json',
async: false,
success: function(result) {
if (result == 0) {
console.log("start to do signing for the PDF");
}
},
error: function(exception) {
window.location.href = errPath;
}
});

$.get(validateUserUrl, { hkid: data.hkid, passport: data.passport, idType: data.idType }).done(function (result) {
if (result == 'T') {
$.get(signAndSubmitUrl, { certificate: data.certificate }).done(function (signature) {
console.log("signature = " + signature.length);
var ciphertext = forge.util.decode64(signature);

var buffer = forge.util.createBuffer(ciphertext, 'binary');

var aesKeyLength = buffer.getInt32();
console.log("aesKeyLength = " + aesKeyLength);

var encryptedAesKey = buffer.getBytes(aesKeyLength);
console.dir(encryptedAesKey);

var aesKey = privateKey.decrypt(encryptedAesKey);
console.log("decrypted aesKey = " + forge.util.encode64(aesKey));

var decipher = forge.cipher.createDecipher('AES-ECB', aesKey);
decipher.start();
decipher.update(forge.util.createBuffer(buffer.getBytes()));

var result = decipher.finish(); // check 'result' for true/false
console.log("result = " + result);
// outputs decrypted hex
// console.log("outputs decrypted hex = " + decipher.output.toHex());

var emptySig = decipher.output.getBytes();

/* debug for signing
var md = forge.md.sha256.create();
var sh = md.update(decipher.output.getBytes()).digest().toHex();
console.log("origEmptySig sha256 hash = " + sh);
var sh = privateKey.sign(md.update(decipher.output.getBytes()))
*/

var p7 = forge.pkcs7.createSignedData();
p7.content = "Arbitrary data"; // privateKey.sign(md.update(decipher.output.getBytes()));

let _cert; // individual's e-cert
if (info.ca.indexOf('HKPOST') != -1) {
_cert = certChain[0];
certChain.reverse();
} else {
_cert = certChain.slice(-1)[0]
}

// e-cert order: root cert, intermediate cert, individual cert
for (var i = 0; i <= certChain.length - 1; i++) {
p7.addCertificate(certChain[i]);
}

p7.addSigner({
key: privateKey,
certificate: _cert,
digestAlgorithm: forge.pki.oids.sha256,
authenticatedAttributes: [{
type: forge.pki.oids.contentType,
value: forge.pki.oids.data
}, {
type: forge.pki.oids.messageDigest,
value: emptySig // custom digest
}, {
type: forge.pki.oids.signingTime,
value: new Date()
}]
});


p7.sign({ detached: true });

//var out = forge.asn1.toDer(p7.toAsn1()).getBytes();
var out = forge.pkcs7.messageToPem(p7)
// console.log(out);
//var signedHash = forge.util.encode64(out);
signedHash = out.replace("-----BEGIN PKCS7-----", "").replace("-----END PKCS7-----", "").split("\r\n").join("");
//console.dir(signedHash);

data.signed = signedHash;
// console.dir(data);

$.post(signAndSubmitUrl, data, function (result) {
//alert("sign result " + result.status);
if (result.status == "F") {
showError(-131);
} else {
showMessage("doc.sign.success");
}
});
}).fail(function(xhr, statusText, error){
//alert("error status code " + xhr.status);
if (xhr.status == 406) {
showError(-134);
} else if (xhr.status == 404) {
showError(-133);
}
});
} else {
showError(-138);
}
});
}).catch(function (info) {
console.log("error code : " + info.errCode);
showError(info.errCode);
});
}

reader.readAsArrayBuffer(file);
return false;
}

following code writtern was studied by the result from openssl asn1parse -inform pem -in pkcs7-hash.pem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
for (var i = 0; i <= certChain.length - 1; i++) {
p7.addCertificate(certChain[i]);
}

p7.addSigner({
key: privateKey,
certificate: _cert,
digestAlgorithm: forge.pki.oids.sha256,
authenticatedAttributes: [{
type: forge.pki.oids.contentType,
value: forge.pki.oids.data
}, {
type: forge.pki.oids.messageDigest,
value: emptySig // custom digest
}, {
type: forge.pki.oids.signingTime,
value: new Date()
}]
});


p7.sign({ detached: true });

forge.js was hacked by adding following code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
for(var ai = 0; ai < signer.authenticatedAttributes.length; ++ai) {
var attr = signer.authenticatedAttributes[ai];
if(attr.type === forge.pki.oids.messageDigest) {
// use content message digest as value
// attr.value = mds[signer.digestAlgorithm].digest();

// ++++++++ change for setting a custom digest for PDF signing
var digest = mds[signer.digestAlgorithm].start().update(attr.value).digest();
attr.value = digest;
// ++++++++

} else if(attr.type === forge.pki.oids.signingTime) {
// auto-populate signing time if not already set
if(!attr.value) {
attr.value = signingTime;
}
}

AuthenticationApplet.java (itext for PDF signing), below code is as the reference for study on PDF signing with js

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
public String signDoc(String emptySig, String keyID, String type, String privateKeyPassword) {
if (privateKeyPassword == null || "".equals(privateKeyPassword.trim())) {
privateKeyPassword = m_password;
}

if ("p12".equals(type)) {
m_providerName = "BC";
}

PrivateKey privateKey = null;
try {
privateKey = (PrivateKey) m_keyStore.getKey(keyID, privateKeyPassword.toCharArray());
} catch (UnrecoverableKeyException | KeyStoreException | NoSuchAlgorithmException e1) {
e1.printStackTrace();
}

String signHashStr = "";

try {
//byte[] origEmptySig = Base64.getUrlDecoder().decode(emptySig);
byte[] origEmptySig = decrypt(Base64.getDecoder().decode(emptySig), privateKey);
// System.out.println("Showing information for debugging PDF signing using javascript
// System.out.println("origEmptySig = " + Hex.toHexString(origEmptySig).substring(0, 300));

ByteArrayInputStream emptySignature = new ByteArrayInputStream(origEmptySig);

Security.addProvider(new BouncyCastleProvider());

PrivateKeySignature signature = new PrivateKeySignature(privateKey, "SHA256", m_providerName);
String hashAlgorithm = signature.getHashAlgorithm();

System.out.println("hashAlgorithm = " + hashAlgorithm);
BouncyCastleDigest digest = new BouncyCastleDigest();
Certificate[] certificateChain = retrieveCertificateChain(keyID);

/*String oid = DigestAlgorithms.getAllowedDigests(hashAlgorithm);
System.out.println("oid = " + oid);*/

MessageDigest messageDigest = digest.getMessageDigest(hashAlgorithm);
byte hash[] = DigestAlgorithms.digest(emptySignature, messageDigest);

//MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
//byte hash[] = messageDigest.digest(origEmptySig);

// System.out.println("origEmptySig sha256 hash = " + Hex.toHexString(hash));

PdfPKCS7 sgn = new PdfPKCS7(null, certificateChain, hashAlgorithm, null, digest, false);
Calendar cal = Calendar.getInstance();
byte[] sh = sgn.getAuthenticatedAttributeBytes(hash, cal, null, null, CryptoStandard.CMS);
//System.out.println("AuthenticatedAttributeBytes sha256 hash = " + Hex.toHexString(sh));

byte[] extSignature = signature.sign(sh);
//System.out.println("signed AuthenticatedAttributeBytes = " + Hex.toHexString(hash));

sgn.setExternalDigest(extSignature, null, signature.getEncryptionAlgorithm());
byte[] signHash = sgn.getEncodedPKCS7(hash, cal, null, null, null, CryptoStandard.CMS);

// System.out.println("signHash = " + Hex.toHexString(signHash).substring(0, 300));

signHashStr = Base64.getEncoder().encodeToString(signHash);
//Files.write(Paths.get("D:/pkcs7-hash.p7b"), signHash, StandardOpenOption.CREATE);

//System.out.println("signHash = " + signHashStr);
} catch (Exception e) {
System.err.println("An unknown error on calculating hash for signature: " + e.getMessage());
e.printStackTrace();
}

return signHashStr;
}

private static final String RSA_ALGORITHM = "RSA/ECB/PKCS1Padding";
private static final String AES_ALGORITHM = "AES/ECB/PKCS5Padding";

private byte[] decrypt(byte[] ciphertext, PrivateKey privateKey) throws IOException {
try {
Cipher rsaCipher = Cipher.getInstance(RSA_ALGORITHM);
rsaCipher.init(Cipher.DECRYPT_MODE, privateKey);

DataInputStream dis = new DataInputStream(new ByteArrayInputStream(ciphertext));

byte[] encryptedKeyBytes = new byte[dis.readInt()];

dis.readFully(encryptedKeyBytes);

byte[] aesKeyBytes = rsaCipher.doFinal(encryptedKeyBytes);

SecretKeySpec key = new SecretKeySpec(aesKeyBytes, "AES");
Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, key);

ByteArrayOutputStream output = new ByteArrayOutputStream();
int len = -1;
byte[] buffer = new byte[4096];

while ((len = dis.read(buffer, 0, buffer.length)) != -1) {
output.write(buffer, 0, len);
}

output.flush();

return cipher.doFinal(output.toByteArray());
} catch (Exception e) {
System.err.println("An unknown error on decrypt key: " + e.getMessage());
}

return null;
}

Reference

https://security.stackexchange.com/questions/73156/whats-the-difference-between-x-509-and-pkcs7-certificate
https://security.stackexchange.com/questions/41399/openssl-pkcs7-vs-s-mime
https://crypto.stackexchange.com/questions/37084/is-pkcs7-a-signature-format-or-a-certificate-format

openssl asn1parse -inform pem -in pkcs7-hash.pem
Manual verify PKCS#7 signed data with OpenSSL