Skip to content

Commit

Permalink
contrib: Add a helper script for generating keys
Browse files Browse the repository at this point in the history
Co-authored-by: lquidfire <[email protected]>
  • Loading branch information
flowerysong and lquidfire committed Oct 30, 2024
1 parent a0d310d commit 1987780
Show file tree
Hide file tree
Showing 10 changed files with 392 additions and 1 deletion.
14 changes: 14 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,20 @@ jobs:
- name: Install dependencies
run: sudo apt install libbsd-dev libidn2-dev libjansson-dev libmilter-dev libssl-dev

- name: Set up Python
uses: actions/setup-python@v5
with:
# 3.8 is listed last because it's the lowest version we support for
# tests, so we want to use it as the default.
python-version: |
3.7
3.9
3.10
3.11
3.12
3.13
3.8
- name: Install Python dependencies
run: sudo pip install pytest miltertest

Expand Down
1 change: 1 addition & 0 deletions .ruff.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
include = ["contrib/openarc-keygen"]
line-length = 160

[lint]
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.

### Added
- `oldest-pass` processing per [RFC 8617 section 5.2](https://datatracker.ietf.org/doc/html/rfc8617#section-5.2).
- `openarc-keygen`
- libopenarc - `arc_chain_oldest_pass()`
- milter - `AuthResIP` configuration option.
- milter - `RequireSafeKeys` configuration option.
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ you will also need:
* [Automake](https://www.gnu.org/software/automake/) >= 1.11.1
* [libtool](https://www.gnu.org/software/libtool/) >= 2.2.6

The core OpenARC software will function without it, but tools distributed
alongside OpenARC (such as `openarc-keygen`) may require:

* Python >= 3.7

### DNF-based systems

```
Expand Down
1 change: 1 addition & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,7 @@ AC_SUBST([SYSCONFDIR])
AC_CONFIG_FILES([
Makefile
contrib/Makefile
contrib/openarc-keygen.1
contrib/init/Makefile
contrib/init/generic/Makefile
contrib/init/redhat/Makefile
Expand Down
3 changes: 3 additions & 0 deletions contrib/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@
# All rights reserved.

SUBDIRS = init spec systemd

dist_bin_SCRIPTS = openarc-keygen
man_MANS = openarc-keygen.1
176 changes: 176 additions & 0 deletions contrib/openarc-keygen
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
#!/usr/bin/env python3

# Copyright 2024 OpenARC contributors.
# See LICENSE.

import argparse
import os
import subprocess
import sys


def main():
parser = argparse.ArgumentParser()
# fmt: off
parser.add_argument(
'-b', '--bits',
type=int,
default=2048,
help='Size of RSA key to generate.',
)
parser.add_argument(
'-d', '--domain',
required=True,
help='The domain which will use this key for signing.',
)
parser.add_argument(
'-D', '--directory',
help='Directory to store the keys in.',
)
parser.add_argument(
'-f', '--format',
default='zone',
choices=['bare', 'testkey', 'text', 'zone'],
help='output format for the public key',
)
parser.add_argument(
'--fqdn',
action='store_true',
help='Use the fully qualified domain name when outputting a DNS zone entry',
)
parser.add_argument(
'--hash-algorithms',
help='Tag the generated DNS record for use with this colon-separated list of algorithms.',
)
parser.add_argument(
'-n', '--note',
help='Free-form text to include in the public key.'
)
parser.add_argument(
'--no-subdomains',
action='store_true',
help='Tag the public key to indicate that identities in a signature are required to be from this exact domain, not subdomains.',
)
parser.add_argument(
'-r', '--restrict',
action='store_true',
help='Tag the public key to indicate that it should only be used for email.',
)
parser.add_argument(
'-s', '--selector',
required=True,
help='A name for the key.',
)
parser.add_argument(
'-t', '--type',
default='rsa',
choices=['rsa', 'ed25519'],
help='Type of key to generate.',
)
parser.add_argument(
'--testing',
action='store_true',
help='Tag the public key to indicate that this domain is testing its deployment of the protocol this key is used for.',
)
# fmt: on

args = parser.parse_args()

fname_base = f'{args.selector}._domainkey.{args.domain}'
if args.directory:
if not os.path.exists(args.directory):
print(f'{args.directory} does not exist', file=sys.stderr)
sys.exit(1)
fname_base = os.path.join(args.directory, fname_base)

binargs = [
'openssl',
'genpkey',
'-algorithm',
args.type,
'-outform',
'PEM',
'-out',
f'{fname_base}.key',
]

if args.type == 'rsa':
binargs.extend(
[
'-pkeyopt',
f'rsa_keygen_bits:{args.bits}',
]
)

res = subprocess.run(binargs, capture_output=True, text=True)
if res.returncode != 0:
print(f'openssl returned error code {res.returncode} while generating the private key: {res.stderr}', file=sys.stderr)
sys.exit(1)

binargs = [
'openssl',
'pkey',
'-in',
f'{fname_base}.key',
'-inform',
'PEM',
'-outform',
'PEM',
'-pubout',
]

res = subprocess.run(binargs, capture_output=True, text=True)
if res.returncode != 0:
print(f'openssl returned error code {res.returncode} while extracting the public key: {res.stderr}', file=sys.stderr)
sys.exit(1)

pkey = ''.join(res.stdout.splitlines()[1:-1])
if args.type == 'ed25519':
# This key type is published without the ASN1 prefix. Conveniently,
# this prefix is 12 bytes so we can strip it off without decoding the
# base64.
pkey = pkey[16:]

# Format the DNS record contents
txt = f'v=DKIM1; k={args.type}'

if args.hash_algorithms:
txt += f'; h={args.hash_algorithms}'

if args.note:
txt += f'; n=\\"{args.note}\\"'

if args.restrict:
txt += '; s=email'

flags = []
if args.testing:
flags.append('y')
if args.no_subdomains:
flags.append('s')
if flags:
txt += f'; t={":".join(flags)}'

txt += f'; p={pkey}'

# Write it out
with open(f'{fname_base}.txt', 'w') as f:
if args.format == 'bare':
f.write(pkey)
elif args.format in ('testkey', 'text'):
if args.format == 'testkey':
f.write(f'{args.selector}._domainkey.{args.domain} ')
f.write(txt.replace('\\"', '"'))
else:
f.write(f'{args.selector}._domainkey')
if args.fqdn:
f.write(f'.{args.domain}.')
f.write('\tIN\tTXT\t( "')
# Individual strings within a TXT record are limited to 255 bytes
f.write('"\n\t"'.join(txt[i : i + 255] for i in range(0, len(txt), 255)))
f.write('" )')
f.write('\n')


if __name__ == '__main__':
main()
131 changes: 131 additions & 0 deletions contrib/openarc-keygen.1.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
.\" Copyright 2024 OpenARC contributors.
.\" See LICENSE.
.Dd 2024-10-30
.Dt OPENARC-KEYGEN 1
.Os OpenARC @VERSION@

.Sh NAME
.Nm openarc-keygen
.Nd DKIM (and ARC) key generation tool

.Sh SYNOPSIS
.Nm openarc-keygen
.Fl d Ar domain
.Fl s Ar selector
.Op options

.Sh DESCRIPTION
.Nm openarc-keygen
outputs a private key suitable for signing messages using
.Xr openarc 8
and also outputs one of several representations of the associated
public key, which can be used in various ways.

The output filenames are based on the
.Ar selector
and
.Ar domain ;
the private key will end in ".key" and the public key will end in ".txt".

.Sh OPTIONS

.Bl -tag -width Ds
.It Fl b , Fl \-bits Ar bits
Size of RSA key to generate.
The default is 2048, which is also the recommended minimum size.
Keys smaller than 1024 bits will almost certainly be rejected by
downstream evaluators.

.It Fl d , Fl \-domain Ar domain
The domain which will use this key for signing.

.It Fl D , Fl -directory Ar directory
Directory to store the keys in.
If this is not specified the keys will be stored in the current
working directory.

.It Fl f , Fl \-format Brq Cm bare | Cm testkey | Cm text | Cm zone
Output format for the public key.
.Cm bare
outputs just the key itself, rendering many flags that this program accepts
irrelevant.
.Cm testkey
outputs a line suitable for use in a file pointed to by
the
.Cm TestKeys
option in
.Xr openarc.conf 5 .
.Cm text
outputs a standard textual representation of the key as specified in RFC 6376.
.Cm zone
is the default, and outputs a DNS record formatted for use in a zone file.

.It Fl \-fqdn
When outputting a DNS zone file entry, use the fully qualified domain name
instead of a relative one.

.It Fl \-hash-algorithms Ar algorithms
Tag the public key to indicate that it should only be used with
this colon-separated list of algorithms.

.It Fl h , Fl \-help
Show a help message and exit.

.It Fl \-no\-subdomains
Tag the public key to indicate that identities in a signature are
required to be from this exact domain, not subdomains.

.It Fl n , Fl \-note Ar note
Free-form text to include in the public key.
This is intended for humans who are reading the record, and should be
kept brief if it is used at all.

.It Fl r , Fl \-restrict
Tag the public key to indicate that it should only be used for email.
There are not currently any other protocols that might use the key, so
this does not have any practical effect.

.It Fl s , Fl \-selector Ar selector
A name for the key.

.It Fl t , Fl \-type Brq Cm rsa | Cm ed25519
Type of key to generate, defaults to RSA.
Note that Ed25519 keys are not currently useful for ARC, nor are
they usable by OpenARC.
This option is for people who are generating DKIM keys for use with
other software.

.It Fl \-testing
Tag the public key to indicate that this domain is testing its
deployment of the protocol this key is used with.
This is a signal that you are more interested in receiving feedback,
it does not affect the handling of messages or signatures.

.El

.Sh NOTES
A suitable
.Nm openssl
executable must be available in the executing user's
.Ev PATH .

.Sh EXAMPLES
You may want to use
.Xr sudo 8
to run this command as the user that the
.Xr openarc 8
daemon is configured to run as, so that the file permissions are correct.

.Dl sudo -u openarc openarc-keygen -D /etc/openarc/keys -d example.com -s 20241004

.Sh SEE ALSO
.Bl -item
.It
.Xr openarc 8
.It
.Xr openssl 1
.It
RFC6376 - DomainKeys Identified Mail
.It
RFC8617 - The Authenticated Received Chain (ARC) Protocol
.El
Loading

0 comments on commit 1987780

Please sign in to comment.