crypto: add support for OCB mode for AEAD

PR-URL: https://github.com/nodejs/node/pull/21447
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Tobias Nießen 2018-06-14 15:18:14 +02:00
parent b75bde3bc5
commit b3f459e6cf
No known key found for this signature in database
GPG Key ID: 718207F8FD156B70
3 changed files with 290 additions and 53 deletions

View File

@ -249,11 +249,11 @@ added: v1.0.0
- `plaintextLength` {number}
* Returns: {Cipher} for method chaining.
When using an authenticated encryption mode (only `GCM` and `CCM` are currently
supported), the `cipher.setAAD()` method sets the value used for the
When using an authenticated encryption mode (`GCM`, `CCM` and `OCB` are
currently supported), the `cipher.setAAD()` method sets the value used for the
_additional authenticated data_ (AAD) input parameter.
The `options` argument is optional for `GCM`. When using `CCM`, the
The `options` argument is optional for `GCM` and `OCB`. When using `CCM`, the
`plaintextLength` option must be specified and its value must match the length
of the plaintext in bytes. See [CCM mode][].
@ -263,8 +263,8 @@ The `cipher.setAAD()` method must be called before [`cipher.update()`][].
<!-- YAML
added: v1.0.0
-->
* Returns: {Buffer} When using an authenticated encryption mode (only `GCM` and
`CCM` are currently supported), the `cipher.getAuthTag()` method returns a
* Returns: {Buffer} When using an authenticated encryption mode (`GCM`, `CCM`
and `OCB` are currently supported), the `cipher.getAuthTag()` method returns a
[`Buffer`][] containing the _authentication tag_ that has been computed from
the given data.
@ -412,8 +412,8 @@ changes:
- `plaintextLength` {number}
* Returns: {Decipher} for method chaining.
When using an authenticated encryption mode (only `GCM` and `CCM` are currently
supported), the `decipher.setAAD()` method sets the value used for the
When using an authenticated encryption mode (`GCM`, `CCM` and `OCB` are
currently supported), the `decipher.setAAD()` method sets the value used for the
_additional authenticated data_ (AAD) input parameter.
The `options` argument is optional for `GCM`. When using `CCM`, the
@ -436,8 +436,8 @@ changes:
* `buffer` {Buffer | TypedArray | DataView}
* Returns: {Decipher} for method chaining.
When using an authenticated encryption mode (only `GCM` and `CCM` are currently
supported), the `decipher.setAuthTag()` method is used to pass in the
When using an authenticated encryption mode (`GCM`, `CCM` and `OCB` are
currently supported), the `decipher.setAuthTag()` method is used to pass in the
received _authentication tag_. If no tag is provided, or if the cipher text
has been tampered with, [`decipher.final()`][] will throw, indicating that the
cipher text should be discarded due to failed authentication. If the tag length
@ -1321,6 +1321,9 @@ This property is deprecated. Please use `crypto.setFips()` and
added: v0.1.94
deprecated: v10.0.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/21447
description: Ciphers in OCB mode are now supported.
- version: v10.2.0
pr-url: https://github.com/nodejs/node/pull/20235
description: The `authTagLength` option can now be used to produce shorter
@ -1338,7 +1341,7 @@ Creates and returns a `Cipher` object that uses the given `algorithm` and
`password`.
The `options` argument controls stream behavior and is optional except when a
cipher in CCM mode is used (e.g. `'aes-128-ccm'`). In that case, the
cipher in CCM or OCB mode is used (e.g. `'aes-128-ccm'`). In that case, the
`authTagLength` option is required and specifies the length of the
authentication tag in bytes, see [CCM mode][]. In GCM mode, the `authTagLength`
option is not required but can be used to set the length of the authentication
@ -1373,6 +1376,9 @@ Adversaries][] for details.
<!-- YAML
added: v0.1.94
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/21447
description: Ciphers in OCB mode are now supported.
- version: v10.2.0
pr-url: https://github.com/nodejs/node/pull/20235
description: The `authTagLength` option can now be used to produce shorter
@ -1392,7 +1398,7 @@ Creates and returns a `Cipher` object, with the given `algorithm`, `key` and
initialization vector (`iv`).
The `options` argument controls stream behavior and is optional except when a
cipher in CCM mode is used (e.g. `'aes-128-ccm'`). In that case, the
cipher in CCM or OCB mode is used (e.g. `'aes-128-ccm'`). In that case, the
`authTagLength` option is required and specifies the length of the
authentication tag in bytes, see [CCM mode][]. In GCM mode, the `authTagLength`
option is not required but can be used to set the length of the authentication
@ -1419,6 +1425,10 @@ of time what a given IV will be.
<!-- YAML
added: v0.1.94
deprecated: v10.0.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/21447
description: Ciphers in OCB mode are now supported.
-->
> Stability: 0 - Deprecated: Use [`crypto.createDecipheriv()`][] instead.
@ -1432,7 +1442,7 @@ Creates and returns a `Decipher` object that uses the given `algorithm` and
`password` (key).
The `options` argument controls stream behavior and is optional except when a
cipher in CCM mode is used (e.g. `'aes-128-ccm'`). In that case, the
cipher in CCM or OCB mode is used (e.g. `'aes-128-ccm'`). In that case, the
`authTagLength` option is required and specifies the length of the
authentication tag in bytes, see [CCM mode][].
@ -1452,6 +1462,9 @@ to create the `Decipher` object.
<!-- YAML
added: v0.1.94
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/21447
description: Ciphers in OCB mode are now supported.
- version: v10.2.0
pr-url: https://github.com/nodejs/node/pull/20039
description: The `authTagLength` option can now be used to restrict accepted
@ -1471,7 +1484,7 @@ Creates and returns a `Decipher` object that uses the given `algorithm`, `key`
and initialization vector (`iv`).
The `options` argument controls stream behavior and is optional except when a
cipher in CCM mode is used (e.g. `'aes-128-ccm'`). In that case, the
cipher in CCM or OCB mode is used (e.g. `'aes-128-ccm'`). In that case, the
`authTagLength` option is required and specifies the length of the
authentication tag in bytes, see [CCM mode][]. In GCM mode, the `authTagLength`
option is not required but can be used to restrict accepted authentication tags
@ -2321,7 +2334,7 @@ See the reference for other recommendations and details.
### CCM mode
CCM is one of the two supported [AEAD algorithms][]. Applications which use this
CCM is one of the supported [AEAD algorithms][]. Applications which use this
mode must adhere to certain restrictions when using the cipher API:
- The authentication tag length must be specified during cipher creation by

View File

@ -2683,6 +2683,11 @@ void CipherBase::Init(const FunctionCallbackInfo<Value>& args) {
cipher->Init(*cipher_type, key_buf, key_buf_len, auth_tag_len);
}
static bool IsSupportedAuthenticatedMode(int mode) {
return mode == EVP_CIPH_CCM_MODE ||
mode == EVP_CIPH_GCM_MODE ||
mode == EVP_CIPH_OCB_MODE;
}
void CipherBase::InitIv(const char* cipher_type,
const char* key,
@ -2700,8 +2705,7 @@ void CipherBase::InitIv(const char* cipher_type,
const int expected_iv_len = EVP_CIPHER_iv_length(cipher);
const int mode = EVP_CIPHER_mode(cipher);
const bool is_gcm_mode = (EVP_CIPH_GCM_MODE == mode);
const bool is_ccm_mode = (EVP_CIPH_CCM_MODE == mode);
const bool is_authenticated_mode = IsSupportedAuthenticatedMode(mode);
const bool has_iv = iv_len >= 0;
// Throw if no IV was passed and the cipher requires an IV
@ -2712,7 +2716,7 @@ void CipherBase::InitIv(const char* cipher_type,
}
// Throw if an IV was passed which does not match the cipher's fixed IV length
if (!is_gcm_mode && !is_ccm_mode && has_iv && iv_len != expected_iv_len) {
if (!is_authenticated_mode && has_iv && iv_len != expected_iv_len) {
return env()->ThrowError("Invalid IV length");
}
@ -2728,7 +2732,7 @@ void CipherBase::InitIv(const char* cipher_type,
"Failed to initialize cipher");
}
if (IsAuthenticatedMode()) {
if (is_authenticated_mode) {
CHECK(has_iv);
if (!InitAuthenticated(cipher_type, iv_len, auth_tag_len))
return;
@ -2803,7 +2807,7 @@ bool CipherBase::InitAuthenticated(const char* cipher_type, int iv_len,
}
const int mode = EVP_CIPHER_CTX_mode(ctx_.get());
if (mode == EVP_CIPH_CCM_MODE) {
if (mode == EVP_CIPH_CCM_MODE || mode == EVP_CIPH_OCB_MODE) {
if (auth_tag_len == kNoAuthTagLength) {
char msg[128];
snprintf(msg, sizeof(msg), "authTagLength required for %s", cipher_type);
@ -2813,25 +2817,29 @@ bool CipherBase::InitAuthenticated(const char* cipher_type, int iv_len,
#ifdef NODE_FIPS_MODE
// TODO(tniessen) Support CCM decryption in FIPS mode
if (kind_ == kDecipher && FIPS_mode()) {
if (mode == EVP_CIPH_CCM_MODE && kind_ == kDecipher && FIPS_mode()) {
env()->ThrowError("CCM decryption not supported in FIPS mode");
return false;
}
#endif
if (!EVP_CIPHER_CTX_ctrl(ctx_.get(), EVP_CTRL_CCM_SET_TAG, auth_tag_len,
// Tell OpenSSL about the desired length.
if (!EVP_CIPHER_CTX_ctrl(ctx_.get(), EVP_CTRL_AEAD_SET_TAG, auth_tag_len,
nullptr)) {
env()->ThrowError("Invalid authentication tag length");
return false;
}
// Remember the given authentication tag length for later.
auth_tag_len_ = auth_tag_len;
// Restrict the message length to min(INT_MAX, 2^(8*(15-iv_len))-1) bytes.
CHECK(iv_len >= 7 && iv_len <= 13);
max_message_size_ = INT_MAX;
if (iv_len == 12) max_message_size_ = 16777215;
if (iv_len == 13) max_message_size_ = 65535;
if (mode == EVP_CIPH_CCM_MODE) {
// Restrict the message length to min(INT_MAX, 2^(8*(15-iv_len))-1) bytes.
CHECK(iv_len >= 7 && iv_len <= 13);
max_message_size_ = INT_MAX;
if (iv_len == 12) max_message_size_ = 16777215;
if (iv_len == 13) max_message_size_ = 65535;
}
} else {
CHECK_EQ(mode, EVP_CIPH_GCM_MODE);
@ -2870,7 +2878,7 @@ bool CipherBase::IsAuthenticatedMode() const {
// Check if this cipher operates in an AEAD mode that we support.
CHECK(ctx_);
const int mode = EVP_CIPHER_CTX_mode(ctx_.get());
return mode == EVP_CIPH_GCM_MODE || mode == EVP_CIPH_CCM_MODE;
return IsSupportedAuthenticatedMode(mode);
}
@ -2903,16 +2911,18 @@ void CipherBase::SetAuthTag(const FunctionCallbackInfo<Value>& args) {
return args.GetReturnValue().Set(false);
}
// Restrict GCM tag lengths according to NIST 800-38d, page 9.
unsigned int tag_len = Buffer::Length(args[0]);
const int mode = EVP_CIPHER_CTX_mode(cipher->ctx_.get());
bool is_valid;
if (mode == EVP_CIPH_GCM_MODE) {
// Restrict GCM tag lengths according to NIST 800-38d, page 9.
is_valid = (cipher->auth_tag_len_ == kNoAuthTagLength ||
cipher->auth_tag_len_ == tag_len) &&
IsValidGCMTagLength(tag_len);
} else {
CHECK_EQ(mode, EVP_CIPH_CCM_MODE);
// At this point, the tag length is already known and must match the
// length of the given authentication tag.
CHECK(mode == EVP_CIPH_CCM_MODE || mode == EVP_CIPH_OCB_MODE);
CHECK_NE(cipher->auth_tag_len_, kNoAuthTagLength);
is_valid = cipher->auth_tag_len_ == tag_len;
}
@ -3008,7 +3018,7 @@ CipherBase::UpdateResult CipherBase::Update(const char* data,
if (kind_ == kDecipher && IsAuthenticatedMode() && auth_tag_len_ > 0 &&
auth_tag_len_ != kNoAuthTagLength && !auth_tag_set_) {
CHECK(EVP_CIPHER_CTX_ctrl(ctx_.get(),
EVP_CTRL_GCM_SET_TAG,
EVP_CTRL_AEAD_SET_TAG,
auth_tag_len_,
reinterpret_cast<unsigned char*>(auth_tag_)));
auth_tag_set_ = true;
@ -3121,10 +3131,12 @@ bool CipherBase::Final(unsigned char** out, int* out_len) {
if (ok && kind_ == kCipher && IsAuthenticatedMode()) {
// In GCM mode, the authentication tag length can be specified in advance,
// but defaults to 16 bytes when encrypting. In CCM mode, it must always
// be given by the user.
if (mode == EVP_CIPH_GCM_MODE && auth_tag_len_ == kNoAuthTagLength)
// but defaults to 16 bytes when encrypting. In CCM and OCB mode, it must
// always be given by the user.
if (auth_tag_len_ == kNoAuthTagLength) {
CHECK(mode == EVP_CIPH_GCM_MODE);
auth_tag_len_ = sizeof(auth_tag_);
}
CHECK_EQ(1, EVP_CIPHER_CTX_ctrl(ctx_.get(), EVP_CTRL_AEAD_GET_TAG,
auth_tag_len_,
reinterpret_cast<unsigned char*>(auth_tag_)));

View File

@ -499,6 +499,207 @@ const TEST_CASES = [
tag: '65a6002b2cdfe9f00027f839332ca6fc',
tampered: false
},
// OCB test cases from RFC7253
{
algo: 'aes-128-ocb',
key: '000102030405060708090a0b0c0d0e0f',
iv: 'bbaa99887766554433221100',
plain: '',
ct: '',
tag: '785407bfffc8ad9edcc5520ac9111ee6'
},
{
algo: 'aes-128-ocb',
key: '000102030405060708090a0b0c0d0e0f',
iv: 'bbaa99887766554433221101',
plain: '0001020304050607',
plainIsHex: true,
aad: '0001020304050607',
ct: '6820b3657b6f615a',
tag: '5725bda0d3b4eb3a257c9af1f8f03009'
},
{
algo: 'aes-128-ocb',
key: '000102030405060708090a0b0c0d0e0f',
iv: 'bbaa99887766554433221102',
plain: '',
aad: '0001020304050607',
ct: '',
tag: '81017f8203f081277152fade694a0a00'
},
{
algo: 'aes-128-ocb',
key: '000102030405060708090a0b0c0d0e0f',
iv: 'bbaa99887766554433221103',
plain: '0001020304050607',
plainIsHex: true,
ct: '45dd69f8f5aae724',
tag: '14054cd1f35d82760b2cd00d2f99bfa9'
},
{
algo: 'aes-128-ocb',
key: '000102030405060708090a0b0c0d0e0f',
iv: 'bbaa99887766554433221104',
plain: '000102030405060708090a0b0c0d0e0f',
plainIsHex: true,
aad: '000102030405060708090a0b0c0d0e0f',
ct: '571d535b60b277188be5147170a9a22c',
tag: '3ad7a4ff3835b8c5701c1ccec8fc3358'
},
{
algo: 'aes-128-ocb',
key: '000102030405060708090a0b0c0d0e0f',
iv: 'bbaa99887766554433221105',
plain: '',
aad: '000102030405060708090a0b0c0d0e0f',
ct: '',
tag: '8cf761b6902ef764462ad86498ca6b97'
},
{
algo: 'aes-128-ocb',
key: '000102030405060708090a0b0c0d0e0f',
iv: 'bbaa99887766554433221106',
plain: '000102030405060708090a0b0c0d0e0f',
plainIsHex: true,
ct: '5ce88ec2e0692706a915c00aeb8b2396',
tag: 'f40e1c743f52436bdf06d8fa1eca343d'
},
{
algo: 'aes-128-ocb',
key: '000102030405060708090a0b0c0d0e0f',
iv: 'bbaa99887766554433221107',
plain: '000102030405060708090a0b0c0d0e0f1011121314151617',
plainIsHex: true,
aad: '000102030405060708090a0b0c0d0e0f1011121314151617',
ct: '1ca2207308c87c010756104d8840ce1952f09673a448a122',
tag: 'c92c62241051f57356d7f3c90bb0e07f'
},
{
algo: 'aes-128-ocb',
key: '000102030405060708090a0b0c0d0e0f',
iv: 'bbaa99887766554433221108',
plain: '',
aad: '000102030405060708090a0b0c0d0e0f1011121314151617',
ct: '',
tag: '6dc225a071fc1b9f7c69f93b0f1e10de'
},
{
algo: 'aes-128-ocb',
key: '000102030405060708090a0b0c0d0e0f',
iv: 'bbaa99887766554433221109',
plain: '000102030405060708090a0b0c0d0e0f1011121314151617',
plainIsHex: true,
ct: '221bd0de7fa6fe993eccd769460a0af2d6cded0c395b1c3c',
tag: 'e725f32494b9f914d85c0b1eb38357ff'
},
{
algo: 'aes-128-ocb',
key: '000102030405060708090a0b0c0d0e0f',
iv: 'bbaa9988776655443322110a',
plain: '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
plainIsHex: true,
aad: '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
ct: 'bd6f6c496201c69296c11efd138a467abd3c707924b964deaffc40319af5a485',
tag: '40fbba186c5553c68ad9f592a79a4240'
},
{
algo: 'aes-128-ocb',
key: '000102030405060708090a0b0c0d0e0f',
iv: 'bbaa9988776655443322110b',
plain: '',
aad: '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
ct: '',
tag: 'fe80690bee8a485d11f32965bc9d2a32'
},
{
algo: 'aes-128-ocb',
key: '000102030405060708090a0b0c0d0e0f',
iv: 'bbaa9988776655443322110c',
plain: '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
plainIsHex: true,
ct: '2942bfc773bda23cabc6acfd9bfd5835bd300f0973792ef46040c53f1432bcdf',
tag: 'b5e1dde3bc18a5f840b52e653444d5df'
},
{
algo: 'aes-128-ocb',
key: '000102030405060708090a0b0c0d0e0f',
iv: 'bbaa9988776655443322110d',
plain: '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f' +
'2021222324252627',
plainIsHex: true,
aad: '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20' +
'21222324252627',
ct: 'd5ca91748410c1751ff8a2f618255b68a0a12e093ff454606e59f9c1d0ddc54b65e8' +
'628e568bad7a',
tag: 'ed07ba06a4a69483a7035490c5769e60'
},
{
algo: 'aes-128-ocb',
key: '000102030405060708090a0b0c0d0e0f',
iv: 'bbaa9988776655443322110e',
plain: '',
plainIsHex: true,
aad: '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20' +
'21222324252627',
ct: '',
tag: 'c5cd9d1850c141e358649994ee701b68'
},
{
algo: 'aes-128-ocb',
key: '000102030405060708090a0b0c0d0e0f',
iv: 'bbaa9988776655443322110f',
plain: '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f' +
'2021222324252627',
plainIsHex: true,
ct: '4412923493c57d5de0d700f753cce0d1d2d95060122e9f15a5ddbfc5787e50b5cc55' +
'ee507bcb084e',
tag: '479ad363ac366b95a98ca5f3000b1479'
},
{
algo: 'aes-128-ocb',
key: '0f0e0d0c0b0a09080706050403020100',
iv: 'bbaa9988776655443322110d',
plain: '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f' +
'2021222324252627',
plainIsHex: true,
aad: '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20' +
'21222324252627',
ct: '1792a4e31e0755fb03e31b22116e6c2ddf9efd6e33d536f1a0124b0a55bae884ed93' +
'481529c76b6a',
tag: 'd0c515f4d1cdd4fdac4f02aa'
},
{
algo: 'aes-128-ocb',
key: '0f0e0d0c0b0a09080706050403020100',
iv: 'bbaa9988776655443322110d',
plain: '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f' +
'2021222324252627',
plainIsHex: true,
aad: '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20' +
'21222324252627',
ct: '1792a4e31e0755fb03e31b22116e6c2ddf9efd6e33d536f1a0124b0a55bae884ed93' +
'481529c76b6a',
tag: 'd0c515f4d1cdd4fdac4f02ab',
tampered: true
}
];
const errMessages = {
@ -554,9 +755,10 @@ for (const test of TEST_CASES) {
}
const isCCM = /^aes-(128|192|256)-ccm$/.test(test.algo);
const isOCB = /^aes-(128|192|256)-ocb$/.test(test.algo);
let options;
if (isCCM)
if (isCCM || isOCB)
options = { authTagLength: test.tag.length / 2 };
const inputEncoding = test.plainIsHex ? 'hex' : 'ascii';
@ -881,30 +1083,40 @@ for (const test of TEST_CASES) {
}
}
// Test that create(De|C)ipher(iv)? throws if the mode is CCM and no
// Test that create(De|C)ipher(iv)? throws if the mode is CCM or OCB and no
// authentication tag has been specified.
{
assert.throws(() => {
crypto.createCipheriv('aes-256-ccm',
'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8',
'qkuZpJWCewa6S');
}, /^Error: authTagLength required for aes-256-ccm$/);
// CCM decryption and create(De|C)ipher are unsupported in FIPS mode.
if (!common.hasFipsCrypto) {
for (const mode of ['ccm', 'ocb']) {
assert.throws(() => {
crypto.createDecipheriv('aes-256-ccm',
'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8',
'qkuZpJWCewa6S');
}, /^Error: authTagLength required for aes-256-ccm$/);
crypto.createCipheriv(`aes-256-${mode}`,
'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8',
'qkuZpJWCewa6S');
}, {
message: `authTagLength required for aes-256-${mode}`
});
assert.throws(() => {
crypto.createCipher('aes-256-ccm', 'very bad password');
}, /^Error: authTagLength required for aes-256-ccm$/);
// CCM decryption and create(De|C)ipher are unsupported in FIPS mode.
if (!common.hasFipsCrypto) {
assert.throws(() => {
crypto.createDecipheriv(`aes-256-${mode}`,
'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8',
'qkuZpJWCewa6S');
}, {
message: `authTagLength required for aes-256-${mode}`
});
assert.throws(() => {
crypto.createDecipher('aes-256-ccm', 'very bad password');
}, /^Error: authTagLength required for aes-256-ccm$/);
assert.throws(() => {
crypto.createCipher(`aes-256-${mode}`, 'very bad password');
}, {
message: `authTagLength required for aes-256-${mode}`
});
assert.throws(() => {
crypto.createDecipher(`aes-256-${mode}`, 'very bad password');
}, {
message: `authTagLength required for aes-256-${mode}`
});
}
}
}