tech-userlevel archive

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index][Old Index]

cgdconfig(8) support for sharing a main key between disks



[bcc tech-crypto@ tech-security@, followups to tech-userlevel@]

The attached patch series implements support for sharing a single main
key between multiple disks.

This way, for instance, you can enter a password once, and the system
will compute Argon2id or talk to an interactive hardware token only
once to derive a main key (expensive), and then use HKDF-HMAC-SHA256
to derive subkeys for each of the disks configured to use the shared
key (cheap).

The disks won't be encrypted with the _same_ key -- their keys are
just derived by HKDF-HMAC-SHA256 from a main key shared between them.
To anyone who doesn't know the main key (and can't break SHA-256), the
disk keys appear to be independent uniform random keys.

Example setup:

        cgdconfig -g -S -o /etc/cgd/wd0 -V gpt adiantum
        cgdconfig -g -S -P /etc/cgd/wd0 -o /etc/cgd/ld1 \
                -V disklabel aes-cbc 256

Then, with wd0 and ld1 listed in /etc/cgd/cgd.conf, `cgdconfig -C'
will prompt for only one password, but will configure both disks.

The generated wd0 and ld1 parameters files might look like this:

        # wd0
        algorithm adiantum;
        iv-method encblkno1;
        keylength 256;
        verify_method gpt;
        keygen pkcs5_pbkdf2/sha1 {
                iterations 463571;
                salt AAAAgBnGwXjRa38XSv+PN/G0Urs=;
                shared "bdec4067-a77d-4fd6-9f1b-71b8b82a8f8e" \
                    algorithm hkdf-hmac-sha256 \
                    subkey AAAAgG9MMEA7Wkl2E/vKtmPXshE=;
        };

        # ld1
        algorithm aes-cbc;
        iv-method encblkno1;
        keylength 256;
        verify_method disklabel;
        keygen pkcs5_pbkdf2/sha1 {
                iterations 463571;
                salt AAAAgBnGwXjRa38XSv+PN/G0Urs=;
                shared "bdec4067-a77d-4fd6-9f1b-71b8b82a8f8e" \
                    algorithm hkdf-hmac-sha256 \
                    subkey AAAAgJFTFfBFBcEuf/jYGPF3s+k=;
        };

Note that the keygen method, number of iterations, salt, and shared
name and algorithm are all the same -- inside the keygen block, only
the subkey identifier is different.

The shared key name, here bdec4067-a77d-4fd6-9f1b-71b8b82a8f8e, is
used by `cgdconfig -C' as a key for a cache of key generations to
reuse between parameters files; it has no other significance.  Each
parameters file can also be used on its own without the others.
Nothing is stored persistently, and there are no background daemons
like ssh-agent: once `cgdconfig -C' exits the cache is discarded.

Thoughts?  Review and testing welcome!


P.S.  Currently this doesn't provide a way to convert an existing
parameters file to be shareable, but in principle `cgdconfig -G' could
be taught to do that by generating a shared clause in the existing
keygen block and then adding an appropriate `keygen storedkey' block
as it already does to make it work -- TBD.
>From e21179d60d2e884fc9b691bfa4aa13ac30696d75 Mon Sep 17 00:00:00 2001
From: Taylor R Campbell <riastradh%NetBSD.org@localhost>
Date: Sun, 26 Jun 2022 15:32:15 +0000
Subject: [PATCH 1/5] cgdconfig(8): New -t operation just prints the derived
 key in base64.

For testing purposes.
---
 distrib/sets/lists/tests/mi  |  1 +
 sbin/cgdconfig/cgdconfig.8   |  5 ++
 sbin/cgdconfig/cgdconfig.c   | 57 ++++++++++++++++++++--
 tests/dev/cgd/Makefile       |  3 +-
 tests/dev/cgd/t_cgdconfig.sh | 94 ++++++++++++++++++++++++++++++++++++
 5 files changed, 156 insertions(+), 4 deletions(-)
 create mode 100644 tests/dev/cgd/t_cgdconfig.sh

diff --git a/distrib/sets/lists/tests/mi b/distrib/sets/lists/tests/mi
index a8f2705bbea7..6dc6afb2d236 100644
--- a/distrib/sets/lists/tests/mi
+++ b/distrib/sets/lists/tests/mi
@@ -1426,6 +1426,7 @@
 ./usr/tests/dev/cgd/t_cgd_adiantum			tests-fs-tests		atf,compattestfile,rump
 ./usr/tests/dev/cgd/t_cgd_aes				tests-fs-tests		atf,compattestfile,rump
 ./usr/tests/dev/cgd/t_cgd_blowfish			tests-fs-tests		atf,compattestfile,rump
+./usr/tests/dev/cgd/t_cgdconfig				tests-fs-tests		compattestfile,atf
 ./usr/tests/dev/clock_subr				tests-fs-tests		compattestfile,atf
 ./usr/tests/dev/clock_subr/Atffile			tests-fs-tests		compattestfile,atf
 ./usr/tests/dev/clock_subr/Kyuafile			tests-fs-tests		compattestfile,atf,kyua
diff --git a/sbin/cgdconfig/cgdconfig.8 b/sbin/cgdconfig/cgdconfig.8
index 8e6652349e94..7cd662bb5ce6 100644
--- a/sbin/cgdconfig/cgdconfig.8
+++ b/sbin/cgdconfig/cgdconfig.8
@@ -60,6 +60,9 @@
 .Ar alg
 .Op Ar keylen
 .Nm
+.Fl t
+.Ar paramsfile
+.Nm
 .Fl l
 .Op Fl v Ns Op Cm v
 .Op Ar cgd
@@ -143,6 +146,8 @@ in question to be unconfigured rather than prompting for the passphrase
 again.
 .It Fl s
 Read the key (nb: not the passphrase) from stdin.
+.It Fl t
+Generate the key and print it to standard output encoded in base64.
 .It Fl U
 Unconfigure all the devices listed in the cgd configuration file.
 .It Fl u
diff --git a/sbin/cgdconfig/cgdconfig.c b/sbin/cgdconfig/cgdconfig.c
index 53bb2d414d04..e2f28625b94a 100644
--- a/sbin/cgdconfig/cgdconfig.c
+++ b/sbin/cgdconfig/cgdconfig.c
@@ -51,6 +51,11 @@ __RCSID("$NetBSD: cgdconfig.c,v 1.53 2021/11/22 14:34:35 nia Exp $");
 #include <paths.h>
 #include <dirent.h>
 
+/* base64 gunk */
+#include <netinet/in.h>
+#include <arpa/nameser.h>
+#include <resolv.h>
+
 #include <sys/ioctl.h>
 #include <sys/stat.h>
 #include <sys/bootblock.h>
@@ -83,7 +88,8 @@ enum action {
 	 ACTION_CONFIGALL,		/* configure all from config file */
 	 ACTION_UNCONFIGALL,		/* unconfigure all from config file */
 	 ACTION_CONFIGSTDIN,		/* configure, key from stdin */
-	 ACTION_LIST			/* list configured devices */
+	 ACTION_LIST,			/* list configured devices */
+	 ACTION_PRINTKEY,		/* print key to stdout */
 };
 
 /* if nflag is set, do not configure/unconfigure the cgd's */
@@ -106,6 +112,7 @@ static int	unconfigure(int, char **, struct params *, int);
 static int	do_all(const char *, int, char **,
 		       int (*)(int, char **, struct params *, int));
 static int	do_list(int, char **);
+static int	do_printkey(int, char **);
 
 #define CONFIG_FLAGS_FROMALL	1	/* called from configure_all() */
 #define CONFIG_FLAGS_FROMMAIN	2	/* called from main() */
@@ -155,6 +162,7 @@ usage(void)
 	(void)fprintf(stderr, "       %s -l [-v[v]] [cgd]\n", getprogname());
 	(void)fprintf(stderr, "       %s -s [-nv] [-i ivmeth] cgd dev alg "
 	    "[keylen]\n", getprogname());
+	(void)fprintf(stderr, "       %s -t paramsfile\n", getprogname());
 	(void)fprintf(stderr, "       %s -U [-nv] [-f configfile]\n",
 	    getprogname());
 	(void)fprintf(stderr, "       %s -u [-nv] cgd\n", getprogname());
@@ -209,7 +217,7 @@ main(int argc, char **argv)
 	p = params_new();
 	kg = NULL;
 
-	while ((ch = getopt(argc, argv, "CGUV:b:ef:gi:k:lno:spuv")) != -1)
+	while ((ch = getopt(argc, argv, "CGUV:b:ef:gi:k:lno:sptuv")) != -1)
 		switch (ch) {
 		case 'C':
 			set_action(&action, ACTION_CONFIGALL);
@@ -276,7 +284,9 @@ main(int argc, char **argv)
 		case 's':
 			set_action(&action, ACTION_CONFIGSTDIN);
 			break;
-
+		case 't':
+			set_action(&action, ACTION_PRINTKEY);
+			break;
 		case 'u':
 			set_action(&action, ACTION_UNCONFIGURE);
 			break;
@@ -319,6 +329,8 @@ main(int argc, char **argv)
 		return configure_stdin(p, argc, argv);
 	case ACTION_LIST:
 		return do_list(argc, argv);
+	case ACTION_PRINTKEY:
+		return do_printkey(argc, argv);
 	default:
 		errx(EXIT_FAILURE, "undefined action");
 		/* NOTREACHED */
@@ -1339,6 +1351,45 @@ do_list(int argc, char **argv)
 	return 0;
 }
 
+static int
+do_printkey(int argc, char **argv)
+{
+	struct params *p;
+	const uint8_t *raw;
+	size_t nbits, nbytes;
+	size_t nb64;
+	char *b64;
+	int ret;
+
+	if (argc != 1)
+		usage();
+	p = params_cget(argv[0]);
+	if (p == NULL)
+		return -1;
+	if (!params_verify(p)) {
+		warnx("invalid parameters file \"%s\"", argv[0]);
+		return -1;
+	}
+	p->key = getkey("key", p->keygen, p->keylen);
+	raw = bits_getbuf(p->key);
+	nbits = bits_len(p->key);
+	assert(nbits <= INT_MAX - 7);
+	nbytes = BITS2BYTES(nbits);
+	assert(nbytes <= 3*(INT_MAX/4) - 2);
+
+	nb64 = 4*((nbytes + 2)/3);
+	b64 = emalloc(nb64 + 2);
+	ret = __b64_ntop(raw, nbytes, b64, nb64 + 1);
+	assert(ret == (int)nb64);
+	b64[nb64] = '\n';
+	b64[nb64 + 1] = '\0';
+
+	if (fwrite(b64, nb64 + 1, 1, stdout) != 1)
+		err(1, "fwrite");
+	fflush(stdout);
+	return ferror(stdout);
+}
+
 static void
 eliminate_cores(void)
 {
diff --git a/tests/dev/cgd/Makefile b/tests/dev/cgd/Makefile
index ffd38063aa59..f1e4f60de63a 100644
--- a/tests/dev/cgd/Makefile
+++ b/tests/dev/cgd/Makefile
@@ -7,7 +7,8 @@ TESTSDIR=	${TESTSBASE}/dev/cgd
 FILES=		paramsfile
 FILESDIR=	${TESTSDIR}
 
-TESTS_SH=	t_cgd
+TESTS_SH+=	t_cgd
+TESTS_SH+=	t_cgdconfig
 
 .if ${MKRUMP} != "no"
 TESTS_C+=	t_cgd_3des
diff --git a/tests/dev/cgd/t_cgdconfig.sh b/tests/dev/cgd/t_cgdconfig.sh
new file mode 100644
index 000000000000..7a4edc21ce1a
--- /dev/null
+++ b/tests/dev/cgd/t_cgdconfig.sh
@@ -0,0 +1,94 @@
+#	$NetBSD$
+#
+# Copyright (c) 2022 The NetBSD Foundation, Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+#    notice, this list of conditions and the following disclaimer in the
+#    documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
+# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
+# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+
+atf_test_case storedkey
+storedkey_head()
+{
+	atf_set descr "Test key generation with storedkey"
+}
+storedkey_body()
+{
+	cat <<EOF >params
+algorithm adiantum;
+iv-method encblkno1;
+keylength 256;
+verify_method none;
+keygen storedkey key AAABAJtnmp3XZspMBAFpCYnB8Hekn0 \
+                     gj5cDVngslfGLSqwcy;
+EOF
+	atf_check -o inline:'m2eanddmykwEAWkJicHwd6SfSCPlwNWeCyV8YtKrBzI=\n' \
+	    cgdconfig -t params
+}
+
+atf_test_case storedkey2a
+storedkey2a_head()
+{
+	atf_set descr "Test key generation with combined storedkeys"
+}
+storedkey2a_body()
+{
+	cat <<EOF >params
+algorithm adiantum;
+iv-method encblkno1;
+keylength 256;
+verify_method none;
+keygen storedkey key AAABAJtnmp3XZspMBAFpCYnB8Hekn0 \
+                     gj5cDVngslfGLSqwcy;
+keygen storedkey key AAABAK1pbgIayXftX0RQ3AaMK4YEd/ \
+                     fowKwQbENxpu3o1k9m;
+EOF
+	atf_check -o inline:'Ng70n82vvaFbRTnVj03b8aDov8slbMXySFTajzp9SFQ=\n' \
+	    cgdconfig -t params
+}
+
+atf_test_case storedkey2b
+storedkey2b_head()
+{
+	atf_set descr "Test key generation with combined storedkeys, reversed"
+}
+storedkey2b_body()
+{
+	cat <<EOF >params
+algorithm adiantum;
+iv-method encblkno1;
+keylength 256;
+verify_method none;
+keygen storedkey key AAABAK1pbgIayXftX0RQ3AaMK4YEd/ \
+                     fowKwQbENxpu3o1k9m;
+keygen storedkey key AAABAJtnmp3XZspMBAFpCYnB8Hekn0 \
+                     gj5cDVngslfGLSqwcy;
+EOF
+	atf_check -o inline:'Ng70n82vvaFbRTnVj03b8aDov8slbMXySFTajzp9SFQ=\n' \
+	    cgdconfig -t params
+}
+
+atf_init_test_cases()
+{
+	atf_add_test_case storedkey
+	atf_add_test_case storedkey2a
+	atf_add_test_case storedkey2b
+}

>From bccdf1bc25306268b3a056bac4aaa81086aa6437 Mon Sep 17 00:00:00 2001
From: Taylor R Campbell <riastradh%NetBSD.org@localhost>
Date: Sat, 6 Aug 2022 16:10:56 +0000
Subject: [PATCH 2/5] cgdconfig(8): New -T operation prints all generated keys
 in cgd.conf.

For testing purposes.
---
 sbin/cgdconfig/cgdconfig.8   |  8 ++++++
 sbin/cgdconfig/cgdconfig.c   | 56 ++++++++++++++++++++++++++++++++----
 tests/dev/cgd/t_cgdconfig.sh | 35 ++++++++++++++++++++++
 3 files changed, 93 insertions(+), 6 deletions(-)

diff --git a/sbin/cgdconfig/cgdconfig.8 b/sbin/cgdconfig/cgdconfig.8
index 7cd662bb5ce6..e006351e36a8 100644
--- a/sbin/cgdconfig/cgdconfig.8
+++ b/sbin/cgdconfig/cgdconfig.8
@@ -60,6 +60,9 @@
 .Ar alg
 .Op Ar keylen
 .Nm
+.Fl T
+.Op Fl f Ar configfile
+.Nm
 .Fl t
 .Ar paramsfile
 .Nm
@@ -146,6 +149,11 @@ in question to be unconfigured rather than prompting for the passphrase
 again.
 .It Fl s
 Read the key (nb: not the passphrase) from stdin.
+.It Fl T
+Generate all keys for all the devices listed in the
+.Nm
+configuration file and print them to standard output encoded in
+base64.
 .It Fl t
 Generate the key and print it to standard output encoded in base64.
 .It Fl U
diff --git a/sbin/cgdconfig/cgdconfig.c b/sbin/cgdconfig/cgdconfig.c
index e2f28625b94a..0629cd9c20d4 100644
--- a/sbin/cgdconfig/cgdconfig.c
+++ b/sbin/cgdconfig/cgdconfig.c
@@ -90,6 +90,7 @@ enum action {
 	 ACTION_CONFIGSTDIN,		/* configure, key from stdin */
 	 ACTION_LIST,			/* list configured devices */
 	 ACTION_PRINTKEY,		/* print key to stdout */
+	 ACTION_PRINTALLKEYS,		/* print all keys to stdout */
 };
 
 /* if nflag is set, do not configure/unconfigure the cgd's */
@@ -112,6 +113,8 @@ static int	unconfigure(int, char **, struct params *, int);
 static int	do_all(const char *, int, char **,
 		       int (*)(int, char **, struct params *, int));
 static int	do_list(int, char **);
+static int	printkey(const char *);
+static int	printkey1(int, char **, struct params *, int);
 static int	do_printkey(int, char **);
 
 #define CONFIG_FLAGS_FROMALL	1	/* called from configure_all() */
@@ -163,6 +166,7 @@ usage(void)
 	(void)fprintf(stderr, "       %s -s [-nv] [-i ivmeth] cgd dev alg "
 	    "[keylen]\n", getprogname());
 	(void)fprintf(stderr, "       %s -t paramsfile\n", getprogname());
+	(void)fprintf(stderr, "       %s -T [-f configfile]\n", getprogname());
 	(void)fprintf(stderr, "       %s -U [-nv] [-f configfile]\n",
 	    getprogname());
 	(void)fprintf(stderr, "       %s -u [-nv] cgd\n", getprogname());
@@ -217,7 +221,7 @@ main(int argc, char **argv)
 	p = params_new();
 	kg = NULL;
 
-	while ((ch = getopt(argc, argv, "CGUV:b:ef:gi:k:lno:sptuv")) != -1)
+	while ((ch = getopt(argc, argv, "CGTUV:b:ef:gi:k:lno:sptuv")) != -1)
 		switch (ch) {
 		case 'C':
 			set_action(&action, ACTION_CONFIGALL);
@@ -225,6 +229,9 @@ main(int argc, char **argv)
 		case 'G':
 			set_action(&action, ACTION_GENERATE_CONVERT);
 			break;
+		case 'T':
+			set_action(&action, ACTION_PRINTALLKEYS);
+			break;
 		case 'U':
 			set_action(&action, ACTION_UNCONFIGALL);
 			break;
@@ -331,6 +338,8 @@ main(int argc, char **argv)
 		return do_list(argc, argv);
 	case ACTION_PRINTKEY:
 		return do_printkey(argc, argv);
+	case ACTION_PRINTALLKEYS:
+		return do_all(cfile, argc, argv, printkey1);
 	default:
 		errx(EXIT_FAILURE, "undefined action");
 		/* NOTREACHED */
@@ -1352,7 +1361,7 @@ do_list(int argc, char **argv)
 }
 
 static int
-do_printkey(int argc, char **argv)
+printkey(const char *paramsfile)
 {
 	struct params *p;
 	const uint8_t *raw;
@@ -1361,13 +1370,11 @@ do_printkey(int argc, char **argv)
 	char *b64;
 	int ret;
 
-	if (argc != 1)
-		usage();
-	p = params_cget(argv[0]);
+	p = params_cget(paramsfile);
 	if (p == NULL)
 		return -1;
 	if (!params_verify(p)) {
-		warnx("invalid parameters file \"%s\"", argv[0]);
+		warnx("invalid parameters file \"%s\"", paramsfile);
 		return -1;
 	}
 	p->key = getkey("key", p->keygen, p->keylen);
@@ -1390,6 +1397,43 @@ do_printkey(int argc, char **argv)
 	return ferror(stdout);
 }
 
+static int
+printkey1(int argc, char **argv, struct params *inparams, int flags)
+{
+	char devicename[PATH_MAX], paramsfilebuf[PATH_MAX];
+	const char *dev, *paramsfile;
+
+	assert(flags == CONFIG_FLAGS_FROMALL);
+
+	if (argc < 2 || argc > 3)
+		return -1;
+
+	dev = getfsspecname(devicename, sizeof(devicename), argv[1]);
+	if (dev == NULL) {
+		warnx("getfsspecname failed: %s", devicename);
+		return -1;
+	}
+
+	if (argc == 2) {
+		strlcpy(paramsfilebuf, dev, sizeof(paramsfilebuf));
+		paramsfile = basename(paramsfilebuf);
+	} else {
+		paramsfile = argv[2];
+	}
+
+	printf("%s: ", dev);
+	return printkey(paramsfile);
+}
+
+static int
+do_printkey(int argc, char **argv)
+{
+
+	if (argc != 1)
+		usage();
+	return printkey(argv[0]);
+}
+
 static void
 eliminate_cores(void)
 {
diff --git a/tests/dev/cgd/t_cgdconfig.sh b/tests/dev/cgd/t_cgdconfig.sh
index 7a4edc21ce1a..d7cccb2db08d 100644
--- a/tests/dev/cgd/t_cgdconfig.sh
+++ b/tests/dev/cgd/t_cgdconfig.sh
@@ -44,6 +44,40 @@ EOF
 	    cgdconfig -t params
 }
 
+atf_test_case storedkeys
+storedkeys_head()
+{
+	atf_set descr "Test multiple stored keys with cgd.conf"
+}
+storedkeys_body()
+{
+	cat <<EOF >wd0e
+algorithm adiantum;
+iv-method encblkno1;
+keylength 256;
+verify_method none;
+keygen storedkey key AAABAJtnmp3XZspMBAFpCYnB8Hekn0 \
+                     gj5cDVngslfGLSqwcy;
+EOF
+	cat <<EOF >ld1e
+algorithm adiantum;
+iv-method encblkno1;
+keylength 256;
+verify_method none;
+keygen storedkey key AAABAK1pbgIayXftX0RQ3AaMK4YEd/ \
+                     fowKwQbENxpu3o1k9m;
+EOF
+	cat <<EOF >cgd.conf
+cgd0	/dev/wd0e	wd0e
+cgd1	/dev/ld1e	ld1e
+EOF
+	cat <<EOF >expected
+/dev/wd0e: m2eanddmykwEAWkJicHwd6SfSCPlwNWeCyV8YtKrBzI=
+/dev/ld1e: rWluAhrJd+1fRFDcBowrhgR39+jArBBsQ3Gm7ejWT2Y=
+EOF
+	atf_check -o file:expected cgdconfig -T -f cgd.conf
+}
+
 atf_test_case storedkey2a
 storedkey2a_head()
 {
@@ -91,4 +125,5 @@ atf_init_test_cases()
 	atf_add_test_case storedkey
 	atf_add_test_case storedkey2a
 	atf_add_test_case storedkey2b
+	atf_add_test_case storedkeys
 }

>From ade8867373774237999e09b3a2e4794d9b7ed416 Mon Sep 17 00:00:00 2001
From: Taylor R Campbell <riastradh%NetBSD.org@localhost>
Date: Mon, 16 May 2022 10:41:49 +0000
Subject: [PATCH 3/5] cgdconfig(8): Add support for shared keys.

New clause `shared <id> algorithm <alg> subkey <info>' in a keygen
block enables `cgdconfig -C' to reuse a key between different params
files, so you can, e.g., use a single password for multiple disks.
This is better than simply caching the password itself because:

- Hashing the password is expensive, so it should only be done once.

  Suppose your budget is time t before you get bored, and you
  calibrate password hash parameters to unlock n disks before you get
  bored waiting for `cgdconfig -C'.

  . With n password hashings the adversary's cost goes up only by a
    factor of t/n.
  . With one password hashing and n subkeys the adversary's cost goes
    up by a factor of n.

  And if you ever add a disk, rehashing it will make `cgdconfig -C'
  go over budget, whereas another subkey adds negligible cost to you.

- Subkeys work for other types of keygen blocks, like shell_cmd,
  which could be used to get a key from a hardware token that needs a
  button press.

The <info> parameter must be different for each params file;
everything else in the keygen block must be the same.  With this
clause, the keygen block determines a shared key used only to derive
keys; the actual key used by cgdconfig is derived from the shared key
by the specified algorithm.

The only supported algorithm is hkdf-hmac-sha256, which uses
HKDF-Expand of RFC 5869 instantiated with SHA-256.

Example:

	algorithm aes-cbc;
	iv-method encblkno1;
	keylength 128;
	verify_method none;
	keygen pkcs5_pbkdf2/sha1 {
		iterations 39361;
		salt AAAAgMoHiYonye6KogdYJAobCHE=;
		shared "pw" algorithm hkdf-hmac-sha256
		    subkey AAAAgFlw0BMQ5gY+haYkZ6JC+yY=;
	};

The key used for this disk will be derived by

	HKDF-HMAC-SHA256_k(WXDQExDmBj6FpiRnokL7Jg==),

where k is the outcome of PBKDF2-SHA1 with the given parameters.

Note that <info> encodes a four-byte prefix giving the big-endian
length in bits of the info argument to HKDF, just like all other bit
strings in cgdconfig parameters files.

If you have multiple disks configured using the same keygen block
except for the info parameter, `cgdconfig -C' will only prompt once
for your passphrase, generate a shared key k with PBKDF2 as usual,
and then reuse it for each of the disks.
---
 sbin/cgdconfig/Makefile           |   1 +
 sbin/cgdconfig/cgdconfig.8        |  91 +++++++++-
 sbin/cgdconfig/cgdconfig.c        | 104 +++++++++++-
 sbin/cgdconfig/cgdlex.l           |   2 +
 sbin/cgdconfig/cgdparse.y         |   5 +-
 sbin/cgdconfig/hkdf_hmac_sha256.c | 273 ++++++++++++++++++++++++++++++
 sbin/cgdconfig/hkdf_hmac_sha256.h |  40 +++++
 sbin/cgdconfig/params.c           |  63 +++++++
 sbin/cgdconfig/params.h           |  10 ++
 tests/dev/cgd/t_cgdconfig.sh      |  48 ++++++
 10 files changed, 633 insertions(+), 4 deletions(-)
 create mode 100644 sbin/cgdconfig/hkdf_hmac_sha256.c
 create mode 100644 sbin/cgdconfig/hkdf_hmac_sha256.h

diff --git a/sbin/cgdconfig/Makefile b/sbin/cgdconfig/Makefile
index 26a62402150d..7714e04ee872 100644
--- a/sbin/cgdconfig/Makefile
+++ b/sbin/cgdconfig/Makefile
@@ -8,6 +8,7 @@ MAN=	cgdconfig.8
 SRCS+=	cgdconfig.c		\
 	cgdlex.l		\
 	cgdparse.y		\
+	hkdf_hmac_sha256.c	\
 	pkcs5_pbkdf2.c		\
 	params.c		\
 	utils.c
diff --git a/sbin/cgdconfig/cgdconfig.8 b/sbin/cgdconfig/cgdconfig.8
index e006351e36a8..e9e5050d1b38 100644
--- a/sbin/cgdconfig/cgdconfig.8
+++ b/sbin/cgdconfig/cgdconfig.8
@@ -395,6 +395,46 @@ Version of Argon2 to use.
 Should be the most recent version, currently
 .Dv 19 .
 Only used for argon2id.
+.It shared Ar name No algorithm Ar kdf No subkey Ar info
+Makes the key generation take an extra step to derive a subkey from the
+main key using the key derivation function
+.Ar kdf
+with input
+.Ar info .
+.Pp
+This enables a single password entry, for example, to decrypt multiple
+disks that use different keys, each derived as a subkey from the main
+key generated from the password.
+.Bl -tag -width 6n
+.It Ar name
+A string used to identify the same main key generation shared between
+parameters files for different disks listed in a single
+.Pa cgd.conf
+configuration file.
+.It Ar kdf
+The name of a key derivation function used to derive a subkey from the
+main key.
+Supported values:
+.Bl -tag -width 6n -offset indent
+.It Li hkdf-hmac-sha256
+The HKDF-Expand function of RFC 5869, instantiated with SHA-256.
+.El
+.It Ar info
+A base64 length-encoded string to distinguish different subkeys derived
+from a shared main key.
+Need not be secret.
+For example, it could be a nickname, or the disk's World-Wide Name, or
+a UUID generated for the disk, or just a random string.
+.El
+.Pp
+It is an error to reuse a shared key
+.Ar name
+with different keygen blocks, other than the
+.Ar info
+parameter,
+between parameters files used by a single
+.Pa cgd.conf
+configuration file.
 .El
 .Sh FILES
 .Bl -tag -width indentxxxxxxxxxxxxxxxxxx -compact
@@ -474,6 +514,45 @@ An example parameters file which stores its key locally:
 			     ly2TdxkFqOkYYcbyUKu/f60L;
 .Ed
 .Pp
+An example pair of configuration files which use shared keys so they
+can be derived from a single passphrase entry, with the 64-bit
+World-Wide Name of each disk (base64 length-encoded) as its subkey
+info:
+.Bl -tag -offset indent -width 6n
+.It Pa /etc/cgd/wd0a
+.Bd -literal
+algorithm       adiantum;
+iv-method       encblkno1;
+keylength       256;
+verify_method	gpt;
+keygen argon2id {
+        iterations 32;
+        memory 5214;
+        parallelism 2;
+        version 19;
+        salt AAAAgLZ5QgleU2m/Ib6wiPYxz98=;
+        shared "my laptop" algorithm hkdf-hmac-sha256 \e
+            subkey AAAAQEGELNr3bj3I;
+};
+.Ed
+.It Pa /etc/cgd/wd1a
+.Bd -literal
+algorithm       adiantum;
+iv-method       encblkno1;
+keylength       256;
+verify_method	gpt;
+keygen argon2id {
+        iterations 32;
+        memory 5214;
+        parallelism 2;
+        version 19;
+        salt AAAAgLZ5QgleU2m/Ib6wiPYxz98=;
+        shared "my laptop" algorithm hkdf-hmac-sha256 \e
+            subkey AAAAQHSC15pr1Pe4;
+};
+.Ed
+.El
+.Pp
 An example
 .Pa /etc/cgd/cgd.conf :
 .Bd -literal
@@ -517,6 +596,15 @@ program's execution.
 .%I University of Luxembourg
 .%U https://www.password-hashing.net/
 .Re
+.Rs
+.%A H. Krawczyk
+.%A P. Eronen
+.%T HMAC-based Extract-and-Expand Key Derivation Function (HKDF)
+.%I Internet Engineering Task Force
+.%U https://www.rfc-editor.org/rfc/rfc5869.html
+.%N RFC 5869
+.%D May 2010
+.Re
 .Pp
 .Dq PKCS #5 v2.0: Password-Based Cryptography Standard ,
 RSA Laboratories, March 25, 1999.
@@ -526,8 +614,9 @@ The
 utility appeared in
 .Nx 2.0 .
 .Pp
+Support for
 .Li argon2id
-support appeared in
+and for shared keys appeared in
 .Nx 10.0 .
 .Sh BUGS
 Pass phrases are limited to 1023 bytes.
diff --git a/sbin/cgdconfig/cgdconfig.c b/sbin/cgdconfig/cgdconfig.c
index 0629cd9c20d4..a146528c3658 100644
--- a/sbin/cgdconfig/cgdconfig.c
+++ b/sbin/cgdconfig/cgdconfig.c
@@ -39,6 +39,7 @@ __RCSID("$NetBSD: cgdconfig.c,v 1.53 2021/11/22 14:34:35 nia Exp $");
 #ifdef HAVE_ARGON2
 #include <argon2.h>
 #endif
+#include <assert.h>
 #include <err.h>
 #include <errno.h>
 #include <fcntl.h>
@@ -66,6 +67,7 @@ __RCSID("$NetBSD: cgdconfig.c,v 1.53 2021/11/22 14:34:35 nia Exp $");
 #include <sys/resource.h>
 #include <sys/statvfs.h>
 #include <sys/bitops.h>
+#include <sys/queue.h>
 
 #include <dev/cgdvar.h>
 
@@ -76,6 +78,7 @@ __RCSID("$NetBSD: cgdconfig.c,v 1.53 2021/11/22 14:34:35 nia Exp $");
 #include "utils.h"
 #include "cgdconfig.h"
 #include "prog_ops.h"
+#include "hkdf_hmac_sha256.h"
 
 #define CGDCONFIG_CFILE		CGDCONFIG_DIR "/cgd.conf"
 
@@ -105,6 +108,19 @@ int	nflag = 0;
 #define	PFLAG_STDIN		0x04
 int	pflag = PFLAG_GETPASS;
 
+/*
+ * When configuring all cgds, save a cache of shared keys for key
+ * derivation.
+ */
+
+struct sharedkey {
+	int			 alg;
+	string_t		*id;
+	bits_t			*key;
+	LIST_ENTRY(sharedkey)	 list;
+};
+LIST_HEAD(, sharedkey) sharedkeys;
+
 static int	configure(int, char **, struct params *, int);
 static int	configure_stdin(struct params *, int argc, char **);
 static int	generate(struct params *, int, char **, const char *);
@@ -215,6 +231,8 @@ main(int argc, char **argv)
 	const char	*outfile = NULL;
 
 	setprogname(*argv);
+	if (hkdf_hmac_sha256_selftest())
+		err(EXIT_FAILURE, "Crypto self-test failed");
 	eliminate_cores();
 	if (mlockall(MCL_FUTURE))
 		err(EXIT_FAILURE, "Can't lock memory");
@@ -347,13 +365,69 @@ main(int argc, char **argv)
 }
 
 static bits_t *
-getkey(const char *dev, struct keygen *kg, size_t len)
+getsubkey_hkdf_hmac_sha256(bits_t *key, bits_t *info, size_t subkeylen)
+{
+	bits_t		*ret = NULL;
+	uint8_t		*tmp;
+
+	tmp = emalloc(BITS2BYTES(subkeylen));
+	if (hkdf_hmac_sha256(tmp, BITS2BYTES(subkeylen),
+		bits_getbuf(key), BITS2BYTES(bits_len(key)),
+		bits_getbuf(info), BITS2BYTES(bits_len(info)))) {
+		warnx("failed to derive HKDF-HMAC-SHA256 subkey");
+		goto out;
+	}
+
+	ret = bits_new(tmp, subkeylen);
+
+out:	free(tmp);
+	return ret;
+}
+
+static bits_t *
+getsubkey(int alg, bits_t *key, bits_t *info, size_t subkeylen)
+{
+
+	switch (alg) {
+	case SHARED_ALG_HKDF_HMAC_SHA256:
+		return getsubkey_hkdf_hmac_sha256(key, info, subkeylen);
+	default:
+		warnx("unrecognised shared key derivation method %d", alg);
+		return NULL;
+	}
+}
+
+static bits_t *
+getkey(const char *dev, struct keygen *kg, size_t len0)
 {
 	bits_t	*ret = NULL;
 	bits_t	*tmp;
 
-	VPRINTF(3, ("getkey(\"%s\", %p, %zu) called\n", dev, kg, len));
+	VPRINTF(3, ("getkey(\"%s\", %p, %zu) called\n", dev, kg, len0));
 	for (; kg; kg=kg->next) {
+		struct sharedkey *sk = NULL;
+		size_t len = len0;
+
+		/*
+		 * If shared, determine the shared key's length and
+		 * probe the cache of shared keys.
+		 */
+		if (kg->kg_sharedid) {
+			const char *id = string_tocharstar(kg->kg_sharedid);
+
+			len = kg->kg_sharedlen;
+			LIST_FOREACH(sk, &sharedkeys, list) {
+				if (kg->kg_sharedalg == sk->alg &&
+				    kg->kg_sharedlen == bits_len(sk->key) &&
+				    strcmp(id, string_tocharstar(sk->id)) == 0)
+					break;
+			}
+			if (sk) {
+				tmp = sk->key;
+				goto derive;
+			}
+		}
+
 		switch (kg->kg_method) {
 		case KEYGEN_STOREDKEY:
 			tmp = getkey_storedkey(dev, kg, len);
@@ -387,6 +461,32 @@ getkey(const char *dev, struct keygen *kg, size_t len)
 			return NULL;
 		}
 
+		/*
+		 * If shared, cache the key.
+		 */
+		if (kg->kg_sharedid) {
+			assert(sk == NULL);
+			sk = ecalloc(1, sizeof(*sk));
+			sk->alg = kg->kg_sharedalg;
+			sk->id = string_dup(kg->kg_sharedid);
+			sk->key = tmp;
+			LIST_INSERT_HEAD(&sharedkeys, sk, list);
+		}
+
+derive:		if (kg->kg_sharedid) {
+			/*
+			 * tmp holds the master key, owned by the
+			 * struct sharedkey record; replace it by the
+			 * derived subkey.
+			 */
+			tmp = getsubkey(kg->kg_sharedalg, tmp,
+			    kg->kg_sharedinfo, len0);
+			if (tmp == NULL) {
+				if (ret)
+					bits_free(ret);
+				return NULL;
+			}
+		}
 		if (ret)
 			ret = bits_xor_d(tmp, ret);
 		else
diff --git a/sbin/cgdconfig/cgdlex.l b/sbin/cgdconfig/cgdlex.l
index 5a4aef9ae9eb..6d596bee21ab 100644
--- a/sbin/cgdconfig/cgdlex.l
+++ b/sbin/cgdconfig/cgdlex.l
@@ -107,6 +107,8 @@ keygen_method				{ RETTOKEN(KEYGEN_METHOD); }
 keygen_salt				{ RETTOKEN(KEYGEN_SALT); }
 keygen_iterations			{ RETTOKEN(KEYGEN_ITERATIONS); }
 xor_key					{ RETTOKEN(XOR_KEY); }
+shared					{ RETTOKEN(SHARED); }
+subkey					{ RETTOKEN(SUBKEY); }
 [;\n]					{ return EOL; }
 \\\n					/* ignore a quoted nl */
 [ \t]					/* ignore spaces and tabs */
diff --git a/sbin/cgdconfig/cgdparse.y b/sbin/cgdconfig/cgdparse.y
index 5d3f70da871a..3befe95aacdc 100644
--- a/sbin/cgdconfig/cgdparse.y
+++ b/sbin/cgdconfig/cgdparse.y
@@ -67,7 +67,8 @@ static struct params *yy_global_params;
 %token <string> STRINGLIT
 
 %token <token> ALGORITHM KEYLENGTH IVMETHOD VERIFY_METHOD
-%token <token> KEYGEN SALT ITERATIONS MEMORY PARALLELISM VERSION KEY CMD
+%token <token> KEYGEN SALT ITERATIONS MEMORY PARALLELISM VERSION KEY CMD SHARED
+%token <token> SUBKEY
 
 %token EOL
 
@@ -104,6 +105,8 @@ kgvar:	  SALT bits EOL			{ $$ = keygen_salt($2); }
 	| VERSION INTEGER EOL		{ $$ = keygen_version($2); }
 	| KEY bits EOL			{ $$ = keygen_key($2); }
 	| CMD stringlit EOL		{ $$ = keygen_cmd($2); }
+	| SHARED stringlit ALGORITHM stringlit SUBKEY bits EOL
+					{ $$ = keygen_shared($2, $4, $6); }
 	| EOL				{ $$ = NULL; }
 
 stringlit:  STRINGLIT | tokstr | intstr
diff --git a/sbin/cgdconfig/hkdf_hmac_sha256.c b/sbin/cgdconfig/hkdf_hmac_sha256.c
new file mode 100644
index 000000000000..21c74f0aa147
--- /dev/null
+++ b/sbin/cgdconfig/hkdf_hmac_sha256.c
@@ -0,0 +1,273 @@
+/*	$NetBSD$	*/
+
+/*-
+ * Copyright (c) 2022 The NetBSD Foundation, Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
+ * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <sys/cdefs.h>
+__RCSID("$NetBSD$");
+
+#include <sys/sha2.h>
+
+#include <stdint.h>
+#include <string.h>
+
+#include "hkdf_hmac_sha256.h"
+
+/* RFC 2104: HMAC */
+
+struct hmacsha256 {
+	SHA256_CTX sha256;
+	uint8_t key[SHA256_BLOCK_LENGTH];
+};
+
+static void
+hmacsha256_init(struct hmacsha256 *H, const void *key, size_t keylen)
+{
+	uint8_t hkey[SHA256_DIGEST_LENGTH];
+	uint8_t ipad[SHA256_BLOCK_LENGTH];
+	unsigned i;
+
+	if (keylen > SHA256_BLOCK_LENGTH) { /* XXX should not happen here */
+		SHA256_Init(&H->sha256);
+		SHA256_Update(&H->sha256, key, keylen);
+		SHA256_Final(hkey, &H->sha256);
+		key = hkey;
+		keylen = sizeof(hkey);
+	}
+
+	memset(H->key, 0, sizeof(H->key));
+	memcpy(H->key, key, keylen);
+
+	for (i = 0; i < SHA256_BLOCK_LENGTH; i++)
+		ipad[i] = 0x36 ^ H->key[i];
+
+	SHA256_Init(&H->sha256);
+	SHA256_Update(&H->sha256, ipad, SHA256_BLOCK_LENGTH);
+
+	explicit_memset(hkey, 0, sizeof(hkey));
+	explicit_memset(ipad, 0, sizeof(ipad));
+}
+
+static void
+hmacsha256_update(struct hmacsha256 *H, const void *buf, size_t buflen)
+{
+
+	SHA256_Update(&H->sha256, buf, buflen);
+}
+
+static void
+hmacsha256_final(uint8_t h[static SHA256_DIGEST_LENGTH],
+    struct hmacsha256 *H)
+{
+	uint8_t opad[SHA256_BLOCK_LENGTH];
+	unsigned i;
+
+	for (i = 0; i < SHA256_BLOCK_LENGTH; i++)
+		opad[i] = 0x5c ^ H->key[i];
+
+	SHA256_Final(h, &H->sha256);
+	SHA256_Init(&H->sha256);
+	SHA256_Update(&H->sha256, opad, SHA256_BLOCK_LENGTH);
+	SHA256_Update(&H->sha256, h, SHA256_DIGEST_LENGTH);
+	SHA256_Final(h, &H->sha256);
+
+	explicit_memset(opad, 0, sizeof(opad));
+	explicit_memset(H, 0, sizeof(*H));
+}
+
+/* RFC 5869 HKDF, Sec. 2.3 HKDF-Expand */
+
+int
+hkdf_hmac_sha256(void *okm, size_t L,
+    const void *prk, size_t prklen,
+    const void *info, size_t infolen)
+{
+	struct hmacsha256 hmacsha256;
+	size_t n, tlen;
+	uint8_t T[SHA256_DIGEST_LENGTH], *p = okm;
+	uint8_t i;
+
+	if (L > 255*SHA256_DIGEST_LENGTH)
+		return -1;
+	if (L == 0)
+		return 0;
+
+	for (tlen = 0, i = 1; L > 0; i++, tlen = SHA256_DIGEST_LENGTH) {
+		hmacsha256_init(&hmacsha256, prk, prklen);
+		hmacsha256_update(&hmacsha256, T, tlen);
+		hmacsha256_update(&hmacsha256, info, infolen);
+		hmacsha256_update(&hmacsha256, &i, 1);
+		hmacsha256_final(T, &hmacsha256);
+		n = (L < SHA256_DIGEST_LENGTH ? L : SHA256_DIGEST_LENGTH);
+		memcpy(p, T, n);
+		p += n;
+		L -= n;
+	}
+
+	explicit_memset(T, 0, sizeof(T));
+	return 0;
+}
+
+#if 0
+#include <stdarg.h>
+#include <stdio.h>
+
+static void
+hexdump(const void *buf, size_t len, const char *fmt, ...)
+{
+	va_list va;
+	const uint8_t *p = buf;
+	size_t i;
+
+	printf("### ");
+	va_start(va, fmt);
+	vprintf(fmt, va);
+	va_end(va);
+	printf(" (%zu bytes):\n", len);
+	for (i = 0; i < len; i++) {
+		if (i % 8 == 0)
+			printf(" ");
+		printf(" %02x", p[i]);
+		if (i % 16 == 15)
+			printf("\n");
+	}
+	if (i % 16)
+		printf("\n");
+}
+#endif
+
+int
+hkdf_hmac_sha256_selftest(void)
+{
+	const struct {
+		size_t L;
+		const uint8_t *okm;
+		size_t prklen;
+		const uint8_t *prk;
+		size_t infolen;
+		const uint8_t *info;
+	} C[] = {
+		[0] = {		/* A.1 Test Case 1 with SHA-256 */
+			.L = 42,
+			.okm = (const uint8_t[]) {
+				0x3c,0xb2,0x5f,0x25, 0xfa,0xac,0xd5,0x7a,
+				0x90,0x43,0x4f,0x64, 0xd0,0x36,0x2f,0x2a,
+				0x2d,0x2d,0x0a,0x90, 0xcf,0x1a,0x5a,0x4c,
+				0x5d,0xb0,0x2d,0x56, 0xec,0xc4,0xc5,0xbf,
+				0x34,0x00,0x72,0x08, 0xd5,0xb8,0x87,0x18,
+				0x58,0x65,
+			},
+			.prklen = 32,
+			.prk = (const uint8_t[]) {
+				0x07,0x77,0x09,0x36, 0x2c,0x2e,0x32,0xdf,
+				0x0d,0xdc,0x3f,0x0d, 0xc4,0x7b,0xba,0x63,
+				0x90,0xb6,0xc7,0x3b, 0xb5,0x0f,0x9c,0x31,
+				0x22,0xec,0x84,0x4a, 0xd7,0xc2,0xb3,0xe5,
+			},
+			.infolen = 10,
+			.info = (const uint8_t[]) {
+				0xf0,0xf1,0xf2,0xf3, 0xf4,0xf5,0xf6,0xf7,
+				0xf8,0xf9,
+			},
+		},
+		[1] = {		/* A.2 Test Case 2 with SHA-256, longer I/O */
+			.L = 82,
+			.okm = (const uint8_t[]) {
+				0xb1,0x1e,0x39,0x8d, 0xc8,0x03,0x27,0xa1,
+				0xc8,0xe7,0xf7,0x8c, 0x59,0x6a,0x49,0x34,
+				0x4f,0x01,0x2e,0xda, 0x2d,0x4e,0xfa,0xd8,
+				0xa0,0x50,0xcc,0x4c, 0x19,0xaf,0xa9,0x7c,
+				0x59,0x04,0x5a,0x99, 0xca,0xc7,0x82,0x72,
+				0x71,0xcb,0x41,0xc6, 0x5e,0x59,0x0e,0x09,
+				0xda,0x32,0x75,0x60, 0x0c,0x2f,0x09,0xb8,
+				0x36,0x77,0x93,0xa9, 0xac,0xa3,0xdb,0x71,
+				0xcc,0x30,0xc5,0x81, 0x79,0xec,0x3e,0x87,
+				0xc1,0x4c,0x01,0xd5, 0xc1,0xf3,0x43,0x4f,
+				0x1d,0x87,
+			},
+			.prklen = 32,
+			.prk = (const uint8_t[]) {
+				0x06,0xa6,0xb8,0x8c, 0x58,0x53,0x36,0x1a,
+				0x06,0x10,0x4c,0x9c, 0xeb,0x35,0xb4,0x5c,
+				0xef,0x76,0x00,0x14, 0x90,0x46,0x71,0x01,
+				0x4a,0x19,0x3f,0x40, 0xc1,0x5f,0xc2,0x44,
+			},
+			.infolen = 80,
+			.info = (const uint8_t[]) {
+				0xb0,0xb1,0xb2,0xb3, 0xb4,0xb5,0xb6,0xb7,
+				0xb8,0xb9,0xba,0xbb, 0xbc,0xbd,0xbe,0xbf,
+				0xc0,0xc1,0xc2,0xc3, 0xc4,0xc5,0xc6,0xc7,
+				0xc8,0xc9,0xca,0xcb, 0xcc,0xcd,0xce,0xcf,
+				0xd0,0xd1,0xd2,0xd3, 0xd4,0xd5,0xd6,0xd7,
+				0xd8,0xd9,0xda,0xdb, 0xdc,0xdd,0xde,0xdf,
+				0xe0,0xe1,0xe2,0xe3, 0xe4,0xe5,0xe6,0xe7,
+				0xe8,0xe9,0xea,0xeb, 0xec,0xed,0xee,0xef,
+				0xf0,0xf1,0xf2,0xf3, 0xf4,0xf5,0xf6,0xf7,
+				0xf8,0xf9,0xfa,0xfb, 0xfc,0xfd,0xfe,0xff,
+			},
+		},
+		[2] = {		/* A.3 Test Case 3 with SHA-256, empty info */
+			.L = 42,
+			.okm = (const uint8_t[]) {
+				0x8d,0xa4,0xe7,0x75, 0xa5,0x63,0xc1,0x8f,
+				0x71,0x5f,0x80,0x2a, 0x06,0x3c,0x5a,0x31,
+				0xb8,0xa1,0x1f,0x5c, 0x5e,0xe1,0x87,0x9e,
+				0xc3,0x45,0x4e,0x5f, 0x3c,0x73,0x8d,0x2d,
+				0x9d,0x20,0x13,0x95, 0xfa,0xa4,0xb6,0x1a,
+				0x96,0xc8,
+			},
+			.prklen = 32,
+			.prk = (const uint8_t[]) {
+				0x19,0xef,0x24,0xa3, 0x2c,0x71,0x7b,0x16,
+				0x7f,0x33,0xa9,0x1d, 0x6f,0x64,0x8b,0xdf,
+				0x96,0x59,0x67,0x76, 0xaf,0xdb,0x63,0x77,
+				0xac,0x43,0x4c,0x1c, 0x29,0x3c,0xcb,0x04,
+			},
+			.infolen = 0,
+			.info = NULL,
+		},
+	};
+	uint8_t okm[128];
+	unsigned i;
+
+	for (i = 0; i < __arraycount(C); i++) {
+		if (hkdf_hmac_sha256(okm, C[i].L, C[i].prk, C[i].prklen,
+			C[i].info, C[i].infolen))
+			return -1;
+		if (memcmp(okm, C[i].okm, C[i].L))
+			return -1;
+	}
+
+	return 0;
+}
+
+#if 0
+int
+main(void)
+{
+	return hkdf_hmac_sha256_selftest();
+}
+#endif
diff --git a/sbin/cgdconfig/hkdf_hmac_sha256.h b/sbin/cgdconfig/hkdf_hmac_sha256.h
new file mode 100644
index 000000000000..733455a02cb8
--- /dev/null
+++ b/sbin/cgdconfig/hkdf_hmac_sha256.h
@@ -0,0 +1,40 @@
+/*	$NetBSD$	*/
+
+/*-
+ * Copyright (c) 2022 The NetBSD Foundation, Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
+ * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef	HKDF_HMAC_SHA256_H
+#define	HKDF_HMAC_SHA256_H
+
+#include <stddef.h>
+
+int hkdf_hmac_sha256(void *, size_t,
+    const void *, size_t,
+    const void *, size_t);
+
+int hkdf_hmac_sha256_selftest(void);
+
+#endif	/* HKDF_HMAC_SHA256_H */
diff --git a/sbin/cgdconfig/params.c b/sbin/cgdconfig/params.c
index c45e0bffd251..6154bafa5cb4 100644
--- a/sbin/cgdconfig/params.c
+++ b/sbin/cgdconfig/params.c
@@ -36,6 +36,8 @@ __RCSID("$NetBSD: params.c,v 1.32 2021/11/22 14:34:35 nia Exp $");
 
 #include <sys/types.h>
 #include <sys/param.h>
+
+#include <sys/sha2.h>
 #include <sys/stat.h>
 
 #include <err.h>
@@ -325,6 +327,10 @@ keygen_new(void)
 	kg->kg_salt = NULL;
 	kg->kg_key = NULL;
 	kg->kg_cmd = NULL;
+	kg->kg_sharedid = NULL;
+	kg->kg_sharedalg = SHARED_ALG_UNKNOWN;
+	kg->kg_sharedlen = (size_t)-1;
+	kg->kg_sharedinfo = NULL;
 	kg->next = NULL;
 	return kg;
 }
@@ -338,6 +344,8 @@ keygen_free(struct keygen *kg)
 	bits_free(kg->kg_salt);
 	bits_free(kg->kg_key);
 	string_free(kg->kg_cmd);
+	string_free(kg->kg_sharedid);
+	bits_free(kg->kg_sharedinfo);
 	keygen_free(kg->next);
 	free(kg);
 }
@@ -432,6 +440,8 @@ keygen_verify(const struct keygen *kg)
 			warnx("keygen [u]randomkey does not need `salt'");
 		if (kg->kg_cmd)
 			warnx("keygen [u]randomkey does not need `cmd'");
+		if (kg->kg_sharedid)
+			warnx("keygen [u]randomkey makes no sense shared");
 		break;
 	case KEYGEN_SHELL_CMD:
 		if (kg->kg_iterations != (size_t)-1)
@@ -550,6 +560,15 @@ keygen_combine(struct keygen *kg1, struct keygen *kg2)
 	if (kg2->kg_cmd)
 		string_assign(&kg1->kg_cmd, kg2->kg_cmd);
 
+	if (kg2->kg_sharedid)
+		string_assign(&kg1->kg_sharedid, kg2->kg_sharedid);
+	if (kg2->kg_sharedalg != SHARED_ALG_UNKNOWN) {
+		kg1->kg_sharedalg = kg2->kg_sharedalg;
+		kg1->kg_sharedlen = kg2->kg_sharedlen;
+	}
+	if (kg2->kg_sharedinfo)
+		bits_assign(&kg1->kg_sharedinfo, kg2->kg_sharedinfo);
+
 	return kg1;
 }
 
@@ -668,6 +687,27 @@ keygen_cmd(string_t *in)
 	return kg;
 }
 
+struct keygen *
+keygen_shared(string_t *id, string_t *alg, bits_t *info)
+{
+	struct keygen *kg = keygen_new();
+	const char *algname = string_tocharstar(alg);
+
+	if (!strcmp("hkdf-hmac-sha256", algname)) {
+		kg->kg_sharedalg = SHARED_ALG_HKDF_HMAC_SHA256;
+		kg->kg_sharedlen = 8*SHA256_DIGEST_LENGTH;
+	}
+
+	if (kg->kg_sharedalg == SHARED_ALG_UNKNOWN) {
+		warnx("unrecognized shared key derivation algorithm \"%s\"",
+		    algname);
+	}
+
+	kg->kg_sharedid = id;
+	kg->kg_sharedinfo = info;
+	return kg;
+}
+
 struct params *
 params_fget(FILE *f)
 {
@@ -806,6 +846,26 @@ print_kvpair_b64(FILE *f, int curpos, int ts, const char *key, bits_t *val)
 	string_free(str);
 }
 
+static void
+print_shared(FILE *f, int ts, struct keygen *kg)
+{
+	static const char *const sharedalgs[] = {
+		[SHARED_ALG_UNKNOWN] = "unknown",
+		[SHARED_ALG_HKDF_HMAC_SHA256] = "hkdf-hmac-sha256",
+	};
+
+	if (kg->kg_sharedid == NULL ||
+	    kg->kg_sharedalg < 0 ||
+	    (size_t)kg->kg_sharedalg >= __arraycount(sharedalgs))
+		return;
+	fprintf(f, "%*sshared \"%s\" \\\n", ts, "",
+	    string_tocharstar(kg->kg_sharedid));
+	ts += 4;
+	fprintf(f, "%*salgorithm %s \\\n", ts, "",
+	    sharedalgs[kg->kg_sharedalg]);
+	print_kvpair_b64(f, 0, ts, "subkey", kg->kg_sharedinfo);
+}
+
 int
 keygen_fput(struct keygen *kg, int ts, FILE *f)
 {
@@ -835,6 +895,7 @@ keygen_fput(struct keygen *kg, int ts, FILE *f)
 		print_kvpair_int(f, ts, "parallelism", kg->kg_parallelism);
 		print_kvpair_int(f, ts, "version", kg->kg_version);
 		print_kvpair_b64(f, 0, ts, "salt", kg->kg_salt);
+		print_shared(f, ts, kg);
 		(void)fprintf(f, "};\n");
 		break;
 #endif
@@ -842,12 +903,14 @@ keygen_fput(struct keygen *kg, int ts, FILE *f)
 		(void)fprintf(f, "pkcs5_pbkdf2 {\n");
 		print_kvpair_int(f, ts, "iterations", kg->kg_iterations);
 		print_kvpair_b64(f, 0, ts, "salt", kg->kg_salt);
+		print_shared(f, ts, kg);
 		(void)fprintf(f, "};\n");
 		break;
 	case KEYGEN_PKCS5_PBKDF2_SHA1:
 		(void)fprintf(f, "pkcs5_pbkdf2/sha1 {\n");
 		print_kvpair_int(f, ts, "iterations", kg->kg_iterations);
 		print_kvpair_b64(f, 0, ts, "salt", kg->kg_salt);
+		print_shared(f, ts, kg);
 		(void)fprintf(f, "};\n");
 		break;
 	default:
diff --git a/sbin/cgdconfig/params.h b/sbin/cgdconfig/params.h
index 754b2afb8155..f2f6fd2307d2 100644
--- a/sbin/cgdconfig/params.h
+++ b/sbin/cgdconfig/params.h
@@ -43,6 +43,10 @@ struct keygen {
 	bits_t		*kg_salt;
 	bits_t		*kg_key;
 	string_t	*kg_cmd;
+	string_t	*kg_sharedid;
+	int		 kg_sharedalg;
+	size_t		 kg_sharedlen;
+	bits_t		*kg_sharedinfo;
 	struct keygen	*next;
 };
 
@@ -78,6 +82,11 @@ struct params {
 #define VERIFY_MBR      	0x5
 #define VERIFY_GPT      	0x6
 
+/* shared key derivation methods */
+
+#define	SHARED_ALG_UNKNOWN		0x0
+#define	SHARED_ALG_HKDF_HMAC_SHA256	0x1
+
 __BEGIN_DECLS
 struct params	*params_new(void);
 void		 params_free(struct params *);
@@ -117,6 +126,7 @@ struct keygen	*keygen_parallelism(size_t);
 struct keygen	*keygen_version(size_t);
 struct keygen	*keygen_key(bits_t *);
 struct keygen	*keygen_cmd(string_t *);
+struct keygen	*keygen_shared(string_t *, string_t *, bits_t *);
 
 int		 keygen_fput(struct keygen *, int, FILE *);
 __END_DECLS
diff --git a/tests/dev/cgd/t_cgdconfig.sh b/tests/dev/cgd/t_cgdconfig.sh
index d7cccb2db08d..513dc9691072 100644
--- a/tests/dev/cgd/t_cgdconfig.sh
+++ b/tests/dev/cgd/t_cgdconfig.sh
@@ -120,8 +120,56 @@ EOF
 	    cgdconfig -t params
 }
 
+atf_test_case sharedstoredkey10
+sharedstoredkey10_head()
+{
+	atf_set descr "Test shared key generation from storedkey, 10-byte info"
+}
+sharedstoredkey10_body()
+{
+	cat <<EOF >params
+algorithm adiantum;
+iv-method encblkno1;
+keylength 256;
+verify_method none;
+keygen storedkey {
+        key AAABAAd3CTYsLjLfDdw/DcR7umOQtsc7tQ+cMSLshErXwrPl;
+        shared "helloworld" algorithm hkdf-hmac-sha256 \
+            subkey AAAAUPDx8vP09fb3+Pk=;
+};
+EOF
+	atf_check -o inline:'PLJfJfqs1XqQQ09k0DYvKi0tCpDPGlpMXbAtVuzExb8=\n' \
+	    cgdconfig -t params
+}
+
+atf_test_case sharedstoredkey80
+sharedstoredkey80_head()
+{
+	atf_set descr "Test shared key generation from storedkey, 80-byte info"
+}
+sharedstoredkey80_body()
+{
+	cat <<EOF >params
+algorithm adiantum;
+iv-method encblkno1;
+keylength 256;
+verify_method none;
+keygen storedkey {
+        key AAABAAamuIxYUzYaBhBMnOs1tFzvdgAUkEZxAUoZP0DBX8JE;
+        shared "helloworld" algorithm hkdf-hmac-sha256 \
+            subkey AAACgLCxsrO0tba3uLm6u7y9vr/AwcLDxMXGx8jJ \
+                   ysvMzc7P0NHS09TV1tfY2drb3N3e3+Dh4uPk5ebn \
+                   6Onq6+zt7u/w8fLz9PX29/j5+vv8/f7/;
+};
+EOF
+	atf_check -o inline:'sR45jcgDJ6HI5/eMWWpJNE8BLtotTvrYoFDMTBmvqXw=\n' \
+	    cgdconfig -t params
+}
+
 atf_init_test_cases()
 {
+	atf_add_test_case sharedstoredkey10
+	atf_add_test_case sharedstoredkey80
 	atf_add_test_case storedkey
 	atf_add_test_case storedkey2a
 	atf_add_test_case storedkey2b

>From 53b8844ad0535e5beec2f854671b3676249492e9 Mon Sep 17 00:00:00 2001
From: Taylor R Campbell <riastradh%NetBSD.org@localhost>
Date: Sat, 6 Aug 2022 13:39:44 +0000
Subject: [PATCH 4/5] cgdconfig(8): Verify shared keys are generated only once.

---
 distrib/sets/lists/tests/mi  |   1 +
 tests/dev/cgd/Makefile       |   3 +
 tests/dev/cgd/h_countkey.sh  |  11 ++++
 tests/dev/cgd/t_cgdconfig.sh | 104 +++++++++++++++++++++++++++++++++++
 4 files changed, 119 insertions(+)
 create mode 100644 tests/dev/cgd/h_countkey.sh

diff --git a/distrib/sets/lists/tests/mi b/distrib/sets/lists/tests/mi
index 6dc6afb2d236..fa69cae6c30d 100644
--- a/distrib/sets/lists/tests/mi
+++ b/distrib/sets/lists/tests/mi
@@ -1417,6 +1417,7 @@
 ./usr/tests/dev/cgd					tests-fs-tests		compattestfile,atf
 ./usr/tests/dev/cgd/Atffile				tests-fs-tests		compattestfile,atf
 ./usr/tests/dev/cgd/Kyuafile				tests-fs-tests		compattestfile,atf,kyua
+./usr/tests/dev/cgd/h_countkey				tests-fs-tests		compattestfile,atf
 ./usr/tests/dev/cgd/h_img2cgd				tests-obsolete		obsolete
 ./usr/tests/dev/cgd/h_img2cgd/cgd.conf			tests-obsolete		obsolete
 ./usr/tests/dev/cgd/h_img2cgd/h_img2cgd			tests-obsolete		obsolete
diff --git a/tests/dev/cgd/Makefile b/tests/dev/cgd/Makefile
index f1e4f60de63a..5a892b3ee901 100644
--- a/tests/dev/cgd/Makefile
+++ b/tests/dev/cgd/Makefile
@@ -10,6 +10,9 @@ FILESDIR=	${TESTSDIR}
 TESTS_SH+=	t_cgd
 TESTS_SH+=	t_cgdconfig
 
+SCRIPTSDIR=	${TESTSDIR}
+SCRIPTS+=	h_countkey.sh
+
 .if ${MKRUMP} != "no"
 TESTS_C+=	t_cgd_3des
 TESTS_C+=	t_cgd_adiantum
diff --git a/tests/dev/cgd/h_countkey.sh b/tests/dev/cgd/h_countkey.sh
new file mode 100644
index 000000000000..a229c140f7f7
--- /dev/null
+++ b/tests/dev/cgd/h_countkey.sh
@@ -0,0 +1,11 @@
+#!/bin/sh
+
+set -Ceu
+
+n=$(cat "$1" 2>/dev/null || echo 0)
+n=$((n + 1))
+echo $n >"$1".tmp
+mv -f "$1".tmp "$1"
+shift
+
+echo ${1+"$@"} | base64 -d
diff --git a/tests/dev/cgd/t_cgdconfig.sh b/tests/dev/cgd/t_cgdconfig.sh
index 513dc9691072..e5876a09e264 100644
--- a/tests/dev/cgd/t_cgdconfig.sh
+++ b/tests/dev/cgd/t_cgdconfig.sh
@@ -25,6 +25,8 @@
 # POSSIBILITY OF SUCH DAMAGE.
 #
 
+COUNTKEY=$(atf_get_srcdir)/h_countkey
+
 atf_test_case storedkey
 storedkey_head()
 {
@@ -166,10 +168,112 @@ EOF
 	    cgdconfig -t params
 }
 
+atf_test_case sharedstoredkeys
+sharedstoredkeys_head()
+{
+	atf_set descr "Test multiple shared key generations from stored keys"
+}
+sharedstoredkeys_body()
+{
+	cat <<EOF >wd0e
+algorithm adiantum;
+iv-method encblkno1;
+keylength 256;
+verify_method none;
+keygen storedkey {
+        key AAABAAd3CTYsLjLfDdw/DcR7umOQtsc7tQ+cMSLshErXwrPl;
+        shared "helloworld" algorithm hkdf-hmac-sha256 \
+            subkey AAAAUPDx8vP09fb3+Pk=;
+};
+EOF
+	cat <<EOF >ld1e
+algorithm adiantum;
+iv-method encblkno1;
+keylength 256;
+verify_method none;
+keygen storedkey {
+        key AAABAAd3CTYsLjLfDdw/DcR7umOQtsc7tQ+cMSLshErXwrPl;
+        shared "helloworld" algorithm hkdf-hmac-sha256 \
+            subkey AAAAQMxUtCBh7ha6mUU=;
+};
+EOF
+	cat <<EOF >cgd.conf0
+cgd0	/dev/wd0e	wd0e
+cgd1	/dev/ld1e	ld1e
+EOF
+	cat <<EOF >expected0
+/dev/wd0e: PLJfJfqs1XqQQ09k0DYvKi0tCpDPGlpMXbAtVuzExb8=
+/dev/ld1e: ADxn574yb7sVdxHphNRRdObZxntMJA/ssMuUX6SXgEY=
+EOF
+	cat <<EOF >cgd.conf1
+cgd0	/dev/ld1e	ld1e
+cgd1	/dev/wd0e	wd0e
+EOF
+	cat <<EOF >expected1
+/dev/ld1e: ADxn574yb7sVdxHphNRRdObZxntMJA/ssMuUX6SXgEY=
+/dev/wd0e: PLJfJfqs1XqQQ09k0DYvKi0tCpDPGlpMXbAtVuzExb8=
+EOF
+	atf_check -o file:expected0 cgdconfig -T -f cgd.conf0
+	atf_check -o file:expected1 cgdconfig -T -f cgd.conf1
+}
+
+atf_test_case sharedshellkeys
+sharedshellkeys_head()
+{
+	atf_set descr "Test multiple shared key generations from shell_cmd"
+}
+sharedshellkeys_body()
+{
+	cat <<EOF >wd0e
+algorithm adiantum;
+iv-method encblkno1;
+keylength 256;
+verify_method none;
+keygen shell_cmd {
+        cmd "${COUNTKEY} n B3cJNiwuMt8N3D8NxHu6Y5C2xzu1D5wxIuyEStfCs+U=";
+        shared "helloworld" algorithm hkdf-hmac-sha256 \
+            subkey AAAAUPDx8vP09fb3+Pk=;
+};
+EOF
+	cat <<EOF >ld1e
+algorithm adiantum;
+iv-method encblkno1;
+keylength 256;
+verify_method none;
+keygen shell_cmd {
+        cmd "${COUNTKEY} n B3cJNiwuMt8N3D8NxHu6Y5C2xzu1D5wxIuyEStfCs+U=";
+        shared "helloworld" algorithm hkdf-hmac-sha256 \
+            subkey AAAAQMxUtCBh7ha6mUU=;
+};
+EOF
+	cat <<EOF >cgd.conf0
+cgd0	/dev/wd0e	wd0e
+cgd1	/dev/ld1e	ld1e
+EOF
+	cat <<EOF >expected0
+/dev/wd0e: PLJfJfqs1XqQQ09k0DYvKi0tCpDPGlpMXbAtVuzExb8=
+/dev/ld1e: ADxn574yb7sVdxHphNRRdObZxntMJA/ssMuUX6SXgEY=
+EOF
+	cat <<EOF >cgd.conf1
+cgd0	/dev/ld1e	ld1e
+cgd1	/dev/wd0e	wd0e
+EOF
+	cat <<EOF >expected1
+/dev/ld1e: ADxn574yb7sVdxHphNRRdObZxntMJA/ssMuUX6SXgEY=
+/dev/wd0e: PLJfJfqs1XqQQ09k0DYvKi0tCpDPGlpMXbAtVuzExb8=
+EOF
+	atf_check -o file:expected0 cgdconfig -T -f cgd.conf0
+	atf_check -o inline:'1\n' cat n
+	atf_check -o file:expected1 cgdconfig -T -f cgd.conf1
+	atf_check -o inline:'2\n' cat n
+}
+
 atf_init_test_cases()
 {
+	atf_add_test_case sharedshellkeys
 	atf_add_test_case sharedstoredkey10
 	atf_add_test_case sharedstoredkey80
+	atf_add_test_case sharedstoredkeys
 	atf_add_test_case storedkey
 	atf_add_test_case storedkey2a
 	atf_add_test_case storedkey2b

>From 1682af53f4cb627a68990ea6e17334b5c1a2b513 Mon Sep 17 00:00:00 2001
From: Taylor R Campbell <riastradh%NetBSD.org@localhost>
Date: Sat, 6 Aug 2022 16:12:46 +0000
Subject: [PATCH 5/5] cgdconfig(8): Add support for generating shared-key
 parameters files.

Usage model:

- Generate a parameters file that supports sharing its main key:

	cgdconfig -g -S -o /etc/cgd/wd0e -V gpt adiantum

- Make another parameters file that uses the same shared main key but
  derives an independent subkey from it:

	cgdconfig -g -S -P /etc/cgd/wd0e -o /etc/cgd/ld1e \
	    -V disklabel aes-cbc 256
---
 sbin/cgdconfig/cgdconfig.8 |  32 +++++++++-
 sbin/cgdconfig/cgdconfig.c |  78 +++++++++++++++++++++----
 sbin/cgdconfig/params.c    | 117 +++++++++++++++++++++++++++++++++++++
 sbin/cgdconfig/params.h    |   3 +
 4 files changed, 217 insertions(+), 13 deletions(-)

diff --git a/sbin/cgdconfig/cgdconfig.8 b/sbin/cgdconfig/cgdconfig.8
index e9e5050d1b38..408e597878e5 100644
--- a/sbin/cgdconfig/cgdconfig.8
+++ b/sbin/cgdconfig/cgdconfig.8
@@ -52,11 +52,12 @@
 .Ar paramsfile
 .Nm
 .Fl g
-.Op Fl v
+.Op Fl Sv
 .Op Fl V Ar vmeth
 .Op Fl i Ar ivmeth
 .Op Fl k Ar kgmeth
 .Op Fl o Ar outfile
+.Op Fl P Ar paramsfile
 .Ar alg
 .Op Ar keylen
 .Nm
@@ -138,6 +139,13 @@ store it in
 If
 .Fl o
 is not given, any paramsfile content is written to standard output.
+.It Fl P Ar paramsfile
+With the
+.Fl S
+option for the
+.Fl g
+action, specify a parameters file with a shared key to reuse for
+deriving this one as a subkey.
 .It Fl p
 Read all passphrases from stdin rather than
 .Pa /dev/tty .
@@ -147,6 +155,15 @@ are prompted.
 If this flag is specified then verification errors will cause the device
 in question to be unconfigured rather than prompting for the passphrase
 again.
+.It Fl S
+When generating a parameters file with
+.Fl g ,
+arrange to use a subkey of a shared key.
+If
+.Fl P Ar paramsfile
+is also specified, reuse the shared key of
+.Ar paramsfile ;
+otherwise a new one will be generated.
 .It Fl s
 Read the key (nb: not the passphrase) from stdin.
 .It Fl T
@@ -485,6 +502,19 @@ parameters file:
 	new file's passphrase:
 .Ed
 .Pp
+To create parameters files for three disks with subkeys derived from a
+shared password-based key:
+.Bd -literal
+	# cgdconfig -g -S -k argon2id -o /etc/cgd/wd0 -V gpt adiantum
+	# cgdconfig -g -S -P /etc/cgd/wd0 -o /etc/cgd/ld1 \e
+	      -V disklabel aes-cbc 256
+.Ed
+.Pp
+Listing these in the same
+.Pa /etc/cgd/cgd.conf
+will allow you to enter a password once to decrypt both disks with
+.Cm cgdconfig -C .
+.Pp
 To configure a cgd that uses aes-cbc with a 192 bit key that it
 reads from stdin:
 .Bd -literal
diff --git a/sbin/cgdconfig/cgdconfig.c b/sbin/cgdconfig/cgdconfig.c
index a146528c3658..b609562ff3cc 100644
--- a/sbin/cgdconfig/cgdconfig.c
+++ b/sbin/cgdconfig/cgdconfig.c
@@ -100,6 +100,10 @@ enum action {
 
 int	nflag = 0;
 
+/* if Sflag is set, generate shared keys */
+
+int	Sflag = 0;
+
 /* if pflag is set to PFLAG_STDIN read from stdin rather than getpass(3) */
 
 #define	PFLAG_GETPASS		0x01
@@ -123,7 +127,8 @@ LIST_HEAD(, sharedkey) sharedkeys;
 
 static int	configure(int, char **, struct params *, int);
 static int	configure_stdin(struct params *, int argc, char **);
-static int	generate(struct params *, int, char **, const char *);
+static int	generate(struct params *, int, char **, const char *,
+		    const char *);
 static int	generate_convert(struct params *, int, char **, const char *);
 static int	unconfigure(int, char **, struct params *, int);
 static int	do_all(const char *, int, char **,
@@ -176,8 +181,8 @@ usage(void)
 	    getprogname());
 	(void)fprintf(stderr, "       %s -G [-enpv] [-i ivmeth] [-k kgmeth] "
 	    "[-o outfile] paramsfile\n", getprogname());
-	(void)fprintf(stderr, "       %s -g [-v] [-i ivmeth] [-k kgmeth] "
-	    "[-o outfile] alg [keylen]\n", getprogname());
+	(void)fprintf(stderr, "       %s -g [-Sv] [-i ivmeth] [-k kgmeth] "
+	    "[-P paramsfile] [-o outfile] alg [keylen]\n", getprogname());
 	(void)fprintf(stderr, "       %s -l [-v[v]] [cgd]\n", getprogname());
 	(void)fprintf(stderr, "       %s -s [-nv] [-i ivmeth] cgd dev alg "
 	    "[keylen]\n", getprogname());
@@ -229,6 +234,7 @@ main(int argc, char **argv)
 	int	ch;
 	const char	*cfile = NULL;
 	const char	*outfile = NULL;
+	const char	*Pfile = NULL;
 
 	setprogname(*argv);
 	if (hkdf_hmac_sha256_selftest())
@@ -239,7 +245,7 @@ main(int argc, char **argv)
 	p = params_new();
 	kg = NULL;
 
-	while ((ch = getopt(argc, argv, "CGTUV:b:ef:gi:k:lno:sptuv")) != -1)
+	while ((ch = getopt(argc, argv, "CGP:STUV:b:ef:gi:k:lno:sptuv")) != -1)
 		switch (ch) {
 		case 'C':
 			set_action(&action, ACTION_CONFIGALL);
@@ -247,6 +253,14 @@ main(int argc, char **argv)
 		case 'G':
 			set_action(&action, ACTION_GENERATE_CONVERT);
 			break;
+		case 'P':
+			if (Pfile)
+				usage();
+			Pfile = estrdup(optarg);
+			break;
+		case 'S':
+			Sflag = 1;
+			break;
 		case 'T':
 			set_action(&action, ACTION_PRINTALLKEYS);
 			break;
@@ -335,6 +349,17 @@ main(int argc, char **argv)
 		err(1, "init failed");
 
 	/* validate the consistency of the arguments */
+	if (Pfile != NULL && action != ACTION_GENERATE) {
+		warnx("-P is only for use with -g action");
+		usage();
+	}
+	if (Pfile != NULL && !Sflag) {
+		warnx("-P only makes sense with -S flag");
+	}
+	if (Sflag && action != ACTION_GENERATE) {
+		warnx("-S is only for use with -g action");
+		usage();
+	}
 
 	switch (action) {
 	case ACTION_DEFAULT:	/* ACTION_CONFIGURE is the default */
@@ -343,7 +368,7 @@ main(int argc, char **argv)
 	case ACTION_UNCONFIGURE:
 		return unconfigure(argc, argv, NULL, CONFIG_FLAGS_FROMMAIN);
 	case ACTION_GENERATE:
-		return generate(p, argc, argv, outfile);
+		return generate(p, argc, argv, outfile, Pfile);
 	case ACTION_GENERATE_CONVERT:
 		return generate_convert(p, argc, argv, outfile);
 	case ACTION_CONFIGALL:
@@ -1196,7 +1221,8 @@ verify_reenter(struct params *p)
 }
 
 static int
-generate(struct params *p, int argc, char **argv, const char *outfile)
+generate(struct params *p, int argc, char **argv, const char *outfile,
+    const char *Pfile)
 {
 	int	 ret;
 
@@ -1218,15 +1244,43 @@ generate(struct params *p, int argc, char **argv, const char *outfile)
 	if (ret)
 		return ret;
 
-	if (!p->keygen) {
-		p->keygen = keygen_generate(KEYGEN_PKCS5_PBKDF2_SHA1);
-		if (!p->keygen)
+	if (Pfile) {
+		struct params *pp;
+
+		pp = params_cget(Pfile);
+		if (pp == NULL)
+			return -1;
+		if (!params_verify(pp)) {
+			params_free(pp);
+			warnx("invalid parameters file \"%s\"", Pfile);
+			return -1;
+		}
+		p = params_combine(pp, p);
+		keygen_stripstored(&p->keygen);
+		if (!p->keygen) {
+			warnx("no keygen in parameters file \"%s\"", Pfile);
 			return -1;
+		}
+	} else {
+		if (!p->keygen) {
+			p->keygen = keygen_generate(KEYGEN_PKCS5_PBKDF2_SHA1);
+			if (!p->keygen)
+				return -1;
+		}
+
+		if (keygen_filldefaults(p->keygen, p->keylen)) {
+			warnx("Failed to generate defaults for keygen");
+			return -1;
+		}
 	}
 
-	if (keygen_filldefaults(p->keygen, p->keylen)) {
-		warnx("Failed to generate defaults for keygen");
-		return -1;
+	if (Sflag) {
+		if (Pfile)
+			ret = keygen_tweakshared(p->keygen);
+		else
+			ret = keygen_makeshared(p->keygen);
+		if (ret)
+			return ret;
 	}
 
 	if (!params_verify(p)) {
diff --git a/sbin/cgdconfig/params.c b/sbin/cgdconfig/params.c
index 6154bafa5cb4..ff0c6d34fde5 100644
--- a/sbin/cgdconfig/params.c
+++ b/sbin/cgdconfig/params.c
@@ -46,6 +46,7 @@ __RCSID("$NetBSD: params.c,v 1.32 2021/11/22 14:34:35 nia Exp $");
 #include <stdlib.h>
 #include <string.h>
 #include <util.h>
+#include <uuid.h>
 
 #ifdef HAVE_ARGON2
 #include <argon2.h>
@@ -524,6 +525,122 @@ keygen_filldefaults(struct keygen *kg, size_t keylen)
 	return keygen_filldefaults(kg->next, keylen);
 }
 
+/*
+ * Strip the storedkey entries in preparation for inserting a shared
+ * clause with a newly generated info string to derive this key from
+ * KDF.  The result is that the key generated here is independent of
+ * whatever storedkeys were involved in the old one, so there is no
+ * need to keep them around,
+ */
+void
+keygen_stripstored(struct keygen **kgp)
+{
+	struct keygen *kg, *to_free = NULL;
+
+	while ((kg = *kgp) != NULL) {
+		if (kg->kg_method == KEYGEN_STOREDKEY) {
+			*kgp = kg->next;
+			kg->next = to_free;
+			to_free = kg;
+		} else {
+			kgp = &kg->next;
+		}
+	}
+	keygen_free(to_free);
+}
+
+int
+keygen_makeshared(struct keygen *kg0)
+{
+	struct keygen *kg;
+
+	for (kg = kg0; kg != NULL; kg = kg->next) {
+		switch (kg->kg_method) {
+		case KEYGEN_RANDOMKEY:
+		case KEYGEN_URANDOMKEY:
+			warnx("(u)randomkey keygen cannot be shared");
+			return -1;
+		case KEYGEN_SHELL_CMD:
+#ifdef HAVE_ARGON2
+		case KEYGEN_ARGON2ID:
+#endif
+		case KEYGEN_PKCS5_PBKDF2_OLD:
+		case KEYGEN_PKCS5_PBKDF2_SHA1:
+			break;
+		case KEYGEN_STOREDKEY:
+			warnx("storedkey does not make sense as shared");
+			return -1;
+		default:
+			return -1;
+		}
+		if (kg->kg_sharedid != NULL) {
+			warnx("keygen already shared");
+			return -1;
+		}
+	}
+	for (kg = kg0; kg != NULL; kg = kg->next) {
+		struct uuid id;
+		char *idstr;
+		uint32_t status;
+
+		if (uuidgen(&id, 1) == -1) {
+			warn("uuidgen");
+			return -1;
+		}
+		uuid_to_string(&id, &idstr, &status);
+		if (status != uuid_s_ok) {
+			warnx("uuid_to_string: %"PRIu32, status);
+			return -1;
+		}
+
+		kg->kg_sharedid = string_fromcharstar(idstr);
+		kg->kg_sharedalg = SHARED_ALG_HKDF_HMAC_SHA256;
+		kg->kg_sharedlen = 8*SHA256_DIGEST_LENGTH;
+		kg->kg_sharedinfo = bits_getrandombits(DEFAULT_SALTLEN, 0);
+
+		free(idstr);
+	}
+	return 0;
+}
+
+int
+keygen_tweakshared(struct keygen *kg0)
+{
+	struct keygen *kg;
+
+	for (kg = kg0; kg != NULL; kg = kg->next) {
+		switch (kg->kg_method) {
+		case KEYGEN_RANDOMKEY:
+		case KEYGEN_URANDOMKEY:
+			warnx("(u)randomkey keygen cannot be shared");
+			return -1;
+		case KEYGEN_SHELL_CMD:
+#ifdef HAVE_ARGON2
+		case KEYGEN_ARGON2ID:
+#endif
+		case KEYGEN_PKCS5_PBKDF2_OLD:
+		case KEYGEN_PKCS5_PBKDF2_SHA1:
+			break;
+		case KEYGEN_STOREDKEY:
+			warnx("storedkey does not make sense as shared");
+			return -1;
+		default:
+			return -1;
+		}
+		if (kg->kg_sharedid == NULL) {
+			warnx("keygen not shared");
+			return -1;
+		}
+	}
+	for (kg = kg0; kg != NULL; kg = kg->next) {
+		if (kg->kg_method == KEYGEN_STOREDKEY)
+			continue;
+		bits_free(kg->kg_sharedinfo);
+		kg->kg_sharedinfo = bits_getrandombits(DEFAULT_SALTLEN, 0);
+	}
+	return 0;
+}
+
 struct keygen *
 keygen_combine(struct keygen *kg1, struct keygen *kg2)
 {
diff --git a/sbin/cgdconfig/params.h b/sbin/cgdconfig/params.h
index f2f6fd2307d2..74e32cfc5eb9 100644
--- a/sbin/cgdconfig/params.h
+++ b/sbin/cgdconfig/params.h
@@ -112,6 +112,9 @@ struct keygen	*keygen_new(void);
 void		 keygen_free(struct keygen *);
 
 int		 keygen_filldefaults(struct keygen *, size_t);
+void		 keygen_stripstored(struct keygen **);
+int		 keygen_makeshared(struct keygen *);
+int		 keygen_tweakshared(struct keygen *);
 int		 keygen_verify(const struct keygen *);
 void		 keygen_addlist(struct keygen **, struct keygen *);
 
.\" $NetBSD: cgdconfig.8,v 1.52 2021/12/04 15:03:58 nia Exp $
.\"
.\" Copyright (c) 2002, The NetBSD Foundation, Inc.
.\" All rights reserved.
.\"
.\" This code is derived from software contributed to The NetBSD Foundation
.\" by Roland C. Dowdeswell.
.\"
.\" Redistribution and use in source and binary forms, with or without
.\" modification, are permitted provided that the following conditions
.\" are met:
.\" 1. Redistributions of source code must retain the above copyright
.\"    notice, this list of conditions and the following disclaimer.
.\" 2. Redistributions in binary form must reproduce the above copyright
.\"    notice, this list of conditions and the following disclaimer in the
.\"    documentation and/or other materials provided with the distribution.
.\"
.\" THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
.\" ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
.\" TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
.\" PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
.\" BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
.\" CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
.\" SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
.\" INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
.\" CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
.\" ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
.\" POSSIBILITY OF SUCH DAMAGE.
.\"
.Dd November 4, 2021
.Dt CGDCONFIG 8
.Os
.Sh NAME
.Nm cgdconfig
.Nd configuration utility for the cryptographic disk driver
.Sh SYNOPSIS
.Nm
.Op Fl enpv
.Op Fl V Ar vmeth
.Ar cgd dev
.Op Ar paramsfile
.Nm
.Fl C
.Op Fl enpv
.Op Fl f Ar configfile
.Nm
.Fl G
.Op Fl enpv
.Op Fl i Ar ivmeth
.Op Fl k Ar kgmeth
.Op Fl o Ar outfile
.Ar paramsfile
.Nm
.Fl g
.Op Fl Sv
.Op Fl V Ar vmeth
.Op Fl i Ar ivmeth
.Op Fl k Ar kgmeth
.Op Fl o Ar outfile
.Op Fl P Ar paramsfile
.Ar alg
.Op Ar keylen
.Nm
.Fl T
.Op Fl f Ar configfile
.Nm
.Fl t
.Ar paramsfile
.Nm
.Fl l
.Op Fl v Ns Op Cm v
.Op Ar cgd
.Nm
.Fl s
.Op Fl nv
.Op Fl i Ar ivmeth
.Ar cgd
.Ar dev
.Ar alg
.Op Ar keylen
.Nm
.Fl U
.Op Fl nv
.Op Fl f Ar configfile
.Nm
.Fl u
.Op Fl nv
.Ar cgd
.Sh DESCRIPTION
.Nm
is used to configure and unconfigure cryptographic disk devices (cgds)
and to maintain the configuration files that are associated with them.
For more information about cryptographic disk devices see
.Xr cgd 4 .
.Pp
The options are as follows:
.Bl -tag -width configfilexxxx
.It Fl C
Configure all the devices listed in the cgd configuration file.
.It Fl e
Echo the passphrase.
.It Fl f Ar configfile
Specify the configuration file explicitly, rather than using the default
configuration file
.Pa /etc/cgd/cgd.conf .
.It Fl G
Generate a new paramsfile (to stdout) using the values from
.Ar paramsfile
which will generate the same key.
This may need to obtain multiple passphrases.
.It Fl g
Generate a paramsfile (to stdout).
.It Fl i Ar ivmeth
Specify the IV method (default: encblkno1).
.Pp
Setting the IV method is needed only for compatibility with disks
written with a very old version of
.Xr cgd 4
from before
.Nx 5.0 ,
released in 2010; see
.Xr cgd 4
for details.
.It Fl k Ar kgmeth
Specify the key generation method (default: pkcs5_pbkdf2/sha1).
.It Fl l Op Ar cgd
List state of all devices or just the one
.Ar cgd
device.
The verbosity level affects the output.
.It Fl n
Do not actually configure or unconfigure a cryptographic disk
device, but instead report the steps that would be taken.
.It Fl o Ar outfile
When generating a
.Ar paramsfile ,
store it in
.Ar outfile .
If
.Fl o
is not given, any paramsfile content is written to standard output.
.It Fl P Ar paramsfile
With the
.Fl S
option for the
.Fl g
action, specify a parameters file with a shared key to reuse for
deriving this one as a subkey.
.It Fl p
Read all passphrases from stdin rather than
.Pa /dev/tty .
Passphrases are separated by newlines.
Users of this flag must be able to predict the order in which passphrases
are prompted.
If this flag is specified then verification errors will cause the device
in question to be unconfigured rather than prompting for the passphrase
again.
.It Fl S
When generating a parameters file with
.Fl g ,
arrange to use a subkey of a shared key.
If
.Fl P Ar paramsfile
is also specified, reuse the shared key of
.Ar paramsfile ;
otherwise a new one will be generated.
.It Fl s
Read the key (nb: not the passphrase) from stdin.
.It Fl T
Generate all keys for all the devices listed in the
.Nm
configuration file and print them to standard output encoded in
base64.
.It Fl t
Generate the key and print it to standard output encoded in base64.
.It Fl U
Unconfigure all the devices listed in the cgd configuration file.
.It Fl u
Unconfigure a cgd.
.It Fl V Ar vmeth
Specify the verification method (default: none).
.It Fl v
Be verbose.
May be specified multiple times.
.El
.Pp
For more information about the cryptographic algorithms supported,
please refer to
.Xr cgd 4 .
.Ss Key Generation Methods
To generate the key which it will use,
.Nm
evaluates all of the key generation methods in the parameters file
and uses the exclusive-or of the outputs of all the methods.
The methods and descriptions are as follows:
.Bl -tag -width indentxxxxxxxxxxx
.It argon2id
This method requires a passphrase which is entered at configuration
time.
Argon2 is a memory-hard password hashing scheme and winner of the
2013-2015 Password Hashing Competition.
It has numerous parameters allowing its hardness to scale with the
performance of the system.
Recommended for passphrase-based initialization.
.It pkcs5_pbkdf2/sha1
This method requires a passphrase which is entered at configuration
time.
It is a salted hmac-based scheme detailed in
.Dq PKCS#5 v2.0: Password-Based Cryptography Standard ,
RSA Laboratories, March 25, 1999, pages 8-10.
PKCS #5 was also republished as RFC 2898.
.It pkcs5_pbkdf2
This is an earlier, slightly incorrect and deprecated implementation
of the above algorithm.
It is retained for backwards compatibility with existing parameters
files, and will be removed.
Existing parameters files should be
converted to use the correct method using the
.Fl G
option, and a new passphrase.
.It storedkey
This method stores its key in the parameters file.
.It randomkey
The method simply reads
.Pa /dev/random
and uses the resulting bits as the key.
It does not require a passphrase to be entered.
This method is typically used to present disk devices that do not
need to survive a reboot.
It is also handy to facilitate overwriting the contents of
a disk volume with meaningless data prior to use.
.It urandomkey
The method simply reads
.Pa /dev/urandom
and uses the resulting bits as the key.
This is similar to the
.Pa randomkey
method, but it guarantees that
.Nm
will not stall waiting for 256 bits of entropy from a hardware RNG
or seed.
.It shell_cmd
This method executes a shell command via
.Xr popen 3
and reads the key from stdout.
.El
.Ss Verification Method
The verification method is how
.Nm
determines if the generated key is correct.
If the newly configured disk fails to verify, then
.Nm
will regenerate the key and re-configure the device.
It only makes sense to specify a verification method if at least one of the
key generation methods is error prone, e.g., uses a user-entered passphrase.
The following verification methods are supported:
.Pp
.Bl -tag -width indentxxx -compact
.It none
perform no verification.
.It disklabel
scan for a valid disklabel.
.It mbr
scan for a valid Master Boot Record.
.It gpt
scan for a valid GUID partition table.
.It ffs
scan for a valid FFS file system.
.It re-enter
prompt for passphrase twice, and ensure entered passphrases are
identical.
This method only works with the argon2id, pkcs5_pbkdf2/sha1, and
pkcs5_pbkdf2 key generators.
.El
.Ss /etc/cgd/cgd.conf
The file
.Pa /etc/cgd/cgd.conf
is used to configure
.Nm
if either of
.Fl C
or
.Fl U
are specified.
Each line of the file is composed of either two or three
tokens: cgd, target, and optional paramsfile.
.Pp
A
.Sq \&#
character is interpreted as a comment and indicates that the
rest of the line should be ignored.
A
.Sq \e
at the end of a line indicates that the next line is a continuation of
the current line.
.Pp
If the second field is of the form
.Dq NAME=<value>
then all the
.Xr dk 4
wedge partitions are searched for one that has a wedge name equal to
.Ar <value>
and the device corresponding to it is selected.
.Pp
If the second field starts with the prefix
.Dq ROOT.
the prefix is replaced with
.Dq /dev/[root_device] ,
where
.Bq root_device
is the value of the
.Dq kern.root_device
sysctl.
.Pp
See
.Sx EXAMPLES
for an example of
.Pa /etc/cgd/cgd.conf .
.Ss Parameters File
The Parameters File contains the required information to generate the
key and configure a device.
These files are typically generated by the
.Fl g
flag and not edited by hand.
When a device is configured the default parameters file is constructed
by taking the basename of the target disk and prepending
.Pa /etc/cgd/
to it.
E.g., if the target is
.Pa /dev/sd0h ,
then the default parameters file will be
.Pa /etc/cgd/sd0h .
.Pp
It is possible to have more than one parameters file for a given
disk which use different key generation methods but will generate
the same key.
To create a parameters file that is equivalent to an existing parameters
file, use
.Nm
with the
.Fl G
flag.
See
.Sx EXAMPLES
for an example of this usage.
.Pp
The parameters file contains a list of statements each terminated
with a semi-colon.
Some statements can contain statement-blocks which are either a
single unadorned statement, or a brace-enclosed list of semicolon
terminated statements.
Three types of data are understood:
.Pp
.Bl -tag -compact -width integerxx
.It integer
a 32 bit signed integer.
.It string
a string.
.It base64
a length-encoded base64 string.
.El
.Pp
The following statements are defined:
.Bl -tag -width indentxx
.It algorithm Ar string
Defines the cryptographic algorithm.
.It iv-method Ar string
Defines the IV generation method.
This should always be
.Sq encblkno1
except when dealing with disks written with a very old version of
.Xr cgd 4
from before
.Nx 5.0 ,
released in 2010; see
.Xr cgd 4
for details.
.It keylength Ar integer
Defines the length of the key.
.It verify_method Ar string
Defines the verification method.
.It keygen Ar string Ar statement_block
Defines a key generation method.
The
.Ar statement_block
contains statements that are specific to the key generation method.
.El
.Pp
The keygen statement's statement block may contain the following statements:
.Bl -tag -width indentxx
.It key Ar string
The key.
Only used for the storedkey key generation method.
.It cmd Ar string
The command to execute.
Only used for the shell_cmd key generation method.
.It iterations Ar integer
The number of iterations.
Only used for argon2id, pkcs5_pbkdf2/sha1, and pkcs5_pbkdf2.
.It salt Ar base64
The salt.
Only used for argon2id, pkcs5_pbkdf2/sha1, and pkcs5_pbkdf2.
.It memory Ar integer
Memory consumption in kilobytes.
Only used for argon2id.
.It parallelism Ar integer
Number of threads to use to compute the password hash.
Should be equivalent to the number of CPUs/hardware threads.
Only used for argon2id.
.It version Ar integer
Version of Argon2 to use.
Should be the most recent version, currently
.Dv 19 .
Only used for argon2id.
.It shared Ar name No algorithm Ar kdf No subkey Ar info
Makes the key generation take an extra step to derive a subkey from the
main key using the key derivation function
.Ar kdf
with input
.Ar info .
.Pp
This enables a single password entry, for example, to decrypt multiple
disks that use different keys, each derived as a subkey from the main
key generated from the password.
.Bl -tag -width 6n
.It Ar name
A string used to identify the same main key generation shared between
parameters files for different disks listed in a single
.Pa cgd.conf
configuration file.
.It Ar kdf
The name of a key derivation function used to derive a subkey from the
main key.
Supported values:
.Bl -tag -width 6n -offset indent
.It Li hkdf-hmac-sha256
The HKDF-Expand function of RFC 5869, instantiated with SHA-256.
.El
.It Ar info
A base64 length-encoded string to distinguish different subkeys derived
from a shared main key.
Need not be secret.
For example, it could be a nickname, or the disk's World-Wide Name, or
a UUID generated for the disk, or just a random string.
.El
.Pp
It is an error to reuse a shared key
.Ar name
with different keygen blocks, other than the
.Ar info
parameter,
between parameters files used by a single
.Pa cgd.conf
configuration file.
.El
.Sh FILES
.Bl -tag -width indentxxxxxxxxxxxxxxxxxx -compact
.It Pa /etc/cgd/
configuration directory, used to store paramsfiles.
.It Pa /etc/cgd/cgd.conf
cgd configuration file.
.El
.Sh EXAMPLES
To set up and configure a cgd that uses adiantum, which takes a 256-bit
key:
.Bd -literal
	# cgdconfig -g -k argon2id -o /etc/cgd/wd0e adiantum 256
	# cgdconfig cgd0 /dev/wd0e
	/dev/wd0e's passphrase:
.Ed
.Pp
When using verification methods, the first time that we configure the
disk the verification method will fail.
We overcome this by supplying
.Fl V Ar re-enter
when we configure the first time to set up the disk.
Here is the
sequence of commands that is recommended:
.Bd -literal
	# cgdconfig -g -k argon2id -o /etc/cgd/dk3 -V gpt adiantum
	# cgdconfig -V re-enter cgd0 /dev/dk3
	/dev/dk3's passphrase:
	re-enter device's passphrase:
	# gpt create cgd0
	# cgdconfig -u cgd0
	# cgdconfig cgd0 /dev/dk3
	/dev/dk3's passphrase:
.Ed
.Pp
To scrub data from a disk before setting up a cgd:
.Bd -literal
	# cgdconfig -s cgd0 /dev/sd0e adiantum 256 < /dev/urandom
	# dd if=/dev/zero of=/dev/rcgd0d bs=32k progress=512
	# cgdconfig -u cgd0
.Ed
.Pp
To create a new parameters file that will generate the same key as an old
parameters file:
.Bd -literal
	# cgdconfig -G -o newparamsfile oldparamsfile
	old file's passphrase:
	new file's passphrase:
.Ed
.Pp
To create parameters files for three disks with subkeys derived from a
shared password-based key:
.Bd -literal
	# cgdconfig -g -S -k argon2id -o /etc/cgd/wd0 -V gpt adiantum
	# cgdconfig -g -S -P /etc/cgd/wd0 -o /etc/cgd/ld1 \e
	      -V disklabel aes-cbc 256
.Ed
.Pp
Listing these in the same
.Pa /etc/cgd/cgd.conf
will allow you to enter a password once to decrypt both disks with
.Cm cgdconfig -C .
.Pp
To configure a cgd that uses aes-cbc with a 192 bit key that it
reads from stdin:
.Bd -literal
	# cgdconfig -s cgd0 /dev/sd0h aes-cbc 192
.Ed
.Pp
An example parameters file which uses PKCS#5 PBKDF2:
.Bd -literal
	algorithm aes-cbc;
	iv-method encblkno1;
	keylength 128;
	verify_method none;
	keygen pkcs5_pbkdf2/sha1 {
		iterations 39361;
		salt AAAAgMoHiYonye6Kog \e
		     dYJAobCHE=;
	};
.Ed
.Pp
An example parameters file which stores its key locally:
.Bd -literal
	algorithm       adiantum;
	iv-method       encblkno1;
	keylength       256;
	verify_method   none;
	keygen storedkey key AAABAK3QO6d7xzLfrXTdsgg4 \e
			     ly2TdxkFqOkYYcbyUKu/f60L;
.Ed
.Pp
An example pair of configuration files which use shared keys so they
can be derived from a single passphrase entry, with the 64-bit
World-Wide Name of each disk (base64 length-encoded) as its subkey
info:
.Bl -tag -offset indent -width 6n
.It Pa /etc/cgd/wd0a
.Bd -literal
algorithm       adiantum;
iv-method       encblkno1;
keylength       256;
verify_method	gpt;
keygen argon2id {
        iterations 32;
        memory 5214;
        parallelism 2;
        version 19;
        salt AAAAgLZ5QgleU2m/Ib6wiPYxz98=;
        shared "my laptop" algorithm hkdf-hmac-sha256 \e
            subkey AAAAQEGELNr3bj3I;
};
.Ed
.It Pa /etc/cgd/wd1a
.Bd -literal
algorithm       adiantum;
iv-method       encblkno1;
keylength       256;
verify_method	gpt;
keygen argon2id {
        iterations 32;
        memory 5214;
        parallelism 2;
        version 19;
        salt AAAAgLZ5QgleU2m/Ib6wiPYxz98=;
        shared "my laptop" algorithm hkdf-hmac-sha256 \e
            subkey AAAAQHSC15pr1Pe4;
};
.Ed
.El
.Pp
An example
.Pa /etc/cgd/cgd.conf :
.Bd -literal
	#
	# /etc/cgd/cgd.conf
	# Configuration file for cryptographic disk devices
	#

	# cgd		target		[paramsfile]
	cgd0		/dev/wd0e
	cgd1		NAME=mycgd	/usr/local/etc/cgd/mycgd
.Ed
.Pp
Note the first entry will store the parameters file as
.Pa /etc/cgd/wd0e .
And use the entered passphrase to generate the key.
.Pp
Although not required, the partition type
.Ar cgd
should be used in the disklabel or GPT type field for the cgd partition.
.Sh DIAGNOSTICS
.Bl -diag
.It "cgdconfig: could not calibrate pkcs5_pbkdf2"
An error greater than 5% in calibration occurred.
This could be the result of dynamic processor frequency scaling technology.
Ensure that the processor clock frequency remains static throughout the
program's execution.
.El
.Sh SEE ALSO
.Xr cgd 4 ,
.Xr dk 4 ,
.Xr fstab 5 ,
.Xr disklabel 8 ,
.Xr gpt 8
.Rs
.%T "Argon2: the memory-hard function for password hashing and other applications"
.%A Alex Biryukov
.%A Daniel Dinu
.%A Dmitry Khovratovich
.%D 2017
.%I University of Luxembourg
.%U https://www.password-hashing.net/
.Re
.Rs
.%A H. Krawczyk
.%A P. Eronen
.%T HMAC-based Extract-and-Expand Key Derivation Function (HKDF)
.%I Internet Engineering Task Force
.%U https://www.rfc-editor.org/rfc/rfc5869.html
.%N RFC 5869
.%D May 2010
.Re
.Pp
.Dq PKCS #5 v2.0: Password-Based Cryptography Standard ,
RSA Laboratories, March 25, 1999.
.Sh HISTORY
The
.Nm
utility appeared in
.Nx 2.0 .
.Pp
Support for
.Li argon2id
and for shared keys appeared in
.Nx 10.0 .
.Sh BUGS
Pass phrases are limited to 1023 bytes.
Title: CGDCONFIG(8)
CGDCONFIG(8) System Manager's Manual CGDCONFIG(8)

cgdconfig
configuration utility for the cryptographic disk driver

cgdconfig [-enpv] [-V vmeth] cgd dev [paramsfile]

cgdconfig -C [-enpv] [-f configfile]

cgdconfig -G [-enpv] [-i ivmeth] [-k kgmeth] [-o outfile] paramsfile

cgdconfig -g [-Sv] [-V vmeth] [-i ivmeth] [-k kgmeth] [-o outfile] [-P paramsfile] alg [keylen]

cgdconfig -T [-f configfile]

cgdconfig -t paramsfile

cgdconfig -l [-v[v]] [cgd]

cgdconfig -s [-nv] [-i ivmeth] cgd dev alg [keylen]

cgdconfig -U [-nv] [-f configfile]

cgdconfig -u [-nv] cgd

cgdconfig is used to configure and unconfigure cryptographic disk devices (cgds) and to maintain the configuration files that are associated with them. For more information about cryptographic disk devices see cgd(4).

The options are as follows:

Configure all the devices listed in the cgd configuration file.
Echo the passphrase.
configfile
Specify the configuration file explicitly, rather than using the default configuration file /etc/cgd/cgd.conf.
Generate a new paramsfile (to stdout) using the values from paramsfile which will generate the same key. This may need to obtain multiple passphrases.
Generate a paramsfile (to stdout).
ivmeth
Specify the IV method (default: encblkno1).

Setting the IV method is needed only for compatibility with disks written with a very old version of cgd(4) from before NetBSD 5.0, released in 2010; see cgd(4) for details.

kgmeth
Specify the key generation method (default: pkcs5_pbkdf2/sha1).
[cgd]
List state of all devices or just the one cgd device. The verbosity level affects the output.
Do not actually configure or unconfigure a cryptographic disk device, but instead report the steps that would be taken.
outfile
When generating a paramsfile, store it in outfile. If -o is not given, any paramsfile content is written to standard output.
paramsfile
With the -S option for the -g action, specify a parameters file with a shared key to reuse for deriving this one as a subkey.
Read all passphrases from stdin rather than /dev/tty. Passphrases are separated by newlines. Users of this flag must be able to predict the order in which passphrases are prompted. If this flag is specified then verification errors will cause the device in question to be unconfigured rather than prompting for the passphrase again.
When generating a parameters file with -g, arrange to use a subkey of a shared key. If -P paramsfile is also specified, reuse the shared key of paramsfile; otherwise a new one will be generated.
Read the key (nb: not the passphrase) from stdin.
Generate all keys for all the devices listed in the cgdconfig configuration file and print them to standard output encoded in base64.
Generate the key and print it to standard output encoded in base64.
Unconfigure all the devices listed in the cgd configuration file.
Unconfigure a cgd.
vmeth
Specify the verification method (default: none).
Be verbose. May be specified multiple times.

For more information about the cryptographic algorithms supported, please refer to cgd(4).

To generate the key which it will use, cgdconfig evaluates all of the key generation methods in the parameters file and uses the exclusive-or of the outputs of all the methods. The methods and descriptions are as follows:
argon2id
This method requires a passphrase which is entered at configuration time. Argon2 is a memory-hard password hashing scheme and winner of the 2013-2015 Password Hashing Competition. It has numerous parameters allowing its hardness to scale with the performance of the system. Recommended for passphrase-based initialization.
pkcs5_pbkdf2/sha1
This method requires a passphrase which is entered at configuration time. It is a salted hmac-based scheme detailed in “PKCS#5 v2.0: Password-Based Cryptography Standard”, RSA Laboratories, March 25, 1999, pages 8-10. PKCS #5 was also republished as RFC 2898.
pkcs5_pbkdf2
This is an earlier, slightly incorrect and deprecated implementation of the above algorithm. It is retained for backwards compatibility with existing parameters files, and will be removed. Existing parameters files should be converted to use the correct method using the -G option, and a new passphrase.
storedkey
This method stores its key in the parameters file.
randomkey
The method simply reads /dev/random and uses the resulting bits as the key. It does not require a passphrase to be entered. This method is typically used to present disk devices that do not need to survive a reboot. It is also handy to facilitate overwriting the contents of a disk volume with meaningless data prior to use.
urandomkey
The method simply reads /dev/urandom and uses the resulting bits as the key. This is similar to the randomkey method, but it guarantees that cgdconfig will not stall waiting for 256 bits of entropy from a hardware RNG or seed.
shell_cmd
This method executes a shell command via popen(3) and reads the key from stdout.

The verification method is how cgdconfig determines if the generated key is correct. If the newly configured disk fails to verify, then cgdconfig will regenerate the key and re-configure the device. It only makes sense to specify a verification method if at least one of the key generation methods is error prone, e.g., uses a user-entered passphrase. The following verification methods are supported:

none
perform no verification.
disklabel
scan for a valid disklabel.
mbr
scan for a valid Master Boot Record.
gpt
scan for a valid GUID partition table.
ffs
scan for a valid FFS file system.
re-enter
prompt for passphrase twice, and ensure entered passphrases are identical. This method only works with the argon2id, pkcs5_pbkdf2/sha1, and pkcs5_pbkdf2 key generators.

The file /etc/cgd/cgd.conf is used to configure cgdconfig if either of -C or -U are specified. Each line of the file is composed of either two or three tokens: cgd, target, and optional paramsfile.

A ‘#’ character is interpreted as a comment and indicates that the rest of the line should be ignored. A ‘\’ at the end of a line indicates that the next line is a continuation of the current line.

If the second field is of the form “NAME=<value>” then all the dk(4) wedge partitions are searched for one that has a wedge name equal to <value> and the device corresponding to it is selected.

If the second field starts with the prefix “ROOT.” the prefix is replaced with “/dev/[root_device]”, where [root_device] is the value of the “kern.root_device” sysctl.

See EXAMPLES for an example of /etc/cgd/cgd.conf.

The Parameters File contains the required information to generate the key and configure a device. These files are typically generated by the -g flag and not edited by hand. When a device is configured the default parameters file is constructed by taking the basename of the target disk and prepending /etc/cgd/ to it. E.g., if the target is /dev/sd0h, then the default parameters file will be /etc/cgd/sd0h.

It is possible to have more than one parameters file for a given disk which use different key generation methods but will generate the same key. To create a parameters file that is equivalent to an existing parameters file, use cgdconfig with the -G flag. See EXAMPLES for an example of this usage.

The parameters file contains a list of statements each terminated with a semi-colon. Some statements can contain statement-blocks which are either a single unadorned statement, or a brace-enclosed list of semicolon terminated statements. Three types of data are understood:

integer
a 32 bit signed integer.
string
a string.
base64
a length-encoded base64 string.

The following statements are defined:

algorithm string
Defines the cryptographic algorithm.
iv-method string
Defines the IV generation method. This should always be ‘encblkno1’ except when dealing with disks written with a very old version of cgd(4) from before NetBSD 5.0, released in 2010; see cgd(4) for details.
keylength integer
Defines the length of the key.
verify_method string
Defines the verification method.
keygen string statement_block
Defines a key generation method. The statement_block contains statements that are specific to the key generation method.

The keygen statement's statement block may contain the following statements:

key string
The key. Only used for the storedkey key generation method.
cmd string
The command to execute. Only used for the shell_cmd key generation method.
iterations integer
The number of iterations. Only used for argon2id, pkcs5_pbkdf2/sha1, and pkcs5_pbkdf2.
salt base64
The salt. Only used for argon2id, pkcs5_pbkdf2/sha1, and pkcs5_pbkdf2.
memory integer
Memory consumption in kilobytes. Only used for argon2id.
parallelism integer
Number of threads to use to compute the password hash. Should be equivalent to the number of CPUs/hardware threads. Only used for argon2id.
version integer
Version of Argon2 to use. Should be the most recent version, currently 19. Only used for argon2id.
shared name algorithm kdf subkey info
Makes the key generation take an extra step to derive a subkey from the main key using the key derivation function kdf with input info.

This enables a single password entry, for example, to decrypt multiple disks that use different keys, each derived as a subkey from the main key generated from the password.

name
A string used to identify the same main key generation shared between parameters files for different disks listed in a single cgd.conf configuration file.
kdf
The name of a key derivation function used to derive a subkey from the main key. Supported values:
The HKDF-Expand function of RFC 5869, instantiated with SHA-256.
info
A base64 length-encoded string to distinguish different subkeys derived from a shared main key. Need not be secret. For example, it could be a nickname, or the disk's World-Wide Name, or a UUID generated for the disk, or just a random string.

It is an error to reuse a shared key name with different keygen blocks, other than the info parameter, between parameters files used by a single cgd.conf configuration file.

/etc/cgd/
configuration directory, used to store paramsfiles.
/etc/cgd/cgd.conf
cgd configuration file.

To set up and configure a cgd that uses adiantum, which takes a 256-bit key:
	# cgdconfig -g -k argon2id -o /etc/cgd/wd0e adiantum 256
	# cgdconfig cgd0 /dev/wd0e
	/dev/wd0e's passphrase:

When using verification methods, the first time that we configure the disk the verification method will fail. We overcome this by supplying -V re-enter when we configure the first time to set up the disk. Here is the sequence of commands that is recommended:

	# cgdconfig -g -k argon2id -o /etc/cgd/dk3 -V gpt adiantum
	# cgdconfig -V re-enter cgd0 /dev/dk3
	/dev/dk3's passphrase:
	re-enter device's passphrase:
	# gpt create cgd0
	# cgdconfig -u cgd0
	# cgdconfig cgd0 /dev/dk3
	/dev/dk3's passphrase:

To scrub data from a disk before setting up a cgd:

	# cgdconfig -s cgd0 /dev/sd0e adiantum 256 < /dev/urandom
	# dd if=/dev/zero of=/dev/rcgd0d bs=32k progress=512
	# cgdconfig -u cgd0

To create a new parameters file that will generate the same key as an old parameters file:

	# cgdconfig -G -o newparamsfile oldparamsfile
	old file's passphrase:
	new file's passphrase:

To create parameters files for three disks with subkeys derived from a shared password-based key:

	# cgdconfig -g -S -k argon2id -o /etc/cgd/wd0 -V gpt adiantum
	# cgdconfig -g -S -P /etc/cgd/wd0 -o /etc/cgd/ld1 \
	      -V disklabel aes-cbc 256

Listing these in the same /etc/cgd/cgd.conf will allow you to enter a password once to decrypt both disks with cgdconfig -C.

To configure a cgd that uses aes-cbc with a 192 bit key that it reads from stdin:

	# cgdconfig -s cgd0 /dev/sd0h aes-cbc 192

An example parameters file which uses PKCS#5 PBKDF2:

	algorithm aes-cbc;
	iv-method encblkno1;
	keylength 128;
	verify_method none;
	keygen pkcs5_pbkdf2/sha1 {
		iterations 39361;
		salt AAAAgMoHiYonye6Kog \
		     dYJAobCHE=;
	};

An example parameters file which stores its key locally:

	algorithm       adiantum;
	iv-method       encblkno1;
	keylength       256;
	verify_method   none;
	keygen storedkey key AAABAK3QO6d7xzLfrXTdsgg4 \
			     ly2TdxkFqOkYYcbyUKu/f60L;

An example pair of configuration files which use shared keys so they can be derived from a single passphrase entry, with the 64-bit World-Wide Name of each disk (base64 length-encoded) as its subkey info:

/etc/cgd/wd0a
algorithm       adiantum;
iv-method       encblkno1;
keylength       256;
verify_method	gpt;
keygen argon2id {
        iterations 32;
        memory 5214;
        parallelism 2;
        version 19;
        salt AAAAgLZ5QgleU2m/Ib6wiPYxz98=;
        shared "my laptop" algorithm hkdf-hmac-sha256 \
            subkey AAAAQEGELNr3bj3I;
};
    
/etc/cgd/wd1a
algorithm       adiantum;
iv-method       encblkno1;
keylength       256;
verify_method	gpt;
keygen argon2id {
        iterations 32;
        memory 5214;
        parallelism 2;
        version 19;
        salt AAAAgLZ5QgleU2m/Ib6wiPYxz98=;
        shared "my laptop" algorithm hkdf-hmac-sha256 \
            subkey AAAAQHSC15pr1Pe4;
};
    

An example /etc/cgd/cgd.conf:

	#
	# /etc/cgd/cgd.conf
	# Configuration file for cryptographic disk devices
	#

	# cgd		target		[paramsfile]
	cgd0		/dev/wd0e
	cgd1		NAME=mycgd	/usr/local/etc/cgd/mycgd

Note the first entry will store the parameters file as /etc/cgd/wd0e. And use the entered passphrase to generate the key.

Although not required, the partition type cgd should be used in the disklabel or GPT type field for the cgd partition.

cgdconfig: could not calibrate pkcs5_pbkdf2
An error greater than 5% in calibration occurred. This could be the result of dynamic processor frequency scaling technology. Ensure that the processor clock frequency remains static throughout the program's execution.

cgd(4), dk(4), fstab(5), disklabel(8), gpt(8)

Alex Biryukov, Daniel Dinu, and Dmitry Khovratovich, Argon2: the memory-hard function for password hashing and other applications, University of Luxembourg, https://www.password-hashing.net/, 2017.

H. Krawczyk and P. Eronen, HMAC-based Extract-and-Expand Key Derivation Function (HKDF), Internet Engineering Task Force, RFC 5869, https://www.rfc-editor.org/rfc/rfc5869.html, May 2010.

“PKCS #5 v2.0: Password-Based Cryptography Standard”, RSA Laboratories, March 25, 1999.

The cgdconfig utility appeared in NetBSD 2.0.

Support for argon2id and for shared keys appeared in NetBSD 10.0.

Pass phrases are limited to 1023 bytes.
November 4, 2021 NetBSD 9.2_STABLE


Home | Main Index | Thread Index | Old Index