257 lines
12 KiB
Markdown
257 lines
12 KiB
Markdown
|
|
# wolfSSL KeyStore (WKS) Design Notes
|
|
|
|
The WKS KeyStore format was designed to be compatible with wolfCrypt FIPS
|
|
140-2 and 140-3, meaning it utilizes FIPS validated cryptographic algorithms.
|
|
This document includes notes on the design and algorithm choices used by WKS.
|
|
For details on the wolfCrypt FIPS 140-2/3 cryptographic module and boundary,
|
|
please reference the appropriate Security Policy or contact fips@wolfssl.com.
|
|
|
|
## User Customizable Properties
|
|
|
|
| Security Property | Default | Min | Description |
|
|
| --- | --- | --- | --- |
|
|
| `wolfjce.wks.iterationCount` | 210,000 | 10,000 | PBKDF2 iteration count |
|
|
| `wolfjce.wks.maxCertChainLength` | 100 | N/A | Max cert chain length |
|
|
|
|
## Notes on Algorithm and Security Properties
|
|
|
|
PBKDF2-HMAC-SHA512 was chosen over PBKDF2-HMAC-SHA256 for AES and HMAC key
|
|
generation to allow use of fewer iterations (210,000, as per current
|
|
[OWASP recommendations](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2)) versus the recommended 600,000 for SHA-256.
|
|
|
|
PBKDF2 salt size of 128-bits (16 bytes) is used to follow recommendations
|
|
in [NIST SP 800-132, Page 6](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-132.pdf).
|
|
|
|
AES-CBC (AES/CBC/PKCS5Padding) was chosen over AES-GCM since AES-GCM requires
|
|
that each {key,nonce} combination be unique. Simply generating a random nonce
|
|
via RNG does not guarantee uniqueness, and we have no way of maintaining an
|
|
accurate counter across KeyStore objects and store/load operations.
|
|
|
|
Different keys are used for PrivateKey/SecretKey encryption and HMAC, and
|
|
derived from one larger PBKDF2 operation (96 bytes) then split between
|
|
encryption (32-byte key) and HMAC (64-byte key) operations. A
|
|
random salt is generated for each PBKDF2 key generation operation.
|
|
|
|
HMAC values are calculated over content but also the PBKDF2 salt length,
|
|
salt, and iteration count, and all other key parameters (ex: IV and length) to
|
|
also include those in the integrity check.
|
|
|
|
## KeyStore Integrity
|
|
|
|
### HMAC Generation During KeyStore Storage
|
|
|
|
When WKS KeyStore objects are stored (`engineStore()`), the following format
|
|
is used. This is composed of a *HEADER* section, an *ENTRIES* section, followed
|
|
lastly by an HMAC generated over the *HEADER* and *ENTRIES*, including the
|
|
PBKDF2 salt, salt length, and iteration count.
|
|
|
|
The *HEADER* includes a magic number specific to the WKS KeyStore type (`7`), a
|
|
WKS KeyStore version (may be incremented in the future as features are added
|
|
or if the WKS type definition changes), and a count of the entries included in
|
|
the store.
|
|
|
|
The *ENTRIES* section is made up of one or more `WKSPrivateKey`,
|
|
`WKSSecretKey`, or `WKSCertificate` entries. These represent the storage of
|
|
a `PrivateKey`, `SecretKey`, and `Certificate` objects respectively.
|
|
|
|
Generation of the HMAC happens during a call to
|
|
`engineStore(OutputStream stream, char[] password)` and is generated in the
|
|
following manner:
|
|
|
|
- Input password must not be null or zero length
|
|
- Input password is converted from `char[]` into `byte[]` using password
|
|
conversion algorithm described below.
|
|
- Random salt of size `WKS_PBKDF2_SALT_SIZE` (128 bits) is generated
|
|
- HMAC-SHA512 key (64-bytes) is generated with PBKDF2-HMAC-SHA512 using:
|
|
+ Password byte array
|
|
+ Random 16-byte salt (`WKS_PBKDF2_SALT_SIZE`)
|
|
+ 210,000 iterations (`WKS_PBKDF2_ITERATION_COUNT`), but can be overriden
|
|
by user by setting `wolfjce.wks.iterationCount` Security property.
|
|
Minimum iteration count is 10,000.
|
|
- The final HMAC-SHA512 is calculated using the derived key over the bytes of
|
|
*HEADER*, *ENTRIES*, salt length, salt, and iteration count. It is then
|
|
written out to the OutputStream.
|
|
|
|
### HMAC Verification During KeyStore Load
|
|
|
|
When a WKS KeyStore is loaded with
|
|
`engineLoad(InputStream stream, char[] password)`, the input password is
|
|
optional. If a password is provided, the KeyStore integrity will be checked
|
|
using the included HMAC, otherwise the integrity check will be skipped.
|
|
This design is to maintain consistency with how the Java JKS format handles
|
|
integrity checks upon KeyStore load, and allows for easy conversion and use
|
|
of files such as `cacerts` to a WKS type where users do not normally provide
|
|
the password when loading the KeyStore file.
|
|
|
|
Since the HMAC is stored at the end of the KeyStore stream, `engineLoad()`
|
|
buffers KeyStore bytes as they are read in, up to and including the PBKDF2
|
|
salt size, salt, and PBKDF2 iteration count. Once all entries have been read,
|
|
the HMAC is read and verified:
|
|
- The salt length is read, sanitized against `WKS_PBKDF2_SALT_SIZE`
|
|
- The salt is read
|
|
- The PBKDF2 iteration count is read, and checked against min size of
|
|
`WKS_PBKDF2_MIN_ITERATIONS`
|
|
- Caching of data is paused while the HMAC is read in next
|
|
- The original HMAC length is read
|
|
- An HMAC-SHA512 is regenerated over the buffered header and entry bytes
|
|
+ Password is converted from char[] to byte[] as explained below
|
|
+ An HMAC-SHA512 key (64-bytes) is calculated as explained above, using
|
|
salt that was read from input KeyStore stream
|
|
+ The generated HMAC value is calculated using this key
|
|
- The generated HMAC is compared in both size and contents against the stored
|
|
HMAC. If these are different, an IOException is thrown.
|
|
|
|
### Stored WKS format:
|
|
|
|
```
|
|
* HEADER:
|
|
* magicNumber (int / 7)
|
|
* keystoreVersion (int)
|
|
* entryCount (int)
|
|
* ENTRIES (can be any of below, depending on type)
|
|
* [WKSPrivateKey]
|
|
* entryId (int / 1)
|
|
* alias (UTF String)
|
|
* creationDate.getTime() (long)
|
|
* kdfSalt.length (int)
|
|
* kdfSalt (byte[])
|
|
* kdfIterations (int)
|
|
* iv.length (int)
|
|
* iv (byte[])
|
|
* encryptedKey.length (int)
|
|
* encryptedKey (byte[])
|
|
* chain.length (int)
|
|
* FOR EACH CERT:
|
|
* chain[i].getType() (UTF String)
|
|
* chain[i].getEncoded().length (int)
|
|
* chain[i].getEncoced() (byte[])
|
|
* hmac.length (int)
|
|
* hmac (HMAC-SHA512) (byte[])
|
|
* [WKSSecretKey]
|
|
* entryId (int / 3)
|
|
* alias (UTF String)
|
|
* creationDate.getTime() (long)
|
|
* key.getAlgorithm() (UTF String)
|
|
* kdfSalt.length (int)
|
|
* kdfIterations (int)
|
|
* kdfSalt (byte[])
|
|
* iv.length (int)
|
|
* iv (byte[])
|
|
* encryptedKey.length (int)
|
|
* encryptedKey (byte[])
|
|
* hmac.length (int)
|
|
* hmac (HMAC-SHA512) (byte[])
|
|
* [WKSCertificate]
|
|
* entryId (int / 2)
|
|
* alias (UTF String)
|
|
* creationDate.getTime() (long)
|
|
* cert.getType() (UTF String)
|
|
* cert.getEncoded().length (int)
|
|
* cert.getEncoced() (byte[])
|
|
* HMAC PBKDF2 salt length int
|
|
* HMAC PBKDF2 salt (byte[])
|
|
* HMAC PBKDF2 iterations int
|
|
* HMAC length int
|
|
* HMAC (HMAC-SHA512) (byte[])
|
|
```
|
|
|
|
## PrivateKey Protection
|
|
|
|
A PrivateKey entry is stored into the KeyStore with the `engineSetKeyEntry()`
|
|
method, exposed publicly through `KeyStore` as `setKeyEntry()`, when
|
|
passing in a `Key` of type `PrivateKey`. The password argument is not allowed
|
|
to be null, otherwise a KeyStoreException is thrown.
|
|
|
|
```
|
|
void setKeyEntry(String alias, Key key, char[] password, Certificate[] chain)
|
|
```
|
|
|
|
Process of storing a PrivateKey is as follows:
|
|
- Sanity check the certificate chain:
|
|
+ Chain is not null or zero length chain
|
|
+ Chain is made up of X509Certificate objects
|
|
+ Chain cert signatures are correct as we walk up the chain. The cert
|
|
chain should be ordered from leaf cert (entity) to top-most intermedate
|
|
certificate. The last cert is loaded as a trusted root, and used to
|
|
verify the rest of the chain, since we don't have the root CA cert
|
|
available at this point.
|
|
- Verify private key (`Key key`) matches the leaf certificate (`chain[0]`)
|
|
- Encrypt private key before storing into KeyStore map:
|
|
+ Generate random PBKDF2 salt, of size `WKS_PBKDF2_SALT_SIZE` bytes
|
|
+ Generate random IV, of size `WKS_ENC_IV_LENGTH` bytes
|
|
+ Convert password from char[] into byte[] using password conversion
|
|
algorithm described below.
|
|
+ Encryption key is derived using PBKDF2-SHA256 using byte array, random
|
|
salt, and `WKS_PBKDF2_ITERATION_COUNT` (or customized) iteration count.
|
|
- 96-byte key is generated with PBKDF2 in total, split between 32-byte
|
|
AES-CBC-256 and 64-byte HMAC-SHA512 keys.
|
|
+ Encrypt key bytes using AES-CBC-256
|
|
+ Generate HMAC-SHA512 over encrypted key and other WKSPrivateKey
|
|
object members
|
|
+ Zeroize KEK and HMAC keys (generated from PBKDF2)
|
|
|
|
When importing a PrivateKey from a KeyStore stream, the process is reversed.
|
|
Initially during `engineLoad()`, parameters are read in as well as the encrypted
|
|
key:
|
|
- Read PBKDF2 salt length, sanity check against `WKS_PBKDF2_SALT_SIZE`
|
|
- Read PBKDF2 salt
|
|
- Read PBKDF2 iterations, sanity check against `WKS_PBKDF2_MIN_ITERATIONS`
|
|
- Read encryption IV, santiy check against `WKS_ENC_IV_LENGTH`
|
|
- Read encrypted key
|
|
- Read certificate chain if present, check length against `WKS_MAX_CHAIN_COUNT`
|
|
- Read HMAC value into object variable, will be checked when user gets key out
|
|
|
|
The PrivateKey is stored encrypted internal to the WolfSSLKeyStore until
|
|
a caller retrieves it with `getKey()`. At that point, WolfSSLKeyStore:
|
|
- Derives the decryption key using PBKDF2-SHA256
|
|
+ Converts password from `char[]` to `byte[]` using algorithm below
|
|
+ Uses salt and iteration count stored internally from encryption
|
|
process or read from KeyStore stream after loading
|
|
+ Derives decryption key and HMAC key with PBKDF2-HMAC-SHA512
|
|
+ Regenerate and verify HMAC against stored value
|
|
+ Decrypts key using AES-CBC-256
|
|
+ Zeroizes KEK and HMAC keys (generated from PBKDF2)
|
|
|
|
## SecretKey Protection
|
|
|
|
A SecretKey entry is stored into the KeyStore with the `engineSetKeyEntry()`
|
|
method, exposed publicly through `KeyStore` as `setKeyEntry()`, when
|
|
passing in a `Key` of type `SecretKey`. The password argument is not allowed
|
|
to be null, otherwise a KeyStoreException is thrown.
|
|
|
|
```
|
|
void setKeyEntry(String alias, Key key, char[] password, Certificate[] chain)
|
|
```
|
|
|
|
Process of storing a SecretKey is the same as PrivateKey above, except
|
|
there is no certificate so no certifiate or private key sanity checks are done.
|
|
The same encrypt/decrypt process is shared between PrivateKey and SecretKey
|
|
protection.
|
|
|
|
## Certificate Protection
|
|
|
|
A Certificate entry is stored into the KeyStore with the
|
|
`engineSetCertificateEntry()` method. Certificate entries are not protected
|
|
and are stored directly into the KeyStore.
|
|
|
|
They are integrity protected by the KeyStore HMAC when a KeyStore is written
|
|
out to a stream with `engineStore()`, but otherwise have no internal
|
|
encryption or integrity protection since no password is provided when storing
|
|
certificates.
|
|
|
|
## Password Conversion Algorithm
|
|
|
|
The Java KeyStore class specifies that passwords are provided by the user as a
|
|
Java character array (`char[]`). Before using a password as input to PBKDF2,
|
|
wolfJCE is converts it into a byte array. In Java, one character (`char`) is
|
|
composed of two bytes (`byte`). RFC 2898 (PBKDF2) considers a password to be an
|
|
octet string and recommends for interop ASCII or UTF-8 encoding be used. SunJCE
|
|
uses UTF-8 for PBKDF2 SecretKeyFactory, so we do the same in WolfSSLKeyStore
|
|
using `WolfCryptSecretKeyFactory.passwordToByteArray(char[])`.
|
|
|
|
# Support
|
|
|
|
Please email support@wolfssl.com with any questions.
|
|
|