Skip to content

Commit d8aa890

Browse files
refactor(DKIM_BUILDER): improve input validation and error handling (#3812)
1 parent 2a4e250 commit d8aa890

File tree

2 files changed

+233
-73
lines changed

2 files changed

+233
-73
lines changed
Lines changed: 48 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,33 @@
11
---
22
name: DKIM_BUILDER
33
parameters:
4-
- label
54
- selector
65
- pubkey
7-
- flags
6+
- label
7+
- version
88
- hashtypes
99
- keytype
10-
- servicetypes
1110
- note
11+
- servicetypes
12+
- flags
1213
- ttl
1314
parameters_object: true
1415
parameter_types:
15-
label: string?
1616
selector: string
17-
pubkey: string
18-
flags: string[]?
19-
hashtypes: string[]?
17+
pubkey: string?
18+
label: string?
19+
version: string?
20+
hashtypes: string|string[]?
2021
keytype: string?
21-
servicetypes: string[]?
2222
note: string?
23+
servicetypes: string|string[]?
24+
flags: string|string[]?
2325
ttl: Duration?
2426
---
2527

26-
DNSControl contains a `DKIM_BUILDER` which can be used to simply create
27-
DKIM policies for your domains.
28-
28+
DNSControl contains a `DKIM_BUILDER` helper function that generates DKIM DNS TXT records according to RFC 6376 (DomainKeys Identified Mail) and its updates.
2929

30-
## Example
30+
## Examples
3131

3232
### Simple example
3333

@@ -54,13 +54,15 @@ s1._domainkey IN TXT "v=DKIM1; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDC5/z4
5454
```javascript
5555
D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
5656
DKIM_BUILDER({
57-
label: "alerts",
5857
selector: "k2",
5958
pubkey: "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDC5/z4L",
60-
flags: ['y'],
61-
hashtypes: ['sha256'],
62-
keytype: 'rsa',
59+
label: "subdomain",
60+
version: "DKIM1",
61+
hashtypes: ['sha1', 'sha256'],
62+
keytype: "rsa",
63+
note: "some human-readable notes",
6364
servicetypes: ['email'],
65+
flags: ['y', 's'],
6466
ttl: 150
6567
}),
6668
);
@@ -70,23 +72,35 @@ D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
7072
This yields the following record:
7173

7274
```text
73-
74-
k2._domainkey.alerts IN TXT "v=DKIM1; k=rsa; s=email; t=y; h=sha256; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDC5/z4L" ttl=150
75-
75+
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
7676
```
7777

78-
### Parameters
79-
80-
* `label:` The DNS label for the DKIM record (`[selector]._domainkey` prefix is added; default: `'@'`)
81-
* `selector:` Selector used for the label. e.g. `s1` or `mail`
82-
* `pubkey:` Public key `p` to be used for DKIM.
83-
* `keytype:` Key type `k`. Defaults to `'rsa'` if omitted (optional)
84-
* `flags:` Which types `t` of flags to activate, ie. 'y' and/or 's'. Array, defaults to 's' (optional)
85-
* `hashtypes:` Acceptable hash algorithms `h` (optional)
86-
* `servicetypes:` Record-applicable service types (optional)
87-
* `note:` Note field `n` for admins. Avoid if possible to keep record length short. (optional)
88-
* `ttl:` Input for `TTL` method (optional)
89-
90-
### Caveats
91-
92-
* DKIM (TXT) records are automatically split using `AUTOSPLIT`.
78+
## Parameters
79+
80+
* `selector` (string, required): The selector subdividing the namespace for the domain.
81+
* `pubkey` (string, optional): The base64-encoded public key (RSA or Ed25519). Default: empty (key revocation or non-sending domain).
82+
* `label` (string, optional): The DNS label for the DKIM record. Default: `@`.
83+
* `version` (string, optional): DKIM version. Maps to the `v=` tag. Default: `DKIM1` (currently the only supported value).
84+
* `hashtypes` (array, optional): Acceptable hash algorithms for signing. Maps to the `h=` tag.
85+
* Supported values for RSA key:
86+
* `sha1`
87+
* `sha256`
88+
* Supported values for Ed25519 key:
89+
* `sha256`
90+
* `keytype` (string, optional): Key algorithm type. Maps to the `k=` tag. Default: `rsa`. Supported values:
91+
* `rsa`
92+
* `ed25519`
93+
* `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.
94+
* `servicetypes` (array, optional): Service types using this key. Maps to the `s=` tag. Supported values:
95+
* `*`: explicity allows all service types
96+
* `email`: restricts key to email service only
97+
* `flags` (array, optional): Flags to modify the interpretation of the selector. Maps to the `t=` tag. Supported values:
98+
* `y`: Testing mode.
99+
* `s`: Subdomain restriction.
100+
* `ttl` (number, optional): DNS TTL value in seconds
101+
102+
## Related RFCs
103+
104+
* RFC 6376: DomainKeys Identified Mail (DKIM) Signatures
105+
* RFC 8301: Cryptographic Algorithm and Key Usage Update to DKIM
106+
* RFC 8463: A New Cryptographic Signature Method for DKIM (Ed25519)

pkg/js/helpers.js

Lines changed: 185 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1832,69 +1832,215 @@ function CAA_BUILDER(value) {
18321832
return r;
18331833
}
18341834

1835-
// DKIM_BUILDER takes an object:
1836-
// label: The DNS label for the DKIM record ([selector]._domainkey prefix is added; default: '@')
1837-
// selector: Selector used for the label. e.g. s1 or mail
1838-
// pubkey: Public key (p) to be used for DKIM (optional)
1839-
// keytype: Key type (k). Defaults to 'rsa' if missing (optional)
1840-
// flags: Which types (t) of flags to activate, ie. 'y' and/or 's'. Array, defaults to 's' (optional)
1841-
// hashtypes: Acceptable hash algorithma (h) (optional)
1842-
// servicetypes: Record-applicable service types (optional)
1843-
// note: Note field fo admins. Avoid if possible to keep record length short. (optional)
1844-
// ttl: The time for TTL, integer or string. (default: not defined, using DefaultTTL)
1835+
/**
1836+
* Encodes a string into DKIM-specific quoted-printable format.
1837+
*
1838+
* This function converts characters that are outside the range of printable ASCII
1839+
* characters, semicolons, DEL, or above ASCII 127 into their quoted-printable
1840+
* hex representation, prefixed by '='. This encoding is used in DKIM signatures
1841+
* to handle characters safely.
1842+
*
1843+
* @param {string} str - The input string to encode.
1844+
* @returns {string} The DKIM quoted-printable encoded string.
1845+
*/
1846+
function _encodeDKIMQuotedPrintable(str) {
1847+
var hexChars = '0123456789ABCDEF'.split('');
1848+
var result = '';
1849+
1850+
for (var i = 0; i < str.length; i++) {
1851+
var charCode = str.charCodeAt(i);
1852+
if (
1853+
charCode < 0x21 ||
1854+
charCode === 0x3b ||
1855+
charCode === 0x3d ||
1856+
charCode === 0x7f ||
1857+
charCode > 0x7f
1858+
) {
1859+
result +=
1860+
'=' + hexChars[(charCode >>> 4) & 15] + hexChars[charCode & 15];
1861+
} else {
1862+
result += str.charAt(i);
1863+
}
1864+
}
1865+
return result;
1866+
}
1867+
1868+
/**
1869+
* Builds a DKIM DNS TXT record according to RFC 6376 its updates
1870+
* @param {Object} value - Configuration object for the DKIM record.
1871+
* @param {string} value.selector - The selector subdividing the namespace for the domain. **(Required)**
1872+
* @param {string} [value.pubkey] - The base64-encoded public key (RSA or Ed25519).
1873+
* May be empty for key revocation or non-sending domains.
1874+
* @param {string} [value.label='@'] - The DNS label for the DKIM record (`[selector]._domainkey` prefix is added).
1875+
* @param {string} [value.version='DKIM1'] - The DKIM version (`v=` tag). Currently, only `"DKIM1"` is supported.
1876+
* @param {string|string[]} [value.hashtypes] - Acceptable hash algorithms for signing (`h=` tag).
1877+
* - Supported values for RSA: `'sha1'`, `'sha256'`
1878+
* - Supported values for Ed25519: `'sha256'`
1879+
* @param {string} [value.keytype='rsa'] - Key algorithm type (`k=` tag).
1880+
* - Supported values: `'rsa'`, `'ed25519'`
1881+
* @param {string|string[]} [value.servicetypes] - Service types using this key (`s=` tag).
1882+
* - Supported values: `'*'`, `'email'`
1883+
* - `'*'` allows all services; `'email'` restricts usage to email only.
1884+
* @param {string|string[]} [value.flags] - Flags modifying selector interpretation (`t=` tag).
1885+
* - Supported values: `'y'` (testing mode), `'s'` (subdomain restriction)
1886+
* @param {string} [value.note] - Human-readable note for the record (`n=` tag).
1887+
* @param {number} [value.ttl] - DNS TTL value in seconds.
1888+
*
1889+
* @throws {Error} If a required field is missing or a value is invalid.
1890+
* @returns {Object} DNS TXT record entries for DKIM
1891+
*/
18451892

18461893
function DKIM_BUILDER(value) {
1847-
if (!value) {
1848-
value = {};
1894+
value = value || {};
1895+
1896+
// ========================================
1897+
// PHASE 1: NORMALIZATION
1898+
// ========================================
1899+
1900+
// Apply defaults using _.defaults()
1901+
value = _.defaults(value, {
1902+
version: 'DKIM1',
1903+
pubkey: '',
1904+
label: '@',
1905+
});
1906+
1907+
// Normalize string|array fields to always be arrays
1908+
if (!_.isEmpty(value.hashtypes)) {
1909+
value.hashtypes = _.isString(value.hashtypes)
1910+
? [value.hashtypes]
1911+
: value.hashtypes;
18491912
}
1850-
kvs = [];
18511913

1852-
if (!value.selector) {
1914+
if (!_.isEmpty(value.servicetypes)) {
1915+
value.servicetypes = _.isString(value.servicetypes)
1916+
? [value.servicetypes]
1917+
: value.servicetypes;
1918+
}
1919+
1920+
if (!_.isEmpty(value.flags)) {
1921+
value.flags = _.isString(value.flags) ? [value.flags] : value.flags;
1922+
}
1923+
1924+
// ========================================
1925+
// PHASE 2: VALIDATION (Fail Fast)
1926+
// ========================================
1927+
1928+
// Static allowed values
1929+
var ALLOWED_VERSIONS = ['DKIM1'];
1930+
var ALLOWED_KEYTYPES = ['rsa', 'ed25519'];
1931+
var ALLOWED_HASHTYPES = {
1932+
rsa: ['sha1', 'sha256'],
1933+
ed25519: ['sha256'],
1934+
};
1935+
var ALLOWED_SERVICETYPES = ['*', 'email'];
1936+
var ALLOWED_FLAGS = ['y', 's'];
1937+
1938+
// Required fields
1939+
if (_.isEmpty(value.selector)) {
18531940
throw 'DKIM_BUILDER selector cannot be empty';
18541941
}
18551942

1856-
// build the label
1857-
if (!value.label) {
1858-
value.label = '@';
1943+
// Version validation
1944+
if (!_.contains(ALLOWED_VERSIONS, value.version)) {
1945+
throw (
1946+
'DKIM_BUILDER version must be one of: ' +
1947+
ALLOWED_VERSIONS.join(', ')
1948+
);
18591949
}
18601950

1861-
if (value.label !== '@') {
1862-
value.label = value.selector + '._domainkey' + '.' + value.label;
1863-
} else {
1864-
value.label = value.selector + '._domainkey';
1951+
// Keytype validation
1952+
if (
1953+
!_.isEmpty(value.keytype) &&
1954+
!_.contains(ALLOWED_KEYTYPES, value.keytype)
1955+
) {
1956+
throw (
1957+
'DKIM_BUILDER keytype must be one of: ' +
1958+
ALLOWED_KEYTYPES.join(', ') +
1959+
', ' +
1960+
value.keytype +
1961+
' given'
1962+
);
18651963
}
18661964

1867-
kvs.push('v=DKIM1');
1868-
if (value.keytype) {
1869-
kvs.push('k=' + value.keytype);
1965+
// Hashtypes validation (now always an array after normalization)
1966+
if (!_.isEmpty(value.hashtypes)) {
1967+
var allowedHashtypes = ALLOWED_HASHTYPES[value.keytype || 'rsa'];
1968+
var invalidHashtypes = _.difference(value.hashtypes, allowedHashtypes);
1969+
if (invalidHashtypes.length > 0) {
1970+
throw (
1971+
'DKIM_BUILDER hashtypes for ' +
1972+
value.keytype +
1973+
' must be one of: ' +
1974+
allowedHashtypes.join(', ')
1975+
);
1976+
}
18701977
}
18711978

1872-
if (value.servicetypes) {
1873-
kvs.push('s=' + value.servicetypes);
1979+
// Servicetypes validation (now always an array after normalization)
1980+
if (!_.isEmpty(value.servicetypes)) {
1981+
var invalidServicetypes = _.difference(
1982+
value.servicetypes,
1983+
ALLOWED_SERVICETYPES
1984+
);
1985+
if (invalidServicetypes.length > 0) {
1986+
throw (
1987+
'DKIM_BUILDER servicetypes must be one of: ' +
1988+
ALLOWED_SERVICETYPES.join(', ')
1989+
);
1990+
}
1991+
}
1992+
1993+
// Flags validation (now always an array after normalization)
1994+
if (!_.isEmpty(value.flags)) {
1995+
var invalidFlags = _.difference(value.flags, ALLOWED_FLAGS);
1996+
if (invalidFlags.length > 0) {
1997+
throw (
1998+
'DKIM_BUILDER flags must be one of: ' + ALLOWED_FLAGS.join(', ')
1999+
);
2000+
}
18742001
}
18752002

1876-
if (value.flags && value.flags.length > 0) {
1877-
kvs.push('t=' + value.flags.join(':'));
2003+
// ========================================
2004+
// PHASE 3: BUILD OUTPUT
2005+
// ========================================
2006+
2007+
// Build record RFC 6376 order: v=, h=, k=, n=, p=, s=, t=
2008+
var record = [];
2009+
2010+
record.push('v=' + value.version);
2011+
2012+
if (value.hashtypes) {
2013+
record.push('h=' + value.hashtypes.join(':'));
18782014
}
18792015

1880-
if (value.hashtypes && value.hashtypes.length > 0) {
1881-
kvs.push('h=' + value.hashtypes.join(':'));
2016+
if (value.keytype) {
2017+
record.push('k=' + value.keytype);
18822018
}
18832019

1884-
if (value.note) {
1885-
kvs.push('n=' + value.note);
2020+
if (!_.isEmpty(value.note)) {
2021+
record.push('n=' + _encodeDKIMQuotedPrintable(value.note));
18862022
}
18872023

1888-
kvs.push('p=' + value.pubkey);
2024+
record.push('p=' + value.pubkey);
18892025

1890-
var DKIM_TTL = function () {};
1891-
if (value.ttl) {
1892-
DKIM_TTL = TTL(value.ttl);
2026+
if (value.servicetypes) {
2027+
record.push('s=' + value.servicetypes.join(':'));
18932028
}
18942029

1895-
r = []; // The list of records to return.
1896-
r.push(TXT(value.label, kvs.join('\; '), DKIM_TTL));
1897-
return r;
2030+
if (value.flags) {
2031+
record.push('t=' + value.flags.join(':'));
2032+
}
2033+
2034+
// Build label
2035+
var fullLabel = value.selector + '._domainkey';
2036+
if (value.label !== '@') {
2037+
fullLabel += '.' + value.label;
2038+
}
2039+
2040+
// Handle TTL
2041+
var DKIM_TTL = value.ttl ? TTL(value.ttl) : function () {};
2042+
2043+
return TXT(fullLabel, record.join('; '), DKIM_TTL);
18982044
}
18992045

19002046
// DMARC_BUILDER takes an object:

0 commit comments

Comments
 (0)