Re: [Proposal] Table-level Transparent Data Encryption (TDE) and KeyManagement Service (KMS) - Mailing list pgsql-hackers

From Jonathan S. Katz
Subject Re: [Proposal] Table-level Transparent Data Encryption (TDE) and KeyManagement Service (KMS)
Date
Msg-id 00edeaf1-74e7-90ab-c24d-eb5ee2be857c@postgresql.org
Whole thread Raw
In response to Re: [Proposal] Table-level Transparent Data Encryption (TDE) and KeyManagement Service (KMS)  (Bruce Momjian <bruce@momjian.us>)
Responses Re: [Proposal] Table-level Transparent Data Encryption (TDE) and KeyManagement Service (KMS)
List pgsql-hackers
Hi,

On 8/6/19 3:01 PM, Bruce Momjian wrote:
> On Tue, Aug  6, 2019 at 01:55:38PM -0400, Bruce Momjian wrote:
>> CTR mode creates a bit stream for the first 16 bytes with nonce of
>> (segment_number, counter = 0), and the next 16 bytes with
>> (segment_number, counter = 1), etc.  We only XOR using the parts of the
>> bit stream we want to use.  We don't care what the WAL content is --- we
>> just XOR it with the stream with the matching counter for that part of
>> the WAL.
>
> The diagram which is part of this section might be helpful:
>
>     https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Counter_(CTR)
>     https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#/media/File:CTR_encryption_2.svg

This is going to be a slightly long (understatement) email that I
thought would be easier to try to communicate all in one place vs.
replying to individual parts on this long thread. My main goal was to
present some things I had researched on TDE, some of which had been
mentioned on thread, and compile it in one place (it's also why I was
slow to respond on some other things on the thread -- sorry!)

While compiling this note, one idea that came to mind is that given the
complexity of this topic and getting the key pieces (no pun intended)
correct, it may be worthwhile if the people interested in working on TDE
make some time where we can all either get together and/or set up a
group call where we hash out the architecture we want to build to ensure
we have a fundamentally sound implementation (even if it takes a few
versions to implement it fully).

Since my last note, I really dove in to understand what other RDBMS
system are doing and what we can learn from them as well as what would
make sense in PostgreSQL. In particular, one goal is to be able to build
a TDE system that satisfies keeping data both confidential and
integrable while at rest while minimizing the amount of overhead we
introduce into the system.

Additionally, I would strongly suggest, if not require, that what we
build follows guidelines such as those outlined by NIST, as failure to
do so could end up that some teams would be unable to utilize our TDE
solution. And of course, mitigate the risk that we introduce security
vulnerabilities :)

(I've also continued to build out my terrible prototype to experiment
with some of the methods suggested. It's written in Python and leverages
the "cryptography"[0] library [which it itself has some good
recommendations on how to use its various parts) and still not worth
sharing yet (though happy to share if asked off-list -- you will be
underwhelmed].)

Below I outline some of my findings from looking at other systems,
looking at our own code, and make recommendations to the best of my
abilities on this matter. I broke it up into these 3 sections, which are
interspersed with research and recommendations:

1. Encryption Key Management
2. Encryption/Decryption of Pages + WAL Records
3. Automated Key Rotation

Of course, they are tightly intertwined, but thought it would be easier
to look at it in this way. It does stop short of certain implementation
details.

Anyway, without further ado:

#1 Encryption Key Management
----------------------------

While I thought what we had proposed on list (KEK, MDEK, TDEK, WDEK)
system made a lot of sense, I still decided to look at what other
systems did. In particular, I looked at SQL Server and the "redwood
city" database.

It turns out the SQL Server has a somewhat similar architecture to what
we propose[1]:


https://docs.microsoft.com/en-us/sql/relational-databases/security/encryption/transparent-data-encryption?view=sql-server-2017

- A Service Master Key (equivalent to the key-encrypting key KEK) is
created when SQL Server is setup (equivalent to initdb)
- SQL Server uses Windows facilities to protect the master cluster key.
We are trying to achieve this with our KEK, which are saying we would
store in the GUC `tde_passphrase` (or equivalent, I forget the name we
proposed, if we proposed one. Sorry.). If we wanted to protect it at the
OS level, Peter Eisentraut shows a way to do this equivalent with the
"ssl_passphrase" GUC and systemd[2]
- SQL Server lets you create a "master key" for all the clusters, explicitly
- Here is where the differences start: SQL Server then lets you create a
certificate that can be protected by the master key.
- For the specific databases in the SQL Server cluster, you can then
create a "database encryption key" (they call it "DEK") that is used to
encrypt the data, and ask that said key is protected by a certificate.

(For SQL Server, it appears that TDE is all or nothing within a
database, but *not* within the entire cluster.)

SQL Server does support a multi-key approach which allows for, amongst
other things, key rotation. For what we are looking to do, it may be too
much, but it seems similar to the ideas proposed.

The redwood city database also has something similar to what we are
proposing[3]:

https://docs.oracle.com/database/121/ASOAG/introduction-to-transparent-data-encryption.htm#ASOAG10139

Basically:

    - A master key (equiv to our MDEK) is managed by an "external
security module".
    - Each tablespace has a key that can be encrypted/decrypted by the
MDEK. This would be similar to our TDEK.

The keys are secured in a wallet[8].

This is a long way of saying that we are in the right direction
architecture-wise.

Here are some thoughts on that. Using terms defined in Joe's email[4].
Note I am assuming we are going with AES-256 (and a bit more on this later).

    0. First, proposing the KEK he defined is the MDEK. More on that in
a second.

    1. To have a MDEK stored in a GUC, we'd have to have people
comfortable storing with random bytes in a GUC. It may be better to either:

        a) Allow for the GUC to point to a file that has the MDEK stored
        b) Allow the GUC to be a passphrase that can unlock the MDEK,
which is stored internally in the catalog.

    For a), we might want to provide some facilities for the user to
generate a MDEK, which there have been suggestions to do that from initdb.

    2. We need to ensure we allow the user to cleanly rotate the MDEK.
Just updating the passphrase would be no good, as they'd basically lock
themselves out of all of their data :) This was proposed on the list,
but would need to ensure we can handle this in a tidy way with what we
can control in the configuration.

    3. We then have a "database encryption key" (DDEK) which is
presumably generated on CREATE DATABASE (as I am not sure I've seen that
explicitly stated anywhere). This is encrypted by the MDEK using a
padded key-wrapping function[5].

    4. Each relation is encrypted by a "table encryption key" (TDEK)
that is generated by a HMAC-based key deriviation function (HKDF) using
SHA-256 and the DDEK. Guidelines for how to safely generate this are
present in NIST 800-56C[6]. We would want a two-step key derivation,
which would include[6]:

        a) A cryptographically-secure randomly generated salt of 64
bytes[6] (Section 5; Table 4) that is created when the relation is created.
        b) Some "fixed info" bit string which is likely to be composed
of the relation OID (4 bytes) and a "key identifier" (which I have
pegged at 1 bytes, but more on this later)

    5. Last but not least, the "WAL encrpytion key" (WDEK). I have
looked at this one the least as I was focused on the pages, but this is
also one where we can use a HKDF. However, I believe the difference is
we need to use the **MDEK** here to generate the WDEK, which would include:

        a) A cryptographically-secure randomly generated salt of 64
bytes[6] (Section 5; Table 4) that is created when the **cluster** is
initialized.
        b) A counter bit string, that is incremented each time we've
encrypted more than 64GB of WAL. (More in key management)

One open question is do we allow users to explicitly add their own DDEK
or TDEK? I would say yes for DDEK (given it would be stored anyway), no
for TDEK for now, if only because of the potential desire to
automatically rotate the TDEK once 64GB of data has been encrypted by
the same TDEK (more on this in a bit). It seems, if we follow the SQL
Server example, the level of granularity it goes down to is the
database, so we may be safe there. Redwood city lets you select the
tablespace to use TDE, so we can be mindful of that as well.

I well understand the nuances are in the details, but this should build
on good practices started by other RDBMS, guidelines recommended by
NIST, and provide something that is relatively easy for our users to
implement.

#2 Encryption/Decryption of Pages + WAL Records
-----------------------------------------------

To me there are two goals with this part:

    1. Ensure confidentiality of the data, in particular, if the storage
device is removed
    2. Ensure the integrity of the data should someone tamper with the
ciphertext

The other thing to consider (per this discussion) is doing so in a way
where we can add minimal overhead to PostgreSQL, both in terms of
additional storage. (Though if you want TDE you are already accepting
some overhead.)

With that all said, I first wanted to understand what other RDBMS did
(surprise) for their encryption algorithms and modes.

I had a bit of trouble trying to figure out what SQL Server did, but if
this[7] is still accurate, it's a combination of AES-{128,192,256} and
the CBC mode. They provide an integrity check with a SHA1 hash.

With redwood city, they appear to support a few algorithms and modes[9]
and I did not narrow down which is used by default[10]. They list out
CBC, CFB, ECB, and OFB, and also indicate they use a SHA1 hash to
provide the integrity check.

This lead me to an exploration of all the different modes we could
utilize and what would make the most sense for PostgreSQL. Depending on
what you read, you could easily get your head (or your drives)
spinning[11][12][13][14].

However, while reading all of these, a few themes emerged:

    1. Unless you're using authentication encryption[15], your data is
subject to tampering (This has been brought up on the list).

    2. All of  modes have tradeoffs given they are focused on
performance of encrypting/decrypting

    3. If you're using anything with counters, you MUST choose a secure
counting method and NEVER repeat the counter with the same key. This has
also been brought up on the list.

(It goes without saying, but we also want to ensure we use something
that is in the clear on intellectual property, which you could run into
with some modes)

No matter which mode we choose, we will have to store an integrity
check, and the exploration I began was how we could do this while taking
up the least amount of space. But we _cannot_ compromise on the
integrity check, otherwise any encrypted data is subject to tampering. I
know I read this in one of the guidelines somewhere, but I can't seem to
find it at the moment I'm typing this.

I believe the mode we are looking for is GCM[16] which has similar
benefits to CTR (parallelism) but allows for an integrity check (via its
GMAC). And if you follow NIST 800-38d[17] (below for convenience), you
can actually create IVs deterministically! It also has the added benefit
where you can associate additional, unencrypted data as part of the
decryption process (a nice example of how to do so in the Python
cryptography library[18]).

Here are the guidelines:
https://csrc.nist.gov/publications/detail/sp/800-38d/final

A lot of what we are concerned about is in Section 8. For IV construction:

    - The size they recommend is 96 bits. If the IV is 96 bits or less,
there is guidance for deterministic construction (Sec 8.2.1)
    - An IV can have a "fixed" field and an "invocation" field. The
"fixed" field could be something like the relation OID (32-bits). The
invocation field could be the page number (32-bits). 64-bit IVs are okay
-- they recommend 96 bits.

    ^ The beauty of this is if we decide we are ok with 64-bit IVs, we
do not need to store any additional data :) If we want 96 bits we'd like
need to store a 4 byte "salt" if you will. Or we could leave it blank in
case we ever have more than 2**32 pages (*hides*).

    - You cannot repeat an IV for a given key.

    - Part of the output of the encryption is a tag, which can be
anywhere from 32 - 128 bits in length. The recommendation is to use 128
bits. This would then be the only additional storage we need. (If
smaller tags are to be considered, there are limits to the number of
invocations of the decryption function).

This means at most we only need to add 8 additional bytes of storage on
the pages + WAL headers.

However, even if we were to use a 96-bit IVs with 64-bits variable
space, we would need to eventually rotate keys (esp. WAL). As such:

# 3. Automated Key Rotation
---------------------------

If need to rotate an encryption key once we are at the point where a
counter will reset, that means we also need to know which key encrypted
the function. If we decide that 64GB is our magic number, based on some
back of the envelope calculation with an assist from a nice blog post by
Simon[19], I determined that each relation would require a maximum of
512 keys (32TB / 64GB). Using the HKDF method of deriving keys, we can
just use a byte to store which "key" should be used to encrypt/decrypt a
particular page. I believe would be similar for WAL.

BUT...AIUI that 64GB limit is for a key/IV pair. If each page has its
own IV and our counter is tied to the total number of pages available in
a relation, do we need to rotate that key? (I apologize at this point,
I've been writing this note for a couple of hours and my brain is
getting a bit mushy, so I'm fine with being told I'm way off base).

WDEK is a different, as we know the counter can wrap. As such, we likely
need to keep some length of a "key id" (4 bytes?) on the WAL to know
which WDEK was used. The key id would be passed in as part of the
"counter bit string" to the HKDF. We want to make it large enough that
the probability of wrapping around is low, and we are within the
guidelines for using a HKDF properly.

---------------

At this point I'm running out of steam as I type this. I realize overall
there is a lot to consider.

I'll also make the suggestion again that perhaps the people who are
interested in working on TDE have a discussion/meetup etc. to iron out
details.

If we can build a system that's architecturally sound (read: no one can
file a CVE on the architecture), adheres to guidelines for teams that
opt to utilize things like TDE, stores data confidentially and with
integrity (where we can detect tampering), and is easy(-ish) to use,
we'll be in a good position :)

Thanks,

Jonathan

[0] https://cryptography.io/en/latest/
[1]

https://docs.microsoft.com/en-us/sql/relational-databases/security/encryption/transparent-data-encryption?view=sql-server-2017
[2]
https://www.2ndquadrant.com/en/blog/postgresql-passphrase-protected-ssl-keys-systemd/
[3]
https://docs.oracle.com/database/121/ASOAG/introduction-to-transparent-data-encryption.htm#ASOAG10139
[4]
https://www.postgresql.org/message-id/c878de71-a0c3-96b2-3e11-9ac2c35357c3%40joeconway.com
[5] https://tools.ietf.org/html/rfc5649.html
[6] https://csrc.nist.gov/publications/detail/sp/800-56c/rev-1/final
[7]
https://blogs.msdn.microsoft.com/sqlsecurity/2009/03/30/sql-server-encryptbykey-cryptographic-message-description/
[8]
https://docs.oracle.com/cd/B19306_01/network.102/b14268/asotrans.htm#BABGHIDE
[9]
https://docs.oracle.com/database/121/DBSEG/data_encryption.htm#DBSEG80084
[10]
https://docs.oracle.com/cd/B19306_01/network.102/b14268/asotrans.htm#BABHJCHD
[11]
https://www.daemonology.net/blog/2009-06-11-cryptographic-right-answers.html
[12]
https://www.kernel.org/doc/html/latest/filesystems/fscrypt.html#encryption-modes-and-usage
[13] https://sockpuppet.org/blog/2014/04/30/you-dont-want-xts/
[14]
https://crypto.stackexchange.com/questions/14628/why-do-we-use-xts-over-ctr-for-disk-encryption
[15] https://en.wikipedia.org/wiki/Authenticated_encryption
[16] https://en.wikipedia.org/wiki/Galois/Counter_Mode
[17] https://csrc.nist.gov/publications/detail/sp/800-38d/final
[18]

https://cryptography.io/en/latest/hazmat/primitives/symmetric-encryption/#cryptography.hazmat.primitives.ciphers.modes.GCM
[19] https://www.2ndquadrant.com/en/blog/postgresql-maximum-table-size/


Attachment

pgsql-hackers by date:

Previous
From: Stephen Frost
Date:
Subject: Re: [PATCH] Stop ALTER SYSTEM from making bad assumptions
Next
From: Alvaro Herrera
Date:
Subject: Re: Problem with default partition pruning