@chengyixu/certinfo 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +242 -0
- package/package.json +30 -0
package/index.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// certinfo — zero-dependency SSL/TLS certificate inspector
|
|
4
|
+
// Usage: certinfo example.com[:443] [--json|-j]
|
|
5
|
+
|
|
6
|
+
const tls = require("tls");
|
|
7
|
+
const net = require("net");
|
|
8
|
+
const crypto = require("crypto");
|
|
9
|
+
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
const jsonOut = args.includes("--json") || args.includes("-j");
|
|
12
|
+
const hostArg = args.find((a) => !a.startsWith("-"));
|
|
13
|
+
|
|
14
|
+
if (!hostArg) {
|
|
15
|
+
console.error("Usage: certinfo <host[:port]> [--json|-j]");
|
|
16
|
+
console.error(" certinfo example.com");
|
|
17
|
+
console.error(" certinfo example.com:8443 --json");
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let hostname, port;
|
|
22
|
+
if (hostArg.includes(":")) {
|
|
23
|
+
const parts = hostArg.split(":");
|
|
24
|
+
hostname = parts[0];
|
|
25
|
+
port = parseInt(parts[1], 10);
|
|
26
|
+
} else {
|
|
27
|
+
hostname = hostArg;
|
|
28
|
+
port = 443;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!hostname || isNaN(port)) {
|
|
32
|
+
console.error("Error: invalid host:port format");
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function pad(n) {
|
|
37
|
+
return String(n).padStart(2, "0");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function fmtDate(d) {
|
|
41
|
+
const y = d.getFullYear();
|
|
42
|
+
const m = pad(d.getMonth() + 1);
|
|
43
|
+
const day = pad(d.getDate());
|
|
44
|
+
const h = pad(d.getHours());
|
|
45
|
+
const min = pad(d.getMinutes());
|
|
46
|
+
const sec = pad(d.getSeconds());
|
|
47
|
+
return `${y}-${m}-${day} ${h}:${min}:${sec} GMT`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function daysUntil(d) {
|
|
51
|
+
return Math.ceil((d.getTime() - Date.now()) / 86400000);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseSANs(altName) {
|
|
55
|
+
if (!altName) return [];
|
|
56
|
+
return altName.split(", ").map((s) => s.replace(/^DNS:/, "").replace(/^IP Address:/, "").trim());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function keyLabel(x509) {
|
|
60
|
+
const pk = x509.publicKey;
|
|
61
|
+
const type = pk.asymmetricKeyType || "unknown";
|
|
62
|
+
const det = pk.asymmetricKeyDetails || {};
|
|
63
|
+
if (type === "ec") {
|
|
64
|
+
return `EC (${det.namedCurve || "unknown"})`;
|
|
65
|
+
}
|
|
66
|
+
if (type === "rsa" || type === "rsa-pss") {
|
|
67
|
+
return `RSA ${det.modulusLength || ""} bits`;
|
|
68
|
+
}
|
|
69
|
+
if (det.modulusLength) return `${type} ${det.modulusLength} bits`;
|
|
70
|
+
return type;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseInfoAccess(ia) {
|
|
74
|
+
if (!ia) return [];
|
|
75
|
+
// ia is a string like "OCSP - URI:http://..."
|
|
76
|
+
return ia.split("\n").filter(Boolean).map((line) => line.trim());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseKeyUsage(ku) {
|
|
80
|
+
if (!ku) return [];
|
|
81
|
+
return ku;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const KNOWN_KEY_USAGE = {
|
|
85
|
+
"1.3.6.1.5.5.7.3.1": "TLS Web Server Authentication",
|
|
86
|
+
"1.3.6.1.5.5.7.3.2": "TLS Web Client Authentication",
|
|
87
|
+
"1.3.6.1.5.5.7.3.3": "Code Signing",
|
|
88
|
+
"1.3.6.1.5.5.7.3.4": "Email Protection",
|
|
89
|
+
"1.3.6.1.5.5.7.3.8": "Time Stamping",
|
|
90
|
+
"1.3.6.1.5.5.7.3.9": "OCSP Signing",
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
function humanKeyUsage(oid) {
|
|
94
|
+
return KNOWN_KEY_USAGE[oid] || oid;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const socket = net.createConnection(
|
|
98
|
+
{ host: hostname, port, servername: hostname },
|
|
99
|
+
() => {
|
|
100
|
+
const tlsSocket = tls.connect(
|
|
101
|
+
{
|
|
102
|
+
socket,
|
|
103
|
+
servername: hostname,
|
|
104
|
+
rejectUnauthorized: false,
|
|
105
|
+
},
|
|
106
|
+
() => {
|
|
107
|
+
const raw = tlsSocket.getPeerCertificate(false);
|
|
108
|
+
let x509;
|
|
109
|
+
try {
|
|
110
|
+
x509 = new crypto.X509Certificate(raw.raw);
|
|
111
|
+
} catch {
|
|
112
|
+
console.error("Error: failed to parse certificate");
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const expires = new Date(x509.validTo);
|
|
117
|
+
const validFrom = new Date(x509.validFrom);
|
|
118
|
+
const daysLeft = daysUntil(expires);
|
|
119
|
+
|
|
120
|
+
if (jsonOut) {
|
|
121
|
+
const result = {
|
|
122
|
+
hostname,
|
|
123
|
+
port,
|
|
124
|
+
subject: x509.subject,
|
|
125
|
+
issuer: x509.issuer,
|
|
126
|
+
validity: {
|
|
127
|
+
from: x509.validFrom,
|
|
128
|
+
to: x509.validTo,
|
|
129
|
+
days_left: daysLeft,
|
|
130
|
+
expired: daysLeft <= 0,
|
|
131
|
+
expiring_soon: daysLeft > 0 && daysLeft <= 30,
|
|
132
|
+
},
|
|
133
|
+
key: keyLabel(x509),
|
|
134
|
+
signature_algorithm: x509.signatureAlgorithm || "unknown",
|
|
135
|
+
fingerprint_sha256: x509.fingerprint256 || "",
|
|
136
|
+
fingerprint_sha1: x509.fingerprint || "",
|
|
137
|
+
san: parseSANs(x509.subjectAltName),
|
|
138
|
+
serial_number: x509.serialNumber ? `0x${x509.serialNumber}` : "",
|
|
139
|
+
info_access: parseInfoAccess(x509.infoAccess),
|
|
140
|
+
key_usage: parseKeyUsage(x509.extKeyUsage),
|
|
141
|
+
};
|
|
142
|
+
console.log(JSON.stringify(result, null, 2));
|
|
143
|
+
} else {
|
|
144
|
+
let statusIcon, statusText;
|
|
145
|
+
if (daysLeft <= 0) {
|
|
146
|
+
statusIcon = "\x1b[31mEXPIRED\x1b[0m";
|
|
147
|
+
statusText = "\x1b[31mEXPIRED\x1b[0m";
|
|
148
|
+
} else if (daysLeft <= 15) {
|
|
149
|
+
statusIcon = "\x1b[31mCRITICAL\x1b[0m";
|
|
150
|
+
statusText = `\x1b[31m${daysLeft} days left\x1b[0m`;
|
|
151
|
+
} else if (daysLeft <= 30) {
|
|
152
|
+
statusIcon = "\x1b[33mWARN\x1b[0m";
|
|
153
|
+
statusText = `\x1b[33m${daysLeft} days left\x1b[0m`;
|
|
154
|
+
} else {
|
|
155
|
+
statusIcon = "\x1b[32mOK\x1b[0m";
|
|
156
|
+
statusText = `\x1b[32m${daysLeft} days left\x1b[0m`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const bar = "-".repeat(54);
|
|
160
|
+
|
|
161
|
+
console.log("");
|
|
162
|
+
console.log(` SSL/TLS Certificate — ${hostname}:${port} ${statusIcon}`);
|
|
163
|
+
console.log(" " + bar);
|
|
164
|
+
console.log(` Subject: ${x509.subject}`);
|
|
165
|
+
console.log(` Issuer: ${x509.issuer}`);
|
|
166
|
+
console.log(` Valid from: ${x509.validFrom}`);
|
|
167
|
+
console.log(` Valid until: ${x509.validTo}`);
|
|
168
|
+
console.log(` Status: ${statusText}`);
|
|
169
|
+
console.log(` Key: ${keyLabel(x509)}`);
|
|
170
|
+
console.log(` Signature: ${x509.signatureAlgorithm || "unknown"}`);
|
|
171
|
+
console.log(` SHA-256: ${x509.fingerprint256 || ""}`);
|
|
172
|
+
console.log(` SHA-1: ${x509.fingerprint || ""}`);
|
|
173
|
+
console.log(` Serial: ${x509.serialNumber ? "0x" + x509.serialNumber : ""}`);
|
|
174
|
+
|
|
175
|
+
const sans = parseSANs(x509.subjectAltName);
|
|
176
|
+
if (sans.length > 0) {
|
|
177
|
+
console.log(` SANs (${sans.length}): ${sans.slice(0, 5).join(", ")}${sans.length > 5 ? " ..." : ""}`);
|
|
178
|
+
if (sans.length > 5) {
|
|
179
|
+
sans.slice(5).forEach((s) => console.log(` ${s}`));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const ia = parseInfoAccess(x509.infoAccess);
|
|
184
|
+
if (ia.length > 0) {
|
|
185
|
+
console.log("");
|
|
186
|
+
console.log(" Info Access:");
|
|
187
|
+
ia.forEach((line) => console.log(` ${line}`));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const ku = parseKeyUsage(x509.extKeyUsage);
|
|
191
|
+
if (ku.length > 0) {
|
|
192
|
+
console.log("");
|
|
193
|
+
console.log(" Extended Key Usage:");
|
|
194
|
+
ku.forEach((oid) => console.log(` ${oid} (${humanKeyUsage(oid)})`));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Chain
|
|
198
|
+
const cert = tlsSocket.getPeerCertificate(true);
|
|
199
|
+
if (cert && cert.issuerCertificate && cert.issuerCertificate !== cert) {
|
|
200
|
+
console.log("");
|
|
201
|
+
console.log(" Certificate Chain:");
|
|
202
|
+
let chain = cert;
|
|
203
|
+
let depth = 0;
|
|
204
|
+
while (chain) {
|
|
205
|
+
const label = chain.issuerCertificate === chain ? " (self-signed)" : "";
|
|
206
|
+
console.log(` ${depth}: ${chain.subject ? chain.subject.CN || chain.subject : "?"}${label}`);
|
|
207
|
+
if (chain === chain.issuerCertificate) break;
|
|
208
|
+
chain = chain.issuerCertificate;
|
|
209
|
+
depth++;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
console.log("");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
tlsSocket.end();
|
|
217
|
+
},
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
tlsSocket.on("error", (err) => {
|
|
221
|
+
console.error(`Error: ${err.message}`);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
tlsSocket.setTimeout(10000);
|
|
226
|
+
tlsSocket.on("timeout", () => {
|
|
227
|
+
console.error("Error: Connection timed out");
|
|
228
|
+
process.exit(1);
|
|
229
|
+
});
|
|
230
|
+
},
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
socket.setTimeout(10000);
|
|
234
|
+
socket.on("error", (err) => {
|
|
235
|
+
console.error(`Error: ${err.message}`);
|
|
236
|
+
process.exit(1);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
socket.on("timeout", () => {
|
|
240
|
+
console.error("Error: Connection timed out");
|
|
241
|
+
process.exit(1);
|
|
242
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chengyixu/certinfo",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Zero-dependency CLI to inspect SSL/TLS certificate details from any host",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"certinfo": "./index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.js"
|
|
11
|
+
],
|
|
12
|
+
"keywords": [
|
|
13
|
+
"ssl",
|
|
14
|
+
"tls",
|
|
15
|
+
"certificate",
|
|
16
|
+
"cli",
|
|
17
|
+
"security",
|
|
18
|
+
"https",
|
|
19
|
+
"expiry",
|
|
20
|
+
"x509"
|
|
21
|
+
],
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/chengyixu/certinfo"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=16"
|
|
29
|
+
}
|
|
30
|
+
}
|