diff options
Diffstat (limited to 'src')
39 files changed, 1833 insertions, 32 deletions
diff --git a/src/backend/Makefile b/src/backend/Makefile index 9706a958488..4ace3020382 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 9867e1b4039..48ca46a941c 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -44,11 +44,13 @@ #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" #include "port/atomics.h" #include "postmaster/bgwriter.h" +#include "postmaster/postmaster.h" #include "postmaster/startup.h" #include "postmaster/walwriter.h" #include "replication/basebackup.h" @@ -81,6 +83,7 @@ #include "utils/timestamp.h" extern uint32 bootstrap_data_checksum_version; +extern int bootstrap_file_encryption_keylen; /* Unsupported old recovery command file names (relative to $PGDATA) */ #define RECOVERY_COMMAND_FILE "recovery.conf" @@ -4618,6 +4621,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->file_encryption_keylen = bootstrap_file_encryption_keylen; } static void @@ -4717,6 +4721,7 @@ ReadControlFile(void) pg_crc32c crc; int fd; static char wal_segsz_str[20]; + static char file_encryption_keylen_str[20]; int r; /* @@ -4905,6 +4910,12 @@ ReadControlFile(void) /* Make the initdb settings visible as GUC variables, too */ SetConfigOption("data_checksums", DataChecksumsEnabled() ? "yes" : "no", PGC_INTERNAL, PGC_S_OVERRIDE); + + Assert(ControlFile != NULL); + snprintf(file_encryption_keylen_str, sizeof(file_encryption_keylen_str), "%d", + ControlFile->file_encryption_keylen); + SetConfigOption("file_encryption_keylen", file_encryption_keylen_str, PGC_INTERNAL, + PGC_S_OVERRIDE); } /* @@ -5354,6 +5365,16 @@ BootStrapXLOG(void) /* some additional ControlFile fields are set in WriteControlFile() */ WriteControlFile(); + /* Enable file encryption if required */ + if (ControlFile->file_encryption_keylen > 0) + BootStrapKmgr(); + + if (terminal_fd != -1) + { + close(terminal_fd); + terminal_fd = -1; + } + /* Bootstrap the commit log, too */ BootStrapCLOG(); BootStrapCommitTs(); diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c index a7ed93fdc14..bf93135a483 100644 --- a/src/backend/bootstrap/bootstrap.c +++ b/src/backend/bootstrap/bootstrap.c @@ -28,12 +28,14 @@ #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" #include "pg_getopt.h" #include "pgstat.h" #include "postmaster/bgwriter.h" +#include "postmaster/postmaster.h" #include "postmaster/startup.h" #include "postmaster/walwriter.h" #include "replication/walreceiver.h" @@ -51,6 +53,8 @@ #include "utils/relmapper.h" uint32 bootstrap_data_checksum_version = 0; /* No checksum */ +int bootstrap_file_encryption_keylen = 0; /* disabled */ +char *bootstrap_old_key_datadir = NULL; /* disabled */ static void CheckerModeMain(void); @@ -224,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:FkK:r:R:u:x:X:-:")) != -1) { switch (flag) { @@ -253,9 +257,18 @@ AuxiliaryProcessMain(int argc, char *argv[]) case 'k': bootstrap_data_checksum_version = PG_DATA_CHECKSUM_VERSION; break; + case 'K': + bootstrap_file_encryption_keylen = atoi(optarg); + break; + case 'u': + bootstrap_old_key_datadir = pstrdup(optarg); + break; case 'r': strlcpy(OutputFileName, optarg, MAXPGPATH); break; + case 'R': + terminal_fd = atoi(optarg); + break; case 'x': MyAuxProcType = atoi(optarg); break; @@ -312,6 +325,12 @@ AuxiliaryProcessMain(int argc, char *argv[]) proc_exit(1); } + if (bootstrap_file_encryption_keylen != 0 && + bootstrap_file_encryption_keylen != 128 && + bootstrap_file_encryption_keylen != 192 && + bootstrap_file_encryption_keylen != 256) + elog(PANIC, "unrecognized file encryption length: %d", bootstrap_file_encryption_keylen); + switch (MyAuxProcType) { case StartupProcess: diff --git a/src/backend/crypto/Makefile b/src/backend/crypto/Makefile new file mode 100644 index 00000000000..c27362029d8 --- /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 00000000000..4e701e02756 --- /dev/null +++ b/src/backend/crypto/kmgr.c @@ -0,0 +1,372 @@ +/*------------------------------------------------------------------------- + * + * kmgr.c + * Cluster file encryption routines + * + * Cluster file encryption is enabled if user requests it during initdb. + * During bootstrap, we generate data encryption keys, wrap them with the + * cluster-level key, 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. All wrapping and unwrapping key + * routines require the OpenSSL library. + * + * Copyright (c) 2020, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/backend/crypto/kmgr.c + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include <sys/stat.h> +#include <unistd.h> + +#include "funcapi.h" +#include "miscadmin.h" +#include "pgstat.h" + +#include "common/file_perm.h" +#include "common/hex_decode.h" +#include "common/kmgr_utils.h" +#include "common/sha2.h" +#include "access/xlog.h" +#include "crypto/kmgr.h" +#include "storage/copydir.h" +#include "storage/fd.h" +#include "storage/ipc.h" +#include "storage/shmem.h" +#include "utils/builtins.h" +#include "utils/memutils.h" +/* Struct stores file encryption keys in plaintext format */ +typedef struct KmgrShmemData +{ + CryptoKey intlKeys[KMGR_MAX_INTERNAL_KEYS]; +} KmgrShmemData; +static KmgrShmemData *KmgrShmem; + +/* GUC variables */ +char *cluster_key_command = NULL; +int file_encryption_keylen = 0; + +CryptoKey bootstrap_keys[KMGR_MAX_INTERNAL_KEYS]; + +extern char *bootstrap_old_key_datadir; +extern int bootstrap_file_encryption_keylen; + +static void bzeroKmgrKeys(int status, Datum arg); +static void KmgrSaveCryptoKeys(const char *dir, CryptoKey *keys); +static CryptoKey *generate_crypto_key(int len); + +/* + * This function must be called ONCE during initdb. + */ +void +BootStrapKmgr(void) +{ + char live_path[MAXPGPATH]; + CryptoKey *keys_wrap; + int nkeys; + char cluster_key_hex[ALLOC_KMGR_CLUSTER_KEY_LEN]; + int cluster_key_hex_len; + unsigned char cluster_key[KMGR_CLUSTER_KEY_LEN]; + +#ifndef USE_OPENSSL + ereport(ERROR, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + (errmsg("cluster file encryption is not supported because OpenSSL is not supported by this build"), + errhint("Compile with --with-openssl to use this feature.")))); +#endif + + snprintf(live_path, sizeof(live_path), "%s/%s", DataDir, LIVE_KMGR_DIR); + + /* copy cluster file encryption keys from an old cluster? */ + if (bootstrap_old_key_datadir != NULL) + { + char old_key_dir[MAXPGPATH]; + + snprintf(old_key_dir, sizeof(old_key_dir), "%s/%s", + bootstrap_old_key_datadir, LIVE_KMGR_DIR); + copydir(old_key_dir, LIVE_KMGR_DIR, true); + } + /* create empty directory */ + else + { + if (mkdir(LIVE_KMGR_DIR, pg_dir_create_mode) < 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not create cluster file encryption directory \"%s\": %m", + LIVE_KMGR_DIR))); + } + + /* + * Get key encryption key from the cluster_key command. The cluster_key + * command might want to check for the existance of files in the + * live directory, so run this _after_ copying the directory in place. + */ + cluster_key_hex_len = kmgr_run_cluster_key_command(cluster_key_command, + cluster_key_hex, + ALLOC_KMGR_CLUSTER_KEY_LEN, + live_path); + + if (hex_decode(cluster_key_hex, cluster_key_hex_len, (char*) cluster_key) != + KMGR_CLUSTER_KEY_LEN) + ereport(ERROR, + (errmsg("cluster key must be %d hexadecimal characters", + KMGR_CLUSTER_KEY_LEN * 2))); + + /* generate new cluster file encryption keys */ + if (bootstrap_old_key_datadir == NULL) + { + CryptoKey bootstrap_keys_wrap[KMGR_MAX_INTERNAL_KEYS]; + PgCipherCtx *cluster_key_ctx; + + /* Create KEK encryption context */ + cluster_key_ctx = pg_cipher_ctx_create(PG_CIPHER_AES_GCM, cluster_key, + KMGR_CLUSTER_KEY_LEN, true); + if (!cluster_key_ctx) + elog(ERROR, "could not initialize encryption context"); + + /* Wrap all data encryption keys by key encryption key */ + for (int id = 0; id < KMGR_MAX_INTERNAL_KEYS; id++) + { + CryptoKey *key; + + /* generate a data encryption key */ + key = generate_crypto_key(bootstrap_file_encryption_keylen); + + /* Set this key's ID */ + key->pgkey_id = id; + + if (!kmgr_wrap_key(cluster_key_ctx, key, &(bootstrap_keys_wrap[id]))) + { + pg_cipher_ctx_free(cluster_key_ctx); + elog(ERROR, "failed to wrap data encryption key"); + } + + explicit_bzero(key, sizeof(CryptoKey)); + } + + /* Save data encryption keys to the disk */ + KmgrSaveCryptoKeys(LIVE_KMGR_DIR, bootstrap_keys_wrap); + + explicit_bzero(bootstrap_keys_wrap, sizeof(bootstrap_keys_wrap)); + pg_cipher_ctx_free(cluster_key_ctx); + } + + /* + * We are either decrypting keys we copied from an old cluster, or + * decrypting keys we just wrote above --- either way, we decrypt + * them here and store them in a file-scoped variable for use in + * later encrypting during bootstrap mode. + */ + + /* Get the crypto keys from the file */ + keys_wrap = kmgr_get_cryptokeys(LIVE_KMGR_DIR, &nkeys); + Assert(nkeys == KMGR_MAX_INTERNAL_KEYS); + + if (!kmgr_verify_cluster_key(cluster_key, keys_wrap, bootstrap_keys, + KMGR_MAX_INTERNAL_KEYS)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("supplied cluster key does not match expected cluster_key"))); + + /* bzero keys on exit */ + on_proc_exit(bzeroKmgrKeys, 0); + + explicit_bzero(cluster_key_hex, cluster_key_hex_len); + explicit_bzero(cluster_key, KMGR_CLUSTER_KEY_LEN); +} + +/* Report shared-memory space needed by KmgrShmem */ +Size +KmgrShmemSize(void) +{ + if (!file_encryption_keylen) + return 0; + + return MAXALIGN(sizeof(KmgrShmemData)); +} + +/* Allocate and initialize key manager memory */ +void +KmgrShmemInit(void) +{ + bool found; + + if (!file_encryption_keylen) + return; + + KmgrShmem = (KmgrShmemData *) ShmemInitStruct("File encryption key manager", + KmgrShmemSize(), &found); + + on_shmem_exit(bzeroKmgrKeys, 0); +} + +/* + * Get cluster key and verify it, then get the data encryption keys. + * This function is called by postmaster at startup time. + */ +void +InitializeKmgr(void) +{ + CryptoKey *keys_wrap; + int nkeys; + char cluster_key_hex[ALLOC_KMGR_CLUSTER_KEY_LEN]; + int cluster_key_hex_len; + struct stat buffer; + char live_path[MAXPGPATH]; + unsigned char cluster_key[KMGR_CLUSTER_KEY_LEN]; + + if (!file_encryption_keylen) + return; + + elog(DEBUG1, "starting up cluster file encryption manager"); + + if (stat(KMGR_DIR, &buffer) != 0 || !S_ISDIR(buffer.st_mode)) + ereport(ERROR, + (errcode(ERRCODE_INTERNAL_ERROR), + (errmsg("cluster file encryption directory %s is missing", KMGR_DIR)))); + + if (stat(KMGR_DIR_PID, &buffer) == 0) + ereport(ERROR, + (errcode(ERRCODE_INTERNAL_ERROR), + (errmsg("cluster had a pg_alterckey failure that needs repair or pg_alterckey is running"), + errhint("Run pg_alterckey --repair or wait for it to complete.")))); + + /* + * We want OLD deleted since it allows access to the data encryption + * keys using the old cluster key. If NEW exists, it means either + * NEW is partly written, or NEW wasn't renamed to LIVE --- in either + * case, it needs to be repaired. + */ + if (stat(OLD_KMGR_DIR, &buffer) == 0 || stat(NEW_KMGR_DIR, &buffer) == 0) + ereport(ERROR, + (errcode(ERRCODE_INTERNAL_ERROR), + (errmsg("cluster had a pg_alterckey failure that needs repair"), + errhint("Run pg_alterckey --repair.")))); + + /* If OLD, NEW, and LIVE do not exist, there is a serious problem. */ + if (stat(LIVE_KMGR_DIR, &buffer) != 0 || !S_ISDIR(buffer.st_mode)) + ereport(ERROR, + (errcode(ERRCODE_INTERNAL_ERROR), + (errmsg("cluster has no data encryption keys")))); + + /* Get cluster key */ + snprintf(live_path, sizeof(live_path), "%s/%s", DataDir, LIVE_KMGR_DIR); + cluster_key_hex_len = kmgr_run_cluster_key_command(cluster_key_command, + cluster_key_hex, + ALLOC_KMGR_CLUSTER_KEY_LEN, + live_path); + + if (hex_decode(cluster_key_hex, cluster_key_hex_len, (char*) cluster_key) != + KMGR_CLUSTER_KEY_LEN) + ereport(ERROR, + (errmsg("cluster key must be %d hexadecimal characters", + KMGR_CLUSTER_KEY_LEN * 2))); + + /* Get the crypto keys from the file */ + keys_wrap = kmgr_get_cryptokeys(LIVE_KMGR_DIR, &nkeys); + Assert(nkeys == KMGR_MAX_INTERNAL_KEYS); + + /* + * Verify cluster key and prepare a data encryption key in plaintext in shared memory. + */ + if (!kmgr_verify_cluster_key(cluster_key, keys_wrap, KmgrShmem->intlKeys, + KMGR_MAX_INTERNAL_KEYS)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("supplied cluster key does not match expected cluster key"))); + + explicit_bzero(cluster_key_hex, cluster_key_hex_len); + explicit_bzero(cluster_key, KMGR_CLUSTER_KEY_LEN); +} + +static void +bzeroKmgrKeys(int status, Datum arg) +{ + if (IsBootstrapProcessingMode()) + explicit_bzero(bootstrap_keys, sizeof(bootstrap_keys)); + else + explicit_bzero(KmgrShmem->intlKeys, sizeof(KmgrShmem->intlKeys)); +} + +const CryptoKey * +KmgrGetKey(int id) +{ + Assert(id < KMGR_MAX_INTERNAL_KEYS); + + return (const CryptoKey *) (IsBootstrapProcessingMode() ? + &(bootstrap_keys[id]) : &(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)); + + /* We store the key as length + key into 'encrypted_key' */ + memcpy(newkey->encrypted_key, &len, sizeof(len)); + + if (!pg_strong_random(newkey->encrypted_key + sizeof(len), len)) + elog(ERROR, "failed to generate new file encryption key"); + + return newkey; +} + +/* + * Save the given file encryption keys to the disk. + */ +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))); + } +} diff --git a/src/backend/main/main.c b/src/backend/main/main.c index b6e51288326..19aa502614e 100644 --- a/src/backend/main/main.c +++ b/src/backend/main/main.c @@ -324,6 +324,7 @@ help(const char *progname) #endif printf(_(" -N MAX-CONNECT maximum number of allowed connections\n")); printf(_(" -p PORT port number to listen on\n")); + printf(_(" -R fd prompt for the cluster key\n")); printf(_(" -s show statistics after each query\n")); printf(_(" -S WORK-MEM set amount of memory for sorts (in kB)\n")); printf(_(" -V, --version output version information, then exit\n")); @@ -351,7 +352,9 @@ help(const char *progname) printf(_("\nOptions for bootstrapping mode:\n")); printf(_(" --boot selects bootstrapping mode (must be first argument)\n")); printf(_(" DBNAME database name (mandatory argument in bootstrapping mode)\n")); + printf(_(" -K LEN enable cluster file encryption with specified key length\n")); printf(_(" -r FILENAME send stdout and stderr to given file\n")); + printf(_(" -u DATADIR copy encryption keys from datadir\n")); printf(_(" -x NUM internal use\n")); printf(_("\nPlease read the documentation for the complete list of run-time\n" diff --git a/src/backend/postmaster/pgstat.c b/src/backend/postmaster/pgstat.c index d87d9d06ee2..71f2b90ca86 100644 --- a/src/backend/postmaster/pgstat.c +++ b/src/backend/postmaster/pgstat.c @@ -4152,6 +4152,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 fff4227e0b6..bf883184b11 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" @@ -231,6 +232,7 @@ static int SendStop = false; /* still more option variables */ bool EnableSSL = false; +int terminal_fd = -1; int PreAuthDelay = 0; int AuthenticationTimeout = 60; @@ -687,7 +689,7 @@ PostmasterMain(int argc, char *argv[]) * tcop/postgres.c (the option sets should not conflict) and with the * common help() function in main/main.c. */ - while ((opt = getopt(argc, argv, "B:bc:C:D:d:EeFf:h:ijk:lN:nOPp:r:S:sTt:W:-:")) != -1) + while ((opt = getopt(argc, argv, "B:bc:C:D:d:EeFf:h:ijk:lN:nOPp:r:R:S:sTt:W:-:")) != -1) { switch (opt) { @@ -778,6 +780,10 @@ PostmasterMain(int argc, char *argv[]) /* only used by single-user backend */ break; + case 'R': + terminal_fd = atoi(optarg); + break; + case 'S': SetConfigOption("work_mem", optarg, PGC_POSTMASTER, PGC_S_ARGV); break; @@ -1326,6 +1332,11 @@ PostmasterMain(int argc, char *argv[]) */ RemovePgTempFiles(); + InitializeKmgr(); + + if (terminal_fd != -1) + close(terminal_fd); + /* * Initialize stats collection subsystem (this does NOT start the * collector process!) diff --git a/src/backend/replication/basebackup.c b/src/backend/replication/basebackup.c index 1d8d1742a73..c9fc2f4b4e1 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" @@ -152,6 +153,10 @@ struct exclude_list_item */ static const char *const excludeDirContents[] = { + /* Skip temporary crypto key directories */ + NEW_KMGR_DIR, + OLD_KMGR_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 96c2aaabbd6..64fe10ca2a7 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -23,6 +23,7 @@ #include "access/syncscan.h" #include "access/twophase.h" #include "commands/async.h" +#include "crypto/kmgr.h" #include "miscadmin.h" #include "pgstat.h" #include "postmaster/autovacuum.h" @@ -149,6 +150,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 @@ -267,6 +269,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 774292fd942..a44805d5c39 100644 --- a/src/backend/storage/lmgr/lwlocknames.txt +++ b/src/backend/storage/lmgr/lwlocknames.txt @@ -53,3 +53,4 @@ XactTruncationLock 44 # 45 was XactTruncationLock until removal of BackendRandomLock WrapLimitsVacuumLock 46 NotifyQueueTailLock 47 +KmgrFileLock 48 diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index d35c5020ea6..81e64616d43 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" @@ -3578,7 +3579,7 @@ process_postgres_switches(int argc, char *argv[], GucContext ctx, * postmaster/postmaster.c (the option sets should not conflict) and with * the common help() function in main/main.c. */ - while ((flag = getopt(argc, argv, "B:bc:C:D:d:EeFf:h:ijk:lN:nOPp:r:S:sTt:v:W:-:")) != -1) + while ((flag = getopt(argc, argv, "B:bc:C:D:d:EeFf:h:ijk:lN:nOPp:r:R:S:sTt:v:W:-:")) != -1) { switch (flag) { @@ -3670,6 +3671,16 @@ process_postgres_switches(int argc, char *argv[], GucContext ctx, strlcpy(OutputFileName, optarg, MAXPGPATH); break; + case 'R': + terminal_fd = atoi(optarg); + if (terminal_fd == -1) + /* + * Allow file descriptor closing to be bypassed via -1. + * We just dup sterr. This is useful for single-user mode. + */ + terminal_fd = dup(2); + break; + case 'S': SetConfigOption("work_mem", optarg, ctx, gucsource); break; @@ -3922,6 +3933,18 @@ PostgresMain(int argc, char *argv[], BaseInit(); /* + * Initialize kmgr for cluster encryption. Since kmgr needs to attach to + * shared memory the initialization must be called after BaseInit(). + */ + if (!IsUnderPostmaster) + { + InitializeKmgr(); + + if (terminal_fd != -1) + close(terminal_fd); + } + + /* * Create a per-backend PGPROC struct in shared memory, except in the * EXEC_BACKEND case where this was done in SubPostmasterMain. We must do * this before we can use LWLocks (and in the EXEC_BACKEND case we already diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c index 878fcc22365..bbaf037bc6e 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -47,6 +47,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" @@ -745,6 +746,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 */ @@ -3389,6 +3392,17 @@ static struct config_int ConfigureNamesInt[] = check_huge_page_size, NULL, NULL }, + { + {"file_encryption_keylen", PGC_INTERNAL, PRESET_OPTIONS, + gettext_noop("Shows the bit length of the file encryption key."), + NULL, + GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE + }, + &file_encryption_keylen, + 0, 0, 256, + NULL, NULL, NULL + }, + /* End-of-list marker */ { {NULL, 0, 0, NULL, NULL}, NULL, 0, 0, 0, NULL, NULL, NULL @@ -4384,6 +4398,16 @@ static struct config_string ConfigureNamesString[] = }, { + {"cluster_key_command", PGC_SIGHUP, ENCRYPTION, + gettext_noop("Command to obtain cluster key for cluster file encryption."), + NULL + }, + &cluster_key_command, + "", + NULL, NULL, NULL + }, + + { {"application_name", PGC_USERSET, LOGGING_WHAT, gettext_noop("Sets the application name to be reported in statistics and logs."), NULL, diff --git a/src/backend/utils/misc/pg_controldata.c b/src/backend/utils/misc/pg_controldata.c index d50d87a6021..6fcab311721 100644 --- a/src/backend/utils/misc/pg_controldata.c +++ b/src/backend/utils/misc/pg_controldata.c @@ -263,8 +263,8 @@ pg_control_recovery(PG_FUNCTION_ARGS) Datum pg_control_init(PG_FUNCTION_ARGS) { - Datum values[11]; - bool nulls[11]; + Datum values[12]; + bool nulls[12]; TupleDesc tupdesc; HeapTuple htup; ControlFileData *ControlFile; @@ -274,7 +274,7 @@ pg_control_init(PG_FUNCTION_ARGS) * Construct a tuple descriptor for the result row. This must match this * function's pg_proc entry! */ - tupdesc = CreateTemplateTupleDesc(11); + tupdesc = CreateTemplateTupleDesc(12); TupleDescInitEntry(tupdesc, (AttrNumber) 1, "max_data_alignment", INT4OID, -1, 0); TupleDescInitEntry(tupdesc, (AttrNumber) 2, "database_block_size", @@ -297,6 +297,8 @@ pg_control_init(PG_FUNCTION_ARGS) BOOLOID, -1, 0); TupleDescInitEntry(tupdesc, (AttrNumber) 11, "data_page_checksum_version", INT4OID, -1, 0); + TupleDescInitEntry(tupdesc, (AttrNumber) 12, "file_encryption_keylen", + INT4OID, -1, 0); tupdesc = BlessTupleDesc(tupdesc); /* read the control file */ @@ -338,6 +340,9 @@ pg_control_init(PG_FUNCTION_ARGS) values[10] = Int32GetDatum(ControlFile->data_checksum_version); nulls[10] = false; + values[11] = Int32GetDatum(ControlFile->file_encryption_keylen); + nulls[11] = false; + htup = heap_form_tuple(tupdesc, values, nulls); PG_RETURN_DATUM(HeapTupleGetDatum(htup)); diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index b7fb2ec1feb..f19cfe5f6ae 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -632,6 +632,11 @@ # autovacuum, -1 means use # vacuum_cost_limit +#------------------------------------------------------------------------------ +# ENCRYPTION +#------------------------------------------------------------------------------ + +#cluster_key_command = '' #------------------------------------------------------------------------------ # CLIENT CONNECTION DEFAULTS diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c index f994c4216bc..92594772f6c 100644 --- a/src/bin/initdb/initdb.c +++ b/src/bin/initdb/initdb.c @@ -141,11 +141,16 @@ static bool debug = false; static bool noclean = false; static bool do_sync = true; static bool sync_only = false; +static bool pass_terminal_fd = false; +static char *term_fd_opt = NULL; +static int file_encryption_keylen = 0; static bool show_setting = false; 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_key_cmd = NULL; +static char *old_key_datadir = NULL; /* internal vars */ @@ -203,6 +208,7 @@ static const char *const subdirs[] = { "global", "pg_wal/archive_status", "pg_commit_ts", + "pg_cryptokeys", "pg_dynshmem", "pg_notify", "pg_serial", @@ -954,12 +960,13 @@ test_config_settings(void) test_buffs = MIN_BUFS_FOR_CONNS(test_conns); snprintf(cmd, sizeof(cmd), - "\"%s\" --boot -x0 %s " + "\"%s\" --boot -x0 %s %s " "-c max_connections=%d " "-c shared_buffers=%d " "-c dynamic_shared_memory_type=%s " "< \"%s\" > \"%s\" 2>&1", backend_exec, boot_options, + term_fd_opt ? term_fd_opt : "", test_conns, test_buffs, dynamic_shared_memory_type, DEVNULL, DEVNULL); @@ -990,12 +997,13 @@ test_config_settings(void) } snprintf(cmd, sizeof(cmd), - "\"%s\" --boot -x0 %s " + "\"%s\" --boot -x0 %s %s " "-c max_connections=%d " "-c shared_buffers=%d " "-c dynamic_shared_memory_type=%s " "< \"%s\" > \"%s\" 2>&1", backend_exec, boot_options, + term_fd_opt ? term_fd_opt : "", n_connections, test_buffs, dynamic_shared_memory_type, DEVNULL, DEVNULL); @@ -1185,6 +1193,13 @@ setup_config(void) "password_encryption = md5"); } + if (cluster_key_cmd) + { + snprintf(repltok, sizeof(repltok), "cluster_key_command = '%s'", + escape_quotes(cluster_key_cmd)); + conflines = replace_token(conflines, "#cluster_key_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 @@ -1394,13 +1409,22 @@ bootstrap_template1(void) /* Also ensure backend isn't confused by this environment var: */ unsetenv("PGCLIENTENCODING"); + if (file_encryption_keylen != 0) + sprintf(buf, "%d", file_encryption_keylen); + else + buf[0] = '\0'; + snprintf(cmd, sizeof(cmd), - "\"%s\" --boot -x1 -X %u %s %s %s", + "\"%s\" --boot -x1 -X %u %s %s %s %s %s %s %s %s", backend_exec, wal_segment_size_mb * (1024 * 1024), data_checksums ? "-k" : "", + cluster_key_cmd ? "-K" : "", buf, + old_key_datadir ? "-u" : "", + old_key_datadir ? old_key_datadir : "", boot_options, - debug ? "-d 5" : ""); + debug ? "-d 5" : "", + term_fd_opt ? term_fd_opt : ""); PG_CMD_OPEN; @@ -2281,19 +2305,25 @@ usage(const char *progname) " set default locale in the respective category for\n" " new databases (default taken from environment)\n")); printf(_(" --no-locale equivalent to --locale=C\n")); - printf(_(" --pwfile=FILE read password for the new superuser from file\n")); + printf(_(" --pwfile=FILE read the new superuser password from file\n")); printf(_(" -T, --text-search-config=CFG\n" " default text search configuration\n")); printf(_(" -U, --username=NAME database superuser name\n")); - printf(_(" -W, --pwprompt prompt for a password for the new superuser\n")); + printf(_(" -W, --pwprompt prompt for the new superuser password\n")); printf(_(" -X, --waldir=WALDIR location for the write-ahead log directory\n")); printf(_(" --wal-segsize=SIZE size of WAL segments, in megabytes\n")); printf(_("\nLess commonly used options:\n")); + printf(_(" -c --cluster-key-command=COMMAND\n" + " enable cluster file encryption and set command\n" + " to obtain the cluster key\n")); printf(_(" -d, --debug generate lots of debugging output\n")); printf(_(" -k, --data-checksums use data page checksums\n")); + printf(_(" -K, --file-encryption-keylen\n" + " bit length of the file encryption key\n")); printf(_(" -L DIRECTORY where to find the input files\n")); printf(_(" -n, --no-clean do not clean up after errors\n")); printf(_(" -N, --no-sync do not wait for changes to be written safely to disk\n")); + printf(_(" -R, --authprompt prompt for a passphrase or PIN\n")); printf(_(" -s, --show show internal settings\n")); printf(_(" -S, --sync-only only sync data directory\n")); printf(_("\nOther options:\n")); @@ -2860,6 +2890,23 @@ initialize_data_directory(void) /* Top level PG_VERSION is checked by bootstrapper, so make it first */ write_version_file(NULL); + if (pass_terminal_fd) + { +#ifndef WIN32 + int terminal_fd = open("/dev/tty", O_RDWR, 0); +#else + int terminal_fd = open("CONOUT$", O_RDWR, 0); +#endif + + if (terminal_fd < 0) + { + pg_log_error(_("%s: could not open terminal: %s\n"), + progname, strerror(errno)); + exit(1); + } + term_fd_opt = psprintf("-R %d", terminal_fd); + } + /* Select suitable configuration settings */ set_null_conf(); test_config_settings(); @@ -2883,8 +2930,9 @@ initialize_data_directory(void) fflush(stdout); snprintf(cmd, sizeof(cmd), - "\"%s\" %s template1 >%s", + "\"%s\" %s %s template1 >%s", backend_exec, backend_options, + term_fd_opt ? term_fd_opt : "", DEVNULL); PG_CMD_OPEN; @@ -2957,7 +3005,11 @@ main(int argc, char *argv[]) {"waldir", required_argument, NULL, 'X'}, {"wal-segsize", required_argument, NULL, 12}, {"data-checksums", no_argument, NULL, 'k'}, + {"authprompt", no_argument, NULL, 'R'}, + {"file-encryption-keylen", no_argument, NULL, 'K'}, {"allow-group-access", no_argument, NULL, 'g'}, + {"cluster-key-command", required_argument, NULL, 'c'}, + {"copy-encryption-keys", required_argument, NULL, 'u'}, {NULL, 0, NULL, 0} }; @@ -2999,7 +3051,7 @@ main(int argc, char *argv[]) /* process command-line options */ - while ((c = getopt_long(argc, argv, "A:dD:E:gkL:nNsST:U:WX:", long_options, &option_index)) != -1) + while ((c = getopt_long(argc, argv, "A:c:dD:E:gkK:L:nNRsST:u:U:WX:", long_options, &option_index)) != -1) { switch (c) { @@ -3045,6 +3097,12 @@ main(int argc, char *argv[]) case 'N': do_sync = false; break; + case 'R': + pass_terminal_fd = true; + break; + case 'K': + file_encryption_keylen = atoi(optarg); + break; case 'S': sync_only = true; break; @@ -3081,6 +3139,12 @@ main(int argc, char *argv[]) case 9: pwfilename = pg_strdup(optarg); break; + case 'c': + cluster_key_cmd = pg_strdup(optarg); + break; + case 'u': + old_key_datadir = pg_strdup(optarg); + break; case 's': show_setting = true; break; @@ -3151,6 +3215,37 @@ main(int argc, char *argv[]) exit(1); } +#ifndef USE_OPENSSL + if (cluster_key_cmd) + { + pg_log_error("cluster file encryption is not supported because OpenSSL is not supported by this build"); + exit(1); + } +#endif + + if (old_key_datadir != NULL && cluster_key_cmd == NULL) + { + pg_log_error("copying encryption keys requires the cluster key command to be specified"); + exit(1); + } + + if (file_encryption_keylen != 0 && cluster_key_cmd == NULL) + { + pg_log_error("a non-zero file encryption key length requires the cluster key command to be specified"); + exit(1); + } + + if (file_encryption_keylen != 0 && file_encryption_keylen != 128 && + file_encryption_keylen != 192 && file_encryption_keylen != 256) + { + pg_log_error("invalid file encrypt key length; supported values are 0 (disabled), 128, 192, and 256"); + exit(1); + } + + /* set the default */ + if (file_encryption_keylen == 0 && cluster_key_cmd != NULL) + file_encryption_keylen = 128; + check_authmethod_unspecified(&authmethodlocal); check_authmethod_unspecified(&authmethodhost); @@ -3218,6 +3313,11 @@ main(int argc, char *argv[]) else printf(_("Data page checksums are disabled.\n")); + if (cluster_key_cmd) + printf(_("Cluster file encryption is enabled.\n")); + else + printf(_("Cluster file encryption 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 3e00ac0f701..c3b38b7c51c 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(_("File encryption key length: %d\n"), + ControlFile->file_encryption_keylen); return 0; } diff --git a/src/bin/pg_ctl/pg_ctl.c b/src/bin/pg_ctl/pg_ctl.c index fc07f1aba6e..5fa1f72ae18 100644 --- a/src/bin/pg_ctl/pg_ctl.c +++ b/src/bin/pg_ctl/pg_ctl.c @@ -79,6 +79,7 @@ typedef enum static bool do_wait = true; static int wait_seconds = DEFAULT_WAIT; static bool wait_seconds_arg = false; +static bool pass_terminal_fd = false; static bool silent_mode = false; static ShutdownMode shutdown_mode = FAST_MODE; static int sig = SIGINT; /* default */ @@ -442,7 +443,7 @@ free_readfile(char **optlines) static pgpid_t start_postmaster(void) { - char cmd[MAXPGPATH]; + char cmd[MAXPGPATH], *term_fd_opt = NULL; #ifndef WIN32 pgpid_t pm_pid; @@ -467,6 +468,19 @@ start_postmaster(void) /* fork succeeded, in child */ + if (pass_terminal_fd) + { + int terminal_fd = open("/dev/tty", O_RDWR, 0); + + if (terminal_fd < 0) + { + write_stderr(_("%s: could not open terminal: %s\n"), + progname, strerror(errno)); + exit(1); + } + term_fd_opt = psprintf(" -R %d", terminal_fd); + } + /* * If possible, detach the postmaster process from the launching process * group and make it a group leader, so that it doesn't get signaled along @@ -487,12 +501,14 @@ start_postmaster(void) * has the same PID as the current child process. */ if (log_file != NULL) - snprintf(cmd, MAXPGPATH, "exec \"%s\" %s%s < \"%s\" >> \"%s\" 2>&1", + snprintf(cmd, MAXPGPATH, "exec \"%s\" %s%s%s < \"%s\" >> \"%s\" 2>&1", exec_path, pgdata_opt, post_opts, + term_fd_opt ? term_fd_opt : "", DEVNULL, log_file); else - snprintf(cmd, MAXPGPATH, "exec \"%s\" %s%s < \"%s\" 2>&1", - exec_path, pgdata_opt, post_opts, DEVNULL); + snprintf(cmd, MAXPGPATH, "exec \"%s\" %s%s%s < \"%s\" 2>&1", + exec_path, pgdata_opt, post_opts, + term_fd_opt ? term_fd_opt : "", DEVNULL); (void) execl("/bin/sh", "/bin/sh", "-c", cmd, (char *) NULL); @@ -513,6 +529,21 @@ start_postmaster(void) PROCESS_INFORMATION pi; const char *comspec; + if (pass_terminal_fd) + { + /* Hopefully we can read and write CONOUT, see simple_prompt() XXX */ + /* Do CreateRestrictedProcess() children even inherit open file descriptors? XXX */ + int terminal_fd = open("CONOUT$", O_RDWR, 0); + + if (terminal_fd < 0) + { + write_stderr(_("%s: could not open terminal: %s\n"), + progname, strerror(errno)); + exit(1); + } + term_fd_opt = psprintf(" -R %d", terminal_fd); + } + /* Find CMD.EXE location using COMSPEC, if it's set */ comspec = getenv("COMSPEC"); if (comspec == NULL) @@ -553,12 +584,14 @@ start_postmaster(void) else close(fd); - snprintf(cmd, MAXPGPATH, "\"%s\" /C \"\"%s\" %s%s < \"%s\" >> \"%s\" 2>&1\"", - comspec, exec_path, pgdata_opt, post_opts, DEVNULL, log_file); + snprintf(cmd, MAXPGPATH, "\"%s\" /C \"\"%s\" %s%s%s < \"%s\" >> \"%s\" 2>&1\"", + comspec, exec_path, pgdata_opt, post_opts, + term_fd_opt ? term_fd_opt : "", DEVNULL, log_file); } else - snprintf(cmd, MAXPGPATH, "\"%s\" /C \"\"%s\" %s%s < \"%s\" 2>&1\"", - comspec, exec_path, pgdata_opt, post_opts, DEVNULL); + snprintf(cmd, MAXPGPATH, "\"%s\" /C \"\"%s\" %s%s%s < \"%s\" 2>&1\"", + comspec, exec_path, pgdata_opt, post_opts, + term_fd_opt ? term_fd_opt : "", DEVNULL); if (!CreateRestrictedProcess(cmd, &pi, false)) { @@ -689,7 +722,8 @@ wait_for_postmaster(pgpid_t pm_pid, bool do_checkpoint) } else #endif - print_msg("."); + if (!pass_terminal_fd) + print_msg("."); } pg_usleep(USEC_PER_SEC / WAITS_PER_SEC); @@ -2066,6 +2100,7 @@ do_help(void) printf(_(" -o, --options=OPTIONS command line options to pass to postgres\n" " (PostgreSQL server executable) or initdb\n")); printf(_(" -p PATH-TO-POSTGRES normally not necessary\n")); + printf(_(" -R, --authprompt prompt for a paasphrase or PIN\n")); printf(_("\nOptions for stop or restart:\n")); printf(_(" -m, --mode=MODE MODE can be \"smart\", \"fast\", or \"immediate\"\n")); @@ -2260,6 +2295,7 @@ main(int argc, char **argv) {"mode", required_argument, NULL, 'm'}, {"pgdata", required_argument, NULL, 'D'}, {"options", required_argument, NULL, 'o'}, + {"authprompt", no_argument, NULL, 'R'}, {"silent", no_argument, NULL, 's'}, {"timeout", required_argument, NULL, 't'}, {"core-files", no_argument, NULL, 'c'}, @@ -2332,7 +2368,7 @@ main(int argc, char **argv) /* process command-line options */ while (optind < argc) { - while ((c = getopt_long(argc, argv, "cD:e:l:m:N:o:p:P:sS:t:U:wW", + while ((c = getopt_long(argc, argv, "cD:e:l:m:N:o:p:P:RsS:t:U:wW", long_options, &option_index)) != -1) { switch (c) @@ -2385,6 +2421,9 @@ main(int argc, char **argv) case 'P': register_password = pg_strdup(optarg); break; + case 'R': + pass_terminal_fd = true; + break; case 's': silent_mode = true; break; diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c index cb6ef191820..8f928b31292 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(_("File encryption key length: %d\n"), + ControlFile.file_encryption_keylen); } diff --git a/src/bin/pg_rewind/filemap.c b/src/bin/pg_rewind/filemap.c index ba34dbac146..b8775cab15d 100644 --- a/src/bin/pg_rewind/filemap.c +++ b/src/bin/pg_rewind/filemap.c @@ -28,6 +28,7 @@ #include "catalog/pg_tablespace_d.h" #include "common/hashfn.h" +#include "common/kmgr_utils.h" #include "common/string.h" #include "datapagemap.h" #include "filemap.h" @@ -108,6 +109,13 @@ static const char *excludeDirContents[] = "pg_notify", /* + * Skip cryptographic keys. It's generally not a good idea to copy the + * cryptographic keys from source database because these might use + * different cluster key. + */ + KMGR_DIR, + + /* * Old contents are loaded for possible debugging but are not required for * normal operation, see SerialInit(). */ diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c index f3afea9d561..ef091cb3e4c 100644 --- a/src/bin/pg_upgrade/check.c +++ b/src/bin/pg_upgrade/check.c @@ -10,6 +10,7 @@ #include "postgres_fe.h" #include "catalog/pg_authid_d.h" +#include "common/kmgr_utils.h" #include "fe_utils/string_utils.h" #include "mb/pg_wchar.h" #include "pg_upgrade.h" @@ -27,6 +28,7 @@ static void check_for_tables_with_oids(ClusterInfo *cluster); static void check_for_reg_data_type_usage(ClusterInfo *cluster); static void check_for_jsonb_9_4_usage(ClusterInfo *cluster); static void check_for_pg_role_prefix(ClusterInfo *cluster); +static void check_for_cluster_key_failure(ClusterInfo *cluster); static void check_for_new_tablespace_dir(ClusterInfo *new_cluster); static char *get_canonical_locale_name(int category, const char *locale); @@ -139,6 +141,9 @@ check_and_dump_old_cluster(bool live_check) if (GET_MAJOR_VERSION(old_cluster.major_version) <= 905) check_for_pg_role_prefix(&old_cluster); + if (GET_MAJOR_VERSION(old_cluster.major_version) >= 1400) + check_for_cluster_key_failure(&old_cluster); + if (GET_MAJOR_VERSION(old_cluster.major_version) == 904 && old_cluster.controldata.cat_ver < JSONB_FORMAT_CHANGE_CAT_VER) check_for_jsonb_9_4_usage(&old_cluster); @@ -173,6 +178,9 @@ check_new_cluster(void) check_loadable_libraries(); + if (GET_MAJOR_VERSION(old_cluster.major_version) >= 1400) + check_for_cluster_key_failure(&new_cluster); + switch (user_opts.transfer_mode) { case TRANSFER_MODE_CLONE: @@ -1270,6 +1278,32 @@ check_for_pg_role_prefix(ClusterInfo *cluster) /* + * check_for_cluster_key_failure() + * + * Make sure there was no unrepaired pg_alterckey failure + */ +static void +check_for_cluster_key_failure(ClusterInfo *cluster) +{ + struct stat buffer; + + if (stat (KMGR_DIR_PID, &buffer) == 0) + { + if (cluster == &old_cluster) + pg_fatal("The source cluster had a pg_alterckey failure that needs repair or\n" + "pg_alterckey is running. Run pg_alterckey --repair or wait for it\n" + "to complete.\n"); + else + pg_fatal("The target cluster had a pg_alterckey failure that needs repair or\n" + "pg_alterckey is running. Run pg_alterckey --repair or wait for it\n" + "to complete.\n"); + } + + check_ok(); +} + + +/* * get_canonical_locale_name * * Send the locale name to the system, and hope we get back a canonical diff --git a/src/bin/pg_upgrade/controldata.c b/src/bin/pg_upgrade/controldata.c index 39bcaa8fe1a..a0aa995bbde 100644 --- a/src/bin/pg_upgrade/controldata.c +++ b/src/bin/pg_upgrade/controldata.c @@ -9,10 +9,16 @@ #include "postgres_fe.h" +#include <dirent.h> #include <ctype.h> #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; + int got_file_encryption_keylen = 0; 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.file_encryption_keylen = 0; + got_file_encryption_keylen = 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, "File encryption key length:")) != 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.file_encryption_keylen = atoi(p); + got_file_encryption_keylen = 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_file_encryption_keylen) { 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 14 */ + if (!got_file_encryption_keylen) + pg_log(PG_REPORT, " file encryption key length\n"); + pg_fatal("Cannot continue without required control information, terminating\n"); } } @@ -669,6 +700,15 @@ 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 file encryption key length + * doesn't match the new one. + + */ + if (oldctrl->file_encryption_keylen != newctrl->file_encryption_keylen) + pg_fatal("old and new clusters use different file encryption key lengths or\n" + "one cluster uses encryption and the other does not"); } diff --git a/src/bin/pg_upgrade/file.c b/src/bin/pg_upgrade/file.c index cc8a675d009..c9851192ec6 100644 --- a/src/bin/pg_upgrade/file.c +++ b/src/bin/pg_upgrade/file.c @@ -11,6 +11,7 @@ #include <sys/stat.h> #include <fcntl.h> +#include <dirent.h> #ifdef HAVE_COPYFILE_H #include <copyfile.h> #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" diff --git a/src/bin/pg_upgrade/option.c b/src/bin/pg_upgrade/option.c index 548d648e8c4..4702998352f 100644 --- a/src/bin/pg_upgrade/option.c +++ b/src/bin/pg_upgrade/option.c @@ -52,6 +52,7 @@ parseCommandLine(int argc, char *argv[]) {"check", no_argument, NULL, 'c'}, {"link", no_argument, NULL, 'k'}, {"retain", no_argument, NULL, 'r'}, + {"authprompt", no_argument, NULL, 'R'}, {"jobs", required_argument, NULL, 'j'}, {"socketdir", required_argument, NULL, 's'}, {"verbose", no_argument, NULL, 'v'}, @@ -102,7 +103,7 @@ parseCommandLine(int argc, char *argv[]) if (os_user_effective_id == 0) pg_fatal("%s: cannot be run as root\n", os_info.progname); - while ((option = getopt_long(argc, argv, "d:D:b:B:cj:ko:O:p:P:rs:U:v", + while ((option = getopt_long(argc, argv, "d:D:b:B:cj:ko:O:p:P:rRs:U:v", long_options, &optindex)) != -1) { switch (option) @@ -180,6 +181,10 @@ parseCommandLine(int argc, char *argv[]) log_opts.retain = true; break; + case 'R': + user_opts.pass_terminal_fd = true; + break; + case 's': user_opts.socketdir = pg_strdup(optarg); break; diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h index ee70243c2e9..53ce195963f 100644 --- a/src/bin/pg_upgrade/pg_upgrade.h +++ b/src/bin/pg_upgrade/pg_upgrade.h @@ -11,6 +11,7 @@ #include <sys/time.h> #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; + int file_encryption_keylen; } ControlData; /* @@ -293,6 +295,7 @@ typedef struct int jobs; /* number of processes/threads to use */ char *socketdir; /* directory to use for Unix sockets */ bool ind_coll_unknown; /* mark unknown index collation versions */ + bool pass_terminal_fd; /* pass -R to pg_ctl? */ } UserOpts; typedef struct diff --git a/src/bin/pg_upgrade/server.c b/src/bin/pg_upgrade/server.c index 713509f5406..9208ad0d8a3 100644 --- a/src/bin/pg_upgrade/server.c +++ b/src/bin/pg_upgrade/server.c @@ -244,8 +244,9 @@ start_postmaster(ClusterInfo *cluster, bool report_and_exit_on_error) * vacuumdb --freeze actually freezes the tuples. */ snprintf(cmd, sizeof(cmd), - "\"%s/pg_ctl\" -w -l \"%s\" -D \"%s\" -o \"-p %d%s%s %s%s\" start", - cluster->bindir, SERVER_LOG_FILE, cluster->pgconfig, cluster->port, + "\"%s/pg_ctl\" -w%s -l \"%s\" -D \"%s\" -o \"-p %d%s%s %s%s\" start", + cluster->bindir, user_opts.pass_terminal_fd ? " -R" : "", + SERVER_LOG_FILE, cluster->pgconfig, cluster->port, (cluster->controldata.cat_ver >= BINARY_UPGRADE_SERVER_FLAG_CAT_VER) ? " -b" : " -c autovacuum=off -c autovacuum_freeze_max_age=2000000000", diff --git a/src/common/Makefile b/src/common/Makefile index f6249779395..85d1388a646 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_common.o \ @@ -82,10 +83,12 @@ OBJS_COMMON = \ ifeq ($(with_openssl),yes) OBJS_COMMON += \ + cipher_openssl.o \ protocol_openssl.o \ cryptohash_openssl.o else OBJS_COMMON += \ + cipher.o \ cryptohash.o \ md5.o \ sha2.o diff --git a/src/common/cipher.c b/src/common/cipher.c new file mode 100644 index 00000000000..e42d9844492 --- /dev/null +++ b/src/common/cipher.c @@ -0,0 +1,67 @@ +/*------------------------------------------------------------------------- + * + * cipher.c + * Shared frontend/backend for cryptographic functions + * + * Copyright (c) 2020, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/common/cipher.c + * + *------------------------------------------------------------------------- + */ + +#ifndef FRONTEND +#include "postgres.h" +#else +#include "postgres_fe.h" +#endif + +#include "common/cipher.h" + +static cipher_failure(void); + +PgCipherCtx * +pg_cipher_ctx_create(int cipher, uint8 *key, int klen, bool enc) +{ + cipher_failure(); +} + +void +pg_cipher_ctx_free(PgCipherCtx *ctx) +{ + cipher_failure(); +} + +bool +pg_cipher_encrypt(PgCipherCtx *ctx, const unsigned char *plaintext, + const int inlen, unsigned char *ciphertext, int *outlen, + const unsigned char *iv, const int ivlen, + unsigned char *outtag, const int taglen) +{ + cipher_failure(); +} + +bool +pg_cipher_decrypt(PgCipherCtx *ctx, const unsigned char *ciphertext, + const int inlen, unsigned char *plaintext, int *outlen, + const unsigned char *iv, const int ivlen, + const unsigned char *intag, const int taglen) +{ + cipher_failure(); +} + +static +cipher_failure(void) +{ +#ifndef FRONTEND + ereport(ERROR, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + (errmsg("cluster file encryption is not supported because OpenSSL is not supported by this build"), + errhint("Compile with --with-openssl to use this feature.")))); +#else + fprintf(stderr, _("cluster file encryption is not supported because OpenSSL is not supported by this build")); + exit(1); +#endif +} + diff --git a/src/common/cipher_openssl.c b/src/common/cipher_openssl.c new file mode 100644 index 00000000000..e7a1dc7ad9e --- /dev/null +++ b/src/common/cipher_openssl.c @@ -0,0 +1,268 @@ +/*------------------------------------------------------------------------- + * cipher_openssl.c + * Cryptographic function using OpenSSL + * + * This contains the common low-level functions needed in both frontend and + * backend, for implement the database encryption. + * + * Portions Copyright (c) 2020, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/common/cipher_openssl.c + * + *------------------------------------------------------------------------- + */ +#ifndef FRONTEND +#include "postgres.h" +#else +#include "postgres_fe.h" +#endif + +#include "common/cipher.h" +#include <openssl/conf.h> +#include <openssl/err.h> +#include <openssl/evp.h> +#include <openssl/ssl.h> + +/* + * prototype for the EVP functions that return an algorithm, e.g. + * EVP_aes_128_gcm(). + */ +typedef const EVP_CIPHER *(*ossl_EVP_cipher_func) (void); + +static ossl_EVP_cipher_func get_evp_aes_gcm(int klen); +static EVP_CIPHER_CTX *ossl_cipher_ctx_create(int cipher, uint8 *key, int klen, + bool enc); + +/* + * Return a newly created cipher context. 'cipher' specifies cipher algorithm + * by identifer like PG_CIPHER_XXX. + */ +PgCipherCtx * +pg_cipher_ctx_create(int cipher, uint8 *key, int klen, bool enc) +{ + PgCipherCtx *ctx = NULL; + + if (cipher >= PG_MAX_CIPHER_ID) + return NULL; + + ctx = ossl_cipher_ctx_create(cipher, key, klen, enc); + + return ctx; +} + +void +pg_cipher_ctx_free(PgCipherCtx *ctx) +{ + EVP_CIPHER_CTX_free(ctx); +} + +/* + * Encryption routine to encrypt data provided. + * + * ctx is the encryption context which must have been created previously. + * + * plaintext is the data we are going to encrypt + * inlen is the length of the data to encrypt + * + * ciphertext is the encrypted result + * outlen is the encrypted length + * + * iv is the IV to use. + * ivlen is the IV length to use. + * + * outtag is the resulting tag. + * taglen is the length of the tag. + */ +bool +pg_cipher_encrypt(PgCipherCtx *ctx, + const unsigned char *plaintext, const int inlen, + unsigned char *ciphertext, int *outlen, + const unsigned char *iv, const int ivlen, + unsigned char *outtag, const int taglen) +{ + int len; + int enclen; + + Assert(ctx != NULL); + + /* + * Here we are setting the IV for the context which was passed + * in. Note that we signal to OpenSSL that we are configuring + * a new value for the context by passing in 'NULL' for the + * 2nd ('type') parameter. + */ + + /* Set the IV length first */ + if (!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, ivlen, NULL)) + return false; + + /* Set the IV for this encryption. */ + if (!EVP_EncryptInit_ex(ctx, NULL, NULL, NULL, iv)) + return false; + + /* + * This is the function which is actually performing the + * encryption for us. + */ + if (!EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext, inlen)) + return false; + + enclen = len; + + /* Finalize the encryption, which could add more to output. */ + if (!EVP_EncryptFinal_ex(ctx, ciphertext + enclen, &len)) + return false; + + *outlen = enclen + len; + + /* + * Once all of the encryption has been completed we grab + * the tag. + */ + if (!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, taglen, outtag)) + return false; + + return true; +} +/* + * Decryption routine + * + * ctx is the encryption context which must have been created previously. + * + * ciphertext is the data we are going to decrypt + * inlen is the length of the data to decrypt + * + * plaintext is the decrypted result + * outlen is the decrypted length + * + * iv is the IV to use. + * ivlen is the length of the IV. + * + * intag is the tag to use to verify. + * taglen is the length of the tag. + */ +bool +pg_cipher_decrypt(PgCipherCtx *ctx, + const unsigned char *ciphertext, const int inlen, + unsigned char *plaintext, int *outlen, + const unsigned char *iv, const int ivlen, + unsigned char *intag, const int taglen) +{ + int declen; + int len; + + /* + * Here we are setting the IV for the context which was passed + * in. Note that we signal to OpenSSL that we are configuring + * a new value for the context by passing in 'NULL' for the + * 2nd ('type') parameter. + */ + + /* Set the IV length first */ + if (!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, ivlen, NULL)) + return false; + + /* Set the IV for this decryption. */ + if (!EVP_DecryptInit_ex(ctx, NULL, NULL, NULL, iv)) + return false; + + /* + * This is the function which is actually performing the + * decryption for us. + */ + if (!EVP_DecryptUpdate(ctx, plaintext, &len, ciphertext, inlen)) + return false; + + declen = len; + + /* Set the expected tag value. */ + if (!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, taglen, intag)) + return false; + + /* + * Finalize the decryption, which could add more to output, + * this is also the step which checks the tag and we MUST + * fail if this indicates an invalid tag! + */ + if (!EVP_DecryptFinal_ex(ctx, plaintext + declen, &len)) + return false; + + *outlen = declen + len; + + return true; +} + +/* + * Returns the correct cipher functions for OpenSSL based + * on the key length requested. + */ +static ossl_EVP_cipher_func +get_evp_aes_gcm(int klen) +{ + switch (klen) + { + case PG_AES128_KEY_LEN: + return EVP_aes_128_gcm; + case PG_AES192_KEY_LEN: + return EVP_aes_192_gcm; + case PG_AES256_KEY_LEN: + return EVP_aes_256_gcm; + default: + return NULL; + } +} + +/* + * Initialize and return an EVP_CIPHER_CTX. Returns NULL if the given + * cipher algorithm is not supported or on failure. + */ +static EVP_CIPHER_CTX * +ossl_cipher_ctx_create(int cipher, uint8 *key, int klen, bool enc) +{ + EVP_CIPHER_CTX *ctx; + ossl_EVP_cipher_func func; + int ret; + + ctx = EVP_CIPHER_CTX_new(); + + /* + * We currently only support AES GCM but others could be + * added in the future. + */ + switch (cipher) + { + case PG_CIPHER_AES_GCM: + func = get_evp_aes_gcm(klen); + if (!func) + goto failed; + break; + default: + goto failed; + } + + /* + * We create the context here based on the cipher requested and the provided + * key. Note that the IV will be provided in the actual encryption call + * through another EVP_EncryptInit_ex call- this is fine as long as 'type' + * is passed in as NULL! + */ + if (enc) + ret = EVP_EncryptInit_ex(ctx, (const EVP_CIPHER *) func(), NULL, key, NULL); + else + ret = EVP_DecryptInit_ex(ctx, (const EVP_CIPHER *) func(), NULL, key, NULL); + + if (!ret) + goto failed; + + /* Set the key length based on the key length requested. */ + if (!EVP_CIPHER_CTX_set_key_length(ctx, klen)) + goto failed; + + return ctx; + +failed: + EVP_CIPHER_CTX_free(ctx); + return NULL; +} + diff --git a/src/common/kmgr_utils.c b/src/common/kmgr_utils.c new file mode 100644 index 00000000000..d031976f500 --- /dev/null +++ b/src/common/kmgr_utils.c @@ -0,0 +1,507 @@ +/*------------------------------------------------------------------------- + * + * kmgr_utils.c + * Shared frontend/backend for cluster file encryption + * + * 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 <unistd.h> +#include <sys/stat.h> + +#ifdef FRONTEND +#include "common/logging.h" +#endif +#include "common/cryptohash.h" +#include "common/file_perm.h" +#include "common/kmgr_utils.h" +#include "common/hex_decode.h" +#include "common/string.h" +#include "crypto/kmgr.h" +#include "lib/stringinfo.h" +#include "postmaster/postmaster.h" +#include "storage/fd.h" + +#ifndef FRONTEND +#include "pgstat.h" +#include "storage/fd.h" +#endif + +#define KMGR_PROMPT_MSG "Enter authentication needed to generate the cluster key: " + +#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); + +/* + * Encrypt the given data. Return true and set encrypted data to 'out' if + * success. Otherwise return false. The caller must allocate sufficient space + * for cipher data calculated by using KmgrSizeOfCipherText(). Please note that + * this function modifies 'out' data even on failure case. + */ +bool +kmgr_wrap_key(PgCipherCtx *ctx, CryptoKey *in, CryptoKey *out) +{ + int len, enclen; + unsigned char iv[sizeof(in->pgkey_id) + sizeof(in->counter)]; + + Assert(ctx && in && out); + + /* Get the actual length of the key we are wrapping */ + memcpy(&len, in->encrypted_key, sizeof(len)); + + /* Key ID remains the same */ + out->pgkey_id = in->pgkey_id; + + /* Increment the counter */ + out->counter = in->counter + 1; + + /* Construct the IV we are going to use, see kmgr_utils.h */ + memcpy(iv, &out->pgkey_id, sizeof(out->pgkey_id)); + memcpy(iv + sizeof(out->pgkey_id), &out->counter, sizeof(out->counter)); + + if (!pg_cipher_encrypt(ctx, + in->encrypted_key, /* Plaintext source, key length + key */ + sizeof(in->encrypted_key), /* Full data length */ + out->encrypted_key, /* Ciphertext result */ + &enclen, /* Resulting length, must match input for us */ + iv, /* Generated IV from above */ + sizeof(iv), /* Length of the IV */ + (unsigned char *) &out->tag, /* Resulting tag */ + sizeof(out->tag))) /* Length of our tag */ + return false; + + Assert(enclen == sizeof(in->encrypted_key)); + + return true; +} + +/* + * Decrypt the given Data. Return true and set plain text data to `out` if + * success. Otherwise return false. The caller must allocate sufficient space + * for cipher data calculated by using KmgrSizeOfPlainText(). Please note that + * this function modifies 'out' data even on failure case. + */ +bool +kmgr_unwrap_key(PgCipherCtx *ctx, CryptoKey *in, CryptoKey *out) +{ + int declen; + unsigned char iv[sizeof(in->pgkey_id) + sizeof(in->counter)]; + + Assert(ctx && in && out); + + out->pgkey_id = in->pgkey_id; + out->counter = in->counter; + out->tag = in->tag; + + /* Construct the IV we are going to use, see kmgr_utils.h */ + memcpy(iv, &out->pgkey_id, sizeof(out->pgkey_id)); + memcpy(iv + sizeof(out->pgkey_id), &out->counter, sizeof(out->counter)); + + /* Decrypt encrypted data */ + if (!pg_cipher_decrypt(ctx, + in->encrypted_key, /* Encrypted source */ + sizeof(in->encrypted_key), /* Length of encrypted data */ + out->encrypted_key, /* Plaintext result */ + &declen, /* Length of plaintext */ + iv, /* IV we constructed above */ + sizeof(iv), /* Size of our IV */ + (unsigned char *) &in->tag, /* Tag which will be verified */ + sizeof(in->tag))) /* Size of our tag */ + return false; + + Assert(declen == sizeof(in->encrypted_key)); + + return true; +} + +/* + * Verify the correctness of the given cluster key by unwrapping the given keys. + * If the given cluster key is correct we set unwrapped keys to out_keys and return + * true. Otherwise return false. Please note that this function changes the + * contents of out_keys even on failure. Both in_keys and out_keys must be the + * same length, nkey. + */ +bool +kmgr_verify_cluster_key(unsigned char *cluster_key, + CryptoKey *in_keys, CryptoKey *out_keys, int nkeys) +{ + PgCipherCtx *ctx; + + /* + * Create decryption context with cluster KEK. + */ + ctx = pg_cipher_ctx_create(PG_CIPHER_AES_GCM, cluster_key, + KMGR_CLUSTER_KEY_LEN, false); + + for (int i = 0; i < nkeys; i++) + { + if (!kmgr_unwrap_key(ctx, &(in_keys[i]), &(out_keys[i]))) + { + /* The cluster key is not correct */ + pg_cipher_ctx_free(ctx); + return false; + } + explicit_bzero(&(in_keys[i]), sizeof(in_keys[i])); + } + + /* The cluster key is correct, free the cipher context */ + pg_cipher_ctx_free(ctx); + + return true; +} + +/* + * Run cluster key command. + * + * prompt will be substituted for %p, file descriptor for %R + * + * 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_key_command(char *cluster_key_command, char *buf, + int size, char *dir) +{ + StringInfoData command; + const char *sp; + FILE *fh; + int pclose_rc; + size_t len = 0; + + buf[0] = '\0'; + + Assert(size > 0); + + /* + * Build the command to be executed. + */ + initStringInfo(&command); + + for (sp = cluster_key_command; *sp; sp++) + { + if (*sp == '%') + { + switch (sp[1]) + { + case 'd': + { + char *nativePath; + + sp++; + + /* + * This needs to use a placeholder to not modify the + * input with the conversion done via + * make_native_path(). + */ + nativePath = pstrdup(dir); + make_native_path(nativePath); + appendStringInfoString(&command, nativePath); + pfree(nativePath); + break; + } + case 'p': + sp++; + appendStringInfoString(&command, KMGR_PROMPT_MSG); + break; + case 'R': + { + char fd_str[20]; + + if (terminal_fd == -1) + { +#ifdef FRONTEND + pg_log_fatal("cluster key command referenced %%R, but --authprompt not specified"); + exit(EXIT_FAILURE); +#else + ereport(ERROR, + (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("cluster key command referenced %%R, but --authprompt not specified"))); +#endif + } + + sp++; + snprintf(fd_str, sizeof(fd_str), "%d", terminal_fd); + appendStringInfoString(&command, fd_str); + break; + } + case '%': + /* convert %% to a single % */ + sp++; + appendStringInfoChar(&command, *sp); + break; + default: + /* otherwise treat the % as not special */ + appendStringInfoChar(&command, *sp); + break; + } + } + else + { + appendStringInfoChar(&command, *sp); + } + } + +#ifdef FRONTEND + fh = open_pipe_stream(command.data); + if (fh == NULL) + { + pg_log_fatal("could not execute command \"%s\": %m", + command.data); + exit(EXIT_FAILURE); + } +#else + fh = OpenPipeStream(command.data, "r"); + if (fh == NULL) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not execute command \"%s\": %m", + command.data))); +#endif + + if (!fgets(buf, size, fh)) + { + if (ferror(fh)) + { +#ifdef FRONTEND + pg_log_fatal("could not read from command \"%s\": %m", + command.data); + exit(EXIT_FAILURE); +#else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not read from command \"%s\": %m", + command.data))); +#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.data); + exit(EXIT_FAILURE); +#else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("command \"%s\" failed", + command.data), + errdetail_internal("%s", wait_result_to_str(pclose_rc)))); +#endif + } + + /* strip trailing newline and carriage return */ + len = pg_strip_crlf(buf); + + pfree(command.data); + + 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, LIVE_KMGR_DIR)) != NULL) +#else + while ((de = readdir(dir)) != NULL) +#endif + { + if (strspn(de->d_name, "0123456789") == strlen(de->d_name)) + { + uint32 id = strtoul(de->d_name, NULL, 10); + + 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 keys"); +#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/catalog/pg_control.h b/src/include/catalog/pg_control.h index 06bed90c5e9..a4c12599f74 100644 --- a/src/include/catalog/pg_control.h +++ b/src/include/catalog/pg_control.h @@ -22,7 +22,7 @@ /* Version identifier for this pg_control format */ -#define PG_CONTROL_VERSION 1300 +#define PG_CONTROL_VERSION 1400 /* Nonce key length, see below */ #define MOCK_AUTH_NONCE_LEN 32 @@ -226,6 +226,9 @@ typedef struct ControlFileData */ char mock_authentication_nonce[MOCK_AUTH_NONCE_LEN]; + /* File encryption key length. Zero if disabled. */ + int file_encryption_keylen; + /* CRC of all above ... MUST BE LAST! */ pg_crc32c crc; } ControlFileData; diff --git a/src/include/common/cipher.h b/src/include/common/cipher.h new file mode 100644 index 00000000000..598ef11289f --- /dev/null +++ b/src/include/common/cipher.h @@ -0,0 +1,62 @@ +/*------------------------------------------------------------------------- + * + * cipher.h + * Declarations for cryptographic functions + * + * Portions Copyright (c) 2020, PostgreSQL Global Development Group + * + * src/include/common/cipher.h + * + *------------------------------------------------------------------------- + */ +#ifndef PG_CIPHER_H +#define PG_CIPHER_H + +#ifdef USE_OPENSSL +#include <openssl/evp.h> +#include <openssl/conf.h> +#include <openssl/err.h> +#endif + +/* + * Supported symmetric encryption algorithm. These identifiers are passed + * to pg_cipher_ctx_create() function, and then actual encryption + * implementations need to initialize their context of the given encryption + * algorithm. + */ +#define PG_CIPHER_AES_GCM 0 +#define PG_MAX_CIPHER_ID 1 + +/* AES128/192/256 various length definitions */ +#define PG_AES128_KEY_LEN (128 / 8) +#define PG_AES192_KEY_LEN (192 / 8) +#define PG_AES256_KEY_LEN (256 / 8) + +/* + * The encrypted data is a series of blocks of size. Initialization + * vector(IV) is the same size of cipher block. + */ +#define PG_AES_BLOCK_SIZE 16 +#define PG_AES_IV_SIZE (PG_AES_BLOCK_SIZE) + +#ifdef USE_OPENSSL +typedef EVP_CIPHER_CTX PgCipherCtx; +#else +typedef void PgCipherCtx; +#endif + +extern PgCipherCtx *pg_cipher_ctx_create(int cipher, uint8 *key, int klen, + bool enc); +extern void pg_cipher_ctx_free(PgCipherCtx *ctx); +extern bool pg_cipher_encrypt(PgCipherCtx *ctx, + const unsigned char *plaintext, const int inlen, + unsigned char *ciphertext, int *outlen, + const unsigned char *iv, const int ivlen, + unsigned char *tag, const int taglen); +extern bool pg_cipher_decrypt(PgCipherCtx *ctx, + const unsigned char *ciphertext, const int inlen, + unsigned char *plaintext, int *outlen, + const unsigned char *iv, const int ivlen, + unsigned char *intag, const int taglen); + +#endif /* PG_CIPHER_H */ diff --git a/src/include/common/kmgr_utils.h b/src/include/common/kmgr_utils.h new file mode 100644 index 00000000000..23124a7fc60 --- /dev/null +++ b/src/include/common/kmgr_utils.h @@ -0,0 +1,98 @@ +/*------------------------------------------------------------------------- + * + * kmgr_utils.h + * Declarations for utility function for file encryption key + * + * Portions Copyright (c) 2020, PostgreSQL Global Development Group + * + * src/include/common/kmgr_utils.h + * + *------------------------------------------------------------------------- + */ +#ifndef KMGR_UTILS_H +#define KMGR_UTILS_H + +#include "common/cipher.h" + +/* Current version number */ +#define KMGR_VERSION 1 + +/* + * Directories where cluster file encryption keys reside within PGDATA. + */ +#define KMGR_DIR "pg_cryptokeys" +#define KMGR_DIR_PID KMGR_DIR"/pg_alterckey.pid" +#define LIVE_KMGR_DIR KMGR_DIR"/live" +/* used during cluster key rotation */ +#define NEW_KMGR_DIR KMGR_DIR"/new" +#define OLD_KMGR_DIR KMGR_DIR"/old" + +/* CryptoKey file name is keys id */ +#define CryptoKeyFilePath(path, dir, id) \ + snprintf((path), MAXPGPATH, "%s/%d", (dir), (id)) + +/* + * Identifiers of internal keys. + */ +#define KMGR_KEY_ID_REL 0 +#define KMGR_KEY_ID_WAL 1 +#define KMGR_MAX_INTERNAL_KEYS 2 + +/* We always, today, use a 256-bit AES key. */ +#define KMGR_CLUSTER_KEY_LEN PG_AES256_KEY_LEN + +/* double for hex format, plus some for spaces, \r,\n, and null byte */ +#define ALLOC_KMGR_CLUSTER_KEY_LEN (KMGR_CLUSTER_KEY_LEN * 2 + 10 + 2 + 1) + +/* Maximum length of key the key manager can store */ +#define KMGR_MAX_KEY_LEN 256 +#define KMGR_MAX_KEY_LEN_BYTES KMGR_MAX_KEY_LEN / 8 +#define KMGR_MAX_WRAPPED_KEY_LEN KmgrSizeOfCipherText(KMGR_MAX_KEY_LEN) + + +/* + * Cryptographic key data structure. + * + * This is the structure we use to write out the encrypted keys. + * + * pgkey_id is the identifier for this key (should be same as the + * file name and be one of KMGR_KEY_ID_* from above). This is what + * we consider our 'context' or 'fixed' portion of the deterministic + * IV we create. + * + * counter is updated each time we use the cluster KEK to encrypt a + * new key. This is our the 'invocation' field of the deterministic + * IV we create. + * + * Absolutely essential when using GCM (or CTR) is that the IV is unique, + * for a given key, but a deterministic IV such as this is perfectly + * acceptable and encouraged. If (and only if!) the KEK is changed to a + * new key, then we can re-initialize the counter. + * + * Detailed discussion of deterministic IV creation can be found here: + * + * https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf + * + * tag is the GCM tag which is produced and must be validated in order + * to be able to trust the results of our decryption. + * + * encrypted_key is the encrypted key length (as an int) + encrypted key. + */ +typedef struct CryptoKey +{ + uint64 pgkey_id; /* Upper half of IV */ + uint64 counter; /* Lower half of IV */ + uint128 tag; /* GCM tag */ + unsigned char encrypted_key[sizeof(int) + KMGR_MAX_KEY_LEN_BYTES]; +} CryptoKey; + +extern bool kmgr_wrap_key(PgCipherCtx *ctx, CryptoKey *in, CryptoKey *out); +extern bool kmgr_unwrap_key(PgCipherCtx *ctx, CryptoKey *in, CryptoKey *out); +extern bool kmgr_verify_cluster_key(unsigned char *cluster_key, + CryptoKey *in_keys, CryptoKey *out_keys, + int nkey); +extern int kmgr_run_cluster_key_command(char *cluster_key_command, + char *buf, int size, char *dir); +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 00000000000..386ac1cb4a8 --- /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 int file_encryption_keylen; +extern char *cluster_key_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 5954068dec5..b8f98f9a58a 100644 --- a/src/include/pgstat.h +++ b/src/include/pgstat.h @@ -1010,6 +1010,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/postmaster/postmaster.h b/src/include/postmaster/postmaster.h index babc87dfc9d..b1f0721b856 100644 --- a/src/include/postmaster/postmaster.h +++ b/src/include/postmaster/postmaster.h @@ -30,6 +30,8 @@ extern bool enable_bonjour; extern char *bonjour_name; extern bool restart_after_crash; +extern int terminal_fd; + #ifdef WIN32 extern HANDLE PostmasterHandle; #else diff --git a/src/include/utils/guc_tables.h b/src/include/utils/guc_tables.h index 7f36e1146f2..c0dbf691165 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 ab1ef9a4753..730efbf9c4a 100644 --- a/src/test/Makefile +++ b/src/test/Makefile @@ -30,7 +30,7 @@ endif endif ifeq ($(with_openssl),yes) ifneq (,$(filter ssl,$(PG_TEST_EXTRA))) -SUBDIRS += ssl +SUBDIRS += ssl crypto endif endif diff --git a/src/tools/msvc/Mkvcbuild.pm b/src/tools/msvc/Mkvcbuild.pm index 7f014a12c9a..0e2104a3455 100644 --- a/src/tools/msvc/Mkvcbuild.pm +++ b/src/tools/msvc/Mkvcbuild.pm @@ -122,18 +122,20 @@ sub mkvcbuild archive.c base64.c checksum_helper.c config_info.c controldata_utils.c d2s.c encnames.c exec.c f2s.c file_perm.c file_utils.c hashfn.c hex_decode.c ip.c jsonapi.c - keywords.c kwlookup.c link-canary.c md5_common.c + keywords.c kmgr_utils.c kwlookup.c link-canary.c md5_common.c pg_get_line.c pg_lzcompress.c pgfnames.c psprintf.c relpath.c rmtree.c saslprep.c scram-common.c string.c stringinfo.c unicode_norm.c username.c wait_error.c wchar.c); if ($solution->{options}->{openssl}) { + push(@pgcommonallfiles, 'cipher_openssl.c'); push(@pgcommonallfiles, 'cryptohash_openssl.c'); push(@pgcommonallfiles, 'protocol_openssl.c'); } else { + push(@pgcommonallfiles, 'cipher.c'); push(@pgcommonallfiles, 'cryptohash.c'); push(@pgcommonallfiles, 'md5.c'); push(@pgcommonallfiles, 'sha2.c'); |