1 | 1 | var jsrsasign = require('jsrsasign'); |
|
2 | 1 | var ProtoBuf = require('../protobuf'); |
|
3 | 1 | var PKIType = require('./pkitype'); |
|
4 | 1 | var X509Certificates = ProtoBuf.X509Certificates; |
|
5 | |||
6 | /** |
||
7 | * Checks if certificate serial numbers match. |
||
8 | * @param certA |
||
9 | * @param certB |
||
10 | * @returns {boolean} |
||
11 | */ |
||
12 | function hasEqualSerialNumber(certA, certB) { |
||
13 | 10 | return certA.getSerialNumberHex() === certB.getSerialNumberHex(); |
|
14 | } |
||
15 | |||
16 | /** |
||
17 | * Checks if certificate subjects match |
||
18 | * @param certA |
||
19 | * @param certB |
||
20 | * @returns {boolean} |
||
21 | */ |
||
22 | function hasEqualSubject(certA, certB) { |
||
23 | return certA.getSubjectHex() === certB.getSubjectHex(); |
||
24 | } |
||
25 | |||
26 | /** |
||
27 | * Checks if certificate public keys match |
||
28 | * @param certA |
||
29 | * @param certB |
||
30 | * @returns {boolean} |
||
31 | */ |
||
32 | function hasEqualPublicKey(certA, certB) { |
||
33 | return certA.getPublicKeyHex() === certB.getPublicKeyHex(); |
||
34 | } |
||
35 | |||
36 | /** |
||
37 | * Checks if certificates are equal |
||
38 | * @param certA |
||
39 | * @param certB |
||
40 | * @returns {boolean} |
||
41 | */ |
||
42 | function checkCertsEqual(certA, certB) { |
||
43 | 10 | return hasEqualSerialNumber(certA, certB) && |
|
44 | hasEqualSubject(certA, certB) && |
||
45 | hasEqualPublicKey(certA, certB); |
||
46 | } |
||
47 | |||
48 | /** |
||
49 | * Checks if the certificate is self issued |
||
50 | * @param certificate |
||
51 | * @returns {boolean} |
||
52 | */ |
||
53 | function isSelfSigned(certificate) { |
||
54 | 11 | return certificate.getSubjectString() === certificate.getIssuerString(); |
|
55 | } |
||
56 | |||
57 | /** |
||
58 | * Returns a callback that filters by the |
||
59 | * provided subject key identifier. |
||
60 | * @param subjectKeyId |
||
61 | * @returns {Function} |
||
62 | */ |
||
63 | function makeFilterBySubjectKey(subjectKeyId) { |
||
64 | 46 | return function(certificate) { |
|
65 | 43 | try { |
|
66 | 43 | return certificate.getExtSubjectKeyIdentifier() === subjectKeyId; |
|
67 | } catch (e) {} |
||
0 ignored issues
–
show
Coding Style
Comprehensibility
Best Practice
introduced
by
![]() |
|||
68 | |||
69 | return false; |
||
70 | }; |
||
71 | } |
||
72 | |||
73 | /** |
||
74 | * Look in bundle for certificates for the |
||
75 | * issuer of `target` |
||
76 | * @param target |
||
77 | * @param bundle |
||
78 | * @returns {Array} |
||
79 | */ |
||
80 | function findIssuers(target, bundle) { |
||
81 | 46 | try { |
|
82 | 46 | var authorityKeyIdentifier = target.getExtAuthorityKeyIdentifier(); |
|
83 | |||
84 | 46 | if (typeof authorityKeyIdentifier.kid === "string") { |
|
85 | 46 | var issuerName = target.getIssuerString(); |
|
86 | 46 | return bundle |
|
87 | .filter(makeFilterBySubjectKey(authorityKeyIdentifier.kid)) |
||
88 | .filter(function(issuerCert) { |
||
89 | 21 | return issuerName === issuerCert.getSubjectString(); |
|
90 | }); |
||
91 | } |
||
92 | } catch (e) { } |
||
0 ignored issues
–
show
Coding Style
Comprehensibility
Best Practice
introduced
by
|
|||
93 | |||
94 | return []; |
||
95 | } |
||
96 | |||
97 | /** |
||
98 | * ChainPathBuilder builds an unverified chain |
||
99 | * of certificates by linking issuer<>subject |
||
100 | * @param trustStore |
||
101 | * @constructor |
||
102 | */ |
||
103 | function ChainPathBuilder(trustStore) { |
||
104 | 12 | if (typeof trustStore === "undefined") { |
|
105 | trustStore = []; |
||
106 | } |
||
107 | |||
108 | 12 | this.trustStore = trustStore; |
|
109 | } |
||
110 | |||
111 | /** |
||
112 | * Returns all paths to the target. First checks if target |
||
113 | * was issued by a trustStore certificate, then attempts |
||
114 | * against issuers (if any) |
||
115 | * @param target |
||
116 | * @param intermediates |
||
117 | * @returns {Array} |
||
118 | * @private |
||
119 | */ |
||
120 | 1 | ChainPathBuilder.prototype._pathsToTarget = function(target, intermediates) { |
|
121 | 23 | var paths = []; |
|
122 | 23 | findIssuers(target, this.trustStore).map(function(issuer) { |
|
123 | 10 | if (checkCertsEqual(target, issuer)) { |
|
124 | paths.push(target); |
||
125 | } else { |
||
126 | 10 | paths.push([].concat(issuer, target)); |
|
127 | } |
||
128 | }); |
||
129 | |||
130 | 23 | if (Array.isArray(intermediates)) { |
|
131 | 23 | var self = this; |
|
132 | 23 | findIssuers(target, intermediates) |
|
133 | .map(function(issuer) { |
||
134 | 11 | if (isSelfSigned(issuer)) { |
|
135 | return; |
||
136 | } |
||
137 | |||
138 | 11 | var subpaths = self._pathsToTarget(issuer, intermediates); |
|
139 | 11 | subpaths.map(function(path) { |
|
140 | 10 | paths.push([].concat(path, target)); |
|
141 | }); |
||
142 | }); |
||
143 | } |
||
144 | |||
145 | 23 | return paths; |
|
146 | }; |
||
147 | |||
148 | /** |
||
149 | * Searches for paths which qualify `target`, |
||
150 | * and returns the shortest. |
||
151 | * @param target |
||
152 | * @param intermediates |
||
153 | * @returns {*} |
||
154 | */ |
||
155 | 1 | ChainPathBuilder.prototype.shortestPathToTarget = function(target, intermediates) { |
|
156 | 12 | var paths = this._pathsToTarget(target, intermediates); |
|
157 | 12 | if (paths.length === 0) { |
|
158 | 2 | throw new Error("No certificate paths found"); |
|
159 | } |
||
160 | |||
161 | 10 | paths.sort(function(a, b) { |
|
162 | 2 | if (a < b) { |
|
163 | return -1; |
||
164 | } |
||
165 | 2 | if (b > a) { |
|
166 | return 1; |
||
167 | } |
||
168 | |||
169 | return 0; |
||
170 | }); |
||
171 | |||
172 | 10 | return paths[0]; |
|
173 | }; |
||
174 | |||
175 | /** |
||
176 | * Encapsulation for some validation state |
||
177 | * @param pubKey |
||
178 | * @param issuerName |
||
179 | * @constructor |
||
180 | */ |
||
181 | function WorkingData(pubKey, issuerName) { |
||
182 | 21 | this.pubKey = pubKey; |
|
183 | 21 | this.issuerName = issuerName; |
|
184 | 21 | this.getPublicKey = function() { |
|
185 | 21 | return this.pubKey; |
|
186 | }; |
||
187 | 21 | this.getIssuerName = function() { |
|
188 | 15 | return this.issuerName; |
|
189 | }; |
||
190 | } |
||
191 | |||
192 | /** |
||
193 | * |
||
194 | * @param {jsrsasign.Certificate} rootCert |
||
195 | * @param {number} chainLength |
||
196 | * @param {object} opts |
||
197 | * @constructor |
||
198 | */ |
||
199 | function ChainValidationState(rootCert, chainLength, opts) { |
||
200 | 10 | this.workingData = new WorkingData(rootCert.getPublicKey(), rootCert.getIssuerString()); |
|
201 | 10 | this.index = 1; |
|
202 | 10 | this.chainLength = chainLength; |
|
203 | |||
204 | 10 | if (opts.currentTime) { |
|
205 | 10 | this.currentTime = opts.currentTime; |
|
206 | } else { |
||
207 | this.currentTime = Date.now(); |
||
208 | } |
||
209 | |||
210 | 10 | var self = this; |
|
211 | 10 | this.updateState = function(cert) { |
|
212 | 11 | self.workingData = new WorkingData(cert.getPublicKey(), cert.getSubjectString()); |
|
213 | }; |
||
214 | 10 | this.isFinal = function() { |
|
215 | 15 | return this.chainLength === self.index; |
|
216 | }; |
||
217 | } |
||
218 | |||
219 | /** |
||
220 | * Constructor for ChainPathValidator. Takes a list of |
||
221 | * certificates, starting from the root, ending with |
||
222 | * the entity certificate. |
||
223 | * |
||
224 | * @param {object} config |
||
225 | * @param {jsrsasign.Certificate[]} certificates |
||
226 | * @constructor |
||
227 | */ |
||
228 | function ChainPathValidator(config, certificates) { |
||
229 | 10 | this._certificates = certificates; |
|
230 | 10 | this._config = config; |
|
231 | 10 | this._trustAnchor = certificates[0]; |
|
232 | } |
||
233 | |||
234 | /** |
||
235 | * |
||
236 | * @param {ChainValidationState} state |
||
237 | * @param {KJUR.asn1.x509.Certificate} cert |
||
238 | */ |
||
239 | function checkSignature(state, cert) { |
||
240 | 21 | if (!cert.verifySignature(state.workingData.getPublicKey())) { |
|
241 | throw new Error("Failed to verify signature"); |
||
242 | } |
||
243 | } |
||
244 | |||
245 | /** |
||
246 | * |
||
247 | * @param {ChainValidationState} state |
||
248 | * @param {KJUR.asn1.x509.Certificate} cert |
||
249 | */ |
||
250 | function checkValidity(state, cert) { |
||
251 | 21 | var notBefore = jsrsasign.zulutodate(cert.getNotBefore()); |
|
252 | 21 | var notAfter = jsrsasign.zulutodate(cert.getNotAfter()); |
|
253 | |||
254 | 21 | if (notBefore > Date.parse(state.currentTime) || notAfter < Date.parse(state.currentTime)) { |
|
255 | 6 | throw new Error("Certificate is not valid"); |
|
256 | } |
||
257 | } |
||
258 | function checkRevocation(cert) { |
||
0 ignored issues
–
show
|
|||
259 | |||
260 | } |
||
261 | function checkIssuer(state, cert) { |
||
262 | 15 | if (state.workingData.getIssuerName() !== cert.getIssuerString()) { |
|
263 | throw new Error("Certificate issuer doesn't match"); |
||
264 | } |
||
265 | } |
||
266 | function processCertificate(state, cert) { |
||
267 | 21 | checkSignature(state, cert); |
|
268 | 21 | checkValidity(state, cert); |
|
269 | // crl handling |
||
270 | 15 | checkIssuer(state, cert); |
|
271 | } |
||
272 | |||
273 | 1 | ChainPathValidator.prototype.validate = function() { |
|
274 | 10 | var state = new ChainValidationState(this._trustAnchor, this._certificates.length, this._config); |
|
275 | 10 | for (var i = 0; i < this._certificates.length; i++) { |
|
276 | 21 | state.index = i + 1; |
|
277 | 21 | var cert = this._certificates[i]; |
|
278 | 21 | processCertificate(state, cert); |
|
279 | 15 | if (!state.isFinal()) { |
|
280 | 11 | state.updateState(cert); |
|
281 | } |
||
282 | } |
||
283 | }; |
||
284 | |||
285 | 1 | var RequestValidator = function(opts) { |
|
286 | 2 | if (typeof opts === "undefined") { |
|
287 | opts = {}; |
||
288 | } |
||
289 | 2 | opts.trustStore = opts.trustStore ? opts.trustStore : []; |
|
290 | 2 | this.opts = opts; |
|
291 | }; |
||
292 | |||
293 | 1 | RequestValidator.prototype.verifyX509Details = function(paymentRequest) { |
|
294 | 2 | var x509 = X509Certificates.decode(paymentRequest.pkiData); |
|
295 | |||
296 | function certFromDER(derBuf) { |
||
297 | 4 | var cert = new jsrsasign.X509(); |
|
298 | 4 | cert.readCertHex(new Buffer(derBuf).toString('hex')); |
|
0 ignored issues
–
show
The variable
Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.
This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed. To learn more about declaring variables in Javascript, see the MDN. ![]() |
|||
299 | 4 | return cert; |
|
300 | } |
||
301 | |||
302 | 2 | var entityCert = certFromDER(x509.certificate[0]); |
|
303 | 2 | var intermediates = x509.certificate.slice(1).map(certFromDER); |
|
304 | 2 | var path = this.validateCertificateChain(entityCert, intermediates); |
|
305 | |||
306 | 2 | if (!this.validateSignature(paymentRequest, entityCert)) { |
|
307 | 1 | throw new Error("Invalid signature on request"); |
|
308 | } |
||
309 | |||
310 | 1 | return path; |
|
311 | }; |
||
312 | |||
313 | 1 | RequestValidator.prototype.validateCertificateChain = function(entityCert, intermediates) { |
|
314 | 3 | var builder = new ChainPathBuilder(this.opts.trustStore); |
|
315 | 3 | var path = builder.shortestPathToTarget(entityCert, intermediates); |
|
316 | 3 | var validator = new ChainPathValidator(this.opts, path); |
|
317 | 3 | validator.validate(); |
|
318 | 3 | return path; |
|
319 | }; |
||
320 | |||
321 | 1 | RequestValidator.prototype.validateSignature = function(request, entityCert) { |
|
322 | 4 | var sig = new jsrsasign.Signature({alg: getSignatureAlgorithm(entityCert, request.pkiType)}); |
|
323 | 4 | sig.init(entityCert.getPublicKey()); |
|
324 | 4 | sig.updateHex(getDataToSign(request).toString('hex')); |
|
325 | 4 | return sig.verify(Buffer.from(request.signature).toString('hex')); |
|
0 ignored issues
–
show
The variable
Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.
This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed. To learn more about declaring variables in Javascript, see the MDN. ![]() |
|||
326 | }; |
||
327 | |||
328 | function getDataToSign(request) { |
||
329 | 4 | var tmp = request.signature; |
|
330 | 4 | request.signature = ''; |
|
331 | 4 | var encoded = new Buffer(ProtoBuf.PaymentRequest.encode(request).finish()); |
|
0 ignored issues
–
show
The variable
Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.
This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed. To learn more about declaring variables in Javascript, see the MDN. ![]() |
|||
332 | 4 | request.signature = tmp; |
|
333 | 4 | return encoded; |
|
334 | } |
||
335 | |||
336 | function getSignatureAlgorithm(entityCert, pkiType) { |
||
337 | 11 | var publicKey = entityCert.getPublicKey(); |
|
338 | |||
339 | var keyType; |
||
340 | 11 | if (publicKey.type === "ECDSA") { |
|
341 | 4 | keyType = "ECDSA"; |
|
342 | 7 | } else if (publicKey.type === "RSA") { |
|
343 | 6 | keyType = "RSA"; |
|
344 | } else { |
||
345 | 1 | throw new Error("Unknown public key type"); |
|
346 | } |
||
347 | |||
348 | var hashAlg; |
||
349 | 10 | if (pkiType === PKIType.X509_SHA1) { |
|
350 | 2 | hashAlg = "SHA1"; |
|
351 | 8 | } else if (pkiType === PKIType.X509_SHA256) { |
|
352 | 6 | hashAlg = "SHA256"; |
|
353 | } else { |
||
354 | 2 | throw new Error("Unknown PKI type or no signature algorithm specified."); |
|
355 | } |
||
356 | |||
357 | 8 | return hashAlg + "with" + keyType; |
|
358 | } |
||
359 | |||
360 | 1 | module.exports = { |
|
361 | ChainPathBuilder: ChainPathBuilder, |
||
362 | ChainPathValidator: ChainPathValidator, |
||
363 | RequestValidator: RequestValidator, |
||
364 | GetSignatureAlgorithm: getSignatureAlgorithm |
||
365 | }; |
||
366 |