mirror of
https://github.com/zkemail/zk-email-verify.git
synced 2026-01-09 13:38:03 -05:00
Merge pull request #210 from jayden-sudo/main
Fixed a bug in retrieving the DKIM public key
This commit is contained in:
97
packages/helpers/src/lib/mailauth/DoH.ts
Normal file
97
packages/helpers/src/lib/mailauth/DoH.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
// DoH servers list
|
||||
export enum DoHServer {
|
||||
// Google Public DNS
|
||||
Google = "https://dns.google/resolve",
|
||||
// Cloudflare DNS
|
||||
Cloudflare = "https://cloudflare-dns.com/dns-query",
|
||||
}
|
||||
|
||||
/**
|
||||
* DNS over HTTPS (DoH) resolver
|
||||
*
|
||||
* @export
|
||||
* @class DoH
|
||||
*/
|
||||
export class DoH {
|
||||
|
||||
// DNS response codes
|
||||
static DoHStatusNoError = 0;
|
||||
// DNS RR types
|
||||
static DoHTypeTXT = 16;
|
||||
|
||||
/**
|
||||
* Resolve DKIM public key from DNS
|
||||
*
|
||||
* @static
|
||||
* @param {string} name DKIM record name (e.g. 20230601._domainkey.gmail.com)
|
||||
* @param {string} DNSServer DNS over HTTPS API URL
|
||||
* @return {*} {(Promise<string | null>)} DKIM public key or null if not found
|
||||
* @memberof DoH
|
||||
*/
|
||||
public static async resolveDKIMPublicKey(name: string, DNSServer: string): Promise<string | null> {
|
||||
if (!DNSServer.startsWith('https://')) {
|
||||
DNSServer = 'https://' + DNSServer;
|
||||
}
|
||||
if (DNSServer.endsWith('/')) {
|
||||
DNSServer = DNSServer.slice(0, -1);
|
||||
}
|
||||
const resp = await fetch(
|
||||
DNSServer + "?" +
|
||||
new URLSearchParams({
|
||||
name: name,
|
||||
// DKIM public key record type is TXT
|
||||
type: DoH.DoHTypeTXT.toString(),
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
"accept": "application/dns-json",
|
||||
}
|
||||
}
|
||||
);
|
||||
if (resp.status === 200) {
|
||||
const out = await resp.json();
|
||||
if (typeof out === 'object' && out !== null && 'Status' in out && 'Answer' in out) {
|
||||
const resp = out as DoHResponse;
|
||||
if (resp.Status === DoH.DoHStatusNoError && resp.Answer.length > 0) {
|
||||
for (const ans of resp.Answer) {
|
||||
if (ans.type === DoH.DoHTypeTXT) {
|
||||
let DKIMRecord = ans.data;
|
||||
/*
|
||||
Remove all double quotes
|
||||
Some DNS providers wrap TXT records in double quotes,
|
||||
and others like Cloudflare may include them. According to
|
||||
TXT (potentially multi-line) and DKIM (Base64 data) standards,
|
||||
we can directly remove all double quotes from the DKIM public key.
|
||||
*/
|
||||
DKIMRecord = DKIMRecord.replace(/"/g, '');
|
||||
return DKIMRecord;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
interface DoHResponse {
|
||||
Status: number; // NOERROR - Standard DNS response code (32 bit integer).
|
||||
TC: boolean; // Whether the response is truncated
|
||||
AD: boolean; // Whether all response data was validated with DNSSEC
|
||||
CD: boolean; // Whether the client asked to disable DNSSEC
|
||||
Question: Question[];
|
||||
Answer: Answer[];
|
||||
Comment: string;
|
||||
}
|
||||
|
||||
interface Question {
|
||||
name: string; // FQDN with trailing dot
|
||||
type: number; // A - Standard DNS RR type. 5:CNAME, 16:TXT
|
||||
}
|
||||
|
||||
interface Answer {
|
||||
name: string; // Always matches name in the Question section
|
||||
type: number; // A - Standard DNS RR type. 5:CNAME, 16:TXT
|
||||
TTL: number; // Record's time-to-live in seconds
|
||||
data: string; // Record data
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import crypto, { KeyObject } from "crypto";
|
||||
import parseDkimHeaders from "./parse-dkim-headers";
|
||||
import { DkimVerifier } from "./dkim-verifier";
|
||||
import type { Parsed, SignatureType } from "./dkim-verifier";
|
||||
import { DoH, DoHServer } from './DoH';
|
||||
|
||||
const IS_BROWSER = typeof window !== "undefined";
|
||||
|
||||
@@ -247,16 +248,17 @@ export const formatSignatureHeaderLine = (
|
||||
};
|
||||
|
||||
async function resolveDNSHTTP(name: string, type: string) {
|
||||
const resp = await fetch(
|
||||
"https://dns.google/resolve?" +
|
||||
new URLSearchParams({
|
||||
name: name,
|
||||
type: type,
|
||||
})
|
||||
);
|
||||
const out = await resp.json();
|
||||
// For some DNS, the Answer response here contains more than 1 element in the array. The last element is the one containing the public key
|
||||
return [out.Answer[out.Answer.length - 1].data];
|
||||
if (type !== "TXT") {
|
||||
throw new Error("DKIM record type is not TXT");
|
||||
}
|
||||
const DKIMRecord = await DoH.resolveDKIMPublicKey(name, DoHServer.Google);
|
||||
if (!DKIMRecord) {
|
||||
throw new CustomError("No DKIM record found", "ENODATA");
|
||||
}
|
||||
if (DKIMRecord !== await DoH.resolveDKIMPublicKey(name, DoHServer.Cloudflare)) {
|
||||
console.error("DKIM record mismatch!");
|
||||
}
|
||||
return [DKIMRecord];
|
||||
}
|
||||
|
||||
// from https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String
|
||||
@@ -301,9 +303,7 @@ export const getPublicKey = async (
|
||||
resolver: (...args: [name: string, type: string]) => Promise<any>
|
||||
) => {
|
||||
minBitLength = minBitLength || 1024;
|
||||
if (!IS_BROWSER) {
|
||||
resolver = resolver || require("dns").promises.resolve;
|
||||
} else {
|
||||
if (!resolver) {
|
||||
resolver = resolveDNSHTTP;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user