diff options
author | Bruce Momjian <bruce@momjian.us> | 2020-12-25 10:19:44 -0500 |
---|---|---|
committer | Bruce Momjian <bruce@momjian.us> | 2020-12-25 10:19:44 -0500 |
commit | 978f869b992f9fca343e99d6fdb71073c76e869a (patch) | |
tree | b8020240551aa16da5b4fc9fbf96710de2d667e4 /src/common/kmgr_utils.c | |
parent | 5c31afc49d0b62b357218b6f8b01782509ef8acd (diff) | |
download | postgresql-978f869b992f9fca343e99d6fdb71073c76e869a.tar.gz postgresql-978f869b992f9fca343e99d6fdb71073c76e869a.zip |
Add key management system
This adds a key management system that stores (currently) two data
encryption keys of length 128, 192, or 256 bits. The data keys are
AES256 encrypted using a key encryption key, and validated via GCM
cipher mode. A command to obtain the key encryption key must be
specified at initdb time, and will be run at every database server
start. New parameters allow a file descriptor open to the terminal to
be passed. pg_upgrade support has also been added.
Discussion: https://postgr.es/m/CA+fd4k7q5o6Nc_AaX6BcYM9yqTbC6_pnH-6nSD=54Zp6NBQTCQ@mail.gmail.com
Discussion: https://postgr.es/m/20201202213814.GG20285@momjian.us
Author: Masahiko Sawada, me, Stephen Frost
Diffstat (limited to 'src/common/kmgr_utils.c')
-rw-r--r-- | src/common/kmgr_utils.c | 507 |
1 files changed, 507 insertions, 0 deletions
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 +} |