Files
certd/packages/core/acme-client/src/verify.js
T

182 lines
6.2 KiB
JavaScript
Raw Normal View History

/**
* ACME challenge verification
*/
2024-11-12 12:15:06 +08:00
import dnsSdk from "dns"
import https from 'https'
import {log} from './logger.js'
import axios from './axios.js'
import * as util from './util.js'
import {isAlpnCertificateAuthorizationValid} from './crypto/index.js'
2024-11-12 12:15:06 +08:00
const dns = dnsSdk.promises
/**
* Verify ACME HTTP challenge
*
* https://datatracker.ietf.org/doc/html/rfc8555#section-8.3
*
* @param {object} authz Identifier authorization
* @param {object} challenge Authorization challenge
* @param {string} keyAuthorization Challenge key authorization
* @param {string} [suffix] URL suffix
* @returns {Promise<boolean>}
*/
async function verifyHttpChallenge(authz, challenge, keyAuthorization, suffix = `/.well-known/acme-challenge/${challenge.token}`) {
const httpPort = axios.defaults.acmeSettings.httpChallengePort || 80;
const challengeUrl = `http://${authz.identifier.value}:${httpPort}${suffix}`;
/* May redirect to HTTPS with invalid/self-signed cert - https://letsencrypt.org/docs/challenge-types/#http-01-challenge */
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
log(`Sending HTTP query to ${authz.identifier.value}, suffix: ${suffix}, port: ${httpPort}`);
const resp = await axios.get(challengeUrl, { httpsAgent });
const data = (resp.data || '').replace(/\s+$/, '');
log(`Query successful, HTTP status code: ${resp.status}`);
if (!data || (data !== keyAuthorization)) {
throw new Error(`Authorization not found in HTTP response from ${authz.identifier.value}`);
}
log(`Key authorization match for ${challenge.type}/${authz.identifier.value}, ACME challenge verified`);
return true;
}
/**
* Walk DNS until TXT records are found
*/
async function walkDnsChallengeRecord(recordName, resolver = dns) {
/* Resolve CNAME record first */
2024-07-03 23:36:06 +08:00
// try {
// log(`Checking name for CNAME records: ${recordName}`);
// const cnameRecords = await resolver.resolveCname(recordName);
//
// if (cnameRecords.length) {
// log(`CNAME record found at ${recordName}, new challenge record name: ${cnameRecords[0]}`);
// return walkDnsChallengeRecord(cnameRecords[0]);
// }
// }
// catch (e) {
// log(`No CNAME records found for name: ${recordName}`);
// }
/* Resolve TXT records */
try {
log(`Checking name for TXT records: ${recordName}`);
const txtRecords = await resolver.resolveTxt(recordName);
2024-10-10 02:15:05 +08:00
if (txtRecords && txtRecords.length) {
log(`Found ${txtRecords.length} TXT records at ${recordName}`);
log(`TXT records: ${JSON.stringify(txtRecords)}`);
return [].concat(...txtRecords);
}
2024-10-10 02:15:05 +08:00
return [];
}
catch (e) {
2024-10-10 02:15:05 +08:00
log(`Resolve TXT records error, ${recordName} :${e.message}`);
throw e;
}
2024-10-10 02:15:05 +08:00
}
2024-11-12 12:15:06 +08:00
export async function walkTxtRecord(recordName) {
2025-03-25 11:08:25 +08:00
const txtRecords = []
2024-10-10 02:15:05 +08:00
try {
/* Default DNS resolver first */
2025-03-25 11:08:25 +08:00
log('从本地DNS服务器获取TXT解析记录');
2024-10-10 02:15:05 +08:00
const res = await walkDnsChallengeRecord(recordName);
if (res && res.length > 0) {
2025-03-25 11:08:25 +08:00
for (const item of res) {
txtRecords.push(item)
}
2024-10-10 02:15:05 +08:00
}
2025-03-25 11:08:25 +08:00
} catch (e) {
log(`本地获取TXT解析记录失败:${e.message}`)
2024-10-10 02:15:05 +08:00
}
2025-03-25 11:08:25 +08:00
try{
2024-10-10 02:15:05 +08:00
/* Authoritative DNS resolver */
2025-03-25 11:08:25 +08:00
log(`从域名权威服务器获取TXT解析记录`);
2024-10-10 02:15:05 +08:00
const authoritativeResolver = await util.getAuthoritativeDnsResolver(recordName);
2025-03-25 11:08:25 +08:00
const res = await walkDnsChallengeRecord(recordName, authoritativeResolver);
if (res && res.length > 0) {
for (const item of res) {
txtRecords.push(item)
}
}
}catch (e) {
log(`权威服务器获取TXT解析记录失败:${e.message}`)
}
if (txtRecords.length === 0) {
throw new Error(`没有找到TXT解析记录(${recordName}`);
2024-10-10 02:15:05 +08:00
}
2025-03-25 11:08:25 +08:00
return txtRecords;
}
/**
* Verify ACME DNS challenge
*
* https://datatracker.ietf.org/doc/html/rfc8555#section-8.4
*
* @param {object} authz Identifier authorization
* @param {object} challenge Authorization challenge
* @param {string} keyAuthorization Challenge key authorization
* @param {string} [prefix] DNS prefix
* @returns {Promise<boolean>}
*/
async function verifyDnsChallenge(authz, challenge, keyAuthorization, prefix = '_acme-challenge.') {
const recordName = `${prefix}${authz.identifier.value}`;
2025-03-24 00:05:19 +08:00
log(`Resolving DNS TXT from record(解析DNS TXT记录): ${recordName}`);
2024-10-10 02:15:05 +08:00
const recordValues = await walkTxtRecord(recordName);
2025-03-24 00:05:19 +08:00
log(`DNS query finished successfullyDNS查询成功), found ${recordValues.length} TXT records`);
if (!recordValues.length || !recordValues.includes(keyAuthorization)) {
2025-03-24 00:05:19 +08:00
throw new Error(`Authorization not found in DNS TXT record(没有找到需要的DNS TXT记录): ${recordName}need:${keyAuthorization},found:${recordValues}`);
}
2025-03-24 00:05:19 +08:00
log(`Key authorization match for ${challenge.type}/${recordName}, ACME challenge verified(域名所有权校验成功)`);
return true;
}
/**
* Verify ACME TLS ALPN challenge
*
* https://datatracker.ietf.org/doc/html/rfc8737
*
* @param {object} authz Identifier authorization
* @param {object} challenge Authorization challenge
* @param {string} keyAuthorization Challenge key authorization
* @returns {Promise<boolean>}
*/
async function verifyTlsAlpnChallenge(authz, challenge, keyAuthorization) {
const tlsAlpnPort = axios.defaults.acmeSettings.tlsAlpnChallengePort || 443;
const host = authz.identifier.value;
log(`Establishing TLS connection with host: ${host}:${tlsAlpnPort}`);
const certificate = await util.retrieveTlsAlpnCertificate(host, tlsAlpnPort);
log('Certificate received from server successfully, matching key authorization in ALPN');
if (!isAlpnCertificateAuthorizationValid(certificate, keyAuthorization)) {
throw new Error(`Authorization not found in certificate from ${authz.identifier.value}`);
}
log(`Key authorization match for ${challenge.type}/${authz.identifier.value}, ACME challenge verified`);
return true;
}
/**
* Export API
*/
2024-11-12 12:15:06 +08:00
export default {
'http-01': verifyHttpChallenge,
'dns-01': verifyDnsChallenge,
'tls-alpn-01': verifyTlsAlpnChallenge,
};