sslko
Version:
A simple tool to check SSL/TLS certificate information for a given domain.
211 lines (210 loc) • 8.21 kB
JavaScript
import { DEFAULT_PORT, DEFAULT_TIMEOUT, MIN_RSA_KEY_SIZE, } from "./constants.js";
import { convertPeerCertificate } from "./convert-peer-certificate.js";
import { getCertificate } from "./get-certificate.js";
/**
* Verifies if the given hostname matches the Common Name (CN) or Subject Alternative Names (SANs) of the certificate.
*
* @param host The hostname to check.
* @param certificate The certificate to getCertificateInfo against.
* @returns `true` if the hostname matches, otherwise `false`.
*/
export function verifyHostname(host, certificate) {
const isHostnameMatch = (certName, host) => {
if (certName === host)
return true;
// Wildcard match
if (certName.startsWith("*.") &&
host.split(".").length === certName.split(".").length) {
const certDomain = certName.substring(2);
const hostDomain = host.split(".").slice(1).join(".");
return hostDomain === certDomain;
}
return false;
};
if (certificate.subject?.CN &&
isHostnameMatch(certificate.subject.CN, host)) {
return true;
}
if (certificate.subjectaltname) {
const altNames = certificate.subjectaltname
.split(", ")
.filter((n) => n.startsWith("DNS:"))
.map((n) => n.substring(4));
if (altNames.some((dnsName) => isHostnameMatch(dnsName, host))) {
return true;
}
}
return false;
}
/**
* Default options for the getCertificate function.
*/
const DefaultOptions = {
port: DEFAULT_PORT, // Default port for HTTPS
timeout: DEFAULT_TIMEOUT, // Default timeout of 10 seconds
rejectUnauthorized: false, // We'll do our own verification
};
/**
* Checks if a certificate appears to be self-signed by comparing subject and issuer.
* @param certificate The certificate to check
* @returns true if the certificate appears to be self-signed
*/
export function isSelfSignedCertificate(certificate) {
if (!certificate.subject || !certificate.issuer) {
return false;
}
// Compare Common Names
if (certificate.subject.CN && certificate.issuer.CN) {
return certificate.subject.CN === certificate.issuer.CN;
}
// If no CN, compare the entire subject/issuer objects
return (JSON.stringify(certificate.subject) === JSON.stringify(certificate.issuer));
}
/**
* Validates the basic structure and content of a certificate.
* @param certificate The certificate to validate
* @returns An array of validation warning messages (empty if valid)
*/
export function validateCertificateStructure(certificate) {
const warnings = [];
// Check for missing essential fields
if (!certificate.subject) {
warnings.push("Certificate is missing subject information");
}
else if (!certificate.subject.CN) {
warnings.push("Certificate does not have a Common Name (CN)");
}
if (!certificate.issuer) {
warnings.push("Certificate is missing issuer information");
}
if (!certificate.subjectaltname) {
warnings.push("Certificate does not have Subject Alternative Names (SANs)");
}
return warnings;
}
/**
* Checks for security weaknesses in a certificate's cryptographic properties.
* @param certificate The certificate to analyze for security issues
* @returns An array of security warning messages
*/
export function checkCertificateSecurity(certificate) {
const warnings = [];
// Check RSA key size if available (modulus and exponent are available on DetailedPeerCertificate)
if ("modulus" in certificate &&
certificate.modulus &&
"exponent" in certificate &&
certificate.exponent) {
try {
// Modulus is typically in hex format, convert to estimate bit length
const modulusHex = certificate.modulus.replace(/:/g, "");
const keyBits = modulusHex.length * 4; // Each hex char is 4 bits
if (keyBits < MIN_RSA_KEY_SIZE) {
warnings.push(`Certificate uses weak RSA key size: ${keyBits} bits (minimum recommended: ${MIN_RSA_KEY_SIZE} bits)`);
}
}
catch {
// Ignore parsing errors for modulus
}
}
/** Weak signature algorithms that should be flagged */
const weekSignatures = [
"md5WithRSAEncryption",
"sha1WithRSAEncryption",
"md5WithRSA",
"sha1WithRSA",
];
// Check for weak signature algorithm
if ("signatureAlgorithm" in certificate &&
typeof certificate.signatureAlgorithm === "string" &&
weekSignatures.includes(certificate.signatureAlgorithm)) {
warnings.push(`Certificate uses weak signature algorithm: ${certificate.signatureAlgorithm}`);
}
return warnings;
}
/**
* Retrieves information about a certificate for a given host.
*
* @example Retrieve certificate info for example.com
* ```typescript
* import { getCertificateInfo } from "sslko";
* const info = await getCertificateInfo("example.com");
* console.log(`Valid: ${info.valid}, Days left: ${info.daysLeft}`);
* ```
*
* @example Retrieve expired certificate info for expired.badssl.com
* ```typescript
* import { getCertificateInfo } from "sslko";
* const info = await getCertificateInfo("expired.badssl.com");
* if (!info.valid) {
* console.log(`Errors: ${info.errors.join(", ")}`);
* }
* ```
*
* @example Check certificate for custom port
* ```typescript
* import { getCertificateInfo } from "sslko";
* const info = await getCertificateInfo("example.com", { port: 8443 });
* ```
*
* Will return an object with `valid: false` and an error message.
*/
export async function getCertificateInfo(host, options = {}) {
const certificate = (await getCertificate(host, {
...DefaultOptions,
...options,
detailed: true, // Always return DetailedPeerCertificate for info
}));
const results = {
valid: true,
...convertPeerCertificate(certificate),
errors: [],
warnings: [],
};
// Add issuer (CA) certificate if available
if (certificate.issuerCertificate) {
results.issuerCertificate = convertPeerCertificate(certificate.issuerCertificate);
}
// Check if certificate is not yet valid
if (Date.now() < results.validFromDate.getTime()) {
results.valid = false;
results.errors.push("Certificate is not yet valid");
}
// Check if certificate is approaching expiration (within 30 days)
if (!results.expired && results.daysLeft > 0 && results.daysLeft <= 30) {
results.warnings.push(`Certificate expires in ${results.daysLeft} days`);
}
if (!certificate.issuer) {
results.warnings.push("Certificate is missing issuer information");
}
// Check if the certificate is expired
if (Date.now() > results.validToDate.getTime()) {
results.valid = false;
results.expired = true;
results.errors.push("Certificate has expired");
}
// Unusually short validity period check
if (results.daysTotal < 1) {
results.warnings.push("Certificate has an unusually short validity period (less than 1 day)");
}
// Unusually long validity period check
if (results.daysTotal > 398) {
results.warnings.push(`Certificate has an unusually long validity period (${results.daysTotal} days, max recommended: 398)`);
}
// Validate certificate structure and add any warnings
const structureWarnings = validateCertificateStructure(certificate);
results.warnings.push(...structureWarnings);
// Check for security weaknesses
const securityWarnings = checkCertificateSecurity(certificate);
results.warnings.push(...securityWarnings);
// Host verification
if (!verifyHostname(host, certificate)) {
results.valid = false;
results.errors.push(`Hostname "${host}" does not match the certificate's Common Name (CN) or Subject Alternative Names (SANs)`);
}
// Check if the certificate is self-signed
if (isSelfSignedCertificate(certificate)) {
results.valid = false;
results.errors.push("Certificate is self-signed");
}
return results;
}