From d7c552c4c3d3543c6a6cc754087a8c39e56c3fff Mon Sep 17 00:00:00 2001 From: Zsolt Parragi Date: Thu, 7 Dec 2023 09:32:23 +0000 Subject: [PATCH] Vault KVv2 API support for key storage (#81) This commit implements support for storing keys on a vault server instead of locally. The current implementation only supports the KV v2 engine, which is the default secrets engine in recent vault versions. To use vault for key storage, the following settings have to be used in the keyring configuration file: * `provider` set to `vault-v2` * `url` set to the URL of the vault server * `mountPath` is set to the mount point where the keyring should store the keys * `token` is an access token with read and write access to the above mount point * [optional] `caPath` is the path of the CA file used for SSL verification Multiple servers can use the same vault server, with the following restrictions: * Servers in the same replication group should use the same 'pg_tde.keyringKeyPrefix` to ensure that they see the same keys * Unrelated servers should use different `pg_tde.keyringKeyPrefix` values to ensure that they use different keys without conflicts The source also contains a sample keyring configuration file, `keyring-vault.json`. This configuration matches the settings of the vault development server (`vault server -dev`), only the ROOT_TOKEN has to be replaced to the token of the actual server process. --- .../postgresql-16-pgdg-package-pgxs.yml | 2 +- .github/workflows/postgresql-16-src-make.yml | 2 +- .../postgresql-16-src-meson-perf.yml | 2 +- .github/workflows/postgresql-16-src-meson.yml | 31 +- Makefile.in | 3 +- configure | 95 +++++- configure.ac | 3 +- keyring-vault.json | 6 + meson.build | 4 +- src/include/keyring/keyring_config.h | 8 + src/include/keyring/keyring_vault.h | 19 ++ src/keyring/keyring_api.c | 59 +++- src/keyring/keyring_config.c | 28 +- src/keyring/keyring_file.c | 4 +- src/keyring/keyring_vault.c | 301 ++++++++++++++++++ 15 files changed, 544 insertions(+), 23 deletions(-) create mode 100644 keyring-vault.json create mode 100644 src/include/keyring/keyring_vault.h create mode 100644 src/keyring/keyring_vault.c diff --git a/.github/workflows/postgresql-16-pgdg-package-pgxs.yml b/.github/workflows/postgresql-16-pgdg-package-pgxs.yml index 04190ae5..41456390 100644 --- a/.github/workflows/postgresql-16-pgdg-package-pgxs.yml +++ b/.github/workflows/postgresql-16-pgdg-package-pgxs.yml @@ -24,7 +24,7 @@ jobs: run: | sudo apt-get install -y libreadline6-dev systemtap-sdt-dev wget \ zlib1g-dev libssl-dev libpam0g-dev bison flex libipc-run-perl \ - libjson-c-dev + libjson-c-dev libcurl4-openssl-dev sudo /usr/bin/perl -MCPAN -e 'install IPC::RUN' sudo /usr/bin/perl -MCPAN -e 'install Text::Trim' diff --git a/.github/workflows/postgresql-16-src-make.yml b/.github/workflows/postgresql-16-src-make.yml index 03dc2527..6211610a 100644 --- a/.github/workflows/postgresql-16-src-make.yml +++ b/.github/workflows/postgresql-16-src-make.yml @@ -26,7 +26,7 @@ jobs: libxml2-dev libxslt-dev xsltproc libkrb5-dev libldap2-dev \ libsystemd-dev gettext tcl-dev libperl-dev pkg-config clang-11 \ llvm-11 llvm-11-dev libselinux1-dev python3-dev \ - uuid-dev liblz4-dev libjson-c-dev + uuid-dev liblz4-dev libjson-c-dev libcurl4-openssl-dev sudo /usr/bin/perl -MCPAN -e 'install IPC::RUN' sudo /usr/bin/perl -MCPAN -e 'install Text::Trim' diff --git a/.github/workflows/postgresql-16-src-meson-perf.yml b/.github/workflows/postgresql-16-src-meson-perf.yml index 4284ca04..8d499e4c 100644 --- a/.github/workflows/postgresql-16-src-meson-perf.yml +++ b/.github/workflows/postgresql-16-src-meson-perf.yml @@ -30,7 +30,7 @@ jobs: libsystemd-dev gettext tcl-dev libperl-dev pkg-config clang-11 \ llvm-11 llvm-11-dev libselinux1-dev python3-dev \ uuid-dev liblz4-dev meson ninja-build libjson-c-dev \ - sysbench + sysbench libcurl4-openssl-dev sudo /usr/bin/perl -MCPAN -e 'install IPC::RUN' sudo /usr/bin/perl -MCPAN -e 'install Text::Trim' diff --git a/.github/workflows/postgresql-16-src-meson.yml b/.github/workflows/postgresql-16-src-meson.yml index b36cb2ab..e8efd1e5 100644 --- a/.github/workflows/postgresql-16-src-meson.yml +++ b/.github/workflows/postgresql-16-src-meson.yml @@ -26,9 +26,13 @@ jobs: libxml2-dev libxslt-dev xsltproc libkrb5-dev libldap2-dev \ libsystemd-dev gettext tcl-dev libperl-dev pkg-config clang-11 \ llvm-11 llvm-11-dev libselinux1-dev python3-dev \ - uuid-dev liblz4-dev meson ninja-build libjson-c-dev + uuid-dev liblz4-dev meson ninja-build libjson-c-dev \ + gpg wget libcurl4-openssl-dev sudo /usr/bin/perl -MCPAN -e 'install IPC::RUN' sudo /usr/bin/perl -MCPAN -e 'install Text::Trim' + wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list + sudo apt update && sudo apt install -y vault - name: Clone postgres repository uses: actions/checkout@v2 @@ -52,7 +56,7 @@ jobs: cd build && ninja && ninja install working-directory: src - - name: Test postgres-tde-ext + - name: Test postgres-tde-ext with keyring_file run: | cp ../contrib/postgres-tde-ext/keyring.json /tmp/keyring.json meson test --suite setup -v @@ -68,3 +72,26 @@ jobs: src/build/testrun/postgres-tde-ext/regress/ retention-days: 3 + - name: Test postgres-tde-ext with keyring_vault + run: | + TV=$(mktemp) + { exec >$TV; vault server -dev; } & + sleep 10 + ROOT_TOKEN=$(cat $TV | grep "Root Token" | cut -d ":" -f 2 | xargs echo -n) + echo "Root token: $ROOT_TOKEN" + cp ../contrib/postgres-tde-ext/keyring-vault.json /tmp/keyring.json + sed -i "s/ROOT_TOKEN/$ROOT_TOKEN/g" /tmp/keyring.json + cat /tmp/keyring.json + meson test --suite setup -v + meson test --suite postgres-tde-ext -v --num-processes 1 + working-directory: src/build + + - name: Report on test fail + uses: actions/upload-artifact@v2 + if: ${{ failure() }} + with: + name: Regressions diff and postgresql log + path: | + src/build/testrun/postgres-tde-ext/regress/ + retention-days: 3 + diff --git a/Makefile.in b/Makefile.in index ac2df180..56016e6a 100644 --- a/Makefile.in +++ b/Makefile.in @@ -32,6 +32,7 @@ src/access/pg_tde_ddl.o \ src/transam/pg_tde_xact_handler.o \ src/keyring/keyring_config.o \ src/keyring/keyring_file.o \ +src/keyring/keyring_vault.o \ src/keyring/keyring_api.o \ src/pg_tde.o @@ -50,4 +51,4 @@ include $(top_builddir)/src/Makefile.global include $(top_srcdir)/contrib/contrib-global.mk endif -override SHLIB_LINK += @tde_LDFLAGS@ -lcrypto -lssl +override SHLIB_LINK += @tde_LDFLAGS@ -lcrypto -lssl -lcurl diff --git a/configure b/configure index b5290bdf..2a9e0851 100755 --- a/configure +++ b/configure @@ -710,6 +710,7 @@ ac_subst_files='' ac_user_opts=' enable_option_checking with_jsonc +with_libcurl ' ac_precious_vars='build_alias host_alias @@ -1341,6 +1342,7 @@ Optional Packages: --with-PACKAGE[=ARG] use PACKAGE [ARG=yes] --without-PACKAGE do not use PACKAGE (same as --with-PACKAGE=no) --with-jsonc= Location where json_object.h is installed + --with-libcurl= Location where curl/curl.h is installed Some influential environment variables: CC C compiler command @@ -2407,7 +2409,7 @@ case $host_os in *\ *) host_os=`echo "$host_os" | sed 's/ /-/g'`;; esac -# REQUIRE_LIB(name,lib,testfn,description) +# REQUIRE_LIB(name,lib,testfn,test_include.h) # name = The complete name of the library file without the extension. # lib = The name of the library file without the 'lib' prefix and without the extension. # testfn = One function included in the library that can be used for a test compilation. @@ -3531,6 +3533,96 @@ else $as_nop fi +} + { + +# Check whether --with-libcurl was given. +if test ${with_libcurl+y} +then : + withval=$with_libcurl; +else $as_nop + with_libcurl=default +fi + + if test "x$with_libcurl" == xdefault +then : + + case $host_os in + darwin*) libpathx=($HOMEBREW_CELLAR/curl/*) + tde_CPPFLAGS="$tde_CPPFLAGS -I$libpathx/include/curl" + tde_LDFLAGS="$tde_LDFLAGS -L$libpathx/lib -lcurl" ;; + *) tde_CPPFLAGS="$tde_CPPFLAGS -I/usr/include/curl" + tde_LDFLAGS="$tde_LDFLAGS -lcurl" ;; + esac + +else $as_nop + #AS_ELSE + tde_CPPFLAGS="$tde_CPPFLAGS -I${with_libcurl}/include" + tde_LDFLAGS="$tde_LDFLAGS -L${with_libcurl}/lib -lcurl" + +fi + + LDFLAGS="$LDFLAGS $tde_LDFLAGS" + CPPFLAGS="$CPPFLAGS $tde_CPPFLAGS" + + ac_fn_c_check_header_compile "$LINENO" "curl/curl.h" "ac_cv_header_curl_curl_h" "$ac_includes_default" +if test "x$ac_cv_header_curl_curl_h" = xyes +then : + +else $as_nop + + as_fn_error $? "header file is required, try specifying --with-libcurl" "$LINENO" 5 + +fi + + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for curl_easy_setopt in -lcurl" >&5 +printf %s "checking for curl_easy_setopt in -lcurl... " >&6; } +if test ${ac_cv_lib_curl_curl_easy_setopt+y} +then : + printf %s "(cached) " >&6 +else $as_nop + ac_check_lib_save_LIBS=$LIBS +LIBS="-lcurl $LIBS" +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +/* Override any GCC internal prototype to avoid an error. + Use char because int might match the return type of a GCC + builtin and then its argument prototype would still apply. */ +char curl_easy_setopt (); +int +main (void) +{ +return curl_easy_setopt (); + ; + return 0; +} +_ACEOF +if ac_fn_c_try_link "$LINENO" +then : + ac_cv_lib_curl_curl_easy_setopt=yes +else $as_nop + ac_cv_lib_curl_curl_easy_setopt=no +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam \ + conftest$ac_exeext conftest.$ac_ext +LIBS=$ac_check_lib_save_LIBS +fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_curl_curl_easy_setopt" >&5 +printf "%s\n" "$ac_cv_lib_curl_curl_easy_setopt" >&6; } +if test "x$ac_cv_lib_curl_curl_easy_setopt" = xyes +then : + printf "%s\n" "#define HAVE_LIBCURL 1" >>confdefs.h + + LIBS="-lcurl $LIBS" + +else $as_nop + + as_fn_error $? "curl was not found, try specifying --with-libcurl" "$LINENO" 5 + +fi + } @@ -4689,3 +4781,4 @@ if test -n "$ac_unrecognized_opts" && test "$enable_option_checking" != no; then printf "%s\n" "$as_me: WARNING: unrecognized options: $ac_unrecognized_opts" >&2;} fi + diff --git a/configure.ac b/configure.ac index da52e5d9..79c8d376 100644 --- a/configure.ac +++ b/configure.ac @@ -44,10 +44,11 @@ AC_ARG_WITH([$1], AS_HELP_STRING([--with-$1=],[Location where $4 is instal #======================================= REQUIRE_LIB(jsonc, json-c, json_object_get, json_object.h) +REQUIRE_LIB(libcurl, curl, curl_easy_setopt, curl/curl.h) AC_SUBST(tde_CPPFLAGS) AC_SUBST(tde_LDFLAGS) AC_CONFIG_FILES([Makefile]) -AC_OUTPUT \ No newline at end of file +AC_OUTPUT diff --git a/keyring-vault.json b/keyring-vault.json new file mode 100644 index 00000000..e4123aca --- /dev/null +++ b/keyring-vault.json @@ -0,0 +1,6 @@ +{ + 'provider': 'vault-v2', + 'token': 'ROOT_TOKEN', + 'url': 'http://127.0.0.1:8200', + 'mountPath': 'secret' +} diff --git a/meson.build b/meson.build index 68ba64a0..94ebd482 100644 --- a/meson.build +++ b/meson.build @@ -1,6 +1,7 @@ # libjson-c-dev on ubuntu jsondep = dependency('json-c') +curldep = dependency('libcurl') pg_tde_sources = files( 'src/pg_tde.c', @@ -22,6 +23,7 @@ pg_tde_sources = files( 'src/keyring/keyring_config.c', 'src/keyring/keyring_file.c', + 'src/keyring/keyring_vault.c', 'src/keyring/keyring_api.c', 'src/pg_tde.c', @@ -29,7 +31,7 @@ pg_tde_sources = files( incdir = include_directories('src/include') -deps_update = {'dependencies': contrib_mod_args.get('dependencies') + [jsondep]} +deps_update = {'dependencies': contrib_mod_args.get('dependencies') + [jsondep, curldep]} mod_args = contrib_mod_args + deps_update diff --git a/src/include/keyring/keyring_config.h b/src/include/keyring/keyring_config.h index 4b5747e0..bc70aaae 100644 --- a/src/include/keyring/keyring_config.h +++ b/src/include/keyring/keyring_config.h @@ -6,6 +6,14 @@ #include +enum KeyringProvider +{ + PROVIDER_UNKNOWN, + PROVIDER_FILE, + PROVIDER_VAULT_V2, +} ; + +extern enum KeyringProvider keyringProvider; extern char* keyringConfigFile; extern char* keyringKeyPrefix; diff --git a/src/include/keyring/keyring_vault.h b/src/include/keyring/keyring_vault.h new file mode 100644 index 00000000..bde773b1 --- /dev/null +++ b/src/include/keyring/keyring_vault.h @@ -0,0 +1,19 @@ + +#ifndef KEYRING_VAULT_H +#define KEYRING_VAULT_H + +#include "postgres.h" + +#include + +#include "keyring_api.h" + +int keyringVaultPreloadCache(void); + +int keyringVaultParseConfiguration(json_object* configRoot); + +int keyringVaultStoreKey(const keyInfo* ki); + +int keyringVaultGetKey(keyName name, keyData* outData); + +#endif // KEYRING_FILE_H diff --git a/src/keyring/keyring_api.c b/src/keyring/keyring_api.c index 4cfef91e..44f1b1f5 100644 --- a/src/keyring/keyring_api.c +++ b/src/keyring/keyring_api.c @@ -1,6 +1,7 @@ #include "keyring/keyring_api.h" #include "keyring/keyring_file.h" +#include "keyring/keyring_vault.h" #include "keyring/keyring_config.h" #include "postgres.h" @@ -29,8 +30,18 @@ void keyringInitCache(void) memset(cache, 0, keyringCacheMemorySize()); } - // TODO: HARDCODED FILE PROVIDER - keyringFilePreloadCache(); + switch(keyringProvider) + { + case PROVIDER_FILE: + keyringFilePreloadCache(); + break; + case PROVIDER_VAULT_V2: + keyringVaultPreloadCache(); + break; + case PROVIDER_UNKNOWN: + // nop + break; + } } const keyInfo* keyringCacheStoreKey(keyName name, keyData data) @@ -57,7 +68,28 @@ const keyInfo* keyringGetKey(keyName name) return &cache->keys[i]; } } - // TODO: HARDCODED FILE PROVIDER + // not found in cache, try to look up + switch(keyringProvider) + { + case PROVIDER_FILE: + // nop, not implmeneted + break; + case PROVIDER_VAULT_V2: + { + keyData data; + data.len = 0; + keyringVaultGetKey(name, &data); + if(data.len > 0) + { + return keyringCacheStoreKey(name, data); + } + break; + } + case PROVIDER_UNKNOWN: + // nop + break; + } + #if KEYRING_DEBUG fprintf(stderr, " -- not found\n"); #endif @@ -66,18 +98,27 @@ const keyInfo* keyringGetKey(keyName name) const keyInfo* keyringStoreKey(keyName name, keyData data) { - // TODO: HARDCODED FILE PROVIDER - // Todo: we should first call the provider, and if it succeeds, add the key to the cache - // But as the current file implementation just dumps the cache to disk, this is a good first prototype const keyInfo* ki = keyringCacheStoreKey(name, data); #if KEYRING_DEBUG fprintf(stderr, "Storing key: %s\n", name.name); #endif - if(!keyringFileStoreKey(ki)) + switch(keyringProvider) { - return NULL; + case PROVIDER_FILE: + if(keyringFileStoreKey(ki)) return ki; + break; + case PROVIDER_VAULT_V2: + if(keyringVaultStoreKey(ki)) return ki; + break; + case PROVIDER_UNKNOWN: + // nop + break; } - return ki; + + // if we are here, storeKey failed, remove from cache + cache->keyCount--; + + return NULL; } keyName keyringConstructKeyName(const char* internalName, unsigned version) diff --git a/src/keyring/keyring_config.c b/src/keyring/keyring_config.c index 1a19038f..256304b6 100644 --- a/src/keyring/keyring_config.c +++ b/src/keyring/keyring_config.c @@ -1,6 +1,7 @@ #include "keyring/keyring_config.h" #include "keyring/keyring_file.h" +#include "keyring/keyring_vault.h" #include #include @@ -11,6 +12,7 @@ char* keyringConfigFile = ""; char* keyringKeyPrefix = ""; +enum KeyringProvider keyringProvider = PROVIDER_UNKNOWN; static bool keyringCheckKeyPrefix(char **newval, void **extra, GucSource source) { @@ -116,12 +118,30 @@ bool keyringLoadConfiguration(const char* configFileName) goto cleanup; } - if(strncmp("file", provider, 5) != 0) + + if(strncmp("file", provider, 5) == 0) + { + ret = keyringFileParseConfiguration(root); + if(ret) + { + keyringProvider = PROVIDER_FILE; + } + } + + if(strncmp("vault-v2", provider, 9) == 0) + { + ret = keyringVaultParseConfiguration(root); + if(ret) + { + keyringProvider = PROVIDER_VAULT_V2; + } + } + + if(keyringProvider == PROVIDER_UNKNOWN) { - elog(ERROR, "Invalid pg_tde.keyringConfigFile: Unknown 'provider': %s. Currently only 'file' provider is supported. Keyring is not available.", provider); + elog(ERROR, "Invalid pg_tde.keyringConfigFile: Unknown 'provider': %s. Currently only 'file' and 'vault-v2', providers are supported. Keyring is not available.", provider); } - ret = keyringFileParseConfiguration(root); if (!ret) { @@ -138,7 +158,7 @@ const char* keyringParseStringParam(json_object* object) { if(json_object_get_type(object) == json_type_object) { - elog(WARNING, "Remote parameters are not yet implementeed"); + elog(WARNING, "Remote parameters are not yet implemented"); } return json_object_get_string(object); diff --git a/src/keyring/keyring_file.c b/src/keyring/keyring_file.c index 322c75d4..39943c17 100644 --- a/src/keyring/keyring_file.c +++ b/src/keyring/keyring_file.c @@ -52,13 +52,15 @@ int keyringFilePreloadCache(void) int keyringFileStoreKey(const keyInfo* ki) { + FILE *f; + if (strlen(keyringFileDataFileName) == 0) { elog(ERROR, "Keyring datafile is not set"); return false; } // First very basic prototype: we just dump the cache to disk - FILE* f = fopen(keyringFileDataFileName, "w"); + f = fopen(keyringFileDataFileName, "w"); if(f == NULL) { elog(ERROR, "Couldn't write keyring data into '%s'", keyringFileDataFileName); diff --git a/src/keyring/keyring_vault.c b/src/keyring/keyring_vault.c new file mode 100644 index 00000000..114ab0f6 --- /dev/null +++ b/src/keyring/keyring_vault.c @@ -0,0 +1,301 @@ + +#include "keyring/keyring_vault.h" +#include "keyring/keyring_config.h" +#include "pg_tde_defines.h" + +#include +#include + +#include + +#include "common/base64.h" + +char keyringVaultToken[128]; +char keyringVaultUrl[128]; +char keyringVaultCaPath[256]; +char keyringVaultMountPath[128]; + +CURL* curl = NULL; +struct curl_slist *curlList = NULL; + +typedef struct curlString { + char *ptr; + size_t len; +} curlString; + +static size_t writefunc(void *ptr, size_t size, size_t nmemb, struct curlString *s) +{ + size_t new_len = s->len + size*nmemb; + s->ptr = repalloc(s->ptr, new_len+1); + if (s->ptr == NULL) { + exit(EXIT_FAILURE); + } + memcpy(s->ptr+s->len, ptr, size*nmemb); + s->ptr[new_len] = '\0'; + s->len = new_len; + + return size*nmemb; +} + +static bool curlSetupSession(const char* url, curlString* str) +{ + if(curl == NULL) + { + curl = curl_easy_init(); + + if(curl == NULL) return 0; + } + + if(curlList == NULL) + { + char tokenHeader[256]; + strcpy(tokenHeader, "X-Vault-Token:"); + strcat(tokenHeader, keyringVaultToken); + + curlList = curl_slist_append(curlList, tokenHeader); + if(curlList == NULL) return 0; + + curlList = curl_slist_append(curlList, "Content-Type: application/json"); + if(curlList == NULL) return 0; + } + + + if(curl_easy_setopt(curl, CURLOPT_HTTPHEADER, curlList) != CURLE_OK) return 0; + if(curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1) != CURLE_OK) return 0; + if(curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 1) != CURLE_OK) return 0; + if(curl_easy_setopt(curl, CURLOPT_USE_SSL, CURLUSESSL_ALL) != CURLE_OK) return 0; + if(strlen(keyringVaultCaPath) > 0 && curl_easy_setopt( + curl, CURLOPT_CAINFO, keyringVaultCaPath) != CURLE_OK) return 0; + if(curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L) != CURLE_OK) return 0; + if(curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 3) != CURLE_OK) return 0; + if(curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10) != CURLE_OK) return 0; + if(curl_easy_setopt(curl, CURLOPT_HTTP_VERSION,(long)CURL_HTTP_VERSION_1_1) != CURLE_OK) return 0; + if(curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION,writefunc) != CURLE_OK) return 0; + if(curl_easy_setopt(curl, CURLOPT_WRITEDATA,str) != CURLE_OK) return 0; + if(curl_easy_setopt(curl, CURLOPT_URL, url) != CURLE_OK) return 0; + + return 1; +} + +static bool curlPerform(const char* url, curlString* outStr, long* httpCode, const char* postData) +{ +#if KEYRING_DEBUG + elog(DEBUG1, "Performing Vault HTTP [%s] request to '%s'", postData != NULL ? "POST" : "GET", url); + if(postData != NULL) + { + elog(DEBUG2, "Postdata: '%s'", postData); + } +#endif + outStr->ptr = palloc0(1); + outStr->len = 0; + + if(!curlSetupSession(url, outStr)) return 0; + + if(postData != NULL) + { + if(curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData) != CURLE_OK) return 0; + } else + { + if(curl_easy_setopt(curl, CURLOPT_POSTFIELDS, NULL) != CURLE_OK) return 0; + if(curl_easy_setopt(curl, CURLOPT_POST, 0) != CURLE_OK) return 0; + } + + if(curl_easy_perform(curl) != CURLE_OK) return 0; + if(curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, httpCode) != CURLE_OK) return 0; + +#if KEYRING_DEBUG + elog(DEBUG2, "Vault response [%li] '%s'", *httpCode, outStr->ptr != NULL ? outStr->ptr : ""); +#endif + + return 1; +} + + +int keyringVaultPreloadCache(void) +{ + // nop + return 1; +} + +static bool keyringConfigExtractParameter(json_object* configRoot, const char* name, char* out, unsigned outMaxLen, bool optional) +{ + json_object* dataO; + const char* stringData; + + if(!json_object_object_get_ex(configRoot, name, &dataO)) + { + if(!optional) + { + elog(ERROR, "Missing '%s' attribute.", name); + } + return 0; + } + + stringData = keyringParseStringParam(dataO); + + if(stringData == NULL) + { + if(!optional) + { + elog(ERROR, "Couldn't parse '%s' attribute.", name); + } + return 0; + } + + if(strlen(stringData) > outMaxLen-1) + { + elog(WARNING, "Attribute '%s' is too long, maximum is %u, truncated.", name, outMaxLen); + } + + strncpy(out, stringData, outMaxLen-1); + + return 1; +} + +int keyringVaultParseConfiguration(json_object* configRoot) +{ + if(!keyringConfigExtractParameter(configRoot, "token", keyringVaultToken, 128, 0)) + { + return 0; + } + + if(!keyringConfigExtractParameter(configRoot, "url", keyringVaultUrl, 128, 0)) + { + return 0; + } + + if(!keyringConfigExtractParameter(configRoot, "mountPath", keyringVaultMountPath, 128, 0)) + { + return 0; + } + + keyringConfigExtractParameter(configRoot, "caPath", keyringVaultCaPath, 256, 1); + + return 1; +} + +static void keyringVaultKeyUrl(char* out, keyName name) +{ + strcpy(out, keyringVaultUrl); + strcat(out, "/v1/"); + strcat(out, keyringVaultMountPath); + strcat(out, "/data/"); + strcat(out, name.name); +} + +int keyringVaultStoreKey(const keyInfo* ki) +{ + char url[512]; + curlString str; + long httpCode = 0; + json_object *request = json_object_new_object(); + json_object *data = json_object_new_object(); + char keyData[64]; + int keyLen = 0; + + keyLen = pg_b64_encode((char*)ki->data.data, ki->data.len, keyData, 64); + keyData[keyLen] = 0; + json_object_object_add(data, "key", json_object_new_string(keyData)); + json_object_object_add(request, "data", data); + +#if KEYRING_DEBUG + elog(DEBUG1, "Sending base64 key: %s", keyData); +#endif + + keyringVaultKeyUrl(url, ki->name); + if(!curlPerform(url, &str, &httpCode, json_object_to_json_string(request))) + { + elog(ERROR, "HTTP(S) request to vault failed."); + if(str.ptr != NULL) pfree(str.ptr); + json_object_put(request); + return 0; + } + + json_object_put(request); + if(str.ptr != NULL) pfree(str.ptr); + + return httpCode / 100 == 2; +} + +int keyringVaultGetKey(keyName name, keyData* outData) +{ + char url[512]; + curlString str; + json_object *response = NULL; + long httpCode = 0; + int ret = 0; + + json_object *data = NULL; + json_object *data2 = NULL; + json_object *keyO = NULL; + const char *key = NULL; + + keyringVaultKeyUrl(url, name); + if(!curlPerform(url, &str, &httpCode, NULL)) + { + elog(ERROR, "HTTP(S) request to vault failed."); + goto cleanup; + } + + if(httpCode / 100 != 2) + { + if(httpCode != 404) + { + elog(ERROR, "Unexpected HTTP code: %li", httpCode); + } + goto cleanup; + } + + response = json_tokener_parse(str.ptr); + + if(response == NULL) + { + elog(ERROR, "Vault response is not a json object."); + goto cleanup; + } + + if(!json_object_object_get_ex(response, "data", &data)) + { + elog(ERROR, "No data attribute in Vault response."); + goto cleanup; + } + + if(!json_object_object_get_ex(data, "data", &data2)) + { + elog(ERROR, "No data.data attribute in Vault response."); + goto cleanup; + } + + if(!json_object_object_get_ex(data2, "key", &keyO)) + { + elog(ERROR, "No data.data.key attribute in Vault response."); + goto cleanup; + } + + key = json_object_get_string(keyO); + if(key == NULL || strlen(key) == 0) + { + elog(ERROR, "Key doesn't exist or empty"); + goto cleanup; + } + +#if KEYRING_DEBUG + elog(DEBUG1, "Retrieved base64 key: %s", key); +#endif + + outData->len = pg_b64_decode(key, strlen(key), (char*)outData->data, 32); + + if(outData->len != 32) + { + goto cleanup; + } + + ret = 1; + +cleanup: + if(str.ptr != NULL) pfree(str.ptr); + if(response != NULL) json_object_put(response); + return ret; +} + +