diff --git a/documentation/language-reference/domain-modifiers/DKIM_BUILDER.md b/documentation/language-reference/domain-modifiers/DKIM_BUILDER.md index ee93b37d75..f0e3cd340f 100644 --- a/documentation/language-reference/domain-modifiers/DKIM_BUILDER.md +++ b/documentation/language-reference/domain-modifiers/DKIM_BUILDER.md @@ -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 - -* `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) - -### Caveats - -* DKIM (TXT) records are automatically split using `AUTOSPLIT`. +## Parameters + +* `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 + +## Related RFCs + +* 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) diff --git a/pkg/js/helpers.js b/pkg/js/helpers.js index 48ff786b23..086b0bca90 100644 --- a/pkg/js/helpers.js +++ b/pkg/js/helpers.js @@ -1815,69 +1815,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 = {}; + value = value || {}; + + // ======================================== + // 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; } - kvs = []; - if (!value.selector) { + 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' + ); } - kvs.push('v=DKIM1'); - if (value.keytype) { - kvs.push('k=' + value.keytype); + // 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(', ') + ); + } } - if (value.servicetypes) { - kvs.push('s=' + value.servicetypes); + // 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(', ') + ); + } } - if (value.flags && value.flags.length > 0) { - kvs.push('t=' + value.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(':')); } - if (value.hashtypes && value.hashtypes.length > 0) { - kvs.push('h=' + value.hashtypes.join(':')); + if (value.keytype) { + record.push('k=' + value.keytype); } - if (value.note) { - kvs.push('n=' + value.note); + if (!_.isEmpty(value.note)) { + record.push('n=' + _encodeDKIMQuotedPrintable(value.note)); } - kvs.push('p=' + value.pubkey); + record.push('p=' + value.pubkey); - var DKIM_TTL = function () {}; - if (value.ttl) { - DKIM_TTL = TTL(value.ttl); + if (value.servicetypes) { + record.push('s=' + value.servicetypes.join(':')); } - r = []; // The list of records to return. - r.push(TXT(value.label, kvs.join('\; '), DKIM_TTL)); - return r; + if (value.flags) { + record.push('t=' + value.flags.join(':')); + } + + // Build label + var fullLabel = value.selector + '._domainkey'; + if (value.label !== '@') { + fullLabel += '.' + value.label; + } + + // Handle TTL + var DKIM_TTL = value.ttl ? TTL(value.ttl) : function () {}; + + return TXT(fullLabel, record.join('; '), DKIM_TTL); } // DMARC_BUILDER takes an object: