import DiscordInteractionCommand from "../../util/DiscordInteractionCommand"; import { ChatInputCommandInteraction, EmbedBuilder } from "discord.js"; import axios from "axios"; interface CertificateDetails { bitLength: number; connection: { cipherSuite: string; tlsVersion: string; }; emailAddresses: []; extendedKeyUsage: number[]; extendedKeyUsageAsText: string[]; fingerprint: string; issuer: { commonName: string; country: string[]; locality: never; // TODO: needs clarification province: string[]; organization: string[]; organizationalUnit: never; // TODO: needs clarification }; keyUsageAsText: string[]; notAfter: Date; notBefore: Date; publicKeyAlgorithm: string; san: string[]; serialNumber: string; signatureAlgorithm: string; status: boolean; subject: { commonName: string; country: string[]; locality: never; // TODO: needs clarification province: string[]; organization: string[]; organizationalUnit: never; // TODO: needs clarification }; validationType: "DV" | "OV" | "EV"; } // Define an enum for security levels enum SecurityLevel { MostSecure, Secure, LessSecure, NotSecure, } interface CipherSuite { cipher: string; securityLevel: SecurityLevel; } const CipherSuites: CipherSuite[] = [ // Most Secure (TLS 1.3 AEAD Ciphers) { cipher: "TLS_AES_256_GCM_SHA384", securityLevel: SecurityLevel.MostSecure }, { cipher: "TLS_CHACHA20_POLY1305_SHA256", securityLevel: SecurityLevel.MostSecure }, { cipher: "TLS_AES_128_GCM_SHA256", securityLevel: SecurityLevel.MostSecure }, // Secure (TLS 1.2 AEAD Ciphers with Forward Secrecy) { cipher: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", securityLevel: SecurityLevel.Secure }, { cipher: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", securityLevel: SecurityLevel.Secure }, { cipher: "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", securityLevel: SecurityLevel.Secure }, { cipher: "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", securityLevel: SecurityLevel.Secure }, { cipher: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", securityLevel: SecurityLevel.Secure }, { cipher: "TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256", securityLevel: SecurityLevel.Secure }, { cipher: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", securityLevel: SecurityLevel.Secure }, { cipher: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", securityLevel: SecurityLevel.Secure }, { cipher: "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", securityLevel: SecurityLevel.Secure }, // Less Secure (CBC with TLS 1.2 and SHA-256/SHA-384) { cipher: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", securityLevel: SecurityLevel.LessSecure }, { cipher: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", securityLevel: SecurityLevel.LessSecure }, { cipher: "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256", securityLevel: SecurityLevel.LessSecure }, { cipher: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", securityLevel: SecurityLevel.LessSecure }, { cipher: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", securityLevel: SecurityLevel.LessSecure }, { cipher: "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256", securityLevel: SecurityLevel.LessSecure }, { cipher: "TLS_RSA_WITH_AES_256_CBC_SHA256", securityLevel: SecurityLevel.LessSecure }, { cipher: "TLS_RSA_WITH_AES_128_CBC_SHA256", securityLevel: SecurityLevel.LessSecure }, // Not Secure (CBC with TLS 1.0/1.1 or SHA-1) { cipher: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", securityLevel: SecurityLevel.NotSecure }, { cipher: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", securityLevel: SecurityLevel.NotSecure }, { cipher: "TLS_DHE_RSA_WITH_AES_256_CBC_SHA", securityLevel: SecurityLevel.NotSecure }, { cipher: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", securityLevel: SecurityLevel.NotSecure }, { cipher: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", securityLevel: SecurityLevel.NotSecure }, { cipher: "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", securityLevel: SecurityLevel.NotSecure }, { cipher: "TLS_RSA_WITH_AES_256_CBC_SHA", securityLevel: SecurityLevel.NotSecure }, { cipher: "TLS_RSA_WITH_AES_128_CBC_SHA", securityLevel: SecurityLevel.NotSecure }, { cipher: "TLS_RSA_WITH_3DES_EDE_CBC_SHA", securityLevel: SecurityLevel.NotSecure }, { cipher: "TLS_RSA_WITH_RC4_128_SHA", securityLevel: SecurityLevel.NotSecure }, { cipher: "TLS_RSA_WITH_RC4_128_MD5", securityLevel: SecurityLevel.NotSecure }, ]; export default class TLS extends DiscordInteractionCommand { constructor() { super("tls", "Receives TLS information about an HTTP server with a FQDN."); this.builder.addStringOption((option) => { return option .setName("fqdn") .setDescription( "The Fully Qualified Domain Name (FQDN) for the server you want to perform the TLS/SSL lookup for." ) .setRequired(true) .setMinLength(3); }); } public getCipherSecurityLevel(cipher: string): SecurityLevel | null { const result = CipherSuites.find((entry) => entry.cipher === cipher); return result ? result.securityLevel : null; } public async execute(interaction: ChatInputCommandInteraction) { await interaction.deferReply({ ephemeral: false }); try { const certAPIReq = await axios.get(`https://certapi.libraryofcode.org/`, { params: { q: interaction.options.getString("fqdn", true) }, }); if (certAPIReq.status !== 200) { return interaction.editReply({ content: "Could not fetch information for this FQDN's HTTP server. Please check the FQDN, its server, and try again.", }); } const certData: CertificateDetails = certAPIReq.data; if (!certData.status) { return interaction.editReply({ content: "Issue when fetching this FQDN's TLS certificate. Please try again later.", }); } const embed = new EmbedBuilder(); embed.setAuthor({ name: interaction.options.getString("fqdn", true), iconURL: `https://${interaction.options.getString("fqdn", true)}/favicon.ico`, }); let desc = ""; if (certData.validationType === "EV") { desc += `**Certificate issued to:** __${certData.subject.organization[0]} [${certData.subject.country[0]}]__\n**Verified by:** __${certData.issuer.organization[0]}__\n\n`; } else if (certData.issuer.organization) { desc += `**Verified by:** __${certData.issuer.organization[0]}__\n\n`; } if (certData.subject) { desc += "## Subject\n"; if (certData.subject.organization && certData.issuer.commonName) { desc += `__**${certData.subject.organization[0]} (${certData.subject.commonName})**__\n`; } if (certData.subject.commonName) { desc += `**Common Name:** ${certData.subject.commonName}\n`; } if (certData.subject.organization) { desc += `**Organization:** ${certData.subject.organization[0]}\n`; } if (certData.subject.organizationalUnit) { desc += `**Organizational Unit:** ${certData.subject.organizationalUnit[0]}\n`; } if (certData.subject.locality) { desc += `**Locality:** ${certData.subject.locality[0]}\n`; } if (certData.subject.province) { desc += `**State/Province:** ${certData.subject.province[0]}\n`; } if (certData.subject.country) { desc += `**Country:** ${certData.subject.country[0]}\n`; } } if (certData.issuer) { desc += "## Issuer\n"; if (certData.issuer.organization && certData.issuer.commonName) { desc += `__**${certData.issuer.organization[0]} (${certData.issuer.commonName})**__\n`; } if (certData.issuer.commonName) { desc += `**Common Name:** ${certData.issuer.commonName}\n`; } if (certData.issuer.organization) { desc += `**Organization:** ${certData.issuer.organization[0]}\n`; } if (certData.issuer.organizationalUnit) { desc += `**Organizational Unit:** ${certData.issuer.organizationalUnit[0]}\n`; } if (certData.subject.locality) { desc += `**Locality:** ${certData.subject.locality[0]}\n`; } if (certData.issuer.province) { desc += `**State/Province:** ${certData.issuer.province[0]}\n`; } if (certData.issuer.country) { desc += `**Country:** ${certData.issuer.country[0]}\n`; } } embed.setDescription(desc); let validationType: | "Domain Validation (DV)" | "Organization Validation (OV)" | ":lock: Extended Validation (EV)" | string; switch (certData.validationType) { case "DV": validationType = "Domain Validation (DV)"; break; case "OV": validationType = "Organization Validation (OV)"; embed.setColor("#4287f5"); break; case "EV": embed.setColor("#42f554"); validationType = ":lock: Extended Validation (EV)"; break; default: validationType = "N/A';"; break; } let cipherSuiteText: string = ""; switch (this.getCipherSecurityLevel(certData.connection.cipherSuite)) { case SecurityLevel.MostSecure: cipherSuiteText = `:green_circle: ${certData.connection.cipherSuite} (${certData.connection.tlsVersion})`; break; case SecurityLevel.Secure: cipherSuiteText = `:yellow_circle: ${certData.connection.cipherSuite} (${certData.connection.tlsVersion})`; break; case SecurityLevel.LessSecure: cipherSuiteText = `:orange_circle: ${certData.connection.cipherSuite} (${certData.connection.tlsVersion})`; break; case SecurityLevel.NotSecure: cipherSuiteText = `:red_circle: ${certData.connection.cipherSuite} (${certData.connection.tlsVersion})`; break; default: cipherSuiteText = `:grey_question: ${certData.connection.cipherSuite} (${certData.connection.tlsVersion})`; break; } embed.addFields( { name: "Validation Type", value: validationType, inline: true, }, { name: "Cipher Suite", value: cipherSuiteText, inline: false, }, { name: "Public Key Algorithm", value: `${certData.publicKeyAlgorithm} ${certData.bitLength}`, inline: true, }, { name: "Signature Algorithm", value: certData.signatureAlgorithm, inline: true, }, { name: "Not Before", value: new Date(certData.notBefore).toUTCString(), inline: true }, { name: "Not After", value: new Date(certData.notAfter).toUTCString(), inline: true }, { name: "Serial Number", value: certData.serialNumber, inline: true, } ); console.log(certData); // TODO: Remove after testing. if (certData.keyUsageAsText?.length) embed.addFields({ name: "Key Usages", value: certData.keyUsageAsText.join(", "), inline: true, }); if (certData.extendedKeyUsageAsText?.length) embed.addFields({ name: "Extended Key Usages", value: certData.extendedKeyUsageAsText.join(", "), inline: true, }); // embed.addField('Common Name', x509.data.subject.commonName, true); // embed.addField('Issuer', x509.data.issuer.commonName, true); // embed.addBlankField(); // embed.addField('Public Key Algorithm', x509.data.publicKeyAlgorithm, true); // embed.addField('Not Before', new Date(x509.data.notBefore).toUTCString(), true); // embed.addField('Not After', new Date(x509.data.notAfter).toUTCString(), true); // if (x509.data.keyUsageAsText.length) embed.addField('Key Usages', x509.data.keyUsageAsText.join(', '), true); // if (x509.data.extendedKeyUsageAsText.length) embed.addField('Extended Key Usages', x509.data.extendedKeyUsageAsText.join(', '), true); return await interaction.editReply({ embeds: [embed] }); } catch (err) { return interaction.editReply({ content: `Error processing retrieval from FQDN: ${err}` }); } } }