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

219 lines
7.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* ACME challenge verification
*/
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'
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}`) {
async function doQuery(challengeUrl){
log(`正在测试请求 ${challengeUrl} `)
// const httpsPort = axios.defaults.acmeSettings.httpsChallengePort || 443;
// const challengeUrl = `https://${authz.identifier.value}:${httpsPort}${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}`);
let data = ""
try{
const resp = await axios.get(challengeUrl, { httpsAgent });
data = (resp.data || '').replace(/\s+$/, '');
}catch (e) {
log(`[error] HTTP request error from ${authz.identifier.value}`,e.message);
return false
}
if (!data || (data !== keyAuthorization)) {
log(`[error] Authorization not found in HTTP response from ${authz.identifier.value}`);
return false
}
return true
}
const httpPort = axios.defaults.acmeSettings.httpChallengePort || 80;
const challengeUrl = `http://${authz.identifier.value}:${httpPort}${suffix}`;
if (!await doQuery(challengeUrl)) {
const httpsPort = axios.defaults.acmeSettings.httpsChallengePort || 443;
const httpsChallengeUrl = `https://${authz.identifier.value}:${httpsPort}${suffix}`;
const res = await doQuery(httpsChallengeUrl)
if (!res) {
throw new Error(`[error] 验证失败请检查以上测试url是否可以正常访问`);
}
}
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,deep = 0) {
let records = [];
/* Resolve TXT records */
try {
log(`检查域名 ${recordName} 的TXT记录`);
const txtRecords = await resolver.resolveTxt(recordName);
if (txtRecords && txtRecords.length) {
log(`找到 ${txtRecords.length} 条 TXT记录 ${recordName}`);
log(`TXT records: ${JSON.stringify(txtRecords)}`);
records = records.concat(...txtRecords);
}
} catch (e) {
log(`解析 TXT 记录出错, ${recordName} :${e.message}`);
}
/* Resolve CNAME record first */
try {
log(`检查是否存在CNAME映射: ${recordName}`);
const cnameRecords = await resolver.resolveCname(recordName);
if (cnameRecords.length) {
const cnameRecord = cnameRecords[0];
log(`已找到${recordName}的CNAME记录将检查: ${cnameRecord}`);
let res= await walkTxtRecord(cnameRecord,deep+1);
if (res && res.length) {
log(`从CNAME中找到TXT记录: ${JSON.stringify(res)}`);
records = records.concat(...res);
}
}else{
log(`没有CNAME映射${recordName}`);
}
} catch (e) {
log(`检查CNAME出错${recordName} :${e.message}`);
}
return records
}
export async function walkTxtRecord(recordName,deep = 0) {
if(deep >5){
log(`walkTxtRecord too deep (#${deep}) , skip walk`)
return []
}
const txtRecords = []
try {
/* Default DNS resolver first */
log('从本地DNS服务器获取TXT解析记录');
const res = await walkDnsChallengeRecord(recordName,dns,deep);
if (res && res.length > 0) {
for (const item of res) {
txtRecords.push(item)
}
}
} catch (e) {
log(`本地获取TXT解析记录失败:${e.message}`)
}
try{
/* Authoritative DNS resolver */
log(`从域名权威服务器获取TXT解析记录`);
const authoritativeResolver = await util.getAuthoritativeDnsResolver(recordName);
const res = await walkDnsChallengeRecord(recordName, authoritativeResolver,deep);
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}`);
}
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}`;
log(`本地校验TXT记录: ${recordName}`);
let recordValues = await walkTxtRecord(recordName);
//去重
recordValues = [...new Set(recordValues)];
log(`DNS查询成功, 找到 ${recordValues.length} 条TXT记录${recordValues}`);
if (!recordValues.length || !recordValues.includes(keyAuthorization)) {
throw new Error(`没有找到需要的DNS TXT记录: ${recordName},期望:${keyAuthorization},结果:${recordValues}`);
}
log(`关键授权匹配成功(${challenge.type}/${recordName}:${keyAuthorization},校验成功, 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
*/
export default {
'http-01': verifyHttpChallenge,
'dns-01': verifyDnsChallenge,
'tls-alpn-01': verifyTlsAlpnChallenge,
};