| name | tls-check |
| description | Inspect TLS/SSL certificates in Node.js using the built-in tls module -- connect, extract cert metadata, calculate days until expiry, and handle errors. |
| triggers | ["tls connect","tls module","ssl cert node","certificate inspection","getPeerCertificate"] |
When to use this skill
Use this skill when implementing certificate inspection in Node.js without external HTTP clients: connecting via TLS, extracting expiry dates and issuer information, handling timeouts and errors, and calculating days remaining.
Install
No additional packages needed. Uses Node.js built-in tls module.
Basic TLS Connect and Cert Extraction
import tls from 'node:tls';
interface CertInfo {
expiresAt: Date;
issuedAt: Date;
issuer: string;
subject: string;
sans: string[];
serialNumber: string;
daysUntilExpiry: number;
}
function checkCert(hostname: string, port = 443): Promise<CertInfo> {
return new Promise((resolve, reject) => {
const socket = tls.connect(
{
host: hostname,
port,
servername: hostname,
rejectUnauthorized: false,
},
() => {
const cert = socket.getPeerCertificate(false);
socket.end();
if (!cert || !cert.valid_to) {
reject(new Error('No certificate in TLS handshake'));
return;
}
const expiresAt = new Date(cert.valid_to);
const issuedAt = new Date(cert.valid_from);
const now = new Date();
const daysUntilExpiry = Math.floor(
(expiresAt.getTime() - now.getTime()) / (1000 * 60 * 60 * 24),
);
resolve({
expiresAt,
issuedAt,
issuer: cert.issuer?.O ?? cert.issuer?.CN ?? '',
subject: cert.subject?.CN ?? '',
sans: parseSANs(cert.subjectaltname ?? ''),
serialNumber: cert.serialNumber ?? '',
daysUntilExpiry,
});
},
);
socket.setTimeout(10_000, () => {
socket.destroy();
reject(new Error('Connection timeout after 10000ms'));
});
socket.on('error', (err) => {
reject(err);
});
});
}
function parseSANs(subjectaltname: string): string[] {
if (!subjectaltname) return [];
return subjectaltname
.split(', ')
.filter((s) => s.startsWith('DNS:'))
.map((s) => s.replace(/^DNS:/, ''));
}
Certificate Object Fields
getPeerCertificate(false) returns a tls.PeerCertificate with these useful fields:
| Field | Type | Example |
|---|
subject.CN | string | api.example.com |
issuer.O | string | Let's Encrypt |
issuer.CN | string | R3 |
valid_from | string | Jan 27 00:00:00 2024 GMT |
valid_to | string | Apr 27 00:00:00 2024 GMT |
subjectaltname | string | DNS:api.example.com, DNS:www.api.example.com |
serialNumber | string | 04A13B9C... (hex, uppercase) |
fingerprint | string | AA:BB:CC:... |
valid_from and valid_to are parseable directly with new Date().
Non-Default Ports
const info = await checkCert('api.example.com', 8443);
const info = await checkCert('ldap.corp.net', 636);
Handling Expired Certificates
rejectUnauthorized: false allows the handshake to complete even if:
- The certificate is already expired
- The certificate is self-signed
- The chain is incomplete
This means daysUntilExpiry will be negative for expired certs. Store as-is; the UI renders as "Expired".
Concurrency Limiter
import { checkCert } from './checker.js';
async function checkAll(hosts: Array<{ hostname: string; port: number }>, concurrency = 5) {
const results: Map<string, CertInfo | Error> = new Map();
const queue = [...hosts];
async function worker() {
while (queue.length > 0) {
const host = queue.shift();
if (!host) break;
const key = `${host.hostname}:${host.port}`;
try {
results.set(key, await checkCert(host.hostname, host.port));
} catch (err) {
results.set(key, err instanceof Error ? err : new Error(String(err)));
}
}
}
await Promise.all(.({ : concurrency }, ()));
results;
}
Checking Multiple Certificates on the Same IP
When a server hosts multiple domains, pass servername to use SNI:
const cert1 = await checkCert('app.example.com', 443);
const cert2 = await checkCert('api.example.com', 443);
servername in the options object sets the SNI extension. Always set it equal to the logical hostname, not the IP.
Error Handling
| Error Code | Cause | Action |
|---|
ECONNREFUSED | Nothing listening on the port | Mark as error, show in UI |
ECONNRESET | Connection dropped by server | Retry once, then mark error |
ETIMEDOUT | No response within timeout | Check network path; increase timeout |
ENOTFOUND | DNS resolution failure | Verify hostname spelling |
CERT_HAS_EXPIRED | Not thrown (rejectUnauthorized false) | daysUntilExpiry will be negative |
SELF_SIGNED_CERT | Not thrown (rejectUnauthorized false) | Certificate is still inspectable |
Days Until Expiry Calculation
function daysUntilExpiry(expiresAt: Date): number {
const now = Date.now();
const expiry = expiresAt.getTime();
return Math.floor((expiry - now) / (1000 * 60 * 60 * 24));
}
Checking Self-Signed Certificates
const cert = await checkCert('internal.corp.net', 443);
const isSelfSigned = cert.issuer === cert.subject;
One-Shot vs. Streaming
tls.connect with the callback approach is one-shot: connect, read cert, close. This is appropriate for periodic monitoring. Do not keep the socket open between checks.