-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathglobalsign-example.js
More file actions
194 lines (172 loc) · 7.18 KB
/
globalsign-example.js
File metadata and controls
194 lines (172 loc) · 7.18 KB
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
import axios from "axios";
import https from "https";
import forge from "node-forge";
/**
* Example for signing via eSigning API powered by GlobalSign (https://www.globalsign.com/en/digital-signing-service).
*
* Note that Document Engine needs to obtain the trusted certificate chain up to the root authority
* that issued it when validating signatures. It will search for trusted root certificate
* stores at the /certificate-stores path inside its container. You can mount a folder
* from the host machine containing your certificates, or you can configure the CERTIFICATE_STORE_PATHS environment variable
* before launching Document Engine.
*
* This example ships with the GlobalSign root CA in the certs directory, you need to mount this directory as
* `/certificate-stores` inside the Document Engine container (or configure the CERTIFICATE_STORE_PATHS environment variable)
* to validate signatures created with this method properly.
*/
export default class GlobalSignExample {
constructor() {
this.apiKey = get_env_or_throw("GLOBALSIGN_API_KEY");
this.apiSecret = get_env_or_throw("GLOBALSIGN_API_SECRET");
this.tlsCert = get_env_or_throw("GLOBALSIGN_TLS_CERT").replace(/\\n/g, "\n");
this.tlsKey = get_env_or_throw("GLOBALSIGN_TLS_KEY").replace(/\\n/g, "\n");
this.instance = axios.create({
baseURL: "https://emea.api.dss.globalsign.com:8443/v2",
headers: {
"Content-Type": "application/json;charset=utf-8",
},
httpsAgent: new https.Agent({
cert: this.tlsCert,
key: this.tlsKey,
}),
});
// An object that maps the current GlobalSign identity with The UNIX timestamp of its creation date.
// This is to reuse identities if they haven’t expired (that is, if they are less than 10 minutes old).
// Mutable data.
this.currentIdentity = { id: null, signingCert: null, timestamp: null };
// A promise that acts as a lock for safe concurrent access to the mutable shared identity cache.
this.currentIdentityQueue = Promise.resolve();
}
/**
* Returns the full certificate chain for the signature, needs to include
* signer certificate as well as any CA in the certificate chain.
*
* Used only for RAW RSA signing (see signRaw() function above).
*
* @return Certificates in PEM format.
*/
async getCertificates() {
console.log("🖊️ Retrieving certificates from GlobalSign DSS...");
const identity = await this.#generateOrReuseIdentity();
const intermediateAndRootCAs = await this.#getIntermediateAndRootCACertificates();
const userCertificates = [Buffer.from(identity.signingCert).toString("base64")];
const caCertificates = intermediateAndRootCAs.map((cert) =>
Buffer.from(cert).toString("base64")
);
return {
certificates: userCertificates,
ca_certificates: caCertificates,
};
}
/**
* Signs the data via GlobalSign DSS API.
*
* @param dataToBeSigned Binary data that needs to be signed.
* @param hashAlgorithm The hash algorithm that should be used to create the data digest.
* @returns DER encoded RSASSA-PKCS1-v1_5 signature.
*/
async signRaw(dataToBeSigned, hashAlgorithm) {
console.log("🖊️ Signing with GlobalSign DSS...");
// Calculate the digest of the data we are signing before before handing it to the GlobalSign DSS API.
// The API expects SHA-256.
const md = forge.md.sha256.create();
md.update(dataToBeSigned.toString("binary"));
const identity = await this.#generateOrReuseIdentity();
const signature = await this.#signDigest(identity.id, md.digest().toHex());
return Buffer.from(signature, "hex");
}
async #currentIdentityHandler(action) {
// Chain access and modification of identity cache.
this.currentIdentityQueue = this.currentIdentityQueue.then(action).catch((error) => {
// Reset the queue after a failure so later calls can rebuild the identity cache.
this.currentIdentityQueue = Promise.resolve();
throw error;
});
return this.currentIdentityQueue;
}
async #login() {
const loginPayload = {
api_key: this.apiKey,
api_secret: this.apiSecret,
};
try {
const response = await this.instance.post("/login", loginPayload);
return response.data.access_token;
} catch (e) {
throw new Error(`GlobalSign login failed: ${e}`);
}
}
async #generateOrReuseIdentity() {
return this.#currentIdentityHandler(async () => {
console.log("Generating identity in GlobalSign DSS...");
// If this identity is less than 9 minutes old, we reuse it.
if (this.currentIdentity.id && Date.now() - this.currentIdentity.timestamp <= 540000) {
return {
id: this.currentIdentity.id,
signingCert: this.currentIdentity.signingCert,
};
}
// If you have a production account with GlobalSign, you also need to pass the Common Name (CN). See the documentation
// for the /identity endpoint to know the certificate fields that you can customize.
const identityPayload = {
subject_dn: {
organizational_unit: ["Signing"],
},
};
try {
const response = await this.instance.post("/identity", identityPayload, {
headers: { Authorization: `Bearer ${await this.#login()}` },
});
// Set this as the currently active identity.
this.currentIdentity = {
id: response.data.id,
signingCert: response.data.signing_cert,
timestamp: Date.now(),
};
return {
id: this.currentIdentity.id,
signingCert: this.currentIdentity.signingCert,
};
} catch (e) {
throw new Error(`GlobalSign identity failed: ${JSON.stringify(e.response.data)}`);
}
});
}
async #getIntermediateAndRootCACertificates() {
logger.info("Retrieving GlobalSign intermediate and CA chain...");
try {
const response = await this.instance.get("/trustchain", {
headers: { Authorization: `Bearer ${await this.#login()}` },
});
logger.debug(
`GlobalSign intermediate and root CAs retrieved successfully. Path: ${response.data.trustchain}`
);
return response.data.trustchain;
} catch (e) {
logger.error("GlobalSign trustchain failed:", e);
throw new Error(`GlobalSign trustchain failed: ${JSON.stringify(e.response.data)}`);
}
}
async #signDigest(id, digest) {
return this.#currentIdentityHandler(async () => {
try {
const response = await this.instance.get(`/identity/${id}/sign/${digest}`, {
headers: { Authorization: `Bearer ${await this.#login()}` },
});
return response.data.signature;
} catch (e) {
this.currentIdentity = { id: null, signingCert: null, timestamp: null };
throw new Error(`GlobalSign sign failed: ${JSON.stringify(e.response.data)}`);
}
});
}
async signPkcs7(_documentContentsToSign, _hashAlgorithm) {
throw new Error("Not implemented, use RAW signing instead of contained PKCS#7 signatures.");
}
}
function get_env_or_throw(envName) {
if (!process.env[envName]) {
throw new Error(`${envName} environment variable needs to be set to use GlobalSign example.`);
}
return process.env[envName];
}