Add key management system
authorBruce Momjian <[email protected]>
Fri, 25 Dec 2020 15:19:44 +0000 (10:19 -0500)
committerBruce Momjian <[email protected]>
Fri, 25 Dec 2020 15:19:44 +0000 (10:19 -0500)
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[email protected]

Author: Masahiko Sawada, me, Stephen Frost

49 files changed:
doc/src/sgml/config.sgml
doc/src/sgml/database-encryption.sgml [new file with mode: 0644]
doc/src/sgml/filelist.sgml
doc/src/sgml/installation.sgml
doc/src/sgml/postgres.sgml
doc/src/sgml/ref/initdb.sgml
doc/src/sgml/ref/pg_ctl-ref.sgml
doc/src/sgml/ref/pgupgrade.sgml
doc/src/sgml/ref/postgres-ref.sgml
doc/src/sgml/storage.sgml
src/backend/Makefile
src/backend/access/transam/xlog.c
src/backend/bootstrap/bootstrap.c
src/backend/crypto/Makefile [new file with mode: 0644]
src/backend/crypto/kmgr.c [new file with mode: 0644]
src/backend/main/main.c
src/backend/postmaster/pgstat.c
src/backend/postmaster/postmaster.c
src/backend/replication/basebackup.c
src/backend/storage/ipc/ipci.c
src/backend/storage/lmgr/lwlocknames.txt
src/backend/tcop/postgres.c
src/backend/utils/misc/guc.c
src/backend/utils/misc/pg_controldata.c
src/backend/utils/misc/postgresql.conf.sample
src/bin/initdb/initdb.c
src/bin/pg_controldata/pg_controldata.c
src/bin/pg_ctl/pg_ctl.c
src/bin/pg_resetwal/pg_resetwal.c
src/bin/pg_rewind/filemap.c
src/bin/pg_upgrade/check.c
src/bin/pg_upgrade/controldata.c
src/bin/pg_upgrade/file.c
src/bin/pg_upgrade/option.c
src/bin/pg_upgrade/pg_upgrade.h
src/bin/pg_upgrade/server.c
src/common/Makefile
src/common/cipher.c [new file with mode: 0644]
src/common/cipher_openssl.c [new file with mode: 0644]
src/common/kmgr_utils.c [new file with mode: 0644]
src/include/catalog/pg_control.h
src/include/common/cipher.h [new file with mode: 0644]
src/include/common/kmgr_utils.h [new file with mode: 0644]
src/include/crypto/kmgr.h [new file with mode: 0644]
src/include/pgstat.h
src/include/postmaster/postmaster.h
src/include/utils/guc_tables.h
src/test/Makefile
src/tools/msvc/Mkvcbuild.pm

index 4b60382778f760f713d63d4b3fa8fbadda99f89b..426928f6800332460388fb3e92a762ee8aad45fd 100644 (file)
@@ -7816,6 +7816,52 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
     </variablelist>
    </sect1>
 
+   <sect1 id="runtime-config-encryption">
+    <title>Cluster File Encryption</title>
+
+    <variablelist>
+     <varlistentry id="guc-cluster-key-command" xreflabel="cluster_key_command">
+      <term><varname>cluster_key_command</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>cluster_key_command</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        This option specifies an external command to obtain the cluster-level
+        key for cluster file encryption during server initialization and
+        server start.
+       </para>
+       <para>
+        The command must print the cluster key to the standard output as
+        64 hexadecimal characters, and exit with code 0.  The command
+        can prompt for the passphrase or PIN from the terminal if
+        <option>--authprompt</option> is used.  In the parameter value,
+        <literal>%R</literal> represents the file descriptor number opened
+        to the terminal that started the server.  A file descriptor is only
+        available if enabled at server start.  If <literal>%R</literal>
+        is used and no file descriptor is available, the server will not
+        start.  Value <literal>%p</literal> is replaced by a pre-defined
+        prompt string.  Value <literal>%d</literal> is replaced by the
+        directory containing the keys;  this is useful if the command
+        must create files with the keys, e.g., to store a cluster-level
+        key encryped by a key stored in a hardware security module.
+        (Write <literal>%%</literal> for a literal <literal>%</literal>.)
+        Note that the prompt string will probably contain whitespace,
+        so be sure to quote its use adequately.  Newlines are stripped
+        from the end of the output if present.
+       </para>
+       <para>
+        This parameter can only be set by
+        <application>initdb</application>, in the
+        <filename>postgresql.conf</filename> file, or on the server
+        command line.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </sect1>
+
    <sect1 id="runtime-config-client">
     <title>Client Connection Defaults</title>
 
@@ -9637,6 +9683,22 @@ dynamic_library_path = 'C:\tools\postgresql;H:\my_project\lib;$libdir'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-file-encryption-keylen" xreflabel="file_encryption_keylen">
+      <term><varname>file_encryption_keylen</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary>Cluster file encryption key length</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Reports the bit length of the cluster file
+        encryption key, or zero if disabled.  See <xref
+        linkend="app-initdb-cluster-key-command"/> for more
+        information.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-data-directory-mode" xreflabel="data_directory_mode">
       <term><varname>data_directory_mode</varname> (<type>integer</type>)
       <indexterm>
diff --git a/doc/src/sgml/database-encryption.sgml b/doc/src/sgml/database-encryption.sgml
new file mode 100644 (file)
index 0000000..f938c9f
--- /dev/null
@@ -0,0 +1,97 @@
+<!-- doc/src/sgml/database-encryption.sgml -->
+
+<chapter id="database-file-encryption">
+ <title>Cluster File Encryption</title>
+
+ <indexterm zone="database-file-encryption">
+  <primary>Cluster File Encryption</primary>
+ </indexterm>
+
+ <para>
+  The purpose of cluster file encryption is to prevent users with read
+  access to the directories used to store database files and write-ahead
+  log from being able to access the data stored in those files.
+  For example, when using cluster file encryption, users who have read
+  access to the cluster directories for backup purposes will not be able
+  to decrypt the data stored in the these files.
+ </para>
+
+ <para>
+  Cluster file encryption uses two levels of encryption.  The first level
+  is data encryption keys, specifically keys zero and one.  Key zero is
+  the key used to encrypt database heap and index files which are stored in
+  the file system, plus temporary files created during database operation.
+  Key one is used to encrypt write-ahead log (WAL) files.  Two different
+  keys are used so that primary and standby servers can use different zero
+  (heap/index/temp) keys, but the same one (WAL) key, so that these keys
+  can eventually be rotated by switching the primary to the standby as
+  and then changing the WAL key.
+ </para>
+
+ <para>
+  The second level of encryption is a key used to encrypt first-level
+  keys.  This type of key is often referred to as a Key Encryption Key
+  (<acronym>KEK</acronym>).  This key is <emphasis>not</emphasis> stored
+  in the file system, but provided at <command>initdb</command> time and
+  each time the server is started.  This key prevents anyone with access
+  to the database directories from decrypting the data because they do
+  not know the second-level key which encrypted the first-level keys
+  which encrypted the database cluster files.
+ </para>
+
+ <sect1 id="encryption-file-encryption">
+  <title>Initialization</title>
+
+  <para>
+   Cluster file encryption is enabled when
+   <productname>PostgreSQL</productname> is built
+   with <literal>--with-openssl</literal> and <xref
+   linkend="app-initdb-cluster-key-command"/> is specified
+   during <command>initdb</command>.  The cluster key
+   provided by the <option>--cluster-key-command</option>
+   option during <command>initdb</command> and the one generated
+   by <xref linkend="guc-cluster-key-command"/> in the
+   <filename>postgresql.conf</filename> must match for the database
+   cluster to start. Note that the cluster key command
+   passed to <command>initdb</command> must return a key of
+   64 hexadecimal characters. For example.
+<programlisting>
+initdb -D dbname --cluster-key-command='ckey_passphrase.sh'
+</programlisting>
+  </para>
+ </sect1>
+
+ <sect1 id="key-encryption-key">
+  <title>Internals</title>
+
+  <para>
+   During the <command>initdb</command> process, if
+   <option>--cluster-key-command</option> is specified, two data-level
+   encryption keys are created.   These two keys are then encrypted with
+   the key enryption key (KEK) supplied by the cluster key command before
+   being stored in the database directory.  The key or passphrase that
+   derives the key must be supplied from the terminal or stored in a
+   trusted key store, such as key vault software, hardware security module.
+  </para>
+
+  <para>
+   If the <productname>PostgreSQL</productname> server has
+   been initialized to require a cluster key, each time the
+   server starts the <filename>postgresql.conf</filename>
+   <varname>cluster_key_command</varname> command will be executed
+   and the cluster key retrieved.  The data encryption keys in the
+   <filename>pg_cryptokeys</filename> directory will then be decrypted
+   using the supplied key and integrity-checked to ensure it
+   matches the initdb-supplied key.  If this check fails, the
+   server will refuse to start.
+  </para>
+
+  <para>
+   The data encryption keys are randomly generated and are of 128, 192,
+   or 256-bits in length.  They are encrypted by the key encryption key
+   (KEK) using Advanced Encryption Standard (<acronym>AES256</acronym>)
+   encryption in Galois/Counter Mode (<acronym>GCM</acronym>), which also
+   provides KEK authentication.
+  </para>
+ </sect1>
+</chapter>
index 38e8aa0bbf90f3171f892d0cead5d2cf950fde67..b96f4ace6cf6746a35fd37b9390faf4cb895c0be 100644 (file)
@@ -49,6 +49,7 @@
 <!ENTITY wal           SYSTEM "wal.sgml">
 <!ENTITY logical-replication    SYSTEM "logical-replication.sgml">
 <!ENTITY jit    SYSTEM "jit.sgml">
+<!ENTITY database-encryption SYSTEM "database-encryption.sgml">
 
 <!-- programmer's guide -->
 <!ENTITY bgworker   SYSTEM "bgworker.sgml">
index 0ac1cb999992f18f3282097389d3480a14734d61..bcc802404987fd04a14a1d1478b248cedaa7c489 100644 (file)
@@ -976,8 +976,9 @@ build-postgresql:
        <listitem>
         <para>
          Build with support for <acronym>SSL</acronym> (encrypted)
-         connections. This requires the <productname>OpenSSL</productname>
-         package to be installed.  <filename>configure</filename> will check
+         connections and cluster file encryption. This requires the
+         <productname>OpenSSL</productname> package to be installed.
+         <filename>configure</filename> will check
          for the required header files and libraries to make sure that
          your <productname>OpenSSL</productname> installation is sufficient
          before proceeding.
index 730d5fdc34837d7fee4cf112a39f68c130b4aeb1..0ea7da604b35edeffe761d8add2fb1a61926cdce 100644 (file)
@@ -171,6 +171,7 @@ break is not needed in a wider output rendering.
   &wal;
   &logical-replication;
   &jit;
+  &database-encryption;
   &regress;
 
  </part>
index 385ac2515061ca50ff043b0f6172dae76f4c9b5b..44a2d69d326be1ff8a4d4f8590f955afdf9e993e 100644 (file)
@@ -163,6 +163,17 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry id="app-initdb-cluster-key-command" xreflabel="cluster key command">
+      <term><option>--cluster-key-command=<replaceable class="parameter">command</replaceable></option></term>
+      <listitem>
+       <para>
+        This option specifies an external command to obtain the cluster-level
+        key for cluster file encryption during server initialization and
+        server start;  see <xref linkend="guc-cluster-key-command"/> for details.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
       <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
@@ -223,6 +234,18 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry id="app-initdb-file-encryption-keylen"
+      xreflabel="file encryption">
+      <term><option>-K</option></term>
+      <term><option>--file-encryption-keylen</option></term>
+      <listitem>
+       <para>
+        Specifies the number of bits for the file encryption keys.  The
+        default is 128 bits.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--locale=<replaceable>locale</replaceable></option></term>
       <listitem>
@@ -285,6 +308,17 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-R</option></term>
+      <term><option>--authprompt</option></term>
+      <listitem>
+       <para>
+        Allows the <option>--cluster-key-command</option> command
+        to prompt for a passphrase or PIN.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-S</option></term>
       <term><option>--sync-only</option></term>
@@ -307,6 +341,18 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-u <replaceable>datadir</replaceable></option></term>
+      <term><option>--copy-encryption-keys=<replaceable>datadir</replaceable></option></term>
+      <listitem>
+       <para>
+        Copies cluster file encryption keys from another cluster; required
+        when using <application>pg_upgrade</application> on a cluster
+        with cluster file encryption enabled.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-U <replaceable class="parameter">username</replaceable></option></term>
       <term><option>--username=<replaceable class="parameter">username</replaceable></option></term>
index 3946fa52eab7c5b305f79f243cd3c873a332c5df..f04e417745f06e5917dad58b3ab023b781c780e9 100644 (file)
@@ -38,6 +38,7 @@ PostgreSQL documentation
    <arg choice="opt"><option>-s</option></arg>
    <arg choice="opt"><option>-o</option> <replaceable>options</replaceable></arg>
    <arg choice="opt"><option>-p</option> <replaceable>path</replaceable></arg>
+   <arg choice="opt"><option>-R</option></arg>
    <arg choice="opt"><option>-c</option></arg>
   </cmdsynopsis>
 
@@ -72,6 +73,7 @@ PostgreSQL documentation
    <arg choice="opt"><option>-t</option> <replaceable>seconds</replaceable></arg>
    <arg choice="opt"><option>-s</option></arg>
    <arg choice="opt"><option>-o</option> <replaceable>options</replaceable></arg>
+   <arg choice="opt"><option>-R</option></arg>
    <arg choice="opt"><option>-c</option></arg>
   </cmdsynopsis>
 
@@ -373,6 +375,17 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-R</option></term>
+      <term><option>--authprompt</option></term>
+      <listitem>
+       <para>
+        Allows the <option>--cluster-key-command</option> command
+        to prompt for a passphrase or PIN.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-s</option></term>
       <term><option>--silent</option></term>
index 92e1d09a55caee6f32ddc3ba6161dde6dd5e9c43..98be3921cb18e70eb255e682c533f8b38d2b3f95 100644 (file)
@@ -167,6 +167,13 @@ PostgreSQL documentation
       </para></listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-R</option></term>
+      <term><option>--authprompt</option></term>
+      <listitem><para>allows prompting for a passphrase or PIN
+      </para></listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-s</option> <replaceable>dir</replaceable></term>
       <term><option>--socketdir=</option><replaceable>dir</replaceable></term>
@@ -309,7 +316,9 @@ make prefix=/usr/local/pgsql.new install
      Again, use compatible <command>initdb</command>
      flags that match the old cluster. Many
      prebuilt installers do this step automatically. There is no need to
-     start the new cluster.
+     start the new cluster.  If upgrading a cluster that uses
+     cluster file encryption, the <command>initdb</command> option
+     <option>--copy-encryption-keys</option> must be specified.
     </para>
    </step>
 
@@ -838,6 +847,13 @@ psql --username=postgres --file=script.sql postgres
    is down.
   </para>
 
+  <para>
+   If the old cluster uses file encryption, the new cluster must use
+   the same keys, so <command>pg_upgrade</command> copies them to the
+   new cluster.  It is necessary to initialize the new cluster with
+   the same <varname>cluster_key_command</varname> and the same
+   file encryption key length.
+  </para>
  </refsect1>
 
  <refsect1>
index 4aaa7abe1a28642115795521ac3fcc44749f88cf..805da81e073f3405973b0e40896f1da93fc71f3a 100644 (file)
@@ -297,6 +297,19 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-R <replaceable class="parameter">file-descriptor</replaceable></option></term>
+      <listitem>
+       <para>
+        Makes <command>postgres</command> prompt for a passphrase or PIN
+        from the specified open numeric file descriptor.  The descriptor
+        is closed after the key is read.  The file descriptor number
+        <literal>-1</literal> duplicates standard error for the terminal;
+        this is useful for single-user mode.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-s</option></term>
       <listitem>
index 3234adb639f58b3a4edb00b0e6d58018795b2e5f..cdbc214a51047a8e2b8578a65ac835e4797b6b5e 100644 (file)
@@ -77,6 +77,11 @@ Item
  <entry>Subdirectory containing transaction commit timestamp data</entry>
 </row>
 
+<row>
+ <entry><filename>pg_cryptokeys</filename></entry>
+ <entry>Subdirectory containing file encryption keys</entry>
+</row>
+
 <row>
  <entry><filename>pg_dynshmem</filename></entry>
  <entry>Subdirectory containing files used by the dynamic shared memory
index 9706a95848842fae87fff26619be42655b152d72..4ace30203825e55c53a094f5d9d4c3053a20b453 100644 (file)
@@ -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
 
index 9867e1b4039cde1f25c2c97fdeb7f9ab2a5bc3b6..48ca46a941caae1277c7d0885697567ec3d49020 100644 (file)
 #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();
index a7ed93fdc14d1cf63f378dfa59c5e87447f3d98c..bf93135a48355904abd61fbd18561c7d88f2ff18 100644 (file)
 #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 (file)
index 0000000..c273620
--- /dev/null
@@ -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 (file)
index 0000000..4e701e0
--- /dev/null
@@ -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)));
+   }
+}
index b6e512883269017aa1e3e1bbf9bf3fd008454762..19aa502614e7f22bb7b0ef1d613468e25c91f34a 100644 (file)
@@ -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"
index d87d9d06ee250346c790755844dae5b0f7665cfe..71f2b90ca861c8a6bd31b95488eaaec89396f70c 100644 (file)
@@ -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;
index fff4227e0b609fc04a8ade4cef933222239921a1..bf883184b1137895dce797e0172c58b6fdcfa85d 100644 (file)
 #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!)
index 1d8d1742a73a0b4e49b420b445165bdb2de84866..c9fc2f4b4e11b67aaa327b39d4bb37cbbac6ae44 100644 (file)
@@ -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
index 96c2aaabbd65cec1d7c9fac5a6a16e2396692394..64fe10ca2a718e7bc9644815788b2b1b9e7d4fd4 100644 (file)
@@ -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
 
index 774292fd942774b87f11dbeebb76f6c5e64f02f2..a44805d5c3994e6481d903760f834f4c74f82ae0 100644 (file)
@@ -53,3 +53,4 @@ XactTruncationLock                    44
 # 45 was XactTruncationLock until removal of BackendRandomLock
 WrapLimitsVacuumLock               46
 NotifyQueueTailLock                    47
+KmgrFileLock                   48
index d35c5020ea634d5bbb301f14b59a23b03d269434..81e64616d43664b8230aabc59f722041fdc1a80d 100644 (file)
@@ -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;
@@ -3921,6 +3932,18 @@ PostgresMain(int argc, char *argv[],
    /* Early initialization */
    BaseInit();
 
+   /*
+    * Initialize kmgr for cluster encryption. Since kmgr needs to attach to
+    * shared memory the initialization must be called after BaseInit().
+    */
+   if (!IsUnderPostmaster)
+   {
+       InitializeKmgr();
+
+       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
index 878fcc2236585c197b433d3d4f10ad4500d6b846..bbaf037bc6e17ae808f72dc94483ecc0b53d43ab 100644 (file)
@@ -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
@@ -4383,6 +4397,16 @@ static struct config_string ConfigureNamesString[] =
        NULL, NULL, NULL
    },
 
+   {
+       {"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."),
index d50d87a6021c6e48110720cdd98e51644b799000..6fcab311721e76750c87d35007ad3210ffe02f8c 100644 (file)
@@ -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));
index b7fb2ec1feb6bf00c493adc0156e9cfa67f4a3be..f19cfe5f6aed5c6cfc6c795dcccdbcb9dd1201a1 100644 (file)
                    # autovacuum, -1 means use
                    # vacuum_cost_limit
 
+#------------------------------------------------------------------------------
+# ENCRYPTION
+#------------------------------------------------------------------------------
+
+#cluster_key_command = ''
 
 #------------------------------------------------------------------------------
 # CLIENT CONNECTION DEFAULTS
index f994c4216bcdc810e9fad68d03103ae5580edebc..92594772f6cf2fa0fe8305c9f6a9cbe8082d552f 100644 (file)
@@ -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();
 
index 3e00ac0f701ab82479066421c2bb7b4848f0bc7b..c3b38b7c51c17ec2f0db34c1219ed5ec9484860d 100644 (file)
@@ -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;
 }
index fc07f1aba6e7767420634f1a6c26ccd3b5a0ea4e..5fa1f72ae183ba23b2b73c6d7dd114d4f3d517c3 100644 (file)
@@ -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;
index cb6ef1918206a01551b5c6dab6e864619655f4f0..8f928b3129222d52883c5828faf6f51497a3f0a2 100644 (file)
@@ -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);
 }
 
 
index ba34dbac1468f548e6d981f83d58a9cddce4be6e..b8775cab15db00a2943195a75cb6b3b9af4e8eb6 100644 (file)
@@ -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"
@@ -107,6 +108,13 @@ static const char *excludeDirContents[] =
    /* Contents removed on startup, see AsyncShmemInit(). */
    "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().
index f3afea9d56117af523f7224136443afccc50ef35..ef091cb3e4c4a4d88b806384d39431f4381ad4ab 100644 (file)
@@ -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:
@@ -1269,6 +1277,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
  *
index 39bcaa8fe1a2b7c0d4358fef1fe74016b65dc38c..a0aa995bbdeb8a006bbd81acda68a7458251b707 100644 (file)
@@ -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");
 }
 
 
index cc8a675d0095cbd3995e47bb70d4e06f03424413..c9851192ec679f5fe41849bdb3088e568e46a420 100644 (file)
@@ -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"
index 548d648e8c4e6fcf20935bfab8e76922a04c2235..4702998352ff177c5281078de76f691d7f6d5413 100644 (file)
@@ -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;
index ee70243c2e946eeaf14d500f3e89e96fd1952bb9..53ce195963f18554f9b391b68c317f9d0aea8288 100644 (file)
@@ -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
index 713509f54062a273536b51c54778234bc3b82970..9208ad0d8a3722e42f4fbbe035aa2b8cf3f71dea 100644 (file)
@@ -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",
index f62497793956fb775d5b766f2be518caded9e9cd..85d1388a646f878ca717e90287c3398df803d098 100644 (file)
@@ -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 (file)
index 0000000..e42d984
--- /dev/null
@@ -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 (file)
index 0000000..e7a1dc7
--- /dev/null
@@ -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 (file)
index 0000000..d031976
--- /dev/null
@@ -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
+}
index 06bed90c5e9e5796e222d6e1631818341fd3fea7..a4c12599f74cf2dc493dc17e75de4ddedd6c1cfa 100644 (file)
@@ -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 (file)
index 0000000..598ef11
--- /dev/null
@@ -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 (file)
index 0000000..23124a7
--- /dev/null
@@ -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 (file)
index 0000000..386ac1c
--- /dev/null
@@ -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 */
index 5954068dec534f5e837048c3e347656e7bbe2737..b8f98f9a58a936d0af2a5bb5bfeb296cbf9f251e 100644 (file)
@@ -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,
index babc87dfc9d2aa6814ed7e63ecd5e2cb339f3b9c..b1f0721b856cb90514ae22a90546f3672fca9312 100644 (file)
@@ -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
index 7f36e1146f21a721616a7a2cf3b1a0644b1a48cc..c0dbf6911652fe0da9275f5cfb8b9f7d702e581f 100644 (file)
@@ -89,6 +89,7 @@ enum config_group
    STATS,
    STATS_MONITORING,
    STATS_COLLECTOR,
+   ENCRYPTION,
    AUTOVACUUM,
    CLIENT_CONN,
    CLIENT_CONN_STATEMENT,
index ab1ef9a47532aa11530ba3712e3528d93353faa7..730efbf9c4a21fa4dad566f3cdc9718550a0d2ac 100644 (file)
@@ -30,7 +30,7 @@ endif
 endif
 ifeq ($(with_openssl),yes)
 ifneq (,$(filter ssl,$(PG_TEST_EXTRA)))
-SUBDIRS += ssl
+SUBDIRS += ssl crypto
 endif
 endif
 
index 7f014a12c9a4725c28d30d7a264d69e964399518..0e2104a34558f17a47daa9eea7a157d41a2a026b 100644 (file)
@@ -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');