Skip to content

Commit 2c2df74

Browse files
committed
refactor: simplify codebase for clarity and consistency
- Extract helper functions to reduce complexity and nesting - Use modern JS patterns (startsWith, endsWith, optional chaining, spread) - Rename constants to UPPER_CASE convention - Use filter(Boolean) for cleaner array filtering - Replace switch with Set lookup in vmc.js - Simplify conditional logic with early returns
1 parent 0ae588b commit 2c2df74

File tree

7 files changed

+218
-212
lines changed

7 files changed

+218
-212
lines changed

build-root-store.js

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,25 @@
22

33
// Root certificates from https://bimigroup.org/vmc-issuers/
44

5-
const Path = require('path');
5+
const path = require('path');
66
const fs = require('fs').promises;
77
const { parsePemBundle } = require('./lib/tools');
88

9-
async function main() {
10-
const rootStoreFilesPath = Path.join(__dirname, 'data', 'root-store');
9+
const ROOT_STORE_DIR = path.join(__dirname, 'data', 'root-store');
10+
const OUTPUT_FILE = path.join(__dirname, 'data', 'root-store.json');
1111

12-
let rootCerts = [];
12+
async function main() {
13+
const files = await fs.readdir(ROOT_STORE_DIR);
14+
const pemFiles = files.filter(file => file.endsWith('.pem'));
1315

14-
let files = (await fs.readdir(rootStoreFilesPath)).filter(file => /\.pem$/.test(file));
15-
for (let file of files) {
16-
let certs = parsePemBundle(await fs.readFile(Path.join(rootStoreFilesPath, file)));
17-
for (let cert of certs) {
18-
rootCerts.push(cert);
19-
}
16+
const rootCerts = [];
17+
for (const file of pemFiles) {
18+
const content = await fs.readFile(path.join(ROOT_STORE_DIR, file));
19+
const certs = parsePemBundle(content);
20+
rootCerts.push(...certs);
2021
}
2122

22-
await fs.writeFile(Path.join(__dirname, 'data', 'root-store.json'), JSON.stringify(rootCerts, false, 2));
23+
await fs.writeFile(OUTPUT_FILE, JSON.stringify(rootCerts, null, 2));
2324
}
2425

2526
main().catch(err => {

lib/cert-info.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,17 @@
22

33
const { getDateString, parseSubject } = require('./tools');
44

5-
function certInfo(cert) {
6-
let subject = parseSubject(cert.subject);
7-
8-
let subjectAltName = (cert.subjectAltName || '')
5+
function parseSubjectAltName(subjectAltName) {
6+
return (subjectAltName || '')
97
.split(/^DNS:|,\s*DNS:/g)
10-
.map(subject => subject.trim())
11-
.filter(subject => subject);
8+
.map(name => name.trim())
9+
.filter(Boolean);
10+
}
1211

12+
function certInfo(cert) {
1313
return {
14-
subject,
15-
subjectAltName,
14+
subject: parseSubject(cert.subject),
15+
subjectAltName: parseSubjectAltName(cert.subjectAltName),
1616
fingerprint: cert.fingerprint,
1717
serialNumber: cert.serialNumber,
1818
validFrom: getDateString(cert.validFrom),

lib/parse-vmc.js

Lines changed: 77 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
'use strict';
22

33
const crypto = require('crypto');
4+
const zlib = require('zlib');
5+
const { promisify } = require('util');
46
const { AsnParser } = require('@peculiar/asn1-schema');
57
const { LogotypeExtn } = require('@peculiar/asn1-x509-logotype');
68
const { Certificate } = require('@peculiar/asn1-x509');
7-
const zlib = require('zlib');
8-
const util = require('util');
9-
const gunzip = util.promisify(zlib.gunzip);
109

11-
const hashAlgos = {
10+
const gunzip = promisify(zlib.gunzip);
11+
12+
const HASH_ALGORITHMS = {
1213
'1.3.14.3.2.26': 'sha1',
1314
'2.16.840.1.101.3.4.2.4': 'sha224',
1415
'2.16.840.1.101.3.4.2.1': 'sha256',
@@ -19,93 +20,95 @@ const hashAlgos = {
1920
};
2021

2122
function parseDataUrl(url) {
22-
if (url.indexOf('data:') !== 0) {
23+
if (!url.startsWith('data:')) {
2324
return {};
2425
}
2526

26-
let commaPos = url.indexOf(',');
27+
const commaPos = url.indexOf(',');
2728
if (commaPos < 0) {
2829
return {};
2930
}
3031

31-
let header = url.substring('data:'.length, commaPos);
32-
let content = url.substring(commaPos + 1);
33-
34-
let parts = header.split(';');
35-
let mediaType = parts.shift() || '';
36-
let base64 = parts.shift() === 'base64';
32+
const header = url.substring(5, commaPos);
33+
const content = url.substring(commaPos + 1);
34+
const parts = header.split(';');
35+
const mediaType = parts.shift() || '';
36+
const base64 = parts.shift() === 'base64';
3737

3838
return { header, content, mediaType, base64 };
3939
}
4040

41+
function extractHashInfo(imageDetails) {
42+
const hashObject = imageDetails.logotypeHash?.[0];
43+
if (!hashObject) {
44+
return { hashAlgo: undefined, hashValue: undefined };
45+
}
46+
47+
const hashAlgoOid = hashObject.hashAlg?.algorithm;
48+
const hashAlgo = hashAlgoOid ? HASH_ALGORITHMS[hashAlgoOid] : undefined;
49+
const hashValue = hashObject.hashValue?.buffer ? Buffer.from(hashObject.hashValue.buffer) : undefined;
50+
51+
return { hashAlgo, hashValue };
52+
}
53+
54+
async function extractLogoFile(imageDetails) {
55+
const logotypeURI = imageDetails.logotypeURI?.[0];
56+
if (!logotypeURI) {
57+
return undefined;
58+
}
59+
60+
const { content, base64 } = parseDataUrl(logotypeURI);
61+
if (!content) {
62+
return undefined;
63+
}
64+
65+
const data = base64 ? Buffer.from(content, 'base64') : content;
66+
return gunzip(data);
67+
}
68+
69+
function validateHash(logoFile, hashAlgo, hashValue) {
70+
if (!logoFile || !hashAlgo || !hashValue) {
71+
return undefined;
72+
}
73+
74+
try {
75+
const hasher = crypto.createHash(hashAlgo);
76+
return hashValue.equals(hasher.update(logoFile).digest());
77+
} catch {
78+
return undefined;
79+
}
80+
}
81+
4182
async function parseVMC(buf) {
4283
const cert = AsnParser.parse(buf, Certificate);
4384
const logoExtension = cert.tbsCertificate.extensions.find(ext => ext.extnID === '1.3.6.1.5.5.7.1.12');
4485

45-
if (logoExtension && logoExtension.extnValue) {
46-
let logo = AsnParser.parse(logoExtension.extnValue, LogotypeExtn);
47-
48-
let imageDetails =
49-
logo.subjectLogo &&
50-
logo.subjectLogo.direct &&
51-
logo.subjectLogo.direct.image &&
52-
logo.subjectLogo.direct.image[0] &&
53-
logo.subjectLogo.direct.image[0].imageDetails;
54-
55-
if (imageDetails) {
56-
let hashAlgo, hashValue, hashAlgoOid;
57-
58-
let hashObject = imageDetails.logotypeHash && imageDetails.logotypeHash[0] && imageDetails.logotypeHash[0];
59-
60-
if (hashObject) {
61-
hashAlgoOid = hashObject.hashAlg && hashObject.hashAlg.algorithm;
62-
if (hashAlgoOid && hashAlgos[hashAlgoOid]) {
63-
hashAlgo = hashAlgos[hashAlgoOid];
64-
}
65-
66-
if (hashObject.hashValue && hashObject.hashValue.buffer) {
67-
hashValue = Buffer.from(hashObject.hashValue.buffer);
68-
}
69-
}
70-
71-
let logotypeURI = imageDetails.logotypeURI && imageDetails.logotypeURI[0];
72-
let logoFile;
73-
if (logotypeURI) {
74-
let { content, base64 } = parseDataUrl(logotypeURI);
75-
if (content) {
76-
if (base64) {
77-
content = Buffer.from(content, 'base64');
78-
}
79-
logoFile = await gunzip(content);
80-
}
81-
}
82-
83-
let data = {
84-
mediaType: imageDetails.mediaType,
85-
hashAlgo,
86-
hashValue,
87-
logoFile
88-
};
89-
90-
if (data.logoFile && (data.hashAlgo || data.hashAlgoOid) && data.hashValue) {
91-
let hasher;
92-
try {
93-
hasher = crypto.createHash(data.hashAlgo || data.hashAlgoOid);
94-
} catch {
95-
// invalid hash
96-
}
97-
if (hasher) {
98-
data.validHash = hashValue.equals(hasher.update(logoFile).digest());
99-
}
100-
}
101-
102-
return data;
103-
}
86+
if (!logoExtension?.extnValue) {
87+
const error = new Error('Invalid or missing logotype extension in the certificate');
88+
error.code = 'INVALID_LOGOTYPE_EXT';
89+
throw error;
90+
}
91+
92+
const logo = AsnParser.parse(logoExtension.extnValue, LogotypeExtn);
93+
const imageDetails = logo.subjectLogo?.direct?.image?.[0]?.imageDetails;
94+
95+
if (!imageDetails) {
96+
const error = new Error('Invalid or missing logotype extension in the certificate');
97+
error.code = 'INVALID_LOGOTYPE_EXT';
98+
throw error;
10499
}
105100

106-
let error = new Error('Invalid or missing logotype extension in the certificate');
107-
error.code = 'INVALID_LOGOTYPE_EXT';
108-
throw error;
101+
const { hashAlgo, hashValue } = extractHashInfo(imageDetails);
102+
const logoFile = await extractLogoFile(imageDetails);
103+
const validHash = validateHash(logoFile, hashAlgo, hashValue);
104+
105+
return {
106+
mediaType: imageDetails.mediaType,
107+
hashAlgo,
108+
hashValue,
109+
logoFile,
110+
validHash
111+
};
109112
}
110113

111114
module.exports = { parseVMC };

lib/root-store.js

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
11
'use strict';
22

33
const { X509Certificate } = require('crypto');
4-
const rootCerts = require('../data/root-store.json');
4+
const rootCertPems = require('../data/root-store.json');
5+
6+
function parseRootCert(pem) {
7+
try {
8+
return new X509Certificate(pem);
9+
} catch (err) {
10+
console.error('Failed to parse root cert');
11+
console.error(err);
12+
console.error(pem);
13+
return null;
14+
}
15+
}
516

617
class RootStore {
718
static create(config = {}) {
@@ -10,31 +21,20 @@ class RootStore {
1021

1122
constructor(config) {
1223
this.config = config;
13-
this.rootCerts = rootCerts
14-
.map(pem => {
15-
try {
16-
return new X509Certificate(pem);
17-
} catch (err) {
18-
console.error(`Failed to parse root cert`);
19-
console.error(err);
20-
console.error(pem);
21-
}
22-
})
23-
.filter(cert => cert);
24+
this.rootCerts = rootCertPems.map(parseRootCert).filter(Boolean);
2425
}
2526

2627
addCert(pem) {
27-
let cert = new X509Certificate(pem);
28-
for (let rootCert of this.rootCerts) {
29-
if (rootCert.fingerprint256 === cert.fingerprint256) {
30-
return false;
31-
}
28+
const cert = new X509Certificate(pem);
29+
const isDuplicate = this.rootCerts.some(rootCert => rootCert.fingerprint256 === cert.fingerprint256);
30+
31+
if (isDuplicate) {
32+
return false;
3233
}
34+
3335
this.rootCerts.push(cert);
3436
return true;
3537
}
3638
}
3739

38-
module.exports = {
39-
RootStore
40-
};
40+
module.exports = { RootStore };

0 commit comments

Comments
 (0)