Skip to content

Commit 1987780

Browse files
contrib: Add a helper script for generating keys
Co-authored-by: lquidfire <[email protected]>
1 parent a0d310d commit 1987780

File tree

10 files changed

+392
-1
lines changed

10 files changed

+392
-1
lines changed

.github/workflows/build.yml

+14
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,20 @@ jobs:
4646
- name: Install dependencies
4747
run: sudo apt install libbsd-dev libidn2-dev libjansson-dev libmilter-dev libssl-dev
4848

49+
- name: Set up Python
50+
uses: actions/setup-python@v5
51+
with:
52+
# 3.8 is listed last because it's the lowest version we support for
53+
# tests, so we want to use it as the default.
54+
python-version: |
55+
3.7
56+
3.9
57+
3.10
58+
3.11
59+
3.12
60+
3.13
61+
3.8
62+
4963
- name: Install Python dependencies
5064
run: sudo pip install pytest miltertest
5165

.ruff.toml

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
include = ["contrib/openarc-keygen"]
12
line-length = 160
23

34
[lint]

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.
66

77
### Added
88
- `oldest-pass` processing per [RFC 8617 section 5.2](https://datatracker.ietf.org/doc/html/rfc8617#section-5.2).
9+
- `openarc-keygen`
910
- libopenarc - `arc_chain_oldest_pass()`
1011
- milter - `AuthResIP` configuration option.
1112
- milter - `RequireSafeKeys` configuration option.

README.md

+5
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ you will also need:
6060
* [Automake](https://www.gnu.org/software/automake/) >= 1.11.1
6161
* [libtool](https://www.gnu.org/software/libtool/) >= 2.2.6
6262

63+
The core OpenARC software will function without it, but tools distributed
64+
alongside OpenARC (such as `openarc-keygen`) may require:
65+
66+
* Python >= 3.7
67+
6368
### DNF-based systems
6469

6570
```

configure.ac

+1
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,7 @@ AC_SUBST([SYSCONFDIR])
382382
AC_CONFIG_FILES([
383383
Makefile
384384
contrib/Makefile
385+
contrib/openarc-keygen.1
385386
contrib/init/Makefile
386387
contrib/init/generic/Makefile
387388
contrib/init/redhat/Makefile

contrib/Makefile.am

+3
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@
22
# All rights reserved.
33

44
SUBDIRS = init spec systemd
5+
6+
dist_bin_SCRIPTS = openarc-keygen
7+
man_MANS = openarc-keygen.1

contrib/openarc-keygen

+176
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
#!/usr/bin/env python3
2+
3+
# Copyright 2024 OpenARC contributors.
4+
# See LICENSE.
5+
6+
import argparse
7+
import os
8+
import subprocess
9+
import sys
10+
11+
12+
def main():
13+
parser = argparse.ArgumentParser()
14+
# fmt: off
15+
parser.add_argument(
16+
'-b', '--bits',
17+
type=int,
18+
default=2048,
19+
help='Size of RSA key to generate.',
20+
)
21+
parser.add_argument(
22+
'-d', '--domain',
23+
required=True,
24+
help='The domain which will use this key for signing.',
25+
)
26+
parser.add_argument(
27+
'-D', '--directory',
28+
help='Directory to store the keys in.',
29+
)
30+
parser.add_argument(
31+
'-f', '--format',
32+
default='zone',
33+
choices=['bare', 'testkey', 'text', 'zone'],
34+
help='output format for the public key',
35+
)
36+
parser.add_argument(
37+
'--fqdn',
38+
action='store_true',
39+
help='Use the fully qualified domain name when outputting a DNS zone entry',
40+
)
41+
parser.add_argument(
42+
'--hash-algorithms',
43+
help='Tag the generated DNS record for use with this colon-separated list of algorithms.',
44+
)
45+
parser.add_argument(
46+
'-n', '--note',
47+
help='Free-form text to include in the public key.'
48+
)
49+
parser.add_argument(
50+
'--no-subdomains',
51+
action='store_true',
52+
help='Tag the public key to indicate that identities in a signature are required to be from this exact domain, not subdomains.',
53+
)
54+
parser.add_argument(
55+
'-r', '--restrict',
56+
action='store_true',
57+
help='Tag the public key to indicate that it should only be used for email.',
58+
)
59+
parser.add_argument(
60+
'-s', '--selector',
61+
required=True,
62+
help='A name for the key.',
63+
)
64+
parser.add_argument(
65+
'-t', '--type',
66+
default='rsa',
67+
choices=['rsa', 'ed25519'],
68+
help='Type of key to generate.',
69+
)
70+
parser.add_argument(
71+
'--testing',
72+
action='store_true',
73+
help='Tag the public key to indicate that this domain is testing its deployment of the protocol this key is used for.',
74+
)
75+
# fmt: on
76+
77+
args = parser.parse_args()
78+
79+
fname_base = f'{args.selector}._domainkey.{args.domain}'
80+
if args.directory:
81+
if not os.path.exists(args.directory):
82+
print(f'{args.directory} does not exist', file=sys.stderr)
83+
sys.exit(1)
84+
fname_base = os.path.join(args.directory, fname_base)
85+
86+
binargs = [
87+
'openssl',
88+
'genpkey',
89+
'-algorithm',
90+
args.type,
91+
'-outform',
92+
'PEM',
93+
'-out',
94+
f'{fname_base}.key',
95+
]
96+
97+
if args.type == 'rsa':
98+
binargs.extend(
99+
[
100+
'-pkeyopt',
101+
f'rsa_keygen_bits:{args.bits}',
102+
]
103+
)
104+
105+
res = subprocess.run(binargs, capture_output=True, text=True)
106+
if res.returncode != 0:
107+
print(f'openssl returned error code {res.returncode} while generating the private key: {res.stderr}', file=sys.stderr)
108+
sys.exit(1)
109+
110+
binargs = [
111+
'openssl',
112+
'pkey',
113+
'-in',
114+
f'{fname_base}.key',
115+
'-inform',
116+
'PEM',
117+
'-outform',
118+
'PEM',
119+
'-pubout',
120+
]
121+
122+
res = subprocess.run(binargs, capture_output=True, text=True)
123+
if res.returncode != 0:
124+
print(f'openssl returned error code {res.returncode} while extracting the public key: {res.stderr}', file=sys.stderr)
125+
sys.exit(1)
126+
127+
pkey = ''.join(res.stdout.splitlines()[1:-1])
128+
if args.type == 'ed25519':
129+
# This key type is published without the ASN1 prefix. Conveniently,
130+
# this prefix is 12 bytes so we can strip it off without decoding the
131+
# base64.
132+
pkey = pkey[16:]
133+
134+
# Format the DNS record contents
135+
txt = f'v=DKIM1; k={args.type}'
136+
137+
if args.hash_algorithms:
138+
txt += f'; h={args.hash_algorithms}'
139+
140+
if args.note:
141+
txt += f'; n=\\"{args.note}\\"'
142+
143+
if args.restrict:
144+
txt += '; s=email'
145+
146+
flags = []
147+
if args.testing:
148+
flags.append('y')
149+
if args.no_subdomains:
150+
flags.append('s')
151+
if flags:
152+
txt += f'; t={":".join(flags)}'
153+
154+
txt += f'; p={pkey}'
155+
156+
# Write it out
157+
with open(f'{fname_base}.txt', 'w') as f:
158+
if args.format == 'bare':
159+
f.write(pkey)
160+
elif args.format in ('testkey', 'text'):
161+
if args.format == 'testkey':
162+
f.write(f'{args.selector}._domainkey.{args.domain} ')
163+
f.write(txt.replace('\\"', '"'))
164+
else:
165+
f.write(f'{args.selector}._domainkey')
166+
if args.fqdn:
167+
f.write(f'.{args.domain}.')
168+
f.write('\tIN\tTXT\t( "')
169+
# Individual strings within a TXT record are limited to 255 bytes
170+
f.write('"\n\t"'.join(txt[i : i + 255] for i in range(0, len(txt), 255)))
171+
f.write('" )')
172+
f.write('\n')
173+
174+
175+
if __name__ == '__main__':
176+
main()

contrib/openarc-keygen.1.in

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
.\" Copyright 2024 OpenARC contributors.
2+
.\" See LICENSE.
3+
.Dd 2024-10-30
4+
.Dt OPENARC-KEYGEN 1
5+
.Os OpenARC @VERSION@
6+
7+
.Sh NAME
8+
.Nm openarc-keygen
9+
.Nd DKIM (and ARC) key generation tool
10+
11+
.Sh SYNOPSIS
12+
.Nm openarc-keygen
13+
.Fl d Ar domain
14+
.Fl s Ar selector
15+
.Op options
16+
17+
.Sh DESCRIPTION
18+
.Nm openarc-keygen
19+
outputs a private key suitable for signing messages using
20+
.Xr openarc 8
21+
and also outputs one of several representations of the associated
22+
public key, which can be used in various ways.
23+
24+
The output filenames are based on the
25+
.Ar selector
26+
and
27+
.Ar domain ;
28+
the private key will end in ".key" and the public key will end in ".txt".
29+
30+
.Sh OPTIONS
31+
32+
.Bl -tag -width Ds
33+
.It Fl b , Fl \-bits Ar bits
34+
Size of RSA key to generate.
35+
The default is 2048, which is also the recommended minimum size.
36+
Keys smaller than 1024 bits will almost certainly be rejected by
37+
downstream evaluators.
38+
39+
.It Fl d , Fl \-domain Ar domain
40+
The domain which will use this key for signing.
41+
42+
.It Fl D , Fl -directory Ar directory
43+
Directory to store the keys in.
44+
If this is not specified the keys will be stored in the current
45+
working directory.
46+
47+
.It Fl f , Fl \-format Brq Cm bare | Cm testkey | Cm text | Cm zone
48+
Output format for the public key.
49+
.Cm bare
50+
outputs just the key itself, rendering many flags that this program accepts
51+
irrelevant.
52+
.Cm testkey
53+
outputs a line suitable for use in a file pointed to by
54+
the
55+
.Cm TestKeys
56+
option in
57+
.Xr openarc.conf 5 .
58+
.Cm text
59+
outputs a standard textual representation of the key as specified in RFC 6376.
60+
.Cm zone
61+
is the default, and outputs a DNS record formatted for use in a zone file.
62+
63+
.It Fl \-fqdn
64+
When outputting a DNS zone file entry, use the fully qualified domain name
65+
instead of a relative one.
66+
67+
.It Fl \-hash-algorithms Ar algorithms
68+
Tag the public key to indicate that it should only be used with
69+
this colon-separated list of algorithms.
70+
71+
.It Fl h , Fl \-help
72+
Show a help message and exit.
73+
74+
.It Fl \-no\-subdomains
75+
Tag the public key to indicate that identities in a signature are
76+
required to be from this exact domain, not subdomains.
77+
78+
.It Fl n , Fl \-note Ar note
79+
Free-form text to include in the public key.
80+
This is intended for humans who are reading the record, and should be
81+
kept brief if it is used at all.
82+
83+
.It Fl r , Fl \-restrict
84+
Tag the public key to indicate that it should only be used for email.
85+
There are not currently any other protocols that might use the key, so
86+
this does not have any practical effect.
87+
88+
.It Fl s , Fl \-selector Ar selector
89+
A name for the key.
90+
91+
.It Fl t , Fl \-type Brq Cm rsa | Cm ed25519
92+
Type of key to generate, defaults to RSA.
93+
Note that Ed25519 keys are not currently useful for ARC, nor are
94+
they usable by OpenARC.
95+
This option is for people who are generating DKIM keys for use with
96+
other software.
97+
98+
.It Fl \-testing
99+
Tag the public key to indicate that this domain is testing its
100+
deployment of the protocol this key is used with.
101+
This is a signal that you are more interested in receiving feedback,
102+
it does not affect the handling of messages or signatures.
103+
104+
.El
105+
106+
.Sh NOTES
107+
A suitable
108+
.Nm openssl
109+
executable must be available in the executing user's
110+
.Ev PATH .
111+
112+
.Sh EXAMPLES
113+
You may want to use
114+
.Xr sudo 8
115+
to run this command as the user that the
116+
.Xr openarc 8
117+
daemon is configured to run as, so that the file permissions are correct.
118+
119+
.Dl sudo -u openarc openarc-keygen -D /etc/openarc/keys -d example.com -s 20241004
120+
121+
.Sh SEE ALSO
122+
.Bl -item
123+
.It
124+
.Xr openarc 8
125+
.It
126+
.Xr openssl 1
127+
.It
128+
RFC6376 - DomainKeys Identified Mail
129+
.It
130+
RFC8617 - The Authenticated Received Chain (ARC) Protocol
131+
.El

0 commit comments

Comments
 (0)