Add AES-GCM to crypto layer using OpenSSL EVP.

Also add a EVP_CIPHER_CTX_safe structure to manage allocating and cleaning up
EVP_CIPHER_CTX struct.  IN older versions of OpenSSL you could just declare an
EVP_CIPHER_CTX struct on the stack.  Now you have to go through a generic
allocation mechanism!?  SO we're going to be doing a heap allocation for every
single packet.  That is....terrible.  We should try to find a way around this,
even if it's gross, because doing a heap allocation per packet is not acceptible.
This commit is contained in:
Fletcher Dunn
2019-01-19 09:06:12 -08:00
parent a0ecdbb198
commit 6048226d1e
10 changed files with 397332 additions and 15 deletions
+240 -15
View File
@@ -26,6 +26,7 @@
#include "tier0/memdbgoff.h"
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/evp.h>
#include "tier0/memdbgon.h"
#include "opensslwrapper.h"
@@ -41,6 +42,30 @@ void OneTimeCryptoInitOpenSSL()
}
}
// Allocate a EVP_CIPHER_CTX, and clean it up securely on scope exit
// using RAII
struct EVP_CIPHER_CTX_safe
{
// #if OPENSSL_API_LEVEL < 2
//
// // Nice, we can allocate on the stack, super simple and fast
// EVP_CIPHER_CTX_safe() { EVP_CIPHER_CTX_init( &ctx ); }
// ~EVP_CIPHER_CTX_safe() { EVP_CIPHER_CTX_cleanup( &ctx ); }
// inline EVP_CIPHER_CTX *Ptr() { return &ctx; }
// EVP_CIPHER_CTX ctx;
// #else
// Ug, we have to go through a generic allocator! What the heck
// guys, we need a way to do this efficiently! I don't want to be
// doing heap allocations just to encrypt 1000 bytes! Do they
// expect you to use a thread-local allocation and reuse it?
EVP_CIPHER_CTX_safe() { ctx = EVP_CIPHER_CTX_new(); }
~EVP_CIPHER_CTX_safe() { EVP_CIPHER_CTX_free( ctx ); }
inline EVP_CIPHER_CTX *Ptr() { return ctx; }
EVP_CIPHER_CTX *ctx;
// #endif
};
void CCrypto::Init()
{
OneTimeCryptoInitOpenSSL();
@@ -84,10 +109,6 @@ static bool SymmetricEncryptHelper( const uint8 *pubPlaintextData, const uint32
if ( cubEncryptedData < cubTotalOutput )
return false;
EVPCTXPointer<EVP_CIPHER_CTX *, EVP_CIPHER_CTX_free> ctx(EVP_CIPHER_CTX_new());
if (!ctx.ctx)
return false;
const EVP_CIPHER *cipher = NULL;
switch(cubKey * 8) {
@@ -98,16 +119,17 @@ static bool SymmetricEncryptHelper( const uint8 *pubPlaintextData, const uint32
if (!cipher)
return false;
if (EVP_EncryptInit_ex(ctx.ctx, cipher, NULL, pubKey, pIV) != 1)
EVP_CIPHER_CTX_safe ctx;
if (EVP_EncryptInit_ex( ctx.Ptr(), cipher, NULL, pubKey, pIV) != 1)
return false;
int ciphertext_len, len;
if (EVP_EncryptUpdate(ctx.ctx, pubEncryptedData, &len, pubPlaintextData, cubPlaintextData) != 1)
if (EVP_EncryptUpdate(ctx.Ptr(), pubEncryptedData, &len, pubPlaintextData, cubPlaintextData) != 1)
return false;
ciphertext_len = len;
if (EVP_EncryptFinal(ctx.ctx, pubEncryptedData + len, &len) != 1)
if (EVP_EncryptFinal(ctx.Ptr(), pubEncryptedData + len, &len) != 1)
return false;
ciphertext_len += len;
@@ -161,11 +183,6 @@ static bool BDecryptAESUsingOpenSSL( const uint8 *pubEncryptedData,
if ( *pcubPlaintextData < cubEncryptedData - k_nSymmetricBlockSize )
return false;
EVPCTXPointer<EVP_CIPHER_CTX *, EVP_CIPHER_CTX_free> ctx(EVP_CIPHER_CTX_new());
if (!ctx.ctx)
return false;
const EVP_CIPHER *cipher = NULL;
switch(cubKey * 8) {
case 128: cipher = EVP_aes_128_cbc(); break;
@@ -175,16 +192,17 @@ static bool BDecryptAESUsingOpenSSL( const uint8 *pubEncryptedData,
if (!cipher)
return false;
if (EVP_DecryptInit_ex(ctx.ctx, cipher, NULL, pubKey, pIV) != 1)
EVP_CIPHER_CTX_safe ctx;
if (EVP_DecryptInit_ex(ctx.Ptr(), cipher, NULL, pubKey, pIV) != 1)
return false;
int plaintext_len, len;
if (EVP_DecryptUpdate(ctx.ctx, pubPlaintextData, &len, pubEncryptedData, cubEncryptedData) != 1)
if (EVP_DecryptUpdate(ctx.Ptr(), pubPlaintextData, &len, pubEncryptedData, cubEncryptedData) != 1)
return false;
plaintext_len = len;
if (EVP_DecryptFinal(ctx.ctx, pubPlaintextData + plaintext_len, &len) != 1)
if (EVP_DecryptFinal(ctx.Ptr(), pubPlaintextData + plaintext_len, &len) != 1)
return false;
plaintext_len += len;
@@ -231,6 +249,213 @@ bool CCrypto::SymmetricDecryptWithIV( const uint8 *pubEncryptedData, uint32 cubE
return BDecryptAESUsingOpenSSL( pubEncryptedData, cubEncryptedData, pubPlaintextData, pcubPlaintextData, pubKey, cubKey, pIV, bVerifyPaddingBytes );
}
static const EVP_CIPHER *GetAESGCMCipherForKeyLength( size_t cbKey )
{
switch ( cbKey )
{
case 128/8: return EVP_aes_128_gcm();
case 192/8: return EVP_aes_192_gcm();
case 256/8: return EVP_aes_256_gcm();
default:
return nullptr;
}
}
//-----------------------------------------------------------------------------
bool CCrypto::SymmetricAuthEncryptChosenIV(
const void *pPlaintextData, size_t cbPlaintextData,
const void *pIV, size_t cbIV,
void *pEncryptedDataAndTag, uint32 *pcbEncryptedDataAndTag,
const void *pKey, size_t cbKey,
const void *pAdditionalAuthenticationData, size_t cbAuthenticationData,
size_t cbTag
) {
// Calculate size of encrypted data. Note that GCM does not use padding.
uint32 cbEncryptedWithoutTag = (uint32)cbPlaintextData;
uint32 cbEncryptedTotal = cbEncryptedWithoutTag + (uint32)cbTag;
// Make sure their buffer is big enough
if ( cbEncryptedTotal > *pcbEncryptedDataAndTag )
{
AssertMsg( false, "Buffer isn't big enough to hold padded+encrypted data and tag" );
return false;
}
// This function really shouldn't fail unless you have a bug
// and pass a bad IV, key, or tag size. So people might not
// check the return value. So make sure if we do fail, they
// don't think anything was encrypted.
*pcbEncryptedDataAndTag = 0;
// Select the cipher based on the size of the key
const EVP_CIPHER *cipher = GetAESGCMCipherForKeyLength( cbKey );
if ( cipher == nullptr )
{
AssertMsg( false, "Invalid AES-GCM key size" );
return false;
}
// Reference:
// https://wiki.openssl.org/index.php/EVP_Authenticated_Encryption_and_Decryption
// Setup a context. Don't set the IV right now, since the default size
// might not be the size of the IV they are using.
EVP_CIPHER_CTX_safe ctx;
VerifyFatal( EVP_EncryptInit_ex( ctx.Ptr(), cipher, nullptr, nullptr, nullptr ) == 1 );
// Set IV length
if ( EVP_CIPHER_CTX_ctrl( ctx.Ptr(), EVP_CTRL_GCM_SET_IVLEN, (int)cbIV, NULL) != 1 )
{
AssertMsg( false, "Bad IV size" );
return false;
}
// Set key and IV
VerifyFatal( EVP_EncryptInit_ex( ctx.Ptr(), nullptr, nullptr, (const uint8*)pKey, (const uint8*)pIV ) == 1 );
int nBytesWritten;
// AAD, if any
if ( cbAuthenticationData > 0 && pAdditionalAuthenticationData )
{
VerifyFatal( EVP_EncryptUpdate( ctx.Ptr(), nullptr, &nBytesWritten, (const uint8*)pAdditionalAuthenticationData, (int)cbAuthenticationData ) == 1 );
}
else
{
Assert( cbAuthenticationData == 0 );
}
// Now the actual plaintext to be encrypted
uint8 *pOut = (uint8 *)pEncryptedDataAndTag;
VerifyFatal( EVP_EncryptUpdate( ctx.Ptr(), pOut, &nBytesWritten, (const uint8*)pPlaintextData, (int)cbPlaintextData ) == 1 );
pOut += nBytesWritten;
// Finish up
VerifyFatal( EVP_EncryptFinal_ex( ctx.Ptr(), pOut, &nBytesWritten ) == 1 );
pOut += nBytesWritten;
// Make sure that we have the expected number of encrypted bytes at this point
VerifyFatal( (uint8 *)pEncryptedDataAndTag + cbEncryptedWithoutTag == pOut );
// Append the tag
if ( EVP_CIPHER_CTX_ctrl( ctx.Ptr(), EVP_CTRL_GCM_GET_TAG, (int)cbTag, pOut ) != 1 )
{
AssertMsg( false, "Bad tag size" );
return false;
}
// Give the caller back the size of everything
*pcbEncryptedDataAndTag = cbEncryptedTotal;
// Success.
// NOTE: EVP_CIPHER_CTX_safe destructor cleans up
return true;
}
//-----------------------------------------------------------------------------
bool CCrypto::SymmetricAuthDecryptWithIV(
const void *pEncryptedDataAndTag, size_t cbEncryptedDataAndTag,
const void *pIV, size_t cbIV,
void *pPlaintextData, uint32 *pcbPlaintextData,
const void *pKey, size_t cbKey,
const void *pAdditionalAuthenticationData, size_t cbAuthenticationData,
size_t cbTag
) {
// Make sure buffer and tag sizes aren't totally bogus
if ( cbTag > cbEncryptedDataAndTag )
{
//AssertMsg( false, "Encrypted size doesn't make sense for tag size" );
*pcbPlaintextData = 0;
return false;
}
uint32 cbEncryptedDataWithoutTag = uint32( cbEncryptedDataAndTag - cbTag );
// Make sure their buffer is big enough. Remember that in GCM mode,
// there is no padding, so if this fails, we indeed would have overflowed
if ( cbEncryptedDataWithoutTag > *pcbPlaintextData )
{
AssertMsg( false, "Buffer might not be big enough to hold decrypted data" );
return false;
}
// People really have to check the return value, but in case they
// don't, make sure they don't think we decrypted any data
*pcbPlaintextData = 0;
// Select the cipher based on the size of the key
const EVP_CIPHER *cipher = GetAESGCMCipherForKeyLength( cbKey );
if ( cipher == nullptr )
{
AssertMsg( false, "Invalid AES-GCM key size" );
return false;
}
// Setup a context. Don't set the IV right now, since the default size
// might not be the size of the IV they are using.
EVP_CIPHER_CTX_safe ctx;
VerifyFatal( EVP_DecryptInit_ex( ctx.Ptr(), cipher, nullptr, nullptr, nullptr ) == 1 );
// Set IV length
if ( EVP_CIPHER_CTX_ctrl( ctx.Ptr(), EVP_CTRL_GCM_SET_IVLEN, (int)cbIV, NULL) != 1 )
{
AssertMsg( false, "Bad IV size" );
return false;
}
// Set key and IV
VerifyFatal( EVP_DecryptInit_ex( ctx.Ptr(), nullptr, nullptr, (const uint8*)pKey, (const uint8*)pIV ) == 1 );
int nBytesWritten;
// AAD, if any
if ( cbAuthenticationData > 0 && pAdditionalAuthenticationData )
{
// I don't think it's actually possible to failed here, but
// since the caller really must be checking the return value,
// let's not make this fatal
if ( EVP_DecryptUpdate( ctx.Ptr(), nullptr, &nBytesWritten, (const uint8*)pAdditionalAuthenticationData, (int)cbAuthenticationData ) != 1 )
{
AssertMsg( false, "EVP_DecryptUpdate failed?" );
return false;
}
}
else
{
Assert( cbAuthenticationData == 0 );
}
uint8 *pOut = (uint8 *)pPlaintextData;
const uint8 *pIn = (const uint8 *)pEncryptedDataAndTag;
// Now the actual ciphertext to be decrypted
if ( EVP_DecryptUpdate( ctx.Ptr(), pOut, &nBytesWritten, pIn, (int)cbEncryptedDataWithoutTag ) != 1 )
return false;
pOut += nBytesWritten;
pIn += cbEncryptedDataWithoutTag;
// Set expected tag value
if( EVP_CIPHER_CTX_ctrl( ctx.Ptr(), EVP_CTRL_GCM_SET_TAG, (int)cbTag, const_cast<uint8*>( pIn ) ) != 1)
{
AssertMsg( false, "Bad tag size" );
return false;
}
// Finish up, and check tag
if ( EVP_DecryptFinal_ex( ctx.Ptr(), pOut, &nBytesWritten ) <= 0 )
return false; // data has been tamped with
pOut += nBytesWritten;
// Make sure we got back the size we expected, and return the size
VerifyFatal( pOut == (uint8 *)pPlaintextData + cbEncryptedDataWithoutTag );
*pcbPlaintextData = cbEncryptedDataWithoutTag;
// Success.
// NOTE: EVP_CIPHER_CTX_safe destructor cleans up
return true;
}
//-----------------------------------------------------------------------------
// Purpose: Generate a SHA256 hash
// Input: pchInput - Plaintext string of item to hash (null terminated)
+20
View File
@@ -51,6 +51,26 @@ namespace CCrypto
const uint8 * pIV, uint32 cubIV,
uint8 * pubPlaintextData, uint32 * pcubPlaintextData,
const uint8 * pubKey, uint32 cubKey, bool bVerifyPaddingBytes = true );
// Symmetric encryption and authentication using AES-GCM.
bool SymmetricAuthEncryptChosenIV(
const void *pPlaintextData, size_t cbPlaintextData,
const void *pIV, size_t cbIV,
void *pEncryptedDataAndTag, uint32 *pcbEncryptedDataAndTag,
const void *pKey, size_t cbKey,
const void *pAdditionalAuthenticationData, size_t cbAuthenticationData, // Optional additional authentication data. Not encrypted, but will be included in the tag, so it can be authenticated.
size_t cbTag // Number of tag bytes to append to the end of the buffer
);
// Symmetric decryption and check authentication tag using AES-GCM.
bool SymmetricAuthDecryptWithIV(
const void *pEncryptedDataAndTag, size_t cbEncryptedDataAndTag,
const void *pIV, size_t cbIV,
void *pPlaintextData, uint32 *pcbPlaintextData,
const void *pKey, size_t cbKey,
const void *pAdditionalAuthenticationData, size_t cbAuthenticationData, // Optional additional authentication data. Not encrypted, but will be included in the tag, so it can be authenticated.
size_t cbTag // Last N bytes in your buffer are assumed to be a tag, and will be checked
);
//
// Secure key exchange (curve25519 elliptic-curve Diffie-Hellman key exchange)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
https://csrc.nist.gov/Projects/Cryptographic-Algorithm-Validation-Program/CAVP-TESTING-BLOCK-CIPHER-MODES
https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Algorithm-Validation-Program/documents/mac/gcmtestvectors.zip
+134
View File
@@ -1,4 +1,5 @@
#include <assert.h>
#include <string>
#include <tier1/utlbuffer.h>
#include <crypto.h>
@@ -8,6 +9,8 @@
// A little compatibility glue so I don't have to make any changes to them.
#define CHECK(x) Assert(x)
#define CHECK_EQUAL(a,b) Assert((a)==(b))
#define RETURNIFNOT(x) { if ( !(x) ) { AssertMsg( false, #x ); return; } }
#define RETURNFALSEIFNOT(x) { if ( !(x) ) { AssertMsg( false, #x ); return false; } }
const int k_cSmallBuff = 255; // smallish buffer
const int k_cMedBuff = 1024;
@@ -219,6 +222,136 @@ void TestSymmetricCrypto()
CHECK( !V_strcmp( rgchSrc, (const char *)rgubInplace ) );
}
// https://csrc.nist.gov/Projects/Cryptographic-Algorithm-Validation-Program/CAVP-TESTING-BLOCK-CIPHER-MODES
// https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Algorithm-Validation-Program/documents/mac/gcmtestvectors.zip
class NISTTestVectorFile
{
FILE *f = nullptr;
public:
NISTTestVectorFile( const char *pszFilename )
{
f = fopen( pszFilename, "rt" );
CHECK(f);
}
~NISTTestVectorFile() { if ( f ) fclose( f ); }
bool FindNextTest()
{
if ( !f )
return false;
char line[2048];
while ( fgets( line, sizeof(line), f ) )
{
if ( V_strnicmp( line, "count", 5 ) == 0 )
return true;
}
return false;
}
bool GetBinaryBlob( const char *pszTag, std::string &blob )
{
char line[2048];
RETURNFALSEIFNOT( fgets( line, sizeof(line), f ) );
int lTag = V_strlen(pszTag);
RETURNFALSEIFNOT( V_strnicmp( line, pszTag, lTag ) == 0 );
const char *p = line + lTag;
while ( isspace(*p) )
++p;
CHECK_EQUAL( *p, '=' );
++p;
while ( isspace(*p) )
++p;
uint8 decodedData[ 1024 ];
uint32 cbDecodedData = sizeof(decodedData);
RETURNFALSEIFNOT( CCrypto::HexDecode( p, decodedData, &cbDecodedData ) );
blob.assign( (char *)decodedData, cbDecodedData );
return true;
}
};
void TestSymmetricAuthCrypto_EncryptTestVectorFile( const char *pszFilename )
{
NISTTestVectorFile file( pszFilename );
while ( file.FindNextTest() )
{
std::string key;
RETURNIFNOT( file.GetBinaryBlob( "key", key ) );
std::string iv;
RETURNIFNOT( file.GetBinaryBlob( "iv", iv ) );
std::string pt;
RETURNIFNOT( file.GetBinaryBlob( "pt", pt ) );
std::string aad;
RETURNIFNOT( file.GetBinaryBlob( "aad", aad ) );
std::string ct;
RETURNIFNOT( file.GetBinaryBlob( "ct", ct ) );
std::string tag;
RETURNIFNOT( file.GetBinaryBlob( "tag", tag ) );
uint8 encrypted[ 2048 ];
uint32 cbEncrypted = sizeof(encrypted);
RETURNIFNOT( ct.length() <= sizeof(encrypted) );
// Encrypt it
CHECK( CCrypto::SymmetricAuthEncryptChosenIV(
pt.c_str(), pt.length(),
iv.c_str(), iv.length(),
encrypted, &cbEncrypted,
key.c_str(), key.length(),
aad.c_str(), aad.length(),
tag.length()
) );
// Confirm it matches the test vector
CHECK( cbEncrypted == ct.length() + tag.length() );
CHECK( memcmp( ct.c_str(), encrypted, ct.length() ) == 0 );
CHECK( memcmp( tag.c_str(), encrypted+ct.length(), tag.length() ) == 0 );
// Make sure we can decrypt it successfully
uint8 decrypted[ 2048 ];
uint32 cbDecrypted = sizeof(decrypted);
CHECK( CCrypto::SymmetricAuthDecryptWithIV(
encrypted, cbEncrypted,
iv.c_str(), iv.length(),
decrypted, &cbDecrypted,
key.c_str(), key.length(),
aad.c_str(), aad.length(),
tag.length()
) );
CHECK( cbDecrypted == pt.length() );
CHECK( memcmp( pt.c_str(), decrypted, cbDecrypted ) == 0 );
// Flip a random bit in the ciphertext+tag blob
encrypted[ rand() % cbEncrypted ] ^= ( 1 << (rand() & 7 ) );
// It should fail to decrypt
cbDecrypted = sizeof(decrypted);
CHECK( !CCrypto::SymmetricAuthDecryptWithIV(
encrypted, cbEncrypted,
iv.c_str(), iv.length(),
decrypted, &cbDecrypted,
key.c_str(), key.length(),
aad.c_str(), aad.length(),
tag.length()
) );
}
}
//-----------------------------------------------------------------------------
// Purpose: Test AES-GCM crypto against known vectors
//-----------------------------------------------------------------------------
void TestSymmetricAuthCryptoVectors()
{
#define TEST_VECTOR_DIR "../../tests/aesgcmtestvectors/"
// Check against known test vectors
TestSymmetricAuthCrypto_EncryptTestVectorFile( TEST_VECTOR_DIR "gcmEncryptExtIV128.rsp" );
TestSymmetricAuthCrypto_EncryptTestVectorFile( TEST_VECTOR_DIR "gcmEncryptExtIV192.rsp" );
TestSymmetricAuthCrypto_EncryptTestVectorFile( TEST_VECTOR_DIR "gcmEncryptExtIV256.rsp" );
}
//-----------------------------------------------------------------------------
// Purpose: Test elliptic-curve primitives (ed25519 signing, curve25519 key exchange)
//-----------------------------------------------------------------------------
@@ -633,6 +766,7 @@ int main()
TestCryptoEncoding();
TestSymmetricCrypto();
TestSymmetricAuthCryptoVectors();
TestEllipticCrypto();
TestOpenSSHEd25519();
TestEllipticPerf();