From 5c2468503650a3ea7280edb7068041d62b31131e Mon Sep 17 00:00:00 2001 From: Masahiko Sawada Date: Tue, 26 May 2020 13:47:53 +0900 Subject: [PATCH v11 3/7] Add key management module. --- src/backend/Makefile | 2 +- src/backend/access/transam/xlog.c | 20 + src/backend/bootstrap/bootstrap.c | 7 +- src/backend/crypto/Makefile | 18 + src/backend/crypto/kmgr.c | 454 ++++++++++++++++++ src/backend/postmaster/pgstat.c | 9 + src/backend/postmaster/postmaster.c | 6 + src/backend/replication/basebackup.c | 4 + src/backend/storage/ipc/ipci.c | 3 + src/backend/storage/lmgr/lwlocknames.txt | 1 + src/backend/tcop/postgres.c | 8 + src/backend/utils/misc/guc.c | 24 + src/backend/utils/misc/postgresql.conf.sample | 5 + src/bin/initdb/initdb.c | 34 +- src/bin/pg_controldata/pg_controldata.c | 3 + src/bin/pg_resetwal/pg_resetwal.c | 2 + src/bin/pg_rewind/filemap.c | 8 + src/bin/pg_upgrade/controldata.c | 41 +- src/bin/pg_upgrade/file.c | 77 +++ src/bin/pg_upgrade/pg_upgrade.c | 7 + src/bin/pg_upgrade/pg_upgrade.h | 4 + src/common/Makefile | 1 + src/common/kmgr_utils.c | 435 +++++++++++++++++ src/include/access/xlog.h | 1 + src/include/catalog/pg_control.h | 3 + src/include/catalog/pg_proc.dat | 6 + src/include/common/kmgr_utils.h | 70 +++ src/include/crypto/kmgr.h | 29 ++ src/include/pgstat.h | 3 + src/include/utils/guc_tables.h | 1 + src/test/Makefile | 2 +- 31 files changed, 1281 insertions(+), 7 deletions(-) create mode 100644 src/backend/crypto/Makefile create mode 100644 src/backend/crypto/kmgr.c create mode 100644 src/common/kmgr_utils.c create mode 100644 src/include/common/kmgr_utils.h create mode 100644 src/include/crypto/kmgr.h diff --git a/src/backend/Makefile b/src/backend/Makefile index 9706a95848..4ace302038 100644 --- a/src/backend/Makefile +++ b/src/backend/Makefile @@ -21,7 +21,7 @@ SUBDIRS = access bootstrap catalog parser commands executor foreign lib libpq \ main nodes optimizer partitioning port postmaster \ regex replication rewrite \ statistics storage tcop tsearch utils $(top_builddir)/src/timezone \ - jit + jit crypto include $(srcdir)/common.mk diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index ca09d81b08..7f5c15cba4 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -44,6 +44,7 @@ #include "commands/tablespace.h" #include "common/controldata_utils.h" #include "executor/instrument.h" +#include "crypto/kmgr.h" #include "miscadmin.h" #include "pg_trace.h" #include "pgstat.h" @@ -80,6 +81,7 @@ #include "utils/timestamp.h" extern uint32 bootstrap_data_checksum_version; +extern uint32 bootstrap_key_management_version; /* Unsupported old recovery command file names (relative to $PGDATA) */ #define RECOVERY_COMMAND_FILE "recovery.conf" @@ -4604,6 +4606,7 @@ InitControlFile(uint64 sysidentifier) ControlFile->wal_log_hints = wal_log_hints; ControlFile->track_commit_timestamp = track_commit_timestamp; ControlFile->data_checksum_version = bootstrap_data_checksum_version; + ControlFile->key_management_version = bootstrap_key_management_version; } static void @@ -4891,6 +4894,9 @@ ReadControlFile(void) /* Make the initdb settings visible as GUC variables, too */ SetConfigOption("data_checksums", DataChecksumsEnabled() ? "yes" : "no", PGC_INTERNAL, PGC_S_OVERRIDE); + + SetConfigOption("key_management", KeyManagementEnabled() ? "yes" : "no", + PGC_INTERNAL, PGC_S_OVERRIDE); } /* @@ -4933,6 +4939,16 @@ DataChecksumsEnabled(void) return (ControlFile->data_checksum_version > 0); } +/* + * Are key management enabled? + */ +bool +KeyManagementEnabled(void) +{ + Assert(ControlFile != NULL); + return (ControlFile->key_management_version > 0); +} + /* * Returns a fake LSN for unlogged relations. * @@ -5340,6 +5356,10 @@ BootStrapXLOG(void) /* some additional ControlFile fields are set in WriteControlFile() */ WriteControlFile(); + /* Enable key manager if required */ + if (ControlFile->key_management_version > 0) + BootStrapKmgr(); + /* Bootstrap the commit log, too */ BootStrapCLOG(); BootStrapCommitTs(); diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c index 5480a024e0..e09e442727 100644 --- a/src/backend/bootstrap/bootstrap.c +++ b/src/backend/bootstrap/bootstrap.c @@ -28,6 +28,7 @@ #include "catalog/pg_collation.h" #include "catalog/pg_type.h" #include "common/link-canary.h" +#include "crypto/kmgr.h" #include "libpq/pqsignal.h" #include "miscadmin.h" #include "nodes/makefuncs.h" @@ -51,6 +52,7 @@ #include "utils/relmapper.h" uint32 bootstrap_data_checksum_version = 0; /* No checksum */ +uint32 bootstrap_key_management_version = 0; /* disabled */ #define ALLOC(t, c) \ @@ -226,7 +228,7 @@ AuxiliaryProcessMain(int argc, char *argv[]) /* If no -x argument, we are a CheckerProcess */ MyAuxProcType = CheckerProcess; - while ((flag = getopt(argc, argv, "B:c:d:D:Fkr:x:X:-:")) != -1) + while ((flag = getopt(argc, argv, "B:c:d:D:eFkr:x:X:-:")) != -1) { switch (flag) { @@ -249,6 +251,9 @@ AuxiliaryProcessMain(int argc, char *argv[]) pfree(debugstr); } break; + case 'e': + bootstrap_key_management_version = KMGR_VERSION; + break; case 'F': SetConfigOption("fsync", "false", PGC_POSTMASTER, PGC_S_ARGV); break; diff --git a/src/backend/crypto/Makefile b/src/backend/crypto/Makefile new file mode 100644 index 0000000000..c27362029d --- /dev/null +++ b/src/backend/crypto/Makefile @@ -0,0 +1,18 @@ +#------------------------------------------------------------------------- +# +# Makefile +# Makefile for src/backend/crypto +# +# IDENTIFICATION +# src/backend/crypto/Makefile +# +#------------------------------------------------------------------------- + +subdir = src/backend/crypto +top_builddir = ../../.. +include $(top_builddir)/src/Makefile.global + +OBJS = \ + kmgr.o + +include $(top_srcdir)/src/backend/common.mk diff --git a/src/backend/crypto/kmgr.c b/src/backend/crypto/kmgr.c new file mode 100644 index 0000000000..40d0005b8b --- /dev/null +++ b/src/backend/crypto/kmgr.c @@ -0,0 +1,454 @@ +/*------------------------------------------------------------------------- + * + * kmgr.c + * Key manager routines + * + * Copyright (c) 2020, PostgreSQL Global Development Group + * + * Key manager is enabled if user requests during initdb. We have one key + * encryption key (KEK) and one internal key: SQL key. During bootstrap, + * we generate internal keys (currently only one), wrap them by using + * AEAD algorithm with KEK which is derived from the user-provided passphrase + * and store them into each file located at KMGR_DIR. Once generated, these + * are not changed. During startup, we decrypt all internal keys and load + * them to the shared memory space. Internal keys on the shared memory are + * read-only. + * + * IDENTIFICATION + * src/backend/crypto/kmgr.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include +#include + +#include "funcapi.h" +#include "miscadmin.h" +#include "pgstat.h" + +#include "common/sha2.h" +#include "common/kmgr_utils.h" +#include "crypto/kmgr.h" +#include "storage/fd.h" +#include "storage/ipc.h" +#include "storage/shmem.h" +#include "utils/builtins.h" +#include "utils/guc.h" +#include "utils/memutils.h" + +/* Struct stores internal keys in plaintext format */ +typedef struct KmgrShmemData +{ + /* + * Internal cryptographic keys. Keys are stored at its ID'th. + */ + CryptoKey intlKeys[KMGR_MAX_INTERNAL_KEYS]; +} KmgrShmemData; +static KmgrShmemData *KmgrShmem; + +/* Key lengths of internal keys */ +static int internalKeyLengths[KMGR_MAX_INTERNAL_KEYS] = +{ + PG_AEAD_KEY_LEN /* KMGR_SQL_KEY_ID */ +}; + +/* GUC variables */ +bool key_management_enabled = false;; +char *cluster_passphrase_command = NULL; + +static void KmgrSaveCryptoKeys(const char *dir, CryptoKey *keys); +static CryptoKey *generate_crypto_key(int len); +static void recoverIncompleteRotation(void); + +/* + * This function must be called ONCE on system install. + */ +void +BootStrapKmgr(void) +{ + PgAeadCtx *ctx; + CryptoKey keys_wrap[KMGR_MAX_INTERNAL_KEYS] = {0}; + char passphrase[KMGR_MAX_PASSPHRASE_LEN]; + uint8 kekenc[PG_AEAD_ENC_KEY_LEN]; + uint8 kekhmac[PG_AEAD_MAC_KEY_LEN]; + int passlen; + + /* + * Requirement check. We need openssl library to enable key management + * because all encryption and decryption calls happen via openssl function + * calls. + */ +#ifndef USE_OPENSSL + ereport(ERROR, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + (errmsg("cluster encryption is not supported because OpenSSL is not supported by this build"), + errhint("Compile with --with-openssl to use cluster encryption.")))); +#endif + + /* Get key encryption key from the passphrase command */ + passlen = kmgr_run_cluster_passphrase_command(cluster_passphrase_command, + passphrase, KMGR_MAX_PASSPHRASE_LEN); + if (passlen < KMGR_MIN_PASSPHRASE_LEN) + ereport(ERROR, + (errmsg("passphrase must be more than %d bytes", + KMGR_MIN_PASSPHRASE_LEN))); + + /* Get key encryption key and HMAC key from passphrase */ + kmgr_derive_keys(passphrase, passlen, kekenc, kekhmac); + + /* Create temporarily AEAD context */ + ctx = pg_create_aead_ctx(kekenc, kekhmac); + if (!ctx) + elog(ERROR, "could not initialize encryption contect"); + + /* Wrap all internal keys by key encryption key */ + for (int id = 0; id < KMGR_MAX_INTERNAL_KEYS; id++) + { + CryptoKey *key; + + /* generate an internal key */ + key = generate_crypto_key(internalKeyLengths[id]); + + if (!kmgr_wrap_key(ctx, key, &(keys_wrap[id]))) + { + pg_free_aead_ctx(ctx); + elog(ERROR, "failed to wrap cluster encryption key"); + } + } + + /* Save internal keys to the disk */ + KmgrSaveCryptoKeys(KMGR_DIR, keys_wrap); + + pg_free_aead_ctx(ctx); +} + +/* Report shared-memory space needed by KmgrShmem */ +Size +KmgrShmemSize(void) +{ + if (!key_management_enabled) + return 0; + + return MAXALIGN(sizeof(KmgrShmemData)); +} + +/* Allocate and initialize key manager memory */ +void +KmgrShmemInit(void) +{ + bool found; + + if (!key_management_enabled) + return; + + KmgrShmem = (KmgrShmemData *) ShmemInitStruct("Key manager", + KmgrShmemSize(), &found); + + if (!found) + memset(KmgrShmem, 0, KmgrShmemSize()); +} + +/* + * Get encryption key passphrase and verify it, then get the internal keys. + * This function is called by postmaster at startup time. + */ +void +InitializeKmgr(void) +{ + CryptoKey *keys_wrap; + char passphrase[KMGR_MAX_PASSPHRASE_LEN]; + int passlen; + int nkeys; + + if (!key_management_enabled) + return; + + elog(DEBUG1, "starting up key management system"); + + /* Recover the failure of the last passphrase rotation if necessary */ + recoverIncompleteRotation(); + + /* Get the crypto keys from the file */ + keys_wrap = kmgr_get_cryptokeys(KMGR_DIR, &nkeys); + Assert(nkeys == KMGR_MAX_INTERNAL_KEYS); + + /* Get cluster passphrase */ + passlen = kmgr_run_cluster_passphrase_command(cluster_passphrase_command, + passphrase, KMGR_MAX_PASSPHRASE_LEN); + + /* + * Verify passphrase and prepare an internal key in plaintext on shared memory. + * + * XXX: do we need to prevent internal keys from being swapped out using + * mlock? + */ + if (!kmgr_verify_passphrase(passphrase, passlen, keys_wrap, KmgrShmem->intlKeys, + KMGR_MAX_INTERNAL_KEYS)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cluster passphrase does not match expected passphrase"))); +} + +const CryptoKey * +KmgrGetKey(int id) +{ + Assert(id < KMGR_MAX_INTERNAL_KEYS); + + return (const CryptoKey *) &(KmgrShmem->intlKeys[id]); +} + +/* Generate an empty CryptoKey */ +static CryptoKey * +generate_crypto_key(int len) +{ + CryptoKey *newkey; + + Assert(len < KMGR_MAX_KEY_LEN); + newkey = (CryptoKey *) palloc0(sizeof(CryptoKey)); + + if (!pg_strong_random(newkey->key, len)) + elog(ERROR, "failed to generate new crypto key"); + + newkey->klen = len; + + return newkey; +} + +/* + * Save the given crypto keys to the disk. We don't need CRC check for crypto + * keys because these keys have HMAC which is used for integrity check + * during unwrapping. + */ +static void +KmgrSaveCryptoKeys(const char *dir, CryptoKey *keys) +{ + elog(DEBUG2, "saving all cryptographic keys"); + + for (int i = 0; i < KMGR_MAX_INTERNAL_KEYS; i++) + { + int fd; + char path[MAXPGPATH]; + + CryptoKeyFilePath(path, dir, i); + + if ((fd = BasicOpenFile(path, O_RDWR | O_CREAT | O_EXCL | PG_BINARY)) < 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not open file \"%s\": %m", + path))); + + errno = 0; + pgstat_report_wait_start(WAIT_EVENT_KEY_FILE_WRITE); + if (write(fd, &(keys[i]), sizeof(CryptoKey)) != sizeof(CryptoKey)) + { + /* if write didn't set errno, assume problem is no disk space */ + if (errno == 0) + errno = ENOSPC; + + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not write file \"%s\": %m", + path))); + } + pgstat_report_wait_end(); + + pgstat_report_wait_start(WAIT_EVENT_KEY_FILE_SYNC); + if (pg_fsync(fd) != 0) + ereport(PANIC, + (errcode_for_file_access(), + errmsg("could not fsync file \"%s\": %m", + path))); + pgstat_report_wait_end(); + + if (close(fd) != 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not close file \"%s\": %m", + path))); + } +} + + +/* + * Check the last passphrase rotation was completed. If not, we decide which wrapped + * keys will be used according to the status of temporary directory and its wrapped + * keys. + */ +static void +recoverIncompleteRotation(void) +{ + struct stat st; + struct stat st_tmp; + CryptoKey *keys; + int nkeys_tmp; + + /* The cluster passphrase rotation was completed, nothing to do */ + if (stat(KMGR_TMP_DIR, &st_tmp) != 0) + return; + + /* + * If there is only temporary directory, it means that the previous + * rotation failed after wrapping the all internal keys by the new + * passphrase. Therefore we use the new cluster passphrase. + */ + if (stat(KMGR_DIR, &st) != 0) + { + ereport(DEBUG1, + (errmsg("there is only temporary directory, use the newly wrapped keys"))); + + if (rename(KMGR_TMP_DIR, KMGR_DIR) != 0) + ereport(ERROR, + errmsg("could not rename directory \"%s\" to \"%s\": %m", + KMGR_TMP_DIR, KMGR_DIR)); + ereport(LOG, + errmsg("cryptographic keys wrapped by new passphrase command are chosen"), + errdetail("last cluster passphrase rotation failed in the middle")); + return; + } + + /* + * In case where both the original directory and temporary directory + * exist, there are two possibilities: (a) the all internal keys are + * wrapped by the new passphrase but rotation failed before removing the + * original directory, or (b) the rotation failed during wrapping internal + * keys by the new passphrase. In case of (a) we need to use the wrapped + * keys in the temporary directory as rotation is essentially completed, + * but in case of (b) we use the wrapped keys in the original directory. + * + * To check the possibility of (b) we validate the wrapped keys in the + * temporary directory by checking the number of wrapped keys. Since the + * wrapped key length is smaller than one disk sector, which is 512 bytes + * on common hardware, saving wrapped key is atomic write. So we can + * ensure that the all wrapped keys are valid if the number of wrapped + * keys in the temporary directory is KMGR_MAX_INTERNAL_KEYS. + */ + keys = kmgr_get_cryptokeys(KMGR_TMP_DIR, &nkeys_tmp); + + if (nkeys_tmp == KMGR_MAX_INTERNAL_KEYS) + { + /* + * This is case (a), the all wrapped keys in temporary directory are + * valid. Remove the original directory and rename. + */ + ereport(DEBUG1, + (errmsg("last passphrase rotation failed before renaming direcotry name, use the newly wrapped keys"))); + + if (!rmtree(KMGR_DIR, true)) + ereport(ERROR, + (errmsg("could not remove directory \"%s\"", + KMGR_DIR))); + if (rename(KMGR_TMP_DIR, KMGR_DIR) != 0) + ereport(ERROR, + errmsg("could not rename directory \"%s\" to \"%s\": %m", + KMGR_TMP_DIR, KMGR_DIR)); + + ereport(LOG, + errmsg("cryptographic keys wrapped by new passphrase command are chosen"), + errdetail("last cluster passphrase rotation failed in the middle")); + } + else + { + /* + * This is case (b), the last passphrase rotation failed during + * wrapping keys. Remove the keys in the temporary directory and use + * keys in the original keys. + */ + ereport(DEBUG1, + (errmsg("last passphrase rotation failed during wrapping keys, use the old wrapped keys"))); + + if (!rmtree(KMGR_TMP_DIR, true)) + ereport(ERROR, + (errmsg("could not remove directory \"%s\"", + KMGR_DIR))); + ereport(LOG, + errmsg("cryptographic keys wrapped by old passphrase command are chosen"), + errdetail("last cluster passphrase rotation failed in the middle")); + } + + pfree(keys); +} + +/* + * SQL function to rotate the cluster passphrase. This function assumes that + * the cluster_passphrase_command is already reloaded to the new value. + * All internal keys are wrapped by the new passphrase and saved to the disk. + * To update all crypto keys atomically we save the newly wrapped keys to the + * temporary directory, pg_cryptokeys_tmp, and remove the original directory, + * pg_cryptokeys, and rename it. These operation is performed without the help + * of WAL. In the case of failure during rotationpg_cryptokeys directory and + * pg_cryptokeys_tmp directory can be left in incomplete status. We recover + * the incomplete situation by checkIncompleteRotation. + */ +Datum +pg_rotate_cluster_passphrase(PG_FUNCTION_ARGS) +{ + PgAeadCtx *ctx; + CryptoKey newkeys[KMGR_MAX_INTERNAL_KEYS] = {0}; + char passphrase[KMGR_MAX_PASSPHRASE_LEN]; + uint8 new_kekenc[PG_AEAD_ENC_KEY_LEN]; + uint8 new_kekhmac[PG_AEAD_MAC_KEY_LEN]; + int passlen; + + if (!key_management_enabled) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("could not rotate cluster passphrase because key management is not supported"))); + + /* Recover the failure of the last passphrase rotation if necessary */ + recoverIncompleteRotation(); + + passlen = kmgr_run_cluster_passphrase_command(cluster_passphrase_command, + passphrase, + KMGR_MAX_PASSPHRASE_LEN); + if (passlen < KMGR_MIN_PASSPHRASE_LEN) + ereport(ERROR, + (errmsg("passphrase must be more than %d bytes", + KMGR_MIN_PASSPHRASE_LEN))); + + /* Get new key encryption key and encryption context */ + kmgr_derive_keys(passphrase, passlen, new_kekenc, new_kekhmac); + ctx = pg_create_aead_ctx(new_kekenc, new_kekhmac); + if (!ctx) + elog(ERROR, "could not initialize encryption contect"); + + for (int id = 0; id < KMGR_MAX_INTERNAL_KEYS; id++) + { + if (!kmgr_wrap_key(ctx, &(KmgrShmem->intlKeys[id]), &(newkeys[id]))) + elog(ERROR, "failed to wrap key"); + } + + /* Create temporary directory */ + if (MakePGDirectory(KMGR_TMP_DIR) < 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not create temporary directory \"%s\": %m", + KMGR_TMP_DIR))); + fsync_fname(KMGR_TMP_DIR, true); + + /* Prevent concurrent key rotation */ + LWLockAcquire(KmgrFileLock, LW_EXCLUSIVE); + + /* Save the key wrapped by the new passphrase to the temporary directory */ + KmgrSaveCryptoKeys(KMGR_TMP_DIR, newkeys); + + /* Remove the original directory */ + if (!rmtree(KMGR_DIR, true)) + ereport(ERROR, + (errmsg("could not remove directory \"%s\"", + KMGR_DIR))); + + /* Rename to the original directory */ + if (rename(KMGR_TMP_DIR, KMGR_DIR) != 0) + ereport(ERROR, + (errmsg("could not rename directory \"%s\" to \"%s\": %m", + KMGR_TMP_DIR, KMGR_DIR))); + fsync_fname(KMGR_DIR, true); + + LWLockRelease(KmgrFileLock); + + pg_free_aead_ctx(ctx); + PG_RETURN_BOOL(true); +} diff --git a/src/backend/postmaster/pgstat.c b/src/backend/postmaster/pgstat.c index d7f99d9944..358e0ad36f 100644 --- a/src/backend/postmaster/pgstat.c +++ b/src/backend/postmaster/pgstat.c @@ -3989,6 +3989,15 @@ pgstat_get_wait_io(WaitEventIO w) case WAIT_EVENT_DSM_FILL_ZERO_WRITE: event_name = "DSMFillZeroWrite"; break; + case WAIT_EVENT_KEY_FILE_READ: + event_name = "KeyFileRead"; + break; + case WAIT_EVENT_KEY_FILE_WRITE: + event_name = "KeyFileWrite"; + break; + case WAIT_EVENT_KEY_FILE_SYNC: + event_name = "KeyFileSync"; + break; case WAIT_EVENT_LOCK_FILE_ADDTODATADIR_READ: event_name = "LockFileAddToDataDirRead"; break; diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c index 160afe9f39..77b7872aae 100644 --- a/src/backend/postmaster/postmaster.c +++ b/src/backend/postmaster/postmaster.c @@ -100,6 +100,7 @@ #include "common/file_perm.h" #include "common/ip.h" #include "common/string.h" +#include "crypto/kmgr.h" #include "lib/ilist.h" #include "libpq/auth.h" #include "libpq/libpq.h" @@ -1335,6 +1336,11 @@ PostmasterMain(int argc, char *argv[]) */ autovac_init(); + /* + * Initialize key manager. + */ + InitializeKmgr(); + /* * Load configuration files for client authentication. */ diff --git a/src/backend/replication/basebackup.c b/src/backend/replication/basebackup.c index 3b46bfe9ab..1bcf366142 100644 --- a/src/backend/replication/basebackup.c +++ b/src/backend/replication/basebackup.c @@ -18,6 +18,7 @@ #include "access/xlog_internal.h" /* for pg_start/stop_backup */ #include "catalog/pg_type.h" +#include "common/kmgr_utils.h" #include "common/file_perm.h" #include "commands/progress.h" #include "lib/stringinfo.h" @@ -160,6 +161,9 @@ struct exclude_list_item */ static const char *const excludeDirContents[] = { + /* Skip temporary crypto key files */ + KMGR_TMP_DIR, + /* * Skip temporary statistics files. PG_STAT_TMP_DIR must be skipped even * when stats_temp_directory is set because PGSS_TEXT_FILE is always diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index 427b0d59cd..23ef8f7b00 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -22,6 +22,7 @@ #include "access/subtrans.h" #include "access/twophase.h" #include "commands/async.h" +#include "crypto/kmgr.h" #include "miscadmin.h" #include "pgstat.h" #include "postmaster/autovacuum.h" @@ -147,6 +148,7 @@ CreateSharedMemoryAndSemaphores(void) size = add_size(size, BTreeShmemSize()); size = add_size(size, SyncScanShmemSize()); size = add_size(size, AsyncShmemSize()); + size = add_size(size, KmgrShmemSize()); #ifdef EXEC_BACKEND size = add_size(size, ShmemBackendArraySize()); #endif @@ -263,6 +265,7 @@ CreateSharedMemoryAndSemaphores(void) BTreeShmemInit(); SyncScanShmemInit(); AsyncShmemInit(); + KmgrShmemInit(); #ifdef EXEC_BACKEND diff --git a/src/backend/storage/lmgr/lwlocknames.txt b/src/backend/storage/lmgr/lwlocknames.txt index e6985e8eed..48bf2d4742 100644 --- a/src/backend/storage/lmgr/lwlocknames.txt +++ b/src/backend/storage/lmgr/lwlocknames.txt @@ -50,3 +50,4 @@ MultiXactTruncationLock 41 OldSnapshotTimeMapLock 42 LogicalRepWorkerLock 43 XactTruncationLock 44 +KmgrFileLock 45 diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 8958ec8103..1663be6cab 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -42,6 +42,7 @@ #include "catalog/pg_type.h" #include "commands/async.h" #include "commands/prepare.h" +#include "crypto/kmgr.h" #include "executor/spi.h" #include "jit/jit.h" #include "libpq/libpq.h" @@ -3907,6 +3908,13 @@ PostgresMain(int argc, char *argv[], /* Early initialization */ BaseInit(); + /* + * Initialize kmgr for cluster encryption. Since kmgr needs to attach to + * shared memory the initialization must be called after BaseInit(). + */ + if (!IsUnderPostmaster) + InitializeKmgr(); + /* * Create a per-backend PGPROC struct in shared memory, except in the * EXEC_BACKEND case where this was done in SubPostmasterMain. We must do diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c index 2f3e0a70e0..6271c65e5c 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -44,6 +44,7 @@ #include "commands/vacuum.h" #include "commands/variable.h" #include "common/string.h" +#include "crypto/kmgr.h" #include "funcapi.h" #include "jit/jit.h" #include "libpq/auth.h" @@ -749,6 +750,8 @@ const char *const config_group_names[] = gettext_noop("Statistics / Monitoring"), /* STATS_COLLECTOR */ gettext_noop("Statistics / Query and Index Statistics Collector"), + /* ENCRYPTION */ + gettext_noop("Encryption"), /* AUTOVACUUM */ gettext_noop("Autovacuum"), /* CLIENT_CONN */ @@ -2060,6 +2063,17 @@ static struct config_bool ConfigureNamesBool[] = NULL, NULL, NULL }, + { + {"key_management", PGC_INTERNAL, PRESET_OPTIONS, + gettext_noop("Show whether key management is enabled for this cluster."), + NULL, + GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE + }, + &key_management_enabled, + false, + NULL, NULL, NULL + }, + /* End-of-list marker */ { {NULL, 0, 0, NULL, NULL}, NULL, false, NULL, NULL, NULL @@ -4383,6 +4397,16 @@ static struct config_string ConfigureNamesString[] = NULL, NULL, NULL }, + { + {"cluster_passphrase_command", PGC_SIGHUP, ENCRYPTION, + gettext_noop("Command to obtain passphrase for database encryption."), + NULL + }, + &cluster_passphrase_command, + "", + NULL, NULL, NULL + }, + { {"application_name", PGC_USERSET, LOGGING_WHAT, gettext_noop("Sets the application name to be reported in statistics and logs."), diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index 995b6ca155..1dfec0d05a 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -628,6 +628,11 @@ # autovacuum, -1 means use # vacuum_cost_limit +#------------------------------------------------------------------------------ +# ENCRYPTION +#------------------------------------------------------------------------------ + +#cluster_passphrase_command = '' #------------------------------------------------------------------------------ # CLIENT CONNECTION DEFAULTS diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c index 4ff0c6c700..9127c0b8e1 100644 --- a/src/bin/initdb/initdb.c +++ b/src/bin/initdb/initdb.c @@ -145,6 +145,7 @@ static bool data_checksums = false; static char *xlog_dir = NULL; static char *str_wal_segment_size_mb = NULL; static int wal_segment_size_mb; +static char *cluster_passphrase = NULL; /* internal vars */ @@ -202,6 +203,7 @@ static const char *const subdirs[] = { "global", "pg_wal/archive_status", "pg_commit_ts", + "pg_cryptokeys", "pg_dynshmem", "pg_notify", "pg_serial", @@ -1206,6 +1208,13 @@ setup_config(void) "password_encryption = scram-sha-256"); } + if (cluster_passphrase) + { + snprintf(repltok, sizeof(repltok), "cluster_passphrase_command = '%s'", + escape_quotes(cluster_passphrase)); + conflines = replace_token(conflines, "#cluster_passphrase_command = ''", repltok); + } + /* * If group access has been enabled for the cluster then it makes sense to * ensure that the log files also allow group access. Otherwise a backup @@ -1416,14 +1425,14 @@ bootstrap_template1(void) unsetenv("PGCLIENTENCODING"); snprintf(cmd, sizeof(cmd), - "\"%s\" --boot -x1 -X %u %s %s %s", + "\"%s\" --boot -x1 -X %u %s %s %s %s", backend_exec, wal_segment_size_mb * (1024 * 1024), data_checksums ? "-k" : "", + cluster_passphrase ? "-e" : "", boot_options, debug ? "-d 5" : ""); - PG_CMD_OPEN; for (line = bki_lines; *line != NULL; line++) @@ -2311,6 +2320,8 @@ usage(const char *progname) printf(_(" --wal-segsize=SIZE size of WAL segments, in megabytes\n")); printf(_("\nLess commonly used options:\n")); printf(_(" -d, --debug generate lots of debugging output\n")); + printf(_(" -c --cluster-passphrase-command=COMMAND\n" + " set command to obtain passphrase for key management\n")); printf(_(" -k, --data-checksums use data page checksums\n")); printf(_(" -L DIRECTORY where to find the input files\n")); printf(_(" -n, --no-clean do not clean up after errors\n")); @@ -2984,6 +2995,7 @@ main(int argc, char *argv[]) {"wal-segsize", required_argument, NULL, 12}, {"data-checksums", no_argument, NULL, 'k'}, {"allow-group-access", no_argument, NULL, 'g'}, + {"cluster-passphrase-command", required_argument, NULL, 'c'}, {NULL, 0, NULL, 0} }; @@ -3025,7 +3037,7 @@ main(int argc, char *argv[]) /* process command-line options */ - while ((c = getopt_long(argc, argv, "dD:E:kL:nNU:WA:sST:X:g", long_options, &option_index)) != -1) + while ((c = getopt_long(argc, argv, "c:dD:E:kL:nNU:WA:sST:X:g", long_options, &option_index)) != -1) { switch (c) { @@ -3107,6 +3119,9 @@ main(int argc, char *argv[]) case 9: pwfilename = pg_strdup(optarg); break; + case 'c': + cluster_passphrase = pg_strdup(optarg); + break; case 's': show_setting = true; break; @@ -3177,6 +3192,14 @@ main(int argc, char *argv[]) exit(1); } +#ifndef USE_OPENSSL + if (cluster_passphrase) + { + pg_log_error("cluster encryption is not supported because OpenSSL is not supported by this build"); + exit(1); + } +#endif + check_authmethod_unspecified(&authmethodlocal); check_authmethod_unspecified(&authmethodhost); @@ -3244,6 +3267,11 @@ main(int argc, char *argv[]) else printf(_("Data page checksums are disabled.\n")); + if (cluster_passphrase) + printf(_("Key management system is enabled.\n")); + else + printf(_("Key management system is disabled.\n")); + if (pwprompt || pwfilename) get_su_pwd(); diff --git a/src/bin/pg_controldata/pg_controldata.c b/src/bin/pg_controldata/pg_controldata.c index e73639df74..0318127a92 100644 --- a/src/bin/pg_controldata/pg_controldata.c +++ b/src/bin/pg_controldata/pg_controldata.c @@ -25,6 +25,7 @@ #include "access/xlog_internal.h" #include "catalog/pg_control.h" #include "common/controldata_utils.h" +#include "common/kmgr_utils.h" #include "common/logging.h" #include "getopt_long.h" #include "pg_getopt.h" @@ -334,5 +335,7 @@ main(int argc, char *argv[]) ControlFile->data_checksum_version); printf(_("Mock authentication nonce: %s\n"), mock_auth_nonce_str); + printf(_("Key management version: %u\n"), + ControlFile->key_management_version); return 0; } diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c index 233441837f..fb6e038e1c 100644 --- a/src/bin/pg_resetwal/pg_resetwal.c +++ b/src/bin/pg_resetwal/pg_resetwal.c @@ -804,6 +804,8 @@ PrintControlValues(bool guessed) (ControlFile.float8ByVal ? _("by value") : _("by reference"))); printf(_("Data page checksum version: %u\n"), ControlFile.data_checksum_version); + printf(_("Key management version: %u\n"), + ControlFile.key_management_version); } diff --git a/src/bin/pg_rewind/filemap.c b/src/bin/pg_rewind/filemap.c index 36a2d62341..0245f8989c 100644 --- a/src/bin/pg_rewind/filemap.c +++ b/src/bin/pg_rewind/filemap.c @@ -73,6 +73,14 @@ static const char *excludeDirContents[] = /* Contents removed on startup, see AsyncShmemInit(). */ "pg_notify", + /* + * Skip cryptographic keys. It's generally not good idea to copy the + * cryptographic keys from source database because these might use + * different cluster passphrase. + */ + "pg_cryptokeys", /* defined as KMGR_DIR */ + "pg_cryptokeys_tmp", /* defined as KMGR_TMP_DIR */ + /* * Old contents are loaded for possible debugging but are not required for * normal operation, see SerialInit(). diff --git a/src/bin/pg_upgrade/controldata.c b/src/bin/pg_upgrade/controldata.c index 00d71e3a8a..e126c7b32a 100644 --- a/src/bin/pg_upgrade/controldata.c +++ b/src/bin/pg_upgrade/controldata.c @@ -9,10 +9,16 @@ #include "postgres_fe.h" +#include #include #include "pg_upgrade.h" +#include "access/xlog_internal.h" +#include "common/controldata_utils.h" +#include "common/file_utils.h" +#include "common/kmgr_utils.h" + /* * get_control_data() * @@ -59,6 +65,7 @@ get_control_data(ClusterInfo *cluster, bool live_check) bool got_date_is_int = false; bool got_data_checksum_version = false; bool got_cluster_state = false; + bool got_key_management_enabled = false; char *lc_collate = NULL; char *lc_ctype = NULL; char *lc_monetary = NULL; @@ -202,6 +209,13 @@ get_control_data(ClusterInfo *cluster, bool live_check) got_data_checksum_version = true; } + /* Only in <= 14 */ + if (GET_MAJOR_VERSION(cluster->major_version) <= 1400) + { + cluster->controldata.key_management_enabled = false; + got_key_management_enabled = true; + } + /* we have the result of cmd in "output". so parse it line by line now */ while (fgets(bufin, sizeof(bufin), output)) { @@ -485,6 +499,18 @@ get_control_data(ClusterInfo *cluster, bool live_check) cluster->controldata.data_checksum_version = str2uint(p); got_data_checksum_version = true; } + else if ((p = strstr(bufin, "Key management:")) != NULL) + { + p = strchr(p, ':'); + + if (p == NULL || strlen(p) <= 1) + pg_fatal("%d: controldata retrieval problem\n", __LINE__); + + p++; /* remove ':' char */ + /* used later for contrib check */ + cluster->controldata.key_management_enabled = strstr(p, "on") != NULL; + got_key_management_enabled = true; + } } pclose(output); @@ -539,7 +565,8 @@ get_control_data(ClusterInfo *cluster, bool live_check) !got_index || !got_toast || (!got_large_object && cluster->controldata.ctrl_ver >= LARGE_OBJECT_SIZE_PG_CONTROL_VER) || - !got_date_is_int || !got_data_checksum_version) + !got_date_is_int || !got_data_checksum_version || + !got_key_management_enabled) { if (cluster == &old_cluster) pg_log(PG_REPORT, @@ -605,6 +632,10 @@ get_control_data(ClusterInfo *cluster, bool live_check) if (!got_data_checksum_version) pg_log(PG_REPORT, " data checksum version\n"); + /* value added in Postgres 12 */ + if (!got_key_management_enabled) + pg_log(PG_REPORT, " key management enabled\n"); + pg_fatal("Cannot continue without required control information, terminating\n"); } } @@ -669,6 +700,14 @@ check_control_data(ControlData *oldctrl, pg_fatal("old cluster uses data checksums but the new one does not\n"); else if (oldctrl->data_checksum_version != newctrl->data_checksum_version) pg_fatal("old and new cluster pg_controldata checksum versions do not match\n"); + + /* + * We cannot upgrade if the old cluster enables the key management but + * the new one doesn't support because the old one might already have + * data encrypted by the master encryption key. + */ + if (oldctrl->key_management_enabled && !newctrl->key_management_enabled) + pg_fatal("old cluster uses key management but the new one does not\n"); } diff --git a/src/bin/pg_upgrade/file.c b/src/bin/pg_upgrade/file.c index cc8a675d00..282359feed 100644 --- a/src/bin/pg_upgrade/file.c +++ b/src/bin/pg_upgrade/file.c @@ -11,6 +11,7 @@ #include #include +#include #ifdef HAVE_COPYFILE_H #include #endif @@ -21,6 +22,7 @@ #include "access/visibilitymap.h" #include "common/file_perm.h" +#include "common/file_utils.h" #include "pg_upgrade.h" #include "storage/bufpage.h" #include "storage/checksum.h" @@ -372,3 +374,78 @@ check_hard_link(void) unlink(new_link_file); } + +/* + * Copy cryptographic keys from the old cluster to the new cluster. + */ +void +copy_master_encryption_key(ClusterInfo *old_cluster, ClusterInfo * new_cluster) +{ + DIR *dir; + struct dirent *de; + char path[MAXPGPATH]; + + /* We copy the crypto keys only if both clusters enable the key management */ + if (!old_cluster->controldata.key_management_enabled || + !new_cluster->controldata.key_management_enabled) + return; + + prep_status("Copying master encryption key"); + + snprintf(path, MAXPGPATH, "%s/%s", old_cluster->pgdata, KMGR_DIR); + + if ((dir = opendir(path)) == NULL) + pg_fatal("could not open directory \"%s\": %m", path); + + while ((de = readdir(dir)) != NULL) + { + if (strlen(de->d_name) == 4 && + strspn(de->d_name, "0123456789ABCDEF") == 4) + { + CryptoKey key; + char src_path[MAXPGPATH]; + char dst_path[MAXPGPATH]; + uint32 id; + int src_fd; + int dst_fd; + int len; + + id = strtoul(de->d_name, NULL, 16); + + snprintf(src_path, MAXPGPATH, "%s/%s/%04X", + old_cluster->pgdata, KMGR_DIR, id); + snprintf(dst_path, MAXPGPATH, "%s/%s/%04X", + new_cluster->pgdata, KMGR_DIR, id); + + if ((src_fd = open(src_path, O_RDONLY | PG_BINARY, 0)) < 0) + pg_fatal("could not open file \"%s\": %m", src_path); + + if ((dst_fd = open(dst_path, O_RDWR | O_CREAT | O_TRUNC | PG_BINARY, + pg_file_create_mode)) < 0) + pg_fatal("could not open file \"%s\": %m", dst_path); + + /* Read the source key */ + len = read(src_fd, &key, sizeof(CryptoKey)); + if (len != sizeof(CryptoKey)) + { + if (len < 0) + pg_fatal("could not read file \"%s\": %m", src_path); + else + pg_fatal("could not read file \"%s\": read %d of %zu", + src_path, len, sizeof(CryptoKey)); + } + + /* Write to the dest key */ + len = write(dst_fd, &key, sizeof(CryptoKey)); + if (len != sizeof(CryptoKey)) + pg_fatal("could not write fie \"%s\"", dst_path); + + close(src_fd); + close(dst_fd); + } + } + + closedir(dir); + + check_ok(); +} diff --git a/src/bin/pg_upgrade/pg_upgrade.c b/src/bin/pg_upgrade/pg_upgrade.c index 70194eb096..5dd540e94f 100644 --- a/src/bin/pg_upgrade/pg_upgrade.c +++ b/src/bin/pg_upgrade/pg_upgrade.c @@ -157,6 +157,13 @@ main(int argc, char **argv) transfer_all_new_tablespaces(&old_cluster.dbarr, &new_cluster.dbarr, old_cluster.pgdata, new_cluster.pgdata); + /* + * Copy the internal encryption keys from the old cluster to the new one. + * This is necessary because the data in the old cluster might be + * encrypted with the old master encryption key. + */ + copy_master_encryption_key(&old_cluster, &new_cluster); + /* * Assuming OIDs are only used in system tables, there is no need to * restore the OID counter because we have not transferred any OIDs from diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h index 8b90cefbe0..32ab236265 100644 --- a/src/bin/pg_upgrade/pg_upgrade.h +++ b/src/bin/pg_upgrade/pg_upgrade.h @@ -11,6 +11,7 @@ #include #include "libpq-fe.h" +#include "common/kmgr_utils.h" /* Use port in the private/dynamic port number range */ #define DEF_PGUPORT 50432 @@ -219,6 +220,7 @@ typedef struct bool date_is_int; bool float8_pass_by_value; bool data_checksum_version; + bool key_management_enabled; } ControlData; /* @@ -375,6 +377,8 @@ void rewriteVisibilityMap(const char *fromfile, const char *tofile, const char *schemaName, const char *relName); void check_file_clone(void); void check_hard_link(void); +void copy_master_encryption_key(ClusterInfo *old_cluster, + ClusterInfo * new_cluster); /* fopen_priv() is no longer different from fopen() */ #define fopen_priv(path, mode) fopen(path, mode) diff --git a/src/common/Makefile b/src/common/Makefile index b220fa4dcf..a6993865c8 100644 --- a/src/common/Makefile +++ b/src/common/Makefile @@ -62,6 +62,7 @@ OBJS_COMMON = \ ip.o \ jsonapi.o \ keywords.o \ + kmgr_utils.o \ kwlookup.o \ link-canary.o \ md5.o \ diff --git a/src/common/kmgr_utils.c b/src/common/kmgr_utils.c new file mode 100644 index 0000000000..863eea969f --- /dev/null +++ b/src/common/kmgr_utils.c @@ -0,0 +1,435 @@ +/*------------------------------------------------------------------------- + * + * kmgr_utils.c + * Shared frontend/backend for cryptographic key management + * + * Copyright (c) 2020, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/common/kmgr_utils.c + * + *------------------------------------------------------------------------- + */ + +#ifndef FRONTEND +#include "postgres.h" +#else +#include "postgres_fe.h" +#endif + +#include +#include + +#include "common/aead.h" +#ifdef FRONTEND +#include "common/logging.h" +#endif +#include "common/file_perm.h" +#include "common/kmgr_utils.h" +#include "common/sha2.h" +#include "crypto/kmgr.h" +#include "utils/elog.h" +#include "storage/fd.h" + +#ifndef FRONTEND +#include "pgstat.h" +#include "storage/fd.h" +#endif + +#define KMGR_PROMPT_MSG "Enter database encryption pass phrase:" + +#ifdef FRONTEND +static FILE *open_pipe_stream(const char *command); +static int close_pipe_stream(FILE *file); +#endif + +static void read_one_keyfile(const char *dataDir, uint32 id, + CryptoKey *key_p); + +/* + * Verify the correctness of the given passphrase by unwrapping the given keys. + * If the given passphrase is correct we set unwrapped keys to keys_out and return + * true. Otherwise return false. Please note that this function changes the + * contents of keys_out even on failure. Both keys_in and keys_out must be the + * same length, nkey. + */ +bool +kmgr_verify_passphrase(char *passphrase, int passlen, + CryptoKey *keys_in, CryptoKey *keys_out, int nkeys) +{ + PgAeadCtx *tmpctx; + uint8 user_enckey[PG_AEAD_ENC_KEY_LEN]; + uint8 user_hmackey[PG_AEAD_MAC_KEY_LEN]; + + /* + * Create temporary wrap context with encryption key and HMAC key extracted + * from the passphrase. + */ + kmgr_derive_keys(passphrase, passlen, user_enckey, user_hmackey); + tmpctx = pg_create_aead_ctx(user_enckey, user_hmackey); + + for (int i = 0; i < nkeys; i++) + { + + if (!kmgr_unwrap_key(tmpctx, &(keys_in[i]), &(keys_out[i]))) + { + /* The passphrase is not correct */ + pg_free_aead_ctx(tmpctx); + return false; + } + } + + /* The passphrase is correct, free the cipher context */ + pg_free_aead_ctx(tmpctx); + + return true; +} + +/* Generate encryption key and mac key from given passphrase */ +void +kmgr_derive_keys(char *passphrase, Size passlen, + uint8 enckey[PG_AEAD_ENC_KEY_LEN], + uint8 mackey[PG_AEAD_MAC_KEY_LEN]) +{ + pg_sha256_ctx ctx1; + pg_sha512_ctx ctx2; + + StaticAssertStmt(PG_AEAD_ENC_KEY_LEN == PG_AES256_KEY_LEN, + "derived encryption key size does not match AES256 key size"); + StaticAssertStmt(PG_AEAD_MAC_KEY_LEN == PG_HMAC_SHA512_KEY_LEN, + "derived mac key size does not match HMAC-SHA512 key size"); + + /* Generate encryption key from passphrase */ + pg_sha256_init(&ctx1); + pg_sha256_update(&ctx1, (const uint8 *) passphrase, passlen); + pg_sha256_final(&ctx1, enckey); + + /* Generate mac key from passphrase */ + pg_sha512_init(&ctx2); + pg_sha512_update(&ctx2, (const uint8 *) passphrase, passlen); + pg_sha512_final(&ctx2, mackey); +} + +/* Wrap the given key */ +bool +kmgr_wrap_key(PgAeadCtx *ctx, CryptoKey *in, CryptoKey *out) +{ + if (!pg_aead_encrypt(ctx, in->key, in->klen, out->key, &(out->klen))) + return false; + + return true; +} + +/* Unwrap the given key */ +bool +kmgr_unwrap_key(PgAeadCtx *ctx, CryptoKey *in, CryptoKey *out) +{ + if (!pg_aead_decrypt(ctx, in->key, in->klen, out->key, &(out->klen))) + return false; + + return true; +} + +/* + * Run cluster passphrase command. + * + * prompt will be substituted for %p. + * + * The result will be put in buffer buf, which is of size size. + * The return value is the length of the actual result. + */ +int +kmgr_run_cluster_passphrase_command(char *passphrase_command, char *buf, + int size) +{ + char command[MAXPGPATH]; + char *p; + char *dp; + char *endp; + FILE *fh; + int pclose_rc; + size_t len = 0; + + Assert(size > 0); + buf[0] = '\0'; + + dp = command; + endp = command + MAXPGPATH - 1; + *endp = '\0'; + + for (p = passphrase_command; *p; p++) + { + if (p[0] == '%') + { + switch (p[1]) + { + case 'p': + StrNCpy(dp, KMGR_PROMPT_MSG, strlen(KMGR_PROMPT_MSG)); + dp += strlen(KMGR_PROMPT_MSG); + p++; + break; + case '%': + p++; + if (dp < endp) + *dp++ = *p; + break; + default: + if (dp < endp) + *dp++ = *p; + break; + } + } + else + { + if (dp < endp) + *dp++ = *p; + } + } + *dp = '\0'; + +#ifdef FRONTEND + fh = open_pipe_stream(command); + if (fh == NULL) + { + pg_log_fatal("could not execute command \"%s\": %m", + command); + exit(EXIT_FAILURE); + } +#else + fh = OpenPipeStream(command, "r"); + if (fh == NULL) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not execute command \"%s\": %m", + command))); +#endif + + if ((len = fread(buf, sizeof(char), size, fh)) < size) + { + if (ferror(fh)) + { +#ifdef FRONTEND + pg_log_fatal("could not read from command \"%s\": %m", + command); + exit(EXIT_FAILURE); +#else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not read from command \"%s\": %m", + command))); +#endif + } + } + +#ifdef FRONTEND + pclose_rc = close_pipe_stream(fh); +#else + pclose_rc = ClosePipeStream(fh); +#endif + + if (pclose_rc == -1) + { +#ifdef FRONTEND + pg_log_fatal("could not close pipe to external command: %m"); + exit(EXIT_FAILURE); +#else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not close pipe to external command: %m"))); +#endif + } + else if (pclose_rc != 0) + { +#ifdef FRONTEND + pg_log_fatal("command \"%s\" failed", command); + exit(EXIT_FAILURE); +#else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("command \"%s\" failed", + command), + errdetail_internal("%s", wait_result_to_str(pclose_rc)))); +#endif + } + + return len; +} + +#ifdef FRONTEND +static FILE * +open_pipe_stream(const char *command) +{ + FILE *res; + +#ifdef WIN32 + size_t cmdlen = strlen(command); + char *buf; + int save_errno; + + buf = malloc(cmdlen + 2 + 1); + if (buf == NULL) + { + errno = ENOMEM; + return NULL; + } + buf[0] = '"'; + mempcy(&buf[1], command, cmdlen); + buf[cmdlen + 1] = '"'; + buf[cmdlen + 2] = '\0'; + + res = _popen(buf, "r"); + + save_errno = errno; + free(buf); + errno = save_errno; +#else + res = popen(command, "r"); +#endif /* WIN32 */ + return res; +} + +static int +close_pipe_stream(FILE *file) +{ +#ifdef WIN32 + return _pclose(file); +#else + return pclose(file); +#endif /* WIN32 */ +} +#endif /* FRONTEND */ + +CryptoKey * +kmgr_get_cryptokeys(const char *path, int *nkeys) +{ + struct dirent *de; + DIR *dir; + CryptoKey *keys; + +#ifndef FRONTEND + if ((dir = AllocateDir(path)) == NULL) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not open directory \"%s\": %m", + path))); +#else + if ((dir = opendir(path)) == NULL) + pg_log_fatal("could not open directory \"%s\": %m", path); +#endif + + keys = (CryptoKey *) palloc0(sizeof(CryptoKey) * KMGR_MAX_INTERNAL_KEYS); + *nkeys = 0; + +#ifndef FRONTEND + while ((de = ReadDir(dir, KMGR_DIR)) != NULL) +#else + while ((de = readdir(dir)) != NULL) +#endif + { + if (strlen(de->d_name) == 4 && + strspn(de->d_name, "0123456789ABCDEF") == 4) + { + uint32 id; + + id = strtoul(de->d_name, NULL, 16); + + if (id < 0 || id >= KMGR_MAX_INTERNAL_KEYS) + { +#ifndef FRONTEND + elog(ERROR, "invalid cryptographic key identifier %u", id); +#else + pg_log_fatal("invalid cryptographic key identifier %u", id); +#endif + } + + if (*nkeys >= KMGR_MAX_INTERNAL_KEYS) + { +#ifndef FRONTEND + elog(ERROR, "too many cryptographic kes"); +#else + pg_log_fatal("too many cryptographic keys"); +#endif + } + + read_one_keyfile(path, id, &(keys[id])); + (*nkeys)++; + } + } + +#ifndef FRONTEND + FreeDir(dir); +#else + closedir(dir); +#endif + + return keys; +} + +static void +read_one_keyfile(const char *cryptoKeyDir, uint32 id, CryptoKey *key_p) +{ + char path[MAXPGPATH]; + int fd; + int r; + + CryptoKeyFilePath(path, cryptoKeyDir, id); + +#ifndef FRONTEND + if ((fd = OpenTransientFile(path, O_RDONLY | PG_BINARY)) == -1) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not open file \"%s\" for reading: %m", + path))); +#else + if ((fd = open(path, O_RDONLY | PG_BINARY, 0)) == -1) + pg_log_fatal("could not open file \"%s\" for reading: %m", + path); +#endif + +#ifndef FRONTEND + pgstat_report_wait_start(WAIT_EVENT_KEY_FILE_READ); +#endif + + /* Get key bytes */ + r = read(fd, key_p, sizeof(CryptoKey)); + if (r != sizeof(CryptoKey)) + { + if (r < 0) + { +#ifndef FRONTEND + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not read file \"%s\": %m", path))); +#else + pg_log_fatal("could not read file \"%s\": %m", path); +#endif + } + else + { +#ifndef FRONTEND + ereport(ERROR, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("could not read file \"%s\": read %d of %zu", + path, r, sizeof(CryptoKey)))); +#else + pg_log_fatal("could not read file \"%s\": read %d of %zu", + path, r, sizeof(CryptoKey)); +#endif + } + } + +#ifndef FRONTEND + pgstat_report_wait_end(); +#endif + +#ifndef FRONTEND + if (CloseTransientFile(fd) != 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not close file \"%s\": %m", + path))); +#else + if (close(fd) != 0) + pg_log_fatal("could not close file \"%s\": %m", path); +#endif +} diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h index e917dfe92d..37899e2a0b 100644 --- a/src/include/access/xlog.h +++ b/src/include/access/xlog.h @@ -316,6 +316,7 @@ extern void UpdateControlFile(void); extern uint64 GetSystemIdentifier(void); extern char *GetMockAuthenticationNonce(void); extern bool DataChecksumsEnabled(void); +extern bool KeyManagementEnabled(void); extern XLogRecPtr GetFakeLSNForUnloggedRel(void); extern Size XLOGShmemSize(void); extern void XLOGShmemInit(void); diff --git a/src/include/catalog/pg_control.h b/src/include/catalog/pg_control.h index de5670e538..91aafff91c 100644 --- a/src/include/catalog/pg_control.h +++ b/src/include/catalog/pg_control.h @@ -226,6 +226,9 @@ typedef struct ControlFileData */ char mock_authentication_nonce[MOCK_AUTH_NONCE_LEN]; + /* Key management cipher. Zero if no version */ + uint32 key_management_version; + /* CRC of all above ... MUST BE LAST! */ pg_crc32c crc; } ControlFileData; diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 61f2c2f5b4..9b4f8ddb2e 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -10936,4 +10936,10 @@ proname => 'is_normalized', prorettype => 'bool', proargtypes => 'text text', prosrc => 'unicode_is_normalized' }, +# function for key managements +{ oid => '8200', descr => 'rotate cluter passphrase', + proname => 'pg_rotate_cluster_passphrase', + provolatile => 'v', prorettype => 'bool', + proargtypes => '', prosrc => 'pg_rotate_cluster_passphrase' }, + ] diff --git a/src/include/common/kmgr_utils.h b/src/include/common/kmgr_utils.h new file mode 100644 index 0000000000..15d2d6932a --- /dev/null +++ b/src/include/common/kmgr_utils.h @@ -0,0 +1,70 @@ +/*------------------------------------------------------------------------- + * + * kmgr_utils.h + * Declarations for utility function for key management + * + * Portions Copyright (c) 2020, PostgreSQL Global Development Group + * + * src/include/common/kmgr_utils.h + * + *------------------------------------------------------------------------- + */ +#ifndef KMGR_UTILS_H +#define KMGR_UTILS_H + +#include "common/aead.h" +#include "common/cipher.h" + +/* Current version number */ +#define KMGR_VERSION 1 + +/* + * Directory where cryptographic keys reside within PGDATA. KMGR_DIR_TMP + * is used during cluster passphrase rotation. + */ +#define KMGR_DIR "pg_cryptokeys" +#define KMGR_TMP_DIR "pg_cryptokeys_tmp" + +/* + * Identifiers of internal keys. When adding a new internal key, we + * also need to add its key length to internalKeyLengths. + */ +#define KMGR_SQL_KEY_ID 0 + +#define KMGR_MAX_INTERNAL_KEYS 1 + +/* Allowed length of cluster passphrase */ +#define KMGR_MIN_PASSPHRASE_LEN 64 +#define KMGR_MAX_PASSPHRASE_LEN 1024 + +/* Maximum length of key the key manager can store */ +#define KMGR_MAX_KEY_LEN 128 +#define KMGR_MAX_WRAPPED_KEY_LEN AEADSizeOfCipherText(KMGR_MAX_KEY_LEN) + +/* CryptoKey file name is keys id */ +#define CryptoKeyFilePath(path, dir, id) \ + snprintf((path), MAXPGPATH, "%s/%04X", (dir), (id)) + +/* + * Cryptographic key data structure. This structure is used for + * both on-disk (raw key) and on-memory (wrapped key). + */ +typedef struct CryptoKey +{ + int klen; + uint8 key[KMGR_MAX_WRAPPED_KEY_LEN]; +} CryptoKey; + +extern void kmgr_derive_keys(char *passphrase, Size passlen, + uint8 enckey[PG_AEAD_ENC_KEY_LEN], + uint8 mackey[PG_AEAD_MAC_KEY_LEN]); +extern bool kmgr_verify_passphrase(char *passphrase, int passlen, + CryptoKey *keys_in, CryptoKey *keys_out, + int nkey); +extern bool kmgr_wrap_key(PgAeadCtx *ctx, CryptoKey *in, CryptoKey *out); +extern bool kmgr_unwrap_key(PgAeadCtx *ctx, CryptoKey *in, CryptoKey *out); +extern int kmgr_run_cluster_passphrase_command(char *passphrase_command, + char *buf, int size); +extern CryptoKey *kmgr_get_cryptokeys(const char *path, int *nkeys); + +#endif /* KMGR_UTILS_H */ diff --git a/src/include/crypto/kmgr.h b/src/include/crypto/kmgr.h new file mode 100644 index 0000000000..783f06d4c2 --- /dev/null +++ b/src/include/crypto/kmgr.h @@ -0,0 +1,29 @@ +/*------------------------------------------------------------------------- + * + * kmgr.h + * + * Portions Copyright (c) 2020, PostgreSQL Global Development Group + * + * src/include/crypto/kmgr.h + * + *------------------------------------------------------------------------- + */ +#ifndef KMGR_H +#define KMGR_H + +#include "common/cipher.h" +#include "common/kmgr_utils.h" +#include "storage/relfilenode.h" +#include "storage/bufpage.h" + +/* GUC parameters */ +extern bool key_management_enabled; +extern char *cluster_passphrase_command; + +extern Size KmgrShmemSize(void); +extern void KmgrShmemInit(void); +extern void BootStrapKmgr(void); +extern void InitializeKmgr(void); +extern const CryptoKey *KmgrGetKey(int id); + +#endif /* KMGR_H */ diff --git a/src/include/pgstat.h b/src/include/pgstat.h index c55dc1481c..2a8ad1c860 100644 --- a/src/include/pgstat.h +++ b/src/include/pgstat.h @@ -931,6 +931,9 @@ typedef enum WAIT_EVENT_DATA_FILE_TRUNCATE, WAIT_EVENT_DATA_FILE_WRITE, WAIT_EVENT_DSM_FILL_ZERO_WRITE, + WAIT_EVENT_KEY_FILE_READ, + WAIT_EVENT_KEY_FILE_WRITE, + WAIT_EVENT_KEY_FILE_SYNC, WAIT_EVENT_LOCK_FILE_ADDTODATADIR_READ, WAIT_EVENT_LOCK_FILE_ADDTODATADIR_SYNC, WAIT_EVENT_LOCK_FILE_ADDTODATADIR_WRITE, diff --git a/src/include/utils/guc_tables.h b/src/include/utils/guc_tables.h index 454c2df487..c0c53b1e13 100644 --- a/src/include/utils/guc_tables.h +++ b/src/include/utils/guc_tables.h @@ -89,6 +89,7 @@ enum config_group STATS, STATS_MONITORING, STATS_COLLECTOR, + ENCRYPTION, AUTOVACUUM, CLIENT_CONN, CLIENT_CONN_STATEMENT, diff --git a/src/test/Makefile b/src/test/Makefile index efb206aa75..5276c4184f 100644 --- a/src/test/Makefile +++ b/src/test/Makefile @@ -29,7 +29,7 @@ endif endif ifeq ($(with_openssl),yes) ifneq (,$(filter ssl,$(PG_TEST_EXTRA))) -SUBDIRS += ssl +SUBDIRS += ssl crypto endif endif -- 2.23.0