diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..7387590
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,17 @@
+# top-most EditorConfig file
+root = true
+
+# Unix-style newlines with a newline ending every file
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+end_of_line = lf
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+indent_size = 4
+# In Markdown a trailing double space is interpreted as
+trim_trailing_whitespace = false
+max_line_length = off
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index ecc13c2..fe14f94 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -10,19 +10,20 @@ on:
pull_request:
branches:
- - main
+ - master
paths-ignore:
- '**/*.md'
- '.gitignore'
jobs:
- build:
+ test:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-node@v3
- - run: npm install
- - run: node resources.js
- - run: wget ${{ secrets.CERT_LINK }}
+ - uses: actions/checkout@v4
+ - uses: oven-sh/setup-bun@v1
+ - run: bun install
+ - run: bun type-check
+ - run: bun ca-downloader
+ - run: wget ${{ secrets.CERT_LINK }} -O cert.zip
- run: unzip -P ${{ secrets.CERT_PASS }} cert.zip -d cert-files
- - run: node index.js cert-files | grep 'Revoked'
\ No newline at end of file
+ - run: bun certcheck cert-files/cert.p12 --password ${{ secrets.CERT_PASS }} | grep -q "revoked"
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 7fa72df..01e0dba 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,107 @@
-*.DS_Store
-CA-CER/
-CA-PEM/
-cert.p12
-pass.txt
-*.pem
+.envrc
+CA-PEM
+
+# Dependencies
node_modules/
-cert-files
-pnpm-lock.yaml
\ No newline at end of file
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Build output
+dist/
+*.tsbuildinfo
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Coverage directory used by tools like istanbul
+coverage/
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage
+.grunt
+
+# Bower dependency directory
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons
+build/Release
+
+# Dependency directories
+jspm_packages/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+.env.test
+
+# parcel-bundler cache
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+public
+
+# Storybook build outputs
+.out
+.storybook-out
+
+# Temporary folders
+tmp/
+temp/
+
+# Editor directories and files
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# OS generated files
+.DS_Store
+.DS_Store?
+._*
+.Spotlight-V100
+.Trashes
+ehthumbs.db
+Thumbs.db
diff --git a/README.md b/README.md
index c3dcee5..9cea72c 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,9 @@
# CertCheck
-Node JS utility to check the signature of Apple P12 Certificates.
+Modern TypeScript utility to check the signature of Apple P12 Certificates using idiomatic TypeScript and modern APIs.
*Confirmed to work on macOS, Windows, and Linux.*
-*Works with both enterprise and developer certificates.*
-
-Includes tool to convert CER files to PEM. (See [**cer-to-pem.js**](https://github.com/JailbreaksApp/CertCheck/blob/master/cer-to-pem.js))
+*Works with enterprise, developer, and distribution certificates.*
## Contact
- [**Twitter** **(@iCrazeiOS)**](https://twitter.com/iCrazeiOS)
@@ -16,16 +14,141 @@ Includes tool to convert CER files to PEM. (See [**cer-to-pem.js**](https://gith
- **BTC:** bc1q0ghuykcutljjyh3tcdjv88ek8zjzrtnk8zhuhy
## Requirements
- - Node JS (with `ocsp` & `node-forge` modules)
+ - [Bun](https://bun.sh) (v1.0.0 or higher)
+ - TypeScript (for development)
+
+## Installation
+```bash
+# Install Bun (if not already installed)
+curl -fsSL https://bun.sh/install | bash
+
+# Install dependencies (only if you want to develop, otherwise see the bunx command below)
+bun install
+```
## Usage
- **Standard usage:**
- - Have cert.p12 and pass.txt in the same directory as the script.
- - Run `node index.js`
- **Specify directory:**
- - Have cert.p12 and pass.txt in a different directory than the script.
- - Run `node index.js "/path/to/directory"`
+## Give me quick copy-paste command I’m impatient
+
+```bash
+bunx github:Olympta/CertCheck certcheck cert.p12 --password 123456
+```
+
+No need to clone the repo.
+
+### Using Package Scripts (Recommended)
+```bash
+# Main certificate checker
+bun certcheck cert.p12 --password yourpassword
+
+# With JSON output
+bun certcheck cert.p12 --password yourpassword --json
+
+# Short password flag
+bun certcheck cert.p12 -p yourpassword
+
+# Download Apple CA certificates (run this first)
+bun ca-downloader
+```
+
+### Direct Bun Execution
+```bash
+# Basic usage with P12 file and password
+bun certcheck cert.p12 --password yourpassword
+
+# With JSON output
+bun certcheck cert.p12 --password yourpassword --json
+
+# Short password flag
+bun certcheck cert.p12 -p yourpassword
+
+# Absolute paths work too
+bun certcheck /path/to/cert.p12 --password yourpassword
+```
+
+### Environment Variables
+```bash
+# Set environment variables
+export CERT_P12_PATH=cert.p12
+export CERT_P12_PASSWORD=yourpassword
+
+# Run without arguments
+bun certcheck
+
+# JSON output still works
+bun certcheck --json
+```
+
+### Mixed Usage (CLI overrides environment variables)
+```bash
+# Environment variables as fallback
+CERT_P12_PATH=cert.p12 bun certcheck --password yourpassword
+CERT_P12_PASSWORD=yourpassword bun certcheck cert.p12
+
+# CLI arguments always take priority
+CERT_P12_PATH=wrong.p12 CERT_P12_PASSWORD=wrongpass bun certcheck cert.p12 --password yourpassword
+```
+
+### Download Apple CA Certificates
+```bash
+# Download and convert Apple CA certificates (run this first)
+bun ca-downloader
+```
+
+## File Structure
+```
+src/
+├── index.ts # OCSP certificate revoke checker
+├── caDownloader.ts # Apple CA certificates downloader
+├── p12Utils.ts # P12 to PEM conversion utilities
+├── cerUtils.ts # CER to PEM converter class
+└── types/
+ └── ocsp.d.ts # OCSP module type definitions
+```
+
+## Development
+```bash
+# Install dependencies
+bun install
+
+# Type check
+bun type-check
+
+# Run main application
+bun certcheck cert.p12 --password yourpassword
+
+# Download Apple CA certificates
+bun ca-downloader
+
+# Development mode
+bun dev cert.p12 --password yourpassword
+bun dev:ca-downloader
+```
+
+## Example Output
+
+### Good Certificate
+```bash
+$ bun certcheck cert.p12 --password 123456
+Certificate Name: John Doe
+Certificate Status: good
+Certificate Expiration Date: Sat, 22 Aug 2026 18:21:48 GMT
+```
+
+### Revoked Certificate
+```bash
+$ bun certcheck revoked.p12 --password 123456
+Certificate Name: Jane Smith
+Certificate Status: revoked
+Certificate Expiration Date: Thu, 23 Jul 2026 00:06:58 GMT
+Certificate Revocation Date: Mon, 25 Aug 2025 18:37:02 GMT
+```
+
+### JSON Output
+```bash
+$ bun certcheck cert.p12 --password 123456 --json
+{"name":"John Doe","expirationDate":"Sat, 22 Aug 2026 18:21:48 GMT","status":"good"}
+```
- **JSON output:**
- - Follow steps for other examples, but add `--json` to the end of the command. (MUST be after custom directory, if you are using one)
+## License
+MIT
diff --git a/bun.lock b/bun.lock
new file mode 100644
index 0000000..4ed467f
--- /dev/null
+++ b/bun.lock
@@ -0,0 +1,53 @@
+{
+ "lockfileVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "certcheck",
+ "dependencies": {
+ "node-forge": "^1.3.1",
+ "ocsp": "^1.2.0",
+ },
+ "devDependencies": {
+ "@types/node": "^20.0.0",
+ "@types/node-forge": "^1.3.0",
+ "bun-types": "^1.3.0",
+ "typescript": "^5.0.0",
+ },
+ },
+ },
+ "packages": {
+ "@types/node": ["@types/node@20.19.21", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-CsGG2P3I5y48RPMfprQGfy4JPRZ6csfC3ltBZSRItG3ngggmNY/qs2uZKp4p9VbrpqNNSMzUZNFZKzgOGnd/VA=="],
+
+ "@types/node-forge": ["@types/node-forge@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw=="],
+
+ "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
+
+ "asn1.js": ["asn1.js@4.10.1", "", { "dependencies": { "bn.js": "^4.0.0", "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" } }, "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw=="],
+
+ "asn1.js-rfc2560": ["asn1.js-rfc2560@4.0.6", "", { "dependencies": { "asn1.js-rfc5280": "^2.0.0" }, "peerDependencies": { "asn1.js": "^4.4.0" } }, "sha512-ysf48ni+f/efNPilq4+ApbifUPcSW/xbDeQAh055I+grr2gXgNRQqHew7kkO70WSMQ2tEOURVwsK+dJqUNjIIg=="],
+
+ "asn1.js-rfc5280": ["asn1.js-rfc5280@2.0.1", "", { "dependencies": { "asn1.js": "^4.5.0" } }, "sha512-1e2ypnvTbYD/GdxWK77tdLBahvo1fZUHlQJqAVUuZWdYj0rdjGcf2CWYUtbsyRYpYUMwMWLZFUtLxog8ZXTrcg=="],
+
+ "async": ["async@1.5.2", "", {}, "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w=="],
+
+ "bn.js": ["bn.js@4.12.2", "", {}, "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw=="],
+
+ "bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="],
+
+ "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
+
+ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
+
+ "minimalistic-assert": ["minimalistic-assert@1.0.1", "", {}, "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="],
+
+ "node-forge": ["node-forge@1.3.1", "", {}, "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA=="],
+
+ "ocsp": ["ocsp@1.2.0", "", { "dependencies": { "asn1.js": "^4.8.0", "asn1.js-rfc2560": "^4.0.0", "asn1.js-rfc5280": "^2.0.0", "async": "^1.5.2", "simple-lru-cache": "0.0.2" } }, "sha512-r4Q3oYKU+3b6iD4bn+5O2dQqctu8pFrJfWouUiKjiNXXjdr99lN/EaTVkFQevGlV/lKsomgtt/XRGB8xV8rq3Q=="],
+
+ "simple-lru-cache": ["simple-lru-cache@0.0.2", "", {}, "sha512-uEv/AFO0ADI7d99OHDmh1QfYzQk/izT1vCmu/riQfh7qjBVUUgRT87E5s5h7CxWCA/+YoZerykpEthzVrW3LIw=="],
+
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
+ "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
+ }
+}
diff --git a/cer-to-pem.js b/cer-to-pem.js
deleted file mode 100644
index 0a2415b..0000000
--- a/cer-to-pem.js
+++ /dev/null
@@ -1,33 +0,0 @@
-const fs = require("fs");
-
-const filepath = process.argv[2];
-if (!filepath) {
- console.log("Incorrect usage. Usage: node cer-to-pem.js ");
- process.exit();
-} else if (!fs.existsSync(filepath)) {
- console.log(`File '${filepath}' does not exist`);
- process.exit();
-}
-
-const cert = fs.readFileSync(filepath);
-const certB64 = Buffer.from(cert).toString("base64");
-/*
- Split the certificate into lines of upto 64 characters
-
- https://www.rfc-editor.org/rfc/rfc7468 states:
- Generators MUST wrap the base64-encoded lines so that each line
- consists of exactly 64 characters except for the final line, which
- will encode the remainder of the data
-
-*/
-const lines = certB64.match(/.{1,64}/g);
-
-// Start to build the PEM file
-let output = "-----BEGIN CERTIFICATE-----\n";
-// Add each line of the certificate
-lines.forEach(line => output += line + "\n");
-// Add the ending line of the certificate
-output += "-----END CERTIFICATE-----\n";
-// Write the PEM file to the same directory as the .cer file
-fs.writeFileSync(filepath.replace(".cer", ".pem"), output);
-console.log(`Converted ${filepath} to ${filepath.replace(".cer", ".pem")}`);
diff --git a/index.js b/index.js
deleted file mode 100644
index fe09b05..0000000
--- a/index.js
+++ /dev/null
@@ -1,72 +0,0 @@
-const fs = require("fs");
-const forge = require("node-forge");
-const ocsp = require("ocsp");
-
-// ocsp module throws a deprecated warning, and is no longer maintained
-process.removeAllListeners("warning");
-
-// who needs an actual arg parser??
-const customDirectory = process.argv[2];
-// Only use if not null, does not include '--json', and actually exists
-const useCustomDirectory = (customDirectory && !customDirectory.includes("--json") && fs.existsSync(customDirectory));
-const jsonOutput = process.argv.toString().includes("--json");
-if (useCustomDirectory && !jsonOutput) console.log(`[!] Additional argument passed. Looking in '${customDirectory}' for certificate and password.\n`);
-
-const p12File = (useCustomDirectory ? customDirectory : __dirname)+"/cert.p12";
-const p12PassFile = (useCustomDirectory ? customDirectory : __dirname)+"/pass.txt";
-
-if (!fs.existsSync(p12File)) {
- console.log("[!] Certificate must be stored at ./cert.p12, or within a specified directory (via 'node index.js /path/to/dir').");
- process.exit();
-} else if (!fs.existsSync(p12PassFile)) {
- console.log("[!] Certificate password must be stored within ./pass.txt, or within a specified directory (via 'node index.js /path/to/dir').");
- process.exit();
-} else if (!fs.existsSync("CA-PEM")) {
- console.log("[!] Please run 'resources.js' to retrieve the necessary resources from Apple's servers.\n[*] The downloaded certificates will be automatically converted to the correct format.");
- process.exit();
-}
-
-/* Convert P12 to PEM */
-let cert, certData;
-try {
- const p12Pass = String(fs.readFileSync(p12PassFile, "utf8")).replace("\n", "").trim();
- const p12 = forge.pkcs12.pkcs12FromAsn1(forge.asn1.fromDer(fs.readFileSync(p12File, {encoding:"binary"})), false, p12Pass);
- certData = p12.getBags({bagType: forge.pki.oids.certBag});
- cert = forge.pki.certificateToPem(certData[forge.pki.oids.certBag][0].cert);
-} catch (err) {
- console.log(`Failed to convert P12 to PEM. ${err.message.includes("password") ? "Password is likely incorrect" : "Unknown error"}.`);
- process.exit();
-}
-
-/* GET CERT SIGNATURE STATUS */
-// Loop througn all CA certificates from CA-PEM folder
-// Probably a better way than doing this, but it's fast anyway and doesn't really matter anyway
-fs.readdirSync("CA-PEM").forEach(file => {
- // This next line can break if there is a directory ending with .pem, but that's just intentionally breaking the script so idc
- if (file.endsWith(".pem")) { // If PEM file
- // Check if the certificate is signed by the CA
- ocsp.check({cert: cert, issuer: fs.readFileSync(`CA-PEM/${file}`, "utf8")}, function(error, res) {
- let certStatus, certRevocationDate = null;
- if (error) {
- if (error.toString().includes("revoked")) {
- certStatus = "Revoked";
- certRevocationDate = new Date(res.value.revocationTime).toGMTString();
- }
- } else if (res.type == "good") certStatus = "Signed";
-
- if (certStatus) {
- const certName = certData[forge.pki.oids.certBag][0].cert.subject.attributes.filter(({name}) => name === "organizationName")[0].value;
- const certExpirationDate = new Date(certData[forge.pki.oids.certBag][0].cert.validity.notAfter.getTime()).toGMTString();
- if (jsonOutput) { // If JSON output is requested
- console.log(JSON.stringify({name: certName, status: certStatus, expirationDate: certExpirationDate, revocationDate: certRevocationDate}));
- } else {
- console.log("Certificate Name: " + certName);
- console.log("Certificate Status: " + certStatus);
- console.log("Certificate Expiration Date: " + certExpirationDate);
- if (certRevocationDate) console.log("Certificate Revocation Date: " + certRevocationDate);
- }
- process.exit(); // Exit here so the script doesn't continue to check other certificates
- }
- });
- }
-});
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..df25458
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,334 @@
+{
+ "name": "certcheck",
+ "version": "2.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "certcheck",
+ "version": "2.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "node-forge": "^1.3.1",
+ "ocsp": "^1.2.0"
+ },
+ "devDependencies": {
+ "@types/node": "^20.0.0",
+ "@types/node-forge": "^1.3.0",
+ "ts-node": "^10.9.0",
+ "typescript": "^5.0.0"
+ }
+ },
+ "node_modules/@cspotcode/source-map-support": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
+ "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "0.3.9"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
+ "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.0.3",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ }
+ },
+ "node_modules/@tsconfig/node10": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
+ "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node12": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
+ "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node14": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
+ "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node16": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
+ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "20.19.21",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.21.tgz",
+ "integrity": "sha512-CsGG2P3I5y48RPMfprQGfy4JPRZ6csfC3ltBZSRItG3ngggmNY/qs2uZKp4p9VbrpqNNSMzUZNFZKzgOGnd/VA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/node-forge": {
+ "version": "1.3.14",
+ "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz",
+ "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.15.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-walk": {
+ "version": "8.3.4",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
+ "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.11.0"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/arg": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/asn1.js": {
+ "version": "4.10.1",
+ "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
+ "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==",
+ "license": "MIT",
+ "dependencies": {
+ "bn.js": "^4.0.0",
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0"
+ }
+ },
+ "node_modules/asn1.js-rfc2560": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/asn1.js-rfc2560/-/asn1.js-rfc2560-4.0.6.tgz",
+ "integrity": "sha512-ysf48ni+f/efNPilq4+ApbifUPcSW/xbDeQAh055I+grr2gXgNRQqHew7kkO70WSMQ2tEOURVwsK+dJqUNjIIg==",
+ "license": "MIT",
+ "dependencies": {
+ "asn1.js-rfc5280": "^2.0.0"
+ },
+ "peerDependencies": {
+ "asn1.js": "^4.4.0"
+ }
+ },
+ "node_modules/asn1.js-rfc5280": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/asn1.js-rfc5280/-/asn1.js-rfc5280-2.0.1.tgz",
+ "integrity": "sha512-1e2ypnvTbYD/GdxWK77tdLBahvo1fZUHlQJqAVUuZWdYj0rdjGcf2CWYUtbsyRYpYUMwMWLZFUtLxog8ZXTrcg==",
+ "license": "MIT",
+ "dependencies": {
+ "asn1.js": "^4.5.0"
+ }
+ },
+ "node_modules/async": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
+ "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==",
+ "license": "MIT"
+ },
+ "node_modules/bn.js": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
+ "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
+ "license": "MIT"
+ },
+ "node_modules/create-require": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
+ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/diff": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/make-error": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/minimalistic-assert": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+ "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
+ "license": "ISC"
+ },
+ "node_modules/node-forge": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
+ "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
+ "license": "(BSD-3-Clause OR GPL-2.0)",
+ "engines": {
+ "node": ">= 6.13.0"
+ }
+ },
+ "node_modules/ocsp": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/ocsp/-/ocsp-1.2.0.tgz",
+ "integrity": "sha512-r4Q3oYKU+3b6iD4bn+5O2dQqctu8pFrJfWouUiKjiNXXjdr99lN/EaTVkFQevGlV/lKsomgtt/XRGB8xV8rq3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "asn1.js": "^4.8.0",
+ "asn1.js-rfc2560": "^4.0.0",
+ "asn1.js-rfc5280": "^2.0.0",
+ "async": "^1.5.2",
+ "simple-lru-cache": "0.0.2"
+ }
+ },
+ "node_modules/simple-lru-cache": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/simple-lru-cache/-/simple-lru-cache-0.0.2.tgz",
+ "integrity": "sha512-uEv/AFO0ADI7d99OHDmh1QfYzQk/izT1vCmu/riQfh7qjBVUUgRT87E5s5h7CxWCA/+YoZerykpEthzVrW3LIw=="
+ },
+ "node_modules/ts-node": {
+ "version": "10.9.2",
+ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
+ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@cspotcode/source-map-support": "^0.8.0",
+ "@tsconfig/node10": "^1.0.7",
+ "@tsconfig/node12": "^1.0.7",
+ "@tsconfig/node14": "^1.0.0",
+ "@tsconfig/node16": "^1.0.2",
+ "acorn": "^8.4.1",
+ "acorn-walk": "^8.1.1",
+ "arg": "^4.1.0",
+ "create-require": "^1.1.0",
+ "diff": "^4.0.1",
+ "make-error": "^1.1.1",
+ "v8-compile-cache-lib": "^3.0.1",
+ "yn": "3.1.1"
+ },
+ "bin": {
+ "ts-node": "dist/bin.js",
+ "ts-node-cwd": "dist/bin-cwd.js",
+ "ts-node-esm": "dist/bin-esm.js",
+ "ts-node-script": "dist/bin-script.js",
+ "ts-node-transpile-only": "dist/bin-transpile.js",
+ "ts-script": "dist/bin-script-deprecated.js"
+ },
+ "peerDependencies": {
+ "@swc/core": ">=1.2.50",
+ "@swc/wasm": ">=1.2.50",
+ "@types/node": "*",
+ "typescript": ">=2.7"
+ },
+ "peerDependenciesMeta": {
+ "@swc/core": {
+ "optional": true
+ },
+ "@swc/wasm": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/v8-compile-cache-lib": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
+ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/yn": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+ "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
index 57cd74a..b7369b7 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,33 @@
{
- "type": "commonjs",
+ "name": "certcheck",
+ "version": "3.0.0",
+ "description": "Modern Bun utility to check the signature of Apple P12 Certificates",
+ "type": "module",
+ "bin": {
+ "certcheck": "./src/index.ts"
+ },
+ "scripts": {
+ "certcheck": "bun run src/index.ts",
+ "ca-downloader": "bun run src/caDownloader.ts",
+ "dev": "bun run src/index.ts",
+ "dev:ca-downloader": "bun run src/caDownloader.ts",
+ "type-check": "bun run tsc --noEmit",
+ "clean": "rm -rf dist"
+ },
+ "keywords": ["apple", "certificate", "p12", "ocsp", "bun", "typescript", "modern"],
+ "author": "iCrazeiOS",
+ "license": "MIT",
"dependencies": {
"node-forge": "^1.3.1",
"ocsp": "^1.2.0"
+ },
+ "devDependencies": {
+ "@types/node": "^20.0.0",
+ "@types/node-forge": "^1.3.0",
+ "bun-types": "^1.3.0",
+ "typescript": "^5.0.0"
+ },
+ "engines": {
+ "bun": ">=1.0.0"
}
}
diff --git a/resources.js b/resources.js
deleted file mode 100644
index 3aabe67..0000000
--- a/resources.js
+++ /dev/null
@@ -1,25 +0,0 @@
-const fs = require("fs");
-const https = require("https");
-const execSync = require("child_process").execSync;
-
-const downloadCER = (filename) => {
- const file = fs.createWriteStream(`cctmp-${filename}.cer`);
- // The first CA certificate (AppleWWDRCA) is stored in a different place for some reason...
- const url = filename.endsWith("A") ? "https://developer.apple.com/certificationauthority/AppleWWDRCA.cer" : `https://www.apple.com/certificateauthority/${filename}.cer`;
- https.get(url, (res) => {
- res.pipe(file);
- file.on('finish', () => {
- file.close();
- // Convert CER files to PEM
- console.log(`[*] '${filename}.cer' has been downloaded, converting to PEM format...`);
- execSync(`node ${__dirname}/cer-to-pem.js cctmp-${filename}.cer`);
- fs.renameSync(`cctmp-${filename}.pem`, `CA-PEM/${filename}.pem`);
- fs.unlinkSync(`cctmp-${filename}.cer`);
- })
- })
-}
-console.log('[*] Downloading resources...\n[*] Once complete, you can run the main script again.\n');
-if (!fs.existsSync("CA-PEM")) fs.mkdirSync("CA-PEM/");
-
-const certs = ["AppleWWDRCA", "AppleWWDRCAG2", "AppleWWDRCAG3", "AppleWWDRCAG4", "AppleWWDRCAG5", "AppleWWDRCAG6"];
-certs.forEach(cert => downloadCER(cert));
diff --git a/src/caDownloader.ts b/src/caDownloader.ts
new file mode 100755
index 0000000..1b49b20
--- /dev/null
+++ b/src/caDownloader.ts
@@ -0,0 +1,128 @@
+#!/usr/bin/env bun
+import { CerUtils } from './cerUtils.js';
+
+interface DownloadResult {
+ success: boolean;
+ filename: string;
+ error?: string;
+}
+
+export class CaDownloader {
+ private readonly caDirectory = 'CA-PEM';
+ private readonly certificates = [
+ 'AppleWWDRCA',
+ 'AppleWWDRCAG2',
+ 'AppleWWDRCAG3',
+ 'AppleWWDRCAG4',
+ 'AppleWWDRCAG5',
+ 'AppleWWDRCAG6'
+ ];
+
+ private getCertificateUrl(filename: string): string {
+ // The first CA certificate (AppleWWDRCA) is stored in a different place for some reason...
+ if (filename.endsWith('A')) {
+ return `https://developer.apple.com/certificationauthority/${filename}.cer`;
+ }
+ return `https://www.apple.com/certificateauthority/${filename}.cer`;
+ }
+
+ private async downloadCertificate(filename: string): Promise {
+ try {
+ const url = this.getCertificateUrl(filename);
+ const tempFileName = `cctmp-${filename}.cer`;
+
+ console.error(`[*] Downloading ${filename}.cer...`);
+ const response = await fetch(url);
+
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+ }
+
+ const data = await response.arrayBuffer();
+ await Bun.write(tempFileName, data);
+
+ return { success: true, filename };
+ } catch (error) {
+ return {
+ success: false,
+ filename,
+ error: error instanceof Error ? error.message : 'Unknown error'
+ };
+ }
+ }
+
+ private async convertCerToPem(filename: string): Promise {
+ try {
+ const tempCerPath = `cctmp-${filename}.cer`;
+
+ console.error(`[*] '${filename}.cer' has been downloaded, converting to PEM format...`);
+
+ await new CerUtils(tempCerPath).convertToPem();
+
+ // Move the converted PEM file to the CA directory
+ const tempPemPath = `cctmp-${filename}.pem`;
+ const finalPemPath = `${this.caDirectory}/${filename}.pem`;
+
+ // Ensure CA directory exists
+ if (!(await Bun.file(this.caDirectory).exists())) {
+ await Bun.write(`${this.caDirectory}/.gitkeep`, '');
+ }
+
+ // Move the file
+ const pemContent = await Bun.file(tempPemPath).text();
+ await Bun.write(finalPemPath, pemContent);
+
+ // Clean up temp files
+ await Bun.file(tempCerPath).unlink();
+ await Bun.file(tempPemPath).unlink();
+
+ console.error(`[*] Successfully converted and moved ${filename}.pem to CA-PEM directory`);
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
+ console.error(`[!] Failed to convert ${filename}.cer: ${errorMessage}`);
+ }
+ }
+
+ private async ensureCaDirectory(): Promise {
+ if (!(await Bun.file(this.caDirectory).exists())) {
+ await Bun.write(`${this.caDirectory}/.gitkeep`, '');
+ console.error(`[*] Created ${this.caDirectory} directory`);
+ }
+ }
+
+ public async downloadAllCertificates(): Promise {
+ console.error('[*] Downloading resources...\n[*] Once complete, you can run the main script again.\n');
+
+ await this.ensureCaDirectory();
+
+ const downloadPromises = this.certificates.map(async (cert) => {
+ const result = await this.downloadCertificate(cert);
+
+ if (result.success) {
+ await this.convertCerToPem(result.filename);
+ } else {
+ console.error(`[!] Failed to download ${result.filename}: ${result.error}`);
+ }
+ });
+
+ await Promise.all(downloadPromises);
+ console.error('\n[*] Download process completed!');
+ }
+}
+
+// Export a standalone function for importing
+export async function downloadCaCertificates(): Promise {
+ const downloader = new CaDownloader();
+ await downloader.downloadAllCertificates();
+}
+
+// Main execution (only run if this file is executed directly)
+if (import.meta.main) {
+ const downloader = new CaDownloader();
+ await downloader.downloadAllCertificates().catch(error => {
+ console.error('[!] Unexpected error during download:', error);
+ process.exit(1);
+ });
+}
+
+export {};
diff --git a/src/cerUtils.ts b/src/cerUtils.ts
new file mode 100644
index 0000000..4123722
--- /dev/null
+++ b/src/cerUtils.ts
@@ -0,0 +1,69 @@
+export class CerUtils {
+ private readonly filePath: string;
+
+ constructor(filePath: string) {
+ this.filePath = filePath;
+ this.validateInput();
+ }
+
+ private validateInput(): void {
+ if (!this.filePath) {
+ throw new Error('Incorrect usage. Usage: CerToPemConverter constructor requires a file path');
+ }
+
+ if (!this.filePath.toLowerCase().endsWith('.cer')) {
+ throw new Error('File must have .cer extension');
+ }
+ }
+
+ private async convertCertToBase64(): Promise {
+ const file = Bun.file(this.filePath);
+ if (!(await file.exists())) {
+ throw new Error(`File '${this.filePath}' does not exist`);
+ }
+
+ const cert = await file.arrayBuffer();
+ return Buffer.from(cert).toString('base64');
+ }
+
+ private splitIntoLines(base64String: string): string[] {
+ /*
+ * Split the certificate into lines of up to 64 characters
+ *
+ * https://www.rfc-editor.org/rfc/rfc7468 states:
+ * Generators MUST wrap the base64-encoded lines so that each line
+ * consists of exactly 64 characters except for the final line, which
+ * will encode the remainder of the data
+ */
+ return base64String.match(/.{1,64}/g) || [];
+ }
+
+ private buildPemContent(base64Lines: string[]): string {
+ let output = '-----BEGIN CERTIFICATE-----\n';
+ base64Lines.forEach(line => {
+ output += line + '\n';
+ });
+ output += '-----END CERTIFICATE-----\n';
+ return output;
+ }
+
+ private getOutputPath(): string {
+ return this.filePath.replace(/\.cer$/i, '.pem');
+ }
+
+ public async convertToPem(): Promise {
+ try {
+ const base64String = await this.convertCertToBase64();
+ const base64Lines = this.splitIntoLines(base64String);
+ const pemContent = this.buildPemContent(base64Lines);
+ const outputPath = this.getOutputPath();
+
+ await Bun.write(outputPath, pemContent);
+ console.error(`Converted ${this.filePath} to ${outputPath}`);
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
+ console.error(`[!] Conversion failed: ${errorMessage}`);
+ throw error;
+ }
+ }
+}
diff --git a/src/index.ts b/src/index.ts
new file mode 100755
index 0000000..9752f03
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,166 @@
+#!/usr/bin/env bun
+import * as ocsp from 'ocsp';
+import { P12Utils } from './p12Utils.js';
+import { downloadCaCertificates } from './caDownloader.js';
+
+interface ParsedArgs {
+ p12File?: string | undefined;
+ password?: string | undefined;
+ jsonOutput: boolean;
+}
+
+class CertCheck {
+ private args: ParsedArgs;
+
+ constructor() {
+ this.args = this.parseArguments();
+ this.suppressDeprecationWarnings();
+ }
+
+ private parseArguments(): ParsedArgs {
+ const args = process.argv.slice(2);
+ const jsonOutput = args.includes('--json');
+
+ // Find P12 file path (first non-flag argument) or use env var
+ const p12File = args.find(arg => !arg.startsWith('--') && arg.endsWith('.p12')) || process.env['CERT_P12_PATH'];
+
+ // Find password (argument after --password or -p) or use env var
+ const passwordIndex = args.findIndex(arg => arg === '--password' || arg === '-p');
+ const password = passwordIndex !== -1 && args[passwordIndex + 1] ? args[passwordIndex + 1] : process.env['CERT_P12_PASSWORD'];
+
+ return {
+ p12File,
+ password,
+ jsonOutput
+ };
+ }
+
+ private suppressDeprecationWarnings(): void {
+ // OCSP module throws deprecated warnings and is no longer maintained
+ process.removeAllListeners('warning');
+ }
+
+ private async checkCertificateSignature(certData: any): Promise {
+ // Read CA directory directly with fs/promises
+ const fs = await import('fs/promises');
+ const caFiles = await fs.readdir('CA-PEM');
+ const pemFiles = caFiles.filter(file => file.endsWith('.pem'));
+
+ for (const file of pemFiles) {
+ try {
+ const result = await this.checkWithCA(certData, `CA-PEM/${file}`);
+ if (result) {
+ this.outputResult(result);
+ return;
+ }
+ } catch (error) {
+ // Continue to next CA if this one fails
+ continue;
+ }
+ }
+
+ console.error('[!] Certificate signature could not be verified with any known CA.');
+ }
+
+ private async checkWithCA(certData: any, caFilePath: string): Promise<{ name: string; status: string; expirationDate: string; revocationDate?: string } | null> {
+ try {
+ const issuerPem = await Bun.file(caFilePath).text();
+
+ return new Promise((resolve) => {
+ ocsp.check({
+ cert: certData.pem,
+ issuer: issuerPem
+ }, (error: Error | null, res: ocsp.OCSPResponse) => {
+ const baseResult = {
+ name: P12Utils.getCertificateName(certData.cert),
+ expirationDate: P12Utils.getCertificateExpirationDate(certData.cert)
+ };
+
+ if (error) {
+ if (error.toString().includes('revoked')) {
+ const revocationDate = res.value?.revocationTime ? new Date(res.value.revocationTime).toUTCString() : undefined;
+ resolve({
+ ...baseResult,
+ status: 'revoked',
+ ...(revocationDate && { revocationDate })
+ });
+ } else {
+ resolve(null);
+ }
+ return;
+ }
+
+ resolve({
+ ...baseResult,
+ status: res.type
+ });
+ });
+ });
+ } catch {
+ return null;
+ }
+ }
+
+ private outputResult(result: { name: string; status: string; expirationDate: string; revocationDate?: string }): void {
+ if (this.args.jsonOutput) {
+ console.log(JSON.stringify(result));
+ } else {
+ console.log(`Certificate Name: ${result.name}`);
+ console.log(`Certificate Status: ${result.status}`);
+ console.log(`Certificate Expiration Date: ${result.expirationDate}`);
+ if (result.revocationDate) {
+ console.log(`Certificate Revocation Date: ${result.revocationDate}`);
+ }
+ }
+ }
+
+ private async validateRequiredFiles(): Promise {
+ if (!this.args.p12File) {
+ throw new Error(
+ "P12 file path is required. Usage: bun certcheck --password [--json]\n" +
+ "Or set environment variables: CERT_P12_PATH and CERT_P12_PASSWORD"
+ );
+ }
+
+ if (!this.args.password) {
+ throw new Error(
+ "Password is required. Usage: bun certcheck --password [--json]\n" +
+ "Or set environment variables: CERT_P12_PATH and CERT_P12_PASSWORD"
+ );
+ }
+
+ // Check if P12 file exists
+ if (!(await Bun.file(this.args.p12File).exists())) {
+ throw new Error(`P12 file not found: ${this.args.p12File}`);
+ }
+
+ // Check CA directory with fs/promises
+ const fs = await import('fs/promises');
+ try {
+ await fs.access('CA-PEM');
+ } catch {
+ console.error('[*] CA-PEM directory not found. Downloading Apple CA certificates...');
+ await downloadCaCertificates();
+ console.error('[*] CA certificates downloaded successfully!');
+ }
+ }
+
+ public async run(): Promise {
+ try {
+ await this.validateRequiredFiles();
+
+ const certData = await P12Utils.convertP12ToPem(this.args.p12File!, this.args.password!);
+ await this.checkCertificateSignature(certData);
+ } catch (error) {
+ console.error(`[!] ${error instanceof Error ? error.message : 'Unknown error occurred'}`);
+ process.exit(1);
+ }
+ }
+}
+
+// Run the application
+const app = new CertCheck();
+app.run().catch(error => {
+ console.error('[!] Unexpected error:', error);
+ process.exit(1);
+});
diff --git a/src/p12Utils.ts b/src/p12Utils.ts
new file mode 100644
index 0000000..c985501
--- /dev/null
+++ b/src/p12Utils.ts
@@ -0,0 +1,74 @@
+import * as forge from 'node-forge';
+
+export interface CertificateData {
+ cert: forge.pki.Certificate;
+ pem: string;
+}
+
+export class P12Utils {
+ static async convertP12ToPem(p12File: string, password: string): Promise {
+ try {
+ const p12FileObj = Bun.file(p12File);
+ if (!(await p12FileObj.exists())) {
+ throw new Error(`P12 file not found: ${p12File}`);
+ }
+ const p12Binary = new Uint8Array(await p12FileObj.arrayBuffer());
+
+ const p12 = forge.pkcs12.pkcs12FromAsn1(
+ forge.asn1.fromDer(Buffer.from(p12Binary).toString('binary')),
+ false,
+ password
+ );
+
+ // Get certificate bags
+ const certBags = p12.getBags({ bagType: forge.pki.oids['certBag'] });
+
+ // Try to find certificate in certBag using the actual OID
+ let cert = null;
+ const certBagOid = forge.pki.oids['certBag'];
+ if (certBagOid && certBags[certBagOid] && certBags[certBagOid].length > 0) {
+ const firstBag = certBags[certBagOid][0];
+ if (firstBag && firstBag.cert) {
+ cert = firstBag.cert;
+ }
+ }
+
+ // If not found, try all bags
+ if (!cert) {
+ const allBags = p12.getBags({});
+ for (const [, bags] of Object.entries(allBags)) {
+ if (bags && bags.length > 0 && bags[0]?.cert) {
+ cert = bags[0].cert;
+ break;
+ }
+ }
+ }
+
+ if (!cert) {
+ throw new Error('No certificate found in P12 file');
+ }
+
+ const pem = forge.pki.certificateToPem(cert);
+
+ return { cert, pem };
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
+ if (errorMessage.includes('password')) {
+ throw new Error('Failed to convert P12 to PEM. Password is likely incorrect.');
+ }
+ throw new Error(`Failed to convert P12 to PEM. ${errorMessage}`);
+ }
+ }
+
+ static getCertificateName(cert: forge.pki.Certificate): string {
+ const orgAttribute = cert.subject.attributes.find(
+ attr => attr.name === 'organizationName'
+ );
+ const value = orgAttribute?.value;
+ return (typeof value === 'string' ? value : 'Unknown');
+ }
+
+ static getCertificateExpirationDate(cert: forge.pki.Certificate): string {
+ return new Date(cert.validity.notAfter.getTime()).toUTCString();
+ }
+}
diff --git a/src/types/ocsp.d.ts b/src/types/ocsp.d.ts
new file mode 100644
index 0000000..bbfda0c
--- /dev/null
+++ b/src/types/ocsp.d.ts
@@ -0,0 +1,18 @@
+declare module 'ocsp' {
+ interface OCSPRequest {
+ cert: string;
+ issuer: string;
+ }
+
+ interface OCSPResponse {
+ type: 'good' | 'revoked' | 'unknown';
+ value?: {
+ revocationTime?: Date;
+ };
+ }
+
+ export function check(
+ request: OCSPRequest,
+ callback: (error: Error | null, response: OCSPResponse) => void
+ ): void;
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..c24c708
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,34 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "lib": ["ES2022"],
+ "strict": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "noImplicitAny": true,
+ "noImplicitReturns": true,
+ "noImplicitThis": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "exactOptionalPropertyTypes": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noUncheckedIndexedAccess": true,
+ "types": ["bun-types"]
+ },
+ "include": [
+ "src/**/*"
+ ],
+ "exclude": [
+ "node_modules",
+ "dist",
+ "**/*.test.ts"
+ ]
+}