cert_utils (@techteamer/cert-utils)
A small, library-only npm package that parses X.509 certificates (P12/PKCS#12 containers and PEM) and validates them (expiry, CA chain, OCSP revocation). It shells out to the openssl CLI for the actual cryptographic work and wraps the output in three classes: CertService, CertInfo, CertValidationError. The README states it “implements the CertService, Certinfo and CertvalidationError classes for the timestamps” (cert_utils/README.md:4).
This is a leaf dependency, not a service.
There is no HTTP server, no message queue consumer, no daemon, no
binentrypoint, and no Dockerfile in this repo. It is consumed by other FaceKom packages/services viarequire('@techteamer/cert-utils')/ ESM import. The package only ships thebuild/directory (cert_utils/package.json:17-19). Within FaceKom it is the building block for certificate handling in timestamp/signing flows (README framing: “for the timestamps”,cert_utils/README.md:4); the consumers themselves live in other repos.
1. Purpose & role in FaceKom
| Aspect | Detail | Source |
|---|---|---|
| Package name | @techteamer/cert-utils | package.json:2 |
| Version | 1.1.3 | package.json:3 |
| Description | ”This package contains a CertUtils which can parsing certs” | package.json:4 |
| License | MIT, © 2024 TechTeamer | LICENSE:1-3 |
| Public surface | Exports only CertService (which exposes nested CertType, and returns CertInfo / throws CertValidationError) | src/index.ts:1-3 |
Public API (verified against README + CertService.ts):
parseCert(certBuf, password, certType='P12')→Promise<CertInfo>— dispatches to P12 or PEM parser (CertService.ts:70-83).parseCertFromFile(certPath, password)→ reads a P12 file path via openssl, with legacy-algorithm fallback (CertService.ts:86-130).parsePemCert(pemCert)→ parse a PEM cert buffer/string (CertService.ts:167-180).verifyCert(pemCert, pemCaCert=null, verifyOcsp=false)→ validity + optional CA chain + optional OCSP; throwsCertValidationError(CertService.ts:396-409).getPemCertBody(pemCert)→ returns the first cert’s DER body as aBufferfrom a PEM/chain (CertService.ts:507-561).- Static/instance getter
CertType→{ P12: 'P12', PEM: 'PEM' }(CertService.ts:44-53).
See also architecture-overview, vuer_oss.
2. Tech stack & runtime
| Item | Value | Source |
|---|---|---|
| Language | TypeScript (compiled/bundled to CJS + ESM + .d.ts) | package.json:7-16, rollup.config.js |
| Module type | ESM ("type": "module") | package.json:37 |
| Required Node | >=20.19.0 (engines) | package.json:24-26 |
| Package manager | Yarn classic (1.22.x) — yarn.lock present (134,503 bytes, ~131 KiB); maintenance logs show yarn v1.22.22 | yarn.lock, maintenance/2026-05-04.md:42 |
| Build tool | Rollup + rollup-plugin-esbuild (esbuild target es2022) + rollup-plugin-dts | rollup.config.js:1-39 |
| TS config | module: esnext, target: es2022, moduleResolution: bundler, strict: true, noEmit: true (rollup/esbuild emits, not tsc) | tsconfig.json:2-28 |
| Lint | ESLint flat config (eslint.config.js) — standard-ish rules: no semicolons, single quotes, 2-space indent, no-console: error | eslint.config.js |
| Test runner | Vitest 4.x with v8 coverage | package.json:53,64, test/vitest.config.js |
| External runtime requirement | the openssl CLI binary must be on PATH — every parse/verify path spawns openssl via child_process | CertService.ts:100,146,171,451 |
Runtime dependencies (package.json:42-46):
@techteamer/ocsp^1.0.1— TechTeamer’s own OCSP client, used for revocation checks (CertService.ts:8,482). This is the only first-party/internal dependency.iconv-lite^0.7.2— re-encodes escaped special characters in cert subject/issuer fields (CertService.ts:1,377).tmp^0.2.1— temp files for CA-chain verification (util/TempFileService.ts:1).
3. Build & run
No
start, no server, no Docker. This is a library.There is no
main-as-daemon, nobinfield, no Dockerfile (verified:findforDockerfile*andbin/dirs returned nothing). You build it and other packages import it.
Entrypoints declared in package.json:
main:build/certUtils.cjs(CommonJS) —package.json:7module:build/certUtils.mjs(ESM) —package.json:8types:build/certUtils.d.ts—package.json:13exports["."]: import→.mjs, require→.cjs, types→.d.ts, default→.mjs—package.json:9-16
build/is git-ignored and absent from this clone.
.gitignore:33ignoresbuild, and thepackage.jsononly shipsbuildto npm. So in a fresh checkout the entrypoints do not exist until you runyarn build. The source of truth issrc/index.ts→src/certUtils/CertService.ts.
Every package.json “scripts” entry (verbatim, package.json:27-36)
| Script | Command | What it does |
|---|---|---|
vitest | vitest run --config test/vitest.config.js | Runs the full Vitest suite once (unit + feature), using the dedicated config (coverage on). |
build | rollup -c | Bundles src/index.ts into build/certUtils.{cjs,mjs,d.ts} via rollup.config.js. |
vitest:unit | yarn vitest unit/ | Runs only tests under unit/ (i.e. test/tests/unit/). |
vitest:feature | yarn vitest feature/ | Runs only tests under feature/ (i.e. test/tests/feature/). |
lint | eslint . && echo 'npm run lint: OK' | Lints the repo with the flat ESLint config. |
lint:fix | eslint . --fix | Lints and auto-fixes. |
tsc | tsc | Type-checks (noEmit: true, so this is a type check only, no output). |
test | npm run lint --silent && echo 'npm test: OK' | The test script is lint-only — it does NOT run Vitest. (CI runs yarn vitest separately.) |
yarn test≠ running the test suite.
testonly runseslint(package.json:35). To actually execute unit/feature tests you must runyarn vitest(or the:unit/:featurevariants). CI runs bothyarn test(lint) andyarn vitest(tests) as separate steps — see CI below.
Typical workflow
yarn install --frozen-lockfile # as CI does
yarn tsc # type check
yarn test # lint (NOT tests)
yarn vitest # actual test suite
yarn build # produce build/certUtils.{cjs,mjs,d.ts}(Sequence mirrors .github/workflows/yarn-vitest.yaml:15-18.)
4. Top-level structure
Verified via find (cert_utils/ root):
| Path | Role | Source |
|---|---|---|
src/ | TypeScript source (3 dirs, 6 files, 754 LOC total) | listing + wc -l |
src/index.ts | Public entry — re-exports CertService only | src/index.ts |
src/certUtils/ | The three core classes: CertService.ts, CertInfo.ts, CertValidationError.ts | listing |
src/util/ | Helpers: TempFileService.ts, child_process_promise.ts | listing |
test/ | Vitest config + tests (tests/unit/, tests/feature/) | listing |
maintenance/ | Dated dependency-bump audit logs (yarn outdated) — 2026-04-07.md, 2026-05-04.md | files read |
security/ | Dated yarn audit logs — 2026-04-07.md, 2026-05-04.md | files read |
.github/workflows/ | Single CI workflow yarn-vitest.yaml | file read |
package.json / yarn.lock | Manifest + lockfile | read |
rollup.config.js | Build config | read |
tsconfig.json | TS compiler config | read |
eslint.config.js | ESLint flat config | read |
.editorconfig / .gitignore | Editor + ignore rules | .gitignore read |
README.md / LICENSE | Docs + MIT license | read |
No node_modules, dist, build, or vendored trees are present in the clone (all git-ignored).
5. Helper scripts under first-party bin/
None. There is no bin/ directory in this repo and no bin field in package.json (verified by find and reading package.json). The closest “helper” code is in src/util/ (documented in §6).
eslint.config.js:12ignores abin/mcpath and*.sh, suggesting this config is shared/templated across TechTeamer repos that do have abin/mc(MinIO client) — but no such file or any shell script exists in cert_utils (findfor*.shandbin/returned nothing).
6. Key modules / source files
src/certUtils/CertService.ts (564 LOC) — the core
The single class behind the whole package. Key behaviors verified:
- P12 parsing (
_parsePkcs12Cert,CertService.ts:140-160): pipes the P12 buffer throughopenssl pkcs12 -nodes -passin env:PASSWORD | openssl x509 -noout -subject -issuer -enddate -startdate, passing the password via thePASSWORDenv var (not argv). Wrong password →CertValidationError(INCORRECT_PASSWORD); other failures →INVALID. - P12-from-file (
parseCertFromFile,CertService.ts:86-130): runsopenssl pkcs12 -in <path> -password env:PASSWORD -nodes [-legacy] | openssl x509 -text. If openssl errors mention legacy ciphers (RC2-*-CBC/3DES-*-CBC), it retries with-legacy(CertService.ts:114-118) — this is the OpenSSL 3 legacy-provider workaround. - PEM parsing (
parsePemCert,CertService.ts:167-180):openssl x509 -noout -subject -issuer -enddate -startdate -serialover stdin. - openssl invocation: two helpers —
_execOpenSSLCLI(uses the promisifiedutil/child_process_promise.exec,CertService.ts:215-220) and_execOpenSSLCLIWithBuffer(rawnode:child_process.exec, writes the cert to stdin,CertService.ts:229-240). - Output parsing (
_parseOpenSSLCertOutput,CertService.ts:247-296): line-based switch mappingsubject/issuer/notBefore/notAfter/version/serial/signatureAlgorithminto aCertInfo. - Subject/issuer parsing (
_parseSubjectLine,CertService.ts:338-385): regex-splitsK = V, K = Vpairs, handles quoted values, commas inside values, and re-decodes escaped hex sequences (e.g.\C3\83) usingiconv-lite, auto-switchinglatin1→utf8when the input looks like UTF-8 (CertService.ts:362-379)._addNamedAttributes(CertService.ts:303-328) expands short keys (CN,O,OU,C,ST,L,STREET,EMAILADDRESS,UID,SN,GN,T,I,GQ) into long names likecommonName,organizationName. - CA-chain verify (
_verifyCertCA,CertService.ts:438-465): writes cert + CA to temp files (viaTempFileService) and runsopenssl verify -verbose -CAfile <ca> <cert>; success is the literal string<certPath>: OK. Always cleans up temp files infinally. - OCSP (
_verifyOcsp,CertService.ts:475-498): callsocsp.check({ cert, issuer }, cb)from@techteamer/ocsp; non-goodstatus →CertValidationError(REVOKED). - PEM body extraction (
getPemCertBody,CertService.ts:507-561): manual PEM line-walker — skips bag attributes/headers, validates BEGIN/END markers, requires typeCERTIFICATE, returns base64-decoded body. ThrowsMALFORMEDon bad markers. - Default
encodingis'latin1'(constructor,CertService.ts:38) — matches README default config.
Security-relevant: shell command construction.
parseCertFromFileinterpolates the file path directly into the openssl shell command string (CertService.ts:100), and_verifyCertCAinterpolates temp-file paths (CertService.ts:451). Passwords are passed safely via env vars (PASSWORD), but caller-suppliedcertPathis not shell-escaped. TreatcertPathas trusted input.
src/certUtils/CertInfo.ts (96 LOC)
Data holder for a parsed cert. Fields: decrypted, version, subject, issuer, notBefore, notAfter, serial, signatureAlgorithm (CertInfo.ts:17-36). Computed getters:
isExpired=notAfter < now(CertInfo.ts:51-53),isEffective=notBefore < now(CertInfo.ts:55-57),isValid= effective && !expired (CertInfo.ts:59-61).validatorgetter returns{ ok, info: { isEffective, isExpired }, message }— matches the README example (CertInfo.ts:63-69, README:118-127).
src/certUtils/CertValidationError.ts (32 LOC)
Error subclass with a reason field. Reason enum: INVALID, MALFORMED, EXPIRED, REVOKED, NOT_ACTIVE_YET, INCORRECT_PASSWORD (CertValidationError.ts:10-19). Default message: Failed to validate certificate, reason: <reason> (CertValidationError.ts:25).
src/util/TempFileService.ts (30 LOC)
Wraps the tmp package; createTempFile(options, content) creates a temp file, writes the buffer, returns { tempPath, fd, cleanupCallback }. Calls tmp.setGracefulCleanup() at module load (TempFileService.ts:5).
src/util/child_process_promise.ts (29 LOC)
Promisified child_process.exec returning stdout; optional logger streams stdout→logger.info, stderr→logger.error (child_process_promise.ts:3-29). Used by _execOpenSSLCLI.
7. Configuration
There is no config file, no env-var schema, no AJV/getconfig inside this package — it is a stateless library. Configuration is purely constructor arguments:
new CertService(encoding = 'latin1')— the only config knob. README documents the intended host-app patternconfig.get('certService.encoding', 'latin1')and default config block{ "certService": { "encoding": 'latin1' } }(README.md:30-38), but thatconfig.getlives in the consuming app, not here.- Runtime env vars used internally are ephemeral per-
exec:PASSWORDis injected into the openssl child process env for P12 decryption (CertService.ts:101-103,147-149). It is not read from the ambient environment. .gitignore:17referencesconfig/local.jsonand.gitignore:9-13ignore*.key,*.pem,*.crt, etc. — again templated; noconfig/dir exists in the repo.
8. Tests
| Item | Detail | Source |
|---|---|---|
| Framework | Vitest 4.x, v8 coverage (lcov) | test/vitest.config.js:7-12, package.json:53 |
| Location | test/tests/unit/cert-service.test.ts (717 LOC), test/tests/feature/cert-service.test.ts (137 LOC) | listing |
| Config | test/vitest.config.js — 30 s timeout, excludes node_modules/web/yarn-offline-cache, coverage→test/coverage/vitest | test/vitest.config.js |
| Run all | yarn vitest | package.json:28 |
| Run subset | yarn vitest:unit / yarn vitest:feature | package.json:30-31 |
Tests require
openssland generate real certs.The unit suite’s
beforeAllshells out toopenssl genrsa,openssl req,openssl x509,openssl pkcs12to create./test/certs/*fixtures at runtime (test/tests/unit/cert-service.test.ts:22-47). Running tests withoutopensslonPATHwill fail. Thetest/certs/dir is generated, not committed.
Coverage of behaviors: CertValidationError semantics, parseCert dispatch (P12/PEM/default/invalid), P12 & PEM parse success/failure, verifyCert orchestration, validity (expired / not-active), CA verification (valid/invalid/wrong-CA), OCSP (good/revoked/error), getPemCertBody (valid + 5 malformed cases), and _parseSubjectLine encoding/quote/comma edge cases. Feature tests assert full CertInfo.body shape against canned openssl output (feature/cert-service.test.ts:12-135).
CI
.github/workflows/yarn-vitest.yaml: on PRs to master, Ubuntu, Node 20, yarn install --frozen-lockfile → yarn tsc → yarn test (lint) → yarn vitest (tests). openssl is provided by the ubuntu-latest image.
CI Node 20 vs engines
>=20.19.0vs@types/node ^25.CI pins
node-version: '20'(yarn-vitest.yaml:13) whileenginesrequires>=20.19.0(package.json:25) and devDeps pull@types/node ^25.9.1(package.json:49). The maintenance logs (maintenance/2026-05-04.md) show several deps deliberately held back (“Update scheduled”), so manifest version ranges and the lockfile/CI may diverge in practice.
9. Dependencies on other FaceKom services
None at the network/infra level. This library makes no RabbitMQ, HTTP-API, DB, or Redis calls of its own (verified: no such clients imported anywhere in src/). Its only external interactions are:
- Spawning the local
opensslCLI (subprocess) for all parse/verify operations. - OCSP revocation requests, performed by the
@techteamer/ocspdependency whenverifyCert(..., verifyOcsp=true)is called — the OCSP responder URL comes from the certificate’s AIA extension (handled inside@techteamer/ocsp, not configured here) (CertService.ts:482).
The only first-party FaceKom/TechTeamer code dependency is @techteamer/ocsp (package.json:43). cert_utils itself is a dependency consumed by other FaceKom repos (e.g. timestamp/signing services per README framing); those consumers are out of scope for this doc.
10. Verified gotchas (summary)
opensslCLI is a hard runtime requirement. Every public method ultimately spawnsopenssl. No pure-JS fallback exists.
yarn testruns only ESLint, not the test suite. Useyarn vitest.
build/(the published entrypoints) is git-ignored and absent from source checkouts — runyarn buildfirst.
OpenSSL 3 legacy-cipher P12s only work via
parseCertFromFile's-legacyretry path (CertService.ts:114-118); the buffer-based_parsePkcs12Certhas no legacy fallback and will throwINVALIDon RC2/3DES-encrypted containers.
certPathinparseCertFromFileis interpolated unescaped into a shell command — caller must ensure it is trusted.
_addNamedAttributeshas a latent bug: it guards onattributeMap[key](raw case) but looks upattributeMap[keyUppercase](CertService.ts:323-325); lowercase keys won't get expanded. Not asserted by tests (tests pass uppercase keys likeC,ST).
Unverified / gaps
yarn.lock(134,503 bytes, ~131 KiB) was confirmed present but not read line-by-line; exact transitive pinned versions are not enumerated here (manifest ranges are reported instead).build/artifacts are absent (git-ignored), so the compiled.cjs/.mjs/.d.tswere not inspected — claims about runtime behavior come fromsrc/..editorconfigpresent but not read (cosmetic editor settings only).- Consumer services: the specific FaceKom repos that import
@techteamer/cert-utilswere not traced from within this repo (out of scope; would require cross-repo search). README only states the “for the timestamps” intent (README.md:4). - No Git-LFS / model-weight pointer files exist in this repo (verified: no
.gitattributes, no binary blobs) — N/A for cert_utils. - Git history is a single squashed commit
58437be chore: [fkqa-332] bump packages (#14)with no tags (shallow/source-only clone), so historical evolution could not be traced.
Sources
Files read in full (read-only):
cert_utils/README.mdcert_utils/package.jsoncert_utils/LICENSEcert_utils/rollup.config.jscert_utils/tsconfig.jsoncert_utils/eslint.config.jscert_utils/.gitignorecert_utils/src/index.tscert_utils/src/certUtils/CertService.tscert_utils/src/certUtils/CertInfo.tscert_utils/src/certUtils/CertValidationError.tscert_utils/src/util/TempFileService.tscert_utils/src/util/child_process_promise.tscert_utils/test/vitest.config.jscert_utils/test/tests/unit/cert-service.test.tscert_utils/test/tests/feature/cert-service.test.tscert_utils/.github/workflows/yarn-vitest.yamlcert_utils/maintenance/2026-04-07.md,cert_utils/maintenance/2026-05-04.mdcert_utils/security/2026-04-07.md,cert_utils/security/2026-05-04.md
Commands run (read-only): find (file inventory, Dockerfile/bin/LFS checks), wc -l (LOC), git -C … branch/log/for-each-ref (branch master, single commit 58437be, no tags).