refactor(DKIM_BUILDER): improve input validation and error handling (#3812)

This commit is contained in:
Matteo Trubini 2025-11-03 17:33:09 +01:00 committed by GitHub
parent 2a4e2509bc
commit d8aa89028e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 233 additions and 73 deletions

View file

@ -1,33 +1,33 @@
---
name: DKIM_BUILDER
parameters:
- label
- selector
- pubkey
- flags
- label
- version
- hashtypes
- keytype
- servicetypes
- note
- servicetypes
- flags
- ttl
parameters_object: true
parameter_types:
label: string?
selector: string
pubkey: string
flags: string[]?
hashtypes: string[]?
pubkey: string?
label: string?
version: string?
hashtypes: string|string[]?
keytype: string?
servicetypes: string[]?
note: string?
servicetypes: string|string[]?
flags: string|string[]?
ttl: Duration?
---
DNSControl contains a `DKIM_BUILDER` which can be used to simply create
DKIM policies for your domains.
DNSControl contains a `DKIM_BUILDER` helper function that generates DKIM DNS TXT records according to RFC 6376 (DomainKeys Identified Mail) and its updates.
## Example
## Examples
### Simple example
@ -54,13 +54,15 @@ s1._domainkey IN TXT "v=DKIM1; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDC5/z4
```javascript
D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
DKIM_BUILDER({
label: "alerts",
selector: "k2",
pubkey: "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDC5/z4L",
flags: ['y'],
hashtypes: ['sha256'],
keytype: 'rsa',
label: "subdomain",
version: "DKIM1",
hashtypes: ['sha1', 'sha256'],
keytype: "rsa",
note: "some human-readable notes",
servicetypes: ['email'],
flags: ['y', 's'],
ttl: 150
}),
);
@ -70,23 +72,35 @@ D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
This yields the following record:
```text
k2._domainkey.alerts IN TXT "v=DKIM1; k=rsa; s=email; t=y; h=sha256; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDC5/z4L" ttl=150
k2._domainkey.subdomain IN TXT "v=DKIM1; h=sha1:sha256; k=rsa; n=some=20human-readable=20notes; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDC5/z4L; s=email; t=y:s" ttl=150
```
### Parameters
## Parameters
* `label:` The DNS label for the DKIM record (`[selector]._domainkey` prefix is added; default: `'@'`)
* `selector:` Selector used for the label. e.g. `s1` or `mail`
* `pubkey:` Public key `p` to be used for DKIM.
* `keytype:` Key type `k`. Defaults to `'rsa'` if omitted (optional)
* `flags:` Which types `t` of flags to activate, ie. 'y' and/or 's'. Array, defaults to 's' (optional)
* `hashtypes:` Acceptable hash algorithms `h` (optional)
* `servicetypes:` Record-applicable service types (optional)
* `note:` Note field `n` for admins. Avoid if possible to keep record length short. (optional)
* `ttl:` Input for `TTL` method (optional)
* `selector` (string, required): The selector subdividing the namespace for the domain.
* `pubkey` (string, optional): The base64-encoded public key (RSA or Ed25519). Default: empty (key revocation or non-sending domain).
* `label` (string, optional): The DNS label for the DKIM record. Default: `@`.
* `version` (string, optional): DKIM version. Maps to the `v=` tag. Default: `DKIM1` (currently the only supported value).
* `hashtypes` (array, optional): Acceptable hash algorithms for signing. Maps to the `h=` tag.
* Supported values for RSA key:
* `sha1`
* `sha256`
* Supported values for Ed25519 key:
* `sha256`
* `keytype` (string, optional): Key algorithm type. Maps to the `k=` tag. Default: `rsa`. Supported values:
* `rsa`
* `ed25519`
* `notes` (string, optional): Human-readable notes intended for administrators. Pass normal text here; DKIM-Quoted-Printable encoding will be applied automatically. Maps to the `n=` tag.
* `servicetypes` (array, optional): Service types using this key. Maps to the `s=` tag. Supported values:
* `*`: explicity allows all service types
* `email`: restricts key to email service only
* `flags` (array, optional): Flags to modify the interpretation of the selector. Maps to the `t=` tag. Supported values:
* `y`: Testing mode.
* `s`: Subdomain restriction.
* `ttl` (number, optional): DNS TTL value in seconds
### Caveats
## Related RFCs
* DKIM (TXT) records are automatically split using `AUTOSPLIT`.
* RFC 6376: DomainKeys Identified Mail (DKIM) Signatures
* RFC 8301: Cryptographic Algorithm and Key Usage Update to DKIM
* RFC 8463: A New Cryptographic Signature Method for DKIM (Ed25519)

View file

@ -1832,69 +1832,215 @@ function CAA_BUILDER(value) {
return r;
}
// DKIM_BUILDER takes an object:
// label: The DNS label for the DKIM record ([selector]._domainkey prefix is added; default: '@')
// selector: Selector used for the label. e.g. s1 or mail
// pubkey: Public key (p) to be used for DKIM (optional)
// keytype: Key type (k). Defaults to 'rsa' if missing (optional)
// flags: Which types (t) of flags to activate, ie. 'y' and/or 's'. Array, defaults to 's' (optional)
// hashtypes: Acceptable hash algorithma (h) (optional)
// servicetypes: Record-applicable service types (optional)
// note: Note field fo admins. Avoid if possible to keep record length short. (optional)
// ttl: The time for TTL, integer or string. (default: not defined, using DefaultTTL)
/**
* Encodes a string into DKIM-specific quoted-printable format.
*
* This function converts characters that are outside the range of printable ASCII
* characters, semicolons, DEL, or above ASCII 127 into their quoted-printable
* hex representation, prefixed by '='. This encoding is used in DKIM signatures
* to handle characters safely.
*
* @param {string} str - The input string to encode.
* @returns {string} The DKIM quoted-printable encoded string.
*/
function _encodeDKIMQuotedPrintable(str) {
var hexChars = '0123456789ABCDEF'.split('');
var result = '';
for (var i = 0; i < str.length; i++) {
var charCode = str.charCodeAt(i);
if (
charCode < 0x21 ||
charCode === 0x3b ||
charCode === 0x3d ||
charCode === 0x7f ||
charCode > 0x7f
) {
result +=
'=' + hexChars[(charCode >>> 4) & 15] + hexChars[charCode & 15];
} else {
result += str.charAt(i);
}
}
return result;
}
/**
* Builds a DKIM DNS TXT record according to RFC 6376 its updates
* @param {Object} value - Configuration object for the DKIM record.
* @param {string} value.selector - The selector subdividing the namespace for the domain. **(Required)**
* @param {string} [value.pubkey] - The base64-encoded public key (RSA or Ed25519).
* May be empty for key revocation or non-sending domains.
* @param {string} [value.label='@'] - The DNS label for the DKIM record (`[selector]._domainkey` prefix is added).
* @param {string} [value.version='DKIM1'] - The DKIM version (`v=` tag). Currently, only `"DKIM1"` is supported.
* @param {string|string[]} [value.hashtypes] - Acceptable hash algorithms for signing (`h=` tag).
* - Supported values for RSA: `'sha1'`, `'sha256'`
* - Supported values for Ed25519: `'sha256'`
* @param {string} [value.keytype='rsa'] - Key algorithm type (`k=` tag).
* - Supported values: `'rsa'`, `'ed25519'`
* @param {string|string[]} [value.servicetypes] - Service types using this key (`s=` tag).
* - Supported values: `'*'`, `'email'`
* - `'*'` allows all services; `'email'` restricts usage to email only.
* @param {string|string[]} [value.flags] - Flags modifying selector interpretation (`t=` tag).
* - Supported values: `'y'` (testing mode), `'s'` (subdomain restriction)
* @param {string} [value.note] - Human-readable note for the record (`n=` tag).
* @param {number} [value.ttl] - DNS TTL value in seconds.
*
* @throws {Error} If a required field is missing or a value is invalid.
* @returns {Object} DNS TXT record entries for DKIM
*/
function DKIM_BUILDER(value) {
if (!value) {
value = {};
}
kvs = [];
value = value || {};
if (!value.selector) {
// ========================================
// PHASE 1: NORMALIZATION
// ========================================
// Apply defaults using _.defaults()
value = _.defaults(value, {
version: 'DKIM1',
pubkey: '',
label: '@',
});
// Normalize string|array fields to always be arrays
if (!_.isEmpty(value.hashtypes)) {
value.hashtypes = _.isString(value.hashtypes)
? [value.hashtypes]
: value.hashtypes;
}
if (!_.isEmpty(value.servicetypes)) {
value.servicetypes = _.isString(value.servicetypes)
? [value.servicetypes]
: value.servicetypes;
}
if (!_.isEmpty(value.flags)) {
value.flags = _.isString(value.flags) ? [value.flags] : value.flags;
}
// ========================================
// PHASE 2: VALIDATION (Fail Fast)
// ========================================
// Static allowed values
var ALLOWED_VERSIONS = ['DKIM1'];
var ALLOWED_KEYTYPES = ['rsa', 'ed25519'];
var ALLOWED_HASHTYPES = {
rsa: ['sha1', 'sha256'],
ed25519: ['sha256'],
};
var ALLOWED_SERVICETYPES = ['*', 'email'];
var ALLOWED_FLAGS = ['y', 's'];
// Required fields
if (_.isEmpty(value.selector)) {
throw 'DKIM_BUILDER selector cannot be empty';
}
// build the label
if (!value.label) {
value.label = '@';
// Version validation
if (!_.contains(ALLOWED_VERSIONS, value.version)) {
throw (
'DKIM_BUILDER version must be one of: ' +
ALLOWED_VERSIONS.join(', ')
);
}
if (value.label !== '@') {
value.label = value.selector + '._domainkey' + '.' + value.label;
} else {
value.label = value.selector + '._domainkey';
// Keytype validation
if (
!_.isEmpty(value.keytype) &&
!_.contains(ALLOWED_KEYTYPES, value.keytype)
) {
throw (
'DKIM_BUILDER keytype must be one of: ' +
ALLOWED_KEYTYPES.join(', ') +
', ' +
value.keytype +
' given'
);
}
// Hashtypes validation (now always an array after normalization)
if (!_.isEmpty(value.hashtypes)) {
var allowedHashtypes = ALLOWED_HASHTYPES[value.keytype || 'rsa'];
var invalidHashtypes = _.difference(value.hashtypes, allowedHashtypes);
if (invalidHashtypes.length > 0) {
throw (
'DKIM_BUILDER hashtypes for ' +
value.keytype +
' must be one of: ' +
allowedHashtypes.join(', ')
);
}
}
// Servicetypes validation (now always an array after normalization)
if (!_.isEmpty(value.servicetypes)) {
var invalidServicetypes = _.difference(
value.servicetypes,
ALLOWED_SERVICETYPES
);
if (invalidServicetypes.length > 0) {
throw (
'DKIM_BUILDER servicetypes must be one of: ' +
ALLOWED_SERVICETYPES.join(', ')
);
}
}
// Flags validation (now always an array after normalization)
if (!_.isEmpty(value.flags)) {
var invalidFlags = _.difference(value.flags, ALLOWED_FLAGS);
if (invalidFlags.length > 0) {
throw (
'DKIM_BUILDER flags must be one of: ' + ALLOWED_FLAGS.join(', ')
);
}
}
// ========================================
// PHASE 3: BUILD OUTPUT
// ========================================
// Build record RFC 6376 order: v=, h=, k=, n=, p=, s=, t=
var record = [];
record.push('v=' + value.version);
if (value.hashtypes) {
record.push('h=' + value.hashtypes.join(':'));
}
kvs.push('v=DKIM1');
if (value.keytype) {
kvs.push('k=' + value.keytype);
record.push('k=' + value.keytype);
}
if (!_.isEmpty(value.note)) {
record.push('n=' + _encodeDKIMQuotedPrintable(value.note));
}
record.push('p=' + value.pubkey);
if (value.servicetypes) {
kvs.push('s=' + value.servicetypes);
record.push('s=' + value.servicetypes.join(':'));
}
if (value.flags && value.flags.length > 0) {
kvs.push('t=' + value.flags.join(':'));
if (value.flags) {
record.push('t=' + value.flags.join(':'));
}
if (value.hashtypes && value.hashtypes.length > 0) {
kvs.push('h=' + value.hashtypes.join(':'));
// Build label
var fullLabel = value.selector + '._domainkey';
if (value.label !== '@') {
fullLabel += '.' + value.label;
}
if (value.note) {
kvs.push('n=' + value.note);
}
// Handle TTL
var DKIM_TTL = value.ttl ? TTL(value.ttl) : function () {};
kvs.push('p=' + value.pubkey);
var DKIM_TTL = function () {};
if (value.ttl) {
DKIM_TTL = TTL(value.ttl);
}
r = []; // The list of records to return.
r.push(TXT(value.label, kvs.join('\; '), DKIM_TTL));
return r;
return TXT(fullLabel, record.join('; '), DKIM_TTL);
}
// DMARC_BUILDER takes an object: