Testing backend of allowing wildcard certs with DNS challenge automated

Remove default email of mine from letsencrypt
Bugfix invalid domain input not returning leads to double header send
develop
Thomas Lynch 10 months ago
parent e075aa327b
commit 29443e2fbd
  1. 19
      acme.js
  2. 13
      ca.js
  3. 19
      components/MapRow.js
  4. 51
      controllers/certs.js
  5. 2
      controllers/domains.js
  6. 8
      util.js

@ -3,6 +3,7 @@
const fs = require('fs').promises;
const acme = require('acme-client');
const dev = process.env.NODE_ENV !== 'production';
const redis = require('./redis.js');
/**
* Function used to satisfy an ACME challenge
@ -29,9 +30,14 @@ async function challengeCreateFn(authz, challenge, keyAuthorization) {
const dnsRecord = `_acme-challenge.${authz.identifier.value}`;
const recordValue = keyAuthorization;
console.log(`Creating TXT record for ${authz.identifier.value}: ${dnsRecord}`);
/* Replace this */
console.log(`Would create TXT record "${dnsRecord}" with value "${recordValue}"`);
// await dnsProvider.createRecord(dnsRecord, 'TXT', recordValue);
const record = { ttl: 300, text: recordValue, l: true, t: true };
let recordSetRaw = await redis.hget(`dns:${authz.identifier.value}.`, '_acme-challenge');
if (!recordSetRaw) {
recordSetRaw = {};
}
recordSetRaw['txt'] = [record];
await redis.hset(`dns:${authz.identifier.value}.`, '_acme-challenge', recordSetRaw);
console.log(`Created TXT record "${dnsRecord}" with value "${recordValue}"`);
}
}
@ -60,9 +66,8 @@ async function challengeRemoveFn(authz, challenge, keyAuthorization) {
const dnsRecord = `_acme-challenge.${authz.identifier.value}`;
const recordValue = keyAuthorization;
console.log(`Removing TXT record for ${authz.identifier.value}: ${dnsRecord}`);
/* Replace this */
console.log(`Would remove TXT record "${dnsRecord}" with value "${recordValue}"`);
// await dnsProvider.removeRecord(dnsRecord, 'TXT');
await redis.hdel(`dns:${authz.identifier.value}.`, '_acme-challenge');
console.log(`Removed TXT record "${dnsRecord}" with value "${recordValue}"`);
}
}
@ -80,7 +85,7 @@ module.exports = {
});
},
generate: async function(domain, altnames, email='tom@69420.me') {
generate: async function(domain, altnames, email) {
/* Create CSR */
const [key, csr] = await acme.crypto.createCsr({
commonName: domain,

13
ca.js

@ -1,6 +1,7 @@
'use strict';
const { generateKeyPairSync } = require('crypto')
, { wildcardCheck } = require('./util.js')
, fs = require('fs')
, forge = require('node-forge')
, pki = forge.pki
@ -84,7 +85,13 @@ function generateCertificate(privateKey, publicKey) {
function verifyCSR(csrPem, allowedDomains, serialNumber) {
const csr = pki.certificationRequestFromPem(csrPem);
const subject = csr.subject.getField('CN').value;
if (!allowedDomains.includes(subject)) {
const isWildcard = subject.startsWith('*.');
if (isWildcard) {
const wildcardOk = wildcardCheck(subject, allowedDomains);
if (!wildcardOk) {
throw new Error('No permission for subject');
}
} else if (!allowedDomains.includes(subject)) {
throw new Error('No permission for subject');
}
const exts = csr.getAttribute({name: 'extensionRequest'});
@ -95,7 +102,9 @@ function verifyCSR(csrPem, allowedDomains, serialNumber) {
const badAltNames = altNamesExt.altNames.some(altName => {
return !allowedDomains.includes(altName.value);
});
if (badAltNames) {
if (isWildcard && !altNamesExt.altNames.every(altName => altName.value === subject)) {
throw new Error('No permission for altnames');
} else if (!isWildcard && badAltNames) {
throw new Error('No permission for altnames');
}
}

@ -15,11 +15,22 @@ export default function MapRow({ row, onDeleteSubmit, name, csrf, showValues, ma
{mapValueNames[value] || value}
</td>
)}
{typeof value === 'object' && columnKeys.map((ck, mvi) => (
<td key={`mvi_${mvi}`}>
{value[ck].toString()}
{typeof value === 'object' && columnKeys.map((ck, mvi) => {
let displayValue = mapValueNames[value[ck].toString()] || value[ck].toString();
if (typeof value[ck] === 'boolean') {
console.log(value[ck])
displayValue = value[ck] === true
? <span className="text-success">
<i className="bi-check-lg pe-none me-2" width="16" height="16" />
</span>
: <span className="text-secondary">
<i className="bi-dash-lg pe-none me-2" width="16" height="16" />
</span>;
}
return <td key={`mvi_${mvi}`}>
{displayValue}
</td>
))}
})}
</tr>
);

@ -1,7 +1,7 @@
const db = require('../db.js');
const acme = require('../acme.js');
const url = require('url');
const { dynamicResponse } = require('../util.js');
const { dynamicResponse, wildcardCheck } = require('../util.js');
const { verifyCSR } = require('../ca.js');
/**
@ -27,9 +27,12 @@ exports.certsPage = async (app, req, res) => {
.getAllStorageSSLCertificates()
.then(certs => {
return certs.data.filter(c => {
const approxSubject = c.storage_name
let approxSubject = c.storage_name
.replaceAll('_', '.')
.substr(0, c.storage_name.length-4);
if (approxSubject.startsWith('.')) {
approxSubject = approxSubject.substring(1);
}
return res.locals.user.domains.includes(approxSubject);
});
});
@ -63,9 +66,12 @@ exports.certsJson = async (req, res) => {
.getAllStorageSSLCertificates()
.then(certs => {
return certs.data.filter(c => {
const approxSubject = c.storage_name
let approxSubject = c.storage_name
.replaceAll('_', '.')
.substr(0, c.storage_name.length-4);
.substr(0, c.storage_name.length-4);
if (approxSubject.startsWith('.')) {
approxSubject = approxSubject.substring(1);
}
return res.locals.user.domains.includes(approxSubject);
});
});
@ -83,28 +89,39 @@ exports.certsJson = async (req, res) => {
*/
exports.addCert = async (req, res, next) => {
let wildcardOk = true;
if (req.body.subject.startsWith('*.')) {
wildcardOk = wildcardCheck(req.body.subject, res.locals.user.domains);
}
if (!req.body.subject || typeof req.body.subject !== 'string' || req.body.subject.length === 0
|| !res.locals.user.domains.includes(req.body.subject)) {
return dynamicResponse(req, res, 400, { error: 'Add the domain in the domains page before generating a certficate' });
|| (!res.locals.user.domains.includes(req.body.subject) && !wildcardOk)) {
return dynamicResponse(req, res, 400, { error: 'Add a matching domain in the domains page before generating a certficate' });
}
if (!req.body.altnames || typeof req.body.altnames !== 'object'
|| req.body.altnames.some(d => !res.locals.user.domains.includes(d))) {
|| (req.body.altnames.some(d => !res.locals.user.domains.includes(d)) && !wildcardOk)) {
return dynamicResponse(req, res, 400, { error: 'Add all the altnames on the domains page before generating a certificate' });
}
if (req.body.email && (typeof req.body.email !== 'string'
|| !/^\S+@\S+\.\S+$/.test(req.body.email))) {
return dynamicResponse(req, res, 400, { error: 'Invalid email' });
}
const subject = req.body.subject.toLowerCase();
const altnames = req.body.altnames.map(a => a.toLowerCase());
// const backendMap = await res.locals
// .dataPlane.showRuntimeMap({
// map: process.env.HOSTS_MAP_NAME
// })
// .then(res => res.data);
// const backendDomainEntry = backendMap && backendMap.find(e => e.key === req.body.subject);
// if (!backendDomainEntry) {
// return dynamicResponse(req, res, 400, { error: 'Add a backend for the domain first before generating a certificate' });
// }
if (!req.body.subject.startsWith('*.')) {
const backendMap = await res.locals
.dataPlane.showRuntimeMap({
map: process.env.HOSTS_MAP_NAME
})
.then(res => res.data);
const backendDomainEntry = backendMap && backendMap.find(e => e.key === req.body.subject);
if (!backendDomainEntry) {
return dynamicResponse(req, res, 400, { error: 'Add a backend for the domain first before generating a certificate' });
}
}
// const maintenanceMap = await res.locals
// .dataPlane.showRuntimeMap({
@ -132,7 +149,7 @@ exports.addCert = async (req, res, next) => {
try {
console.log('Add cert request:', subject, altnames);
const { csr, key, cert, haproxyCert, date } = await acme.generate(subject, altnames);
const { csr, key, cert, haproxyCert, date } = await acme.generate(subject, altnames, req.body.email);
const { message, description, file, storage_name: storageName } = await res.locals.postFileAll(
'/v2/services/haproxy/storage/ssl_certificates',
{

@ -79,7 +79,7 @@ exports.addDomain = async (req, res, next) => {
try {
const parsed = psl.parse(domain);
if (!parsed || !parsed.domain) {
dynamicResponse(req, res, 400, { error: 'Invalid input' })
return dynamicResponse(req, res, 400, { error: 'Invalid input' })
}
const domains = [domain, parsed.domain];
const existing = await db.db.collection('accounts')

@ -99,4 +99,12 @@ module.exports = {
return res.status(code).send(data);
},
wildcardCheck: (subject, allowedDomains) => {
if (subject.includes('\\')) { throw new Error('Illegal wildcardCheck'); }
const wcRegex = new RegExp(subject.replace(/\*/g, "[^ ]*\\")+'$');
return allowedDomains.some(d => {
return wcRegex.test(d);
});
}
};

Loading…
Cancel
Save