diff options
Diffstat (limited to 'src/common')
-rw-r--r-- | src/common/Makefile | 3 | ||||
-rw-r--r-- | src/common/cipher.c | 67 | ||||
-rw-r--r-- | src/common/cipher_openssl.c | 268 | ||||
-rw-r--r-- | src/common/kmgr_utils.c | 507 |
4 files changed, 845 insertions, 0 deletions
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 +} |