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" + ] +}