13// Record is a DKIM DNS record, served on <selector>._domainkey.<domain> for a
 
14// given selector and domain (s= and d= in the DKIM-Signature).
 
16// The record is a semicolon-separated list of "="-separated field value pairs.
 
17// Strings should be compared case-insensitively, e.g. k=ed25519 is equivalent to k=ED25519.
 
21//	v=DKIM1;h=sha256;k=ed25519;p=ln5zd/JEX4Jy60WAhUOv33IYm2YZMyTQAdr9stML504=
 
23	Version  string   // Version, fixed "DKIM1" (case sensitive). Field "v".
 
24	Hashes   []string // Acceptable hash algorithms, e.g. "sha1", "sha256". Optional, defaults to all algorithms. Field "h".
 
25	Key      string   // Key type, "rsa" or "ed25519". Optional, default "rsa". Field "k".
 
26	Notes    string   // Debug notes. Field "n".
 
27	Pubkey   []byte   // Public key, as base64 in record. If empty, the key has been revoked. Field "p".
 
28	Services []string // Service types. Optional, default "*" for all services. Other values: "email". Field "s".
 
29	Flags    []string // Flags, colon-separated. Optional, default is no flags. Other values: "y" for testing DKIM, "s" for "i=" must have same domain as "d" in signatures. Field "t".
 
31	PublicKey any `json:"-"` // Parsed form of public key, an *rsa.PublicKey or ed25519.PublicKey.
 
36// ServiceAllowed returns whether service s is allowed by this key.
 
38// The optional field "s" can specify purposes for which the key can be used. If
 
39// value was specified, both "*" and "email" are enough for use with DKIM.
 
40func (r *Record) ServiceAllowed(s string) bool {
 
41	if len(r.Services) == 0 {
 
44	for _, ss := range r.Services {
 
45		if ss == "*" || strings.EqualFold(s, ss) {
 
52// Record returns a DNS TXT record that should be served at
 
53// <selector>._domainkey.<domain>.
 
55// Only values that are not the default values are included.
 
56func (r *Record) Record() (string, error) {
 
58	add := func(s string) {
 
62	if r.Version != "DKIM1" {
 
63		return "", fmt.Errorf("bad version, must be \"DKIM1\"")
 
66	if len(r.Hashes) > 0 {
 
67		add("h=" + strings.Join(r.Hashes, ":"))
 
69	if r.Key != "" && !strings.EqualFold(r.Key, "rsa") {
 
73		add("n=" + qpSection(r.Notes))
 
75	if len(r.Services) > 0 && (len(r.Services) != 1 || r.Services[0] != "*") {
 
76		add("s=" + strings.Join(r.Services, ":"))
 
79		add("t=" + strings.Join(r.Flags, ":"))
 
83	if len(pk) == 0 && r.PublicKey != nil {
 
84		switch k := r.PublicKey.(type) {
 
87			pk, err = x509.MarshalPKIXPublicKey(k)
 
89				return "", fmt.Errorf("marshal rsa public key: %v", err)
 
91		case ed25519.PublicKey:
 
94			return "", fmt.Errorf("unknown public key type %T", r.PublicKey)
 
97	add("p=" + base64.StdEncoding.EncodeToString(pk))
 
98	return strings.Join(l, ";"), nil
 
101func qpSection(s string) string {
 
102	const hex = "0123456789ABCDEF"
 
106	for i, b := range []byte(s) {
 
107		if i > 0 && (b == ' ' || b == '\t') || b > ' ' && b < 0x7f && b != '=' {
 
110			r += "=" + string(hex[b>>4]) + string(hex[(b>>0)&0xf])
 
117	errRecordDuplicateTag     = errors.New("duplicate tag")
 
118	errRecordMissingField     = errors.New("missing field")
 
119	errRecordBadPublicKey     = errors.New("bad public key")
 
120	errRecordUnknownAlgorithm = errors.New("unknown algorithm")
 
121	errRecordVersionFirst     = errors.New("first field must be version")
 
124// ParseRecord parses a DKIM DNS TXT record.
 
126// If the record is a dkim record, but an error occurred, isdkim will be true and
 
127// err will be the error. Such errors must be treated differently from parse errors
 
128// where the record does not appear to be DKIM, which can happen with misconfigured
 
129// DNS (e.g. wildcard records).
 
130func ParseRecord(s string) (record *Record, isdkim bool, err error) {
 
136		if xerr, ok := x.(error); ok {
 
144	xerrorf := func(format string, args ...any) {
 
145		panic(fmt.Errorf(format, args...))
 
151		Services: []string{"*"},
 
154	p := parser{s: s, drop: true}
 
155	seen := map[string]struct{}{}
 
167		if _, ok := seen[k]; ok {
 
169			xerrorf("%w: %q", errRecordDuplicateTag, k)
 
173		// Version must be the first.
 
177			v := p.xtake("DKIM1")
 
178			// Version being set is a signal this appears to be a valid record. We must not
 
179			// treat e.g. DKIM1.1 as valid, so we explicitly check there is no more data before
 
180			// we decide this record is DKIM.
 
187				// If version is present, it must be the first.
 
188				xerrorf("%w", errRecordVersionFirst)
 
198			record.Hashes = []string{p.xhyphenatedWord()}
 
203				record.Hashes = append(record.Hashes, p.xhyphenatedWord())
 
207			record.Key = p.xhyphenatedWord()
 
210			record.Notes = p.xqpSection()
 
213			record.Pubkey = p.xbase64()
 
216			record.Services = []string{p.xhyphenatedWord()}
 
221				record.Services = append(record.Services, p.xhyphenatedWord())
 
225			record.Flags = []string{p.xhyphenatedWord()}
 
230				record.Flags = append(record.Flags, p.xhyphenatedWord())
 
234			for !p.empty() && !p.hasPrefix(";") {
 
250	if _, ok := seen["p"]; !ok {
 
251		xerrorf("%w: public key", errRecordMissingField)
 
254	switch strings.ToLower(record.Key) {
 
256		if len(record.Pubkey) == 0 {
 
257			// Revoked key, nothing to do.
 
258		} else if pk, err := x509.ParsePKIXPublicKey(record.Pubkey); err != nil {
 
259			xerrorf("%w: %s", errRecordBadPublicKey, err)
 
260		} else if _, ok := pk.(*rsa.PublicKey); !ok {
 
261			xerrorf("%w: got %T, need an RSA key", errRecordBadPublicKey, record.PublicKey)
 
263			record.PublicKey = pk
 
266		if len(record.Pubkey) == 0 {
 
267			// Revoked key, nothing to do.
 
268		} else if len(record.Pubkey) != ed25519.PublicKeySize {
 
269			xerrorf("%w: got %d bytes, need %d", errRecordBadPublicKey, len(record.Pubkey), ed25519.PublicKeySize)
 
271			record.PublicKey = ed25519.PublicKey(record.Pubkey)
 
274		xerrorf("%w: %q", errRecordUnknownAlgorithm, record.Key)
 
277	return record, true, nil