Page MenuHome GnuPG

libgcrypt: AEAD API for FIPS 140 (in future)
Open, NormalPublic

Description

In libgcrypt 1.10, there are no AEAD available when FIPS mode.
It's good if we can improve this situation.

In SP 800-38D, there is a chapter "8 Uniqueness Requirement on IVs and Keys ".
It says:

The probability that the authenticated encryption function ever will be invoked with the same IV and the same key on two (or more) distinct sets of input data shall be no greater than 2^-32.

To conform this, I think that we need a set of API for encryption which does:

  • accepts an argument (or more) to specify salt/seed/additional data to open
  • generates key and iv internally, on each invocation of encrypt, updating and returning a counter, key, iv, encrypted result, and tag
    • or offers routines
      • feeds a part of chunk to the encryption engine, possibly, per chunk additional data
      • accesses counter, key and iv
      • finishes one chunk
      • accesses encrypted data, and tag
  • close and get the final tag

This makes auditor happy, because the library can ensure the condition of (key, iv).

Users will be happy, too, if the set of API is good enough for their use cases, because they can rely on the library for correctness (despite giving up more detailed control).

Questions here are:

  • there are multiple practices for methods
    • to generate initial key and iv
      • HKDF (TLS 1.3)
      • HASH (SSH)
      • hash algo should be selected for key size or not
    • to update following iv-s
      • RFC5116: random part + counter (start from zero)
      • TLS 1.3: random part XORed counter (start from zero)
      • SSH: random, incrementing lower octets only (not whole iv)
  • Is it OK for library users to have no flexibility but only selecting from existing methods?

Event Timeline

gniibe triaged this task as Unbreak Now! priority.Mar 3 2022, 1:13 AM
gniibe created this task.
werner lowered the priority of this task from Unbreak Now! to Normal.Mar 3 2022, 9:40 AM
werner added a project: Feature Request.
werner added a subscriber: werner.

I don't think it is justified to tag this as "unbreak now" - which we use for severe bugs inhibiting the use of a deployed version.

I think this is not urgent as we are able to FIPS certify libgcrypt without that, but the modern protocols and algorithm use this and if we want to use libgcrypt to implement these in FIPS compliant way, we certainly need something like that.

I am not sure what is difference from the T4873, which discusses the same issue, which we discussed last year (but is missing the fips tag for some reason).

Is large change to cipher API really needed (new open/encrypt with less flexibility)? How that would affect performance? Would following new interfaces to gcry_cipher API work instead?

  • gcry_cipher_setup_geniv(hd, int ivlen, int method): for setting up IV generator with parameters such as IV length, method id (RFC5116, TLS 1.3, SSH, etc), (other parameters?)
  • gcry_cipher_geniv(hd, byte *outiv): for generating new iv: generate IV using select method, set IV internally and output generated IV to 'ivout'.
  • gcry_cipher_genkey(hd, byte *outkey, int keylen, int method): for generating keys, generate key internally with parameters (method id, other?), setup key internally and output generated key to 'outkey'. (how keys from key exchange protocol be handled? using existing setkey?)

AEAD encryption with current libgcrypt could look something like this:

byte key_buf[16]; <- generated by user (could be generated through key exchange etc)

// Open handle
gcry_cipher_open(&hd, GCRY_CIPHER_MODE_OCB, GCRY_CIPHER_AES, 0);
// Set key
gcry_cipher_setkey(hd, key_buf, 16);

byte message1_iv[16]; <- generated by user, IV for first message
byte message1_data[100]; <- first message to encrypt
byte message1_ad[4]; <- additional data to authenticate for first message
byte message1_encrypted[100];
byte message1_tag[16];

// Set IV for 1st message
gcry_cipher_setiv(hd, message1_iv, 16);
// authenticate additional data for 1st message 
gcry_cipher_authenticate(hd, message1_ad, sizeof(message1_ad));
// mark next buffer as last for message (needed in case of OCB)
gcry_cipher_final(hd);
// encrypt data for 1st message
gcry_cipher_encrypt(hd, message1_encrypted, sizeof(message1_encrypted), message1_data, sizeof(message1_data));
// get tag for 1st message
gcry_cipher_gettag(hd, message1_tag, 16);

// reset handle for next message. same key used.
gcry_cipher_reset(hd)

byte message2_iv[16]; <- generated by user, IV for second message
byte message2_data_part1[100]; <- first part of second message to encrypt
byte message2_data_part2[50]; <- last part of second message to encrypt
byte message2_ad[4]; <- additional data to authenticate for second message
byte message2_encrypted1[100];
byte message2_encrypted2[50];
byte message2_tag[16];

// Set IV for 2nd message
gcry_cipher_setiv(hd, message2_iv, 16);
// authenticate additional data for 2nd message 
gcry_cipher_authenticate(hd, message2_ad, sizeof(message2_ad));
// encrypt data for 2nd message
gcry_cipher_encrypt(hd, message2_encrypted1, sizeof(message2_encrypted1), message2_data_part1, sizeof(message2_data_part1));
// mark next buffer as last for message (needed in case of OCB)
gcry_cipher_final(hd);
// encrypt data for 2nd message
gcry_cipher_encrypt(hd, message2_encrypted2, sizeof(message2_encrypted2), message2_data_part2, sizeof(message2_data_part2));
// get tag for 2nd message
gcry_cipher_gettag(hd, message2_tag, 16);

// reset handle for next message. same key used.
gcry_cipher_reset(hd)
// more messages encrypted using same key ...
// ...

// close handle, for example after connection closed
gcry_cipher_close(hd)

Other AEAD modes have slight differences how to use. For example, CCM requires that aad and plaintext lengths are given to cipher handle before first cry_cipher_authenticate/gcry_cipher_encrypt of message.

With new geniv API above example would look like this:

byte key_buf[16];

// Open handle
gcry_cipher_open(&hd, GCRY_CIPHER_MODE_OCB, GCRY_CIPHER_AES, 0);
// Generate key
gcry_cipher_genkey(hd, key_buf, 16, GCRY_CIPHER_GENKEY_METHOD_???);
// Setup IV generator, IV length: 16 bytes
gcry_cipher_setup_geniv(hd, 16, GCRY_CIPHER_GENIV_METHOD_???);

byte message1_iv[16];
byte message1_data[100]; <- first message to encrypt
byte message1_ad[4]; <- additional data to authenticate for first message
byte message1_encrypted[100];
byte message1_tag[16];

// Generate IV for 1st message (16 bytes)
gcry_cipher_geniv(hd, message1_iv);
// authenticate additional data for 1st message 
gcry_cipher_authenticate(hd, message1_ad, sizeof(message1_ad));
// mark next buffer as last for message (needed in case of OCB)
gcry_cipher_final(hd);
// encrypt data for 1st message
gcry_cipher_encrypt(hd, message1_encrypted, sizeof(message1_encrypted), message1_data, sizeof(message1_data));
// get tag for 1st message
gcry_cipher_gettag(hd, message1_tag, 16);

// reset handle for next message. same key used.
gcry_cipher_reset(hd)

byte message2_iv[16];
byte message2_data_part1[100]; <- first part of second message to encrypt
byte message2_data_part2[50]; <- last part of second message to encrypt
byte message2_ad[4]; <- additional data to authenticate for second message
byte message2_encrypted1[100];
byte message2_encrypted2[50];
byte message2_tag[16];

// Generate IV for 2nd message (16 bytes)
gcry_cipher_getiv(hd, message2_iv);
// authenticate additional data for 2nd message 
gcry_cipher_authenticate(hd, message2_ad, sizeof(message2_ad));
// encrypt data for 2nd message
gcry_cipher_encrypt(hd, message2_encrypted1, sizeof(message2_encrypted1), message2_data_part1, sizeof(message2_data_part1));
// mark next buffer as last for message (needed in case of OCB)
gcry_cipher_final(hd);
// encrypt data for 2nd message
gcry_cipher_encrypt(hd, message2_encrypted2, sizeof(message2_encrypted2), message2_data_part2, sizeof(message2_data_part2));
// get tag for 2nd message
gcry_cipher_gettag(hd, message2_tag, 16);

// reset handle for next message. same key used.
gcry_cipher_reset(hd)
// more messages encrypted using same key ...
// ...

// close handle, for example after connection closed
gcry_cipher_close(hd)

You are combining two concepts here -- the KDF and the AEAD cipher itself (at least from the FIPS terminology). I would like to avoid mixing these two together in the new API. If you would like to implement the SSH/TLS KDF, I would suggest to use the kdf API you already have. Then we are here left only with a new geniv API to implement. In the T4873 I mentioned example how it is now used in libssh using libgcrypt, which implements the iv increment outside of the libgcrypt:

https://gitlab.com/libssh/libssh-mirror/-/blob/master/src/libgcrypt.c#L418

It is a question if the IV increment should be tied to specific kdf or it should be more generic as in OpenSSL, which consists of two operations:

I had thought that we need to combine hkdf so that key and iv can generate within libgcrypt internally.
Probably, this assumption of mine may be wrong.

I was considering implementing TLS 1.3 using libgcrypt with FIPS-mode enabled. Like NTBTLS which we use for GnuPG Project (for Windows).

For SSH, I think that the Implementation Guidance for FIPS 140-3... specifically addressed that the method of SSH is acceptable.

Let me evaluate possible use case in future NTBTLS for TLS 1.3. I'll be back.

TLS 1.3 requires much changes for NTBTLS.

Before the change for TLS 1.3, I reviewed TLS 1.2 (already supported by NTBTLS) and SSH, which use fixed-random-part + incrementing counter. TLS 1.2 and SSH uses AES GCM (only). That's in T4873.

I put T4873 as subtask of this ticket. It handles the case of AES GCM.