diff --git a/src/lib/P11Attributes.cpp b/src/lib/P11Attributes.cpp index e2fd8117f..89ce1a19e 100644 --- a/src/lib/P11Attributes.cpp +++ b/src/lib/P11Attributes.cpp @@ -877,6 +877,7 @@ CK_RV P11AttrCheckValue::updateAttr(Token *token, bool isPrivate, CK_VOID_PTR pV case CKK_SHA3_256_HMAC: case CKK_SHA3_384_HMAC: case CKK_SHA3_512_HMAC: + case CKK_CHACHA20: key.setKeyBits(keybits); key.setBitLen(keybits.size() * 8); checkValue = key.getKeyCheckValue(); @@ -1039,6 +1040,7 @@ CK_RV P11AttrValue::updateAttr(Token *token, bool isPrivate, CK_VOID_PTR pValue, case CKK_SHA3_256_HMAC: case CKK_SHA3_384_HMAC: case CKK_SHA3_512_HMAC: + case CKK_CHACHA20: key.setKeyBits(plaintext); key.setBitLen(plaintext.size() * 8); checkValue = key.getKeyCheckValue(); diff --git a/src/lib/P11Objects.cpp b/src/lib/P11Objects.cpp index 82228f4de..884a554e1 100644 --- a/src/lib/P11Objects.cpp +++ b/src/lib/P11Objects.cpp @@ -1946,6 +1946,44 @@ bool P11GOSTSecretKeyObj::init(OSObject *inobject) return true; } +// Constructor +P11ChaCha20SecretKeyObj::P11ChaCha20SecretKeyObj() +{ + initialized = false; +} + +// Add attributes +bool P11ChaCha20SecretKeyObj::init(OSObject *inobject) +{ + if (initialized) return true; + if (inobject == NULL) return false; + + if (!inobject->attributeExists(CKA_KEY_TYPE) || inobject->getUnsignedLongValue(CKA_KEY_TYPE, CKK_VENDOR_DEFINED) != CKK_CHACHA20) { + OSAttribute setKeyType((unsigned long)CKK_CHACHA20); + inobject->setAttribute(CKA_KEY_TYPE, setKeyType); + } + + // Create parent + if (!P11SecretKeyObj::init(inobject)) return false; + + // ChaCha20 keys always have a fixed 256-bit length; no CKA_VALUE_LEN + P11Attribute* attrValue = new P11AttrValue(osobject,P11Attribute::ck1|P11Attribute::ck4|P11Attribute::ck6|P11Attribute::ck7); + + // Initialize the attributes + if (!attrValue->init()) + { + ERROR_MSG("Could not initialize the attribute"); + delete attrValue; + return false; + } + + // Add them to the map + attributes[attrValue->getType()] = attrValue; + + initialized = true; + return true; +} + // Constructor P11DomainObj::P11DomainObj() { diff --git a/src/lib/P11Objects.h b/src/lib/P11Objects.h index 3afed095f..340bfbdf5 100644 --- a/src/lib/P11Objects.h +++ b/src/lib/P11Objects.h @@ -438,6 +438,19 @@ class P11GOSTSecretKeyObj : public P11SecretKeyObj bool initialized; }; +class P11ChaCha20SecretKeyObj : public P11SecretKeyObj +{ +public: + // Constructor + P11ChaCha20SecretKeyObj(); + + // Add attributes + virtual bool init(OSObject *inobject); + +protected: + bool initialized; +}; + class P11DomainObj : public P11Object { protected: diff --git a/src/lib/SoftHSM.cpp b/src/lib/SoftHSM.cpp index 1f4fdf15a..a9490ccd0 100644 --- a/src/lib/SoftHSM.cpp +++ b/src/lib/SoftHSM.cpp @@ -223,6 +223,10 @@ static CK_RV newP11Object(CK_OBJECT_CLASS objClass, CK_KEY_TYPE keyType, CK_CERT { *p11object = new P11GOSTSecretKeyObj(); } + else if (keyType == CKK_CHACHA20) + { + *p11object = new P11ChaCha20SecretKeyObj(); + } else return CKR_ATTRIBUTE_VALUE_INVALID; break; @@ -860,6 +864,8 @@ void SoftHSM::prepareSupportedMechanisms(std::mapulMaxKeySize = 32; pInfo->flags |= CKF_ENCRYPT | CKF_DECRYPT; break; + case CKM_CHACHA20_KEY_GEN: + pInfo->ulMinKeySize = 32; + pInfo->ulMaxKeySize = 32; + pInfo->flags = CKF_GENERATE; + break; + case CKM_CHACHA20_POLY1305: + pInfo->ulMinKeySize = 32; + pInfo->ulMaxKeySize = 32; + pInfo->flags = CKF_ENCRYPT | CKF_DECRYPT; + break; case CKM_AES_KEY_WRAP: pInfo->ulMinKeySize = 16; pInfo->ulMaxKeySize = 0x80000000; @@ -2379,6 +2395,7 @@ static bool isSymMechanism(CK_MECHANISM_PTR pMechanism) case CKM_AES_CBC_PAD: case CKM_AES_CTR: case CKM_AES_GCM: + case CKM_CHACHA20_POLY1305: return true; default: return false; @@ -2598,6 +2615,49 @@ CK_RV SoftHSM::SymEncryptInit(CK_SESSION_HANDLE hSession, CK_MECHANISM_PTR pMech } tagBytes = tagBytes / 8; break; + case CKM_CHACHA20_POLY1305: + if (keyType != CKK_CHACHA20) + return CKR_KEY_TYPE_INCONSISTENT; + algo = SymAlgo::ChaCha20Poly1305; + mode = SymMode::ChaCha20Poly1305; + if (pMechanism->pParameter == NULL_PTR || + pMechanism->ulParameterLen != sizeof(CK_SALSA20_CHACHA20_POLY1305_PARAMS)) + { + DEBUG_MSG("ChaCha20-Poly1305 requires parameters"); + return CKR_ARGUMENTS_BAD; + } +#if defined(WITH_OPENSSL) + // The OpenSSL backend's ChaCha20-Poly1305 IV length is fixed at + // 96 bits; other lengths are silently truncated/over-read by EVP. + if (CK_SALSA20_CHACHA20_POLY1305_PARAMS_PTR(pMechanism->pParameter)->ulNonceBits != 96) +#else + if (CK_SALSA20_CHACHA20_POLY1305_PARAMS_PTR(pMechanism->pParameter)->ulNonceBits == 0 || + CK_SALSA20_CHACHA20_POLY1305_PARAMS_PTR(pMechanism->pParameter)->ulNonceBits % 8 != 0 || + CK_SALSA20_CHACHA20_POLY1305_PARAMS_PTR(pMechanism->pParameter)->ulNonceBits > 192) +#endif + { + DEBUG_MSG("Invalid ulNonceBits"); + return CKR_MECHANISM_PARAM_INVALID; + } + if (CK_SALSA20_CHACHA20_POLY1305_PARAMS_PTR(pMechanism->pParameter)->pNonce == NULL_PTR) + { + DEBUG_MSG("ChaCha20-Poly1305 requires a nonce"); + return CKR_ARGUMENTS_BAD; + } + iv.resize(CK_SALSA20_CHACHA20_POLY1305_PARAMS_PTR(pMechanism->pParameter)->ulNonceBits / 8); + memcpy(&iv[0], CK_SALSA20_CHACHA20_POLY1305_PARAMS_PTR(pMechanism->pParameter)->pNonce, iv.size()); + aad.resize(CK_SALSA20_CHACHA20_POLY1305_PARAMS_PTR(pMechanism->pParameter)->ulAADLen); + if (aad.size() > 0) + { + if (CK_SALSA20_CHACHA20_POLY1305_PARAMS_PTR(pMechanism->pParameter)->pAAD == NULL_PTR) + { + DEBUG_MSG("Invalid AAD parameter"); + return CKR_ARGUMENTS_BAD; + } + memcpy(&aad[0], CK_SALSA20_CHACHA20_POLY1305_PARAMS_PTR(pMechanism->pParameter)->pAAD, aad.size()); + } + tagBytes = 16; + break; default: return CKR_MECHANISM_INVALID; } @@ -3354,6 +3414,49 @@ CK_RV SoftHSM::SymDecryptInit(CK_SESSION_HANDLE hSession, CK_MECHANISM_PTR pMech } tagBytes = tagBytes / 8; break; + case CKM_CHACHA20_POLY1305: + if (keyType != CKK_CHACHA20) + return CKR_KEY_TYPE_INCONSISTENT; + algo = SymAlgo::ChaCha20Poly1305; + mode = SymMode::ChaCha20Poly1305; + if (pMechanism->pParameter == NULL_PTR || + pMechanism->ulParameterLen != sizeof(CK_SALSA20_CHACHA20_POLY1305_PARAMS)) + { + DEBUG_MSG("ChaCha20-Poly1305 requires parameters"); + return CKR_ARGUMENTS_BAD; + } +#if defined(WITH_OPENSSL) + // The OpenSSL backend's ChaCha20-Poly1305 IV length is fixed at + // 96 bits; other lengths are silently truncated/over-read by EVP. + if (CK_SALSA20_CHACHA20_POLY1305_PARAMS_PTR(pMechanism->pParameter)->ulNonceBits != 96) +#else + if (CK_SALSA20_CHACHA20_POLY1305_PARAMS_PTR(pMechanism->pParameter)->ulNonceBits == 0 || + CK_SALSA20_CHACHA20_POLY1305_PARAMS_PTR(pMechanism->pParameter)->ulNonceBits % 8 != 0 || + CK_SALSA20_CHACHA20_POLY1305_PARAMS_PTR(pMechanism->pParameter)->ulNonceBits > 192) +#endif + { + DEBUG_MSG("Invalid ulNonceBits"); + return CKR_MECHANISM_PARAM_INVALID; + } + if (CK_SALSA20_CHACHA20_POLY1305_PARAMS_PTR(pMechanism->pParameter)->pNonce == NULL_PTR) + { + DEBUG_MSG("ChaCha20-Poly1305 requires a nonce"); + return CKR_ARGUMENTS_BAD; + } + iv.resize(CK_SALSA20_CHACHA20_POLY1305_PARAMS_PTR(pMechanism->pParameter)->ulNonceBits / 8); + memcpy(&iv[0], CK_SALSA20_CHACHA20_POLY1305_PARAMS_PTR(pMechanism->pParameter)->pNonce, iv.size()); + aad.resize(CK_SALSA20_CHACHA20_POLY1305_PARAMS_PTR(pMechanism->pParameter)->ulAADLen); + if (aad.size() > 0) + { + if (CK_SALSA20_CHACHA20_POLY1305_PARAMS_PTR(pMechanism->pParameter)->pAAD == NULL_PTR) + { + DEBUG_MSG("Invalid AAD parameter"); + return CKR_ARGUMENTS_BAD; + } + memcpy(&aad[0], CK_SALSA20_CHACHA20_POLY1305_PARAMS_PTR(pMechanism->pParameter)->pAAD, aad.size()); + } + tagBytes = 16; + break; default: return CKR_MECHANISM_INVALID; } @@ -6786,6 +6889,10 @@ CK_RV SoftHSM::C_GenerateKey(CK_SESSION_HANDLE hSession, CK_MECHANISM_PTR pMecha objClass = CKO_SECRET_KEY; keyType = CKK_AES; break; + case CKM_CHACHA20_KEY_GEN: + objClass = CKO_SECRET_KEY; + keyType = CKK_CHACHA20; + break; case CKM_GENERIC_SECRET_KEY_GEN: objClass = CKO_SECRET_KEY; keyType = CKK_GENERIC_SECRET; @@ -6822,6 +6929,9 @@ CK_RV SoftHSM::C_GenerateKey(CK_SESSION_HANDLE hSession, CK_MECHANISM_PTR pMecha if (pMechanism->mechanism == CKM_AES_KEY_GEN && (objClass != CKO_SECRET_KEY || keyType != CKK_AES)) return CKR_TEMPLATE_INCONSISTENT; + if (pMechanism->mechanism == CKM_CHACHA20_KEY_GEN && + (objClass != CKO_SECRET_KEY || keyType != CKK_CHACHA20)) + return CKR_TEMPLATE_INCONSISTENT; if (pMechanism->mechanism == CKM_GENERIC_SECRET_KEY_GEN && (objClass != CKO_SECRET_KEY || keyType != CKK_GENERIC_SECRET)) return CKR_TEMPLATE_INCONSISTENT; @@ -6874,6 +6984,12 @@ CK_RV SoftHSM::C_GenerateKey(CK_SESSION_HANDLE hSession, CK_MECHANISM_PTR pMecha return this->generateAES(hSession, pTemplate, ulCount, phKey, isOnToken, isPrivate); } + // Generate ChaCha20 secret key + if (pMechanism->mechanism == CKM_CHACHA20_KEY_GEN) + { + return this->generateChaCha20(hSession, pTemplate, ulCount, phKey, isOnToken, isPrivate); + } + // Generate generic secret key if (pMechanism->mechanism == CKM_GENERIC_SECRET_KEY_GEN) { @@ -9806,6 +9922,173 @@ CK_RV SoftHSM::generateAES return rv; } +// Generate a ChaCha20 secret key +CK_RV SoftHSM::generateChaCha20 +(CK_SESSION_HANDLE hSession, + CK_ATTRIBUTE_PTR pTemplate, + CK_ULONG ulCount, + CK_OBJECT_HANDLE_PTR phKey, + CK_BBOOL isOnToken, + CK_BBOOL isPrivate) +{ + *phKey = CK_INVALID_HANDLE; + + // Get the session + Session* session = (Session*)handleManager->getSession(hSession); + if (session == NULL) + return CKR_SESSION_HANDLE_INVALID; + + // Get the token + Token* token = session->getToken(); + if (token == NULL) + return CKR_GENERAL_ERROR; + + // Extract desired parameter information + bool checkValue = true; + for (CK_ULONG i = 0; i < ulCount; i++) + { + switch (pTemplate[i].type) + { + case CKA_CHECK_VALUE: + if (pTemplate[i].ulValueLen > 0) + { + INFO_MSG("CKA_CHECK_VALUE must be a no-value (0 length) entry"); + return CKR_ATTRIBUTE_VALUE_INVALID; + } + checkValue = false; + break; + default: + break; + } + } + + // Generate the secret key; ChaCha20-Poly1305 always uses a 256-bit key + SymmetricKey* key = new SymmetricKey(256); + SymmetricAlgorithm* chacha20 = CryptoFactory::i()->getSymmetricAlgorithm(SymAlgo::ChaCha20Poly1305); + if (chacha20 == NULL) + { + ERROR_MSG("Could not get SymmetricAlgorithm"); + delete key; + return CKR_GENERAL_ERROR; + } + RNG* rng = CryptoFactory::i()->getRNG(); + if (rng == NULL) + { + ERROR_MSG("Could not get RNG"); + chacha20->recycleKey(key); + CryptoFactory::i()->recycleSymmetricAlgorithm(chacha20); + return CKR_GENERAL_ERROR; + } + if (!chacha20->generateKey(*key, rng)) + { + ERROR_MSG("Could not generate ChaCha20 secret key"); + chacha20->recycleKey(key); + CryptoFactory::i()->recycleSymmetricAlgorithm(chacha20); + return CKR_GENERAL_ERROR; + } + + CK_RV rv = CKR_OK; + + // Create the secret key object using C_CreateObject + const CK_ULONG maxAttribs = 32; + CK_OBJECT_CLASS objClass = CKO_SECRET_KEY; + CK_KEY_TYPE keyType = CKK_CHACHA20; + CK_ATTRIBUTE keyAttribs[maxAttribs] = { + { CKA_CLASS, &objClass, sizeof(objClass) }, + { CKA_TOKEN, &isOnToken, sizeof(isOnToken) }, + { CKA_PRIVATE, &isPrivate, sizeof(isPrivate) }, + { CKA_KEY_TYPE, &keyType, sizeof(keyType) }, + }; + CK_ULONG keyAttribsCount = 4; + + // Add the additional + if (ulCount > (maxAttribs - keyAttribsCount)) + rv = CKR_TEMPLATE_INCONSISTENT; + for (CK_ULONG i=0; i < ulCount && rv == CKR_OK; ++i) + { + switch (pTemplate[i].type) + { + case CKA_CLASS: + case CKA_TOKEN: + case CKA_PRIVATE: + case CKA_KEY_TYPE: + case CKA_CHECK_VALUE: + continue; + default: + keyAttribs[keyAttribsCount++] = pTemplate[i]; + } + } + + if (rv == CKR_OK) + rv = this->CreateObject(hSession, keyAttribs, keyAttribsCount, phKey, OBJECT_OP_GENERATE); + + // Store the attributes that are being supplied + if (rv == CKR_OK) + { + OSObject* osobject = (OSObject*)handleManager->getObject(*phKey); + if (osobject == NULL_PTR || !osobject->isValid()) { + rv = CKR_FUNCTION_FAILED; + } else if (osobject->startTransaction()) { + bool bOK = true; + + // Common Attributes + bOK = bOK && osobject->setAttribute(CKA_LOCAL,true); + CK_ULONG ulKeyGenMechanism = (CK_ULONG)CKM_CHACHA20_KEY_GEN; + bOK = bOK && osobject->setAttribute(CKA_KEY_GEN_MECHANISM,ulKeyGenMechanism); + + // Common Secret Key Attributes + bool bAlwaysSensitive = osobject->getBooleanValue(CKA_SENSITIVE, false); + bOK = bOK && osobject->setAttribute(CKA_ALWAYS_SENSITIVE,bAlwaysSensitive); + bool bNeverExtractable = osobject->getBooleanValue(CKA_EXTRACTABLE, false) == false; + bOK = bOK && osobject->setAttribute(CKA_NEVER_EXTRACTABLE, bNeverExtractable); + + // ChaCha20 Secret Key Attributes + ByteString value; + ByteString kcv; + if (isPrivate) + { + bOK = bOK && token->encrypt(key->getKeyBits(), value); + bOK = bOK && token->encrypt(key->getKeyCheckValue(), kcv); + } + else + { + value = key->getKeyBits(); + kcv = key->getKeyCheckValue(); + } + bOK = bOK && osobject->setAttribute(CKA_VALUE, value); + if (checkValue) + bOK = bOK && osobject->setAttribute(CKA_CHECK_VALUE, kcv); + + if (bOK) + bOK = osobject->commitTransaction(); + else + osobject->abortTransaction(); + + if (!bOK) + rv = CKR_FUNCTION_FAILED; + } else + rv = CKR_FUNCTION_FAILED; + } + + // Clean up + chacha20->recycleKey(key); + CryptoFactory::i()->recycleSymmetricAlgorithm(chacha20); + + // Remove the key that may have been created already when the function fails. + if (rv != CKR_OK) + { + if (*phKey != CK_INVALID_HANDLE) + { + OSObject* oskey = (OSObject*)handleManager->getObject(*phKey); + handleManager->destroyObject(*phKey); + if (oskey) oskey->destroyObject(); + *phKey = CK_INVALID_HANDLE; + } + } + + return rv; +} + // Generate a DES secret key CK_RV SoftHSM::generateDES (CK_SESSION_HANDLE hSession, diff --git a/src/lib/SoftHSM.h b/src/lib/SoftHSM.h index 724812c91..468207c0e 100644 --- a/src/lib/SoftHSM.h +++ b/src/lib/SoftHSM.h @@ -274,6 +274,15 @@ class SoftHSM CK_BBOOL isOnToken, CK_BBOOL isPrivate ); + CK_RV generateChaCha20 + ( + CK_SESSION_HANDLE hSession, + CK_ATTRIBUTE_PTR pTemplate, + CK_ULONG ulCount, + CK_OBJECT_HANDLE_PTR phKey, + CK_BBOOL isOnToken, + CK_BBOOL isPrivate + ); CK_RV generateRSA (CK_SESSION_HANDLE hSession, CK_ATTRIBUTE_PTR pPublicKeyTemplate, diff --git a/src/lib/crypto/BotanChaCha20Poly1305.cpp b/src/lib/crypto/BotanChaCha20Poly1305.cpp new file mode 100644 index 000000000..324af946e --- /dev/null +++ b/src/lib/crypto/BotanChaCha20Poly1305.cpp @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2026 SoftHSMv2 contributors + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +/***************************************************************************** + BotanChaCha20Poly1305.cpp + + Botan ChaCha20-Poly1305 implementation + *****************************************************************************/ + +#include "config.h" +#include "BotanChaCha20Poly1305.h" + +std::string BotanChaCha20Poly1305::getCipher() const +{ + if (currentKey == NULL) return ""; + + // ChaCha20-Poly1305 requires a 256-bit key + if (currentKey->getBitLen() != 256) + { + ERROR_MSG("Invalid ChaCha20-Poly1305 currentKey length (%d bits)", currentKey->getBitLen()); + + return ""; + } + + if (currentCipherMode != SymMode::ChaCha20Poly1305) + { + ERROR_MSG("Invalid ChaCha20-Poly1305 cipher mode %i", currentCipherMode); + + return ""; + } + + return "ChaCha20Poly1305"; +} + +size_t BotanChaCha20Poly1305::getBlockSize() const +{ + // ChaCha20-Poly1305 is a stream cipher + return 1; +} diff --git a/src/lib/crypto/BotanChaCha20Poly1305.h b/src/lib/crypto/BotanChaCha20Poly1305.h new file mode 100644 index 000000000..23569a070 --- /dev/null +++ b/src/lib/crypto/BotanChaCha20Poly1305.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026 SoftHSMv2 contributors + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +/***************************************************************************** + BotanChaCha20Poly1305.h + + Botan ChaCha20-Poly1305 implementation + *****************************************************************************/ + +#ifndef _SOFTHSM_V2_BOTANCHACHA20POLY1305_H +#define _SOFTHSM_V2_BOTANCHACHA20POLY1305_H + +#include +#include "config.h" +#include "BotanSymmetricAlgorithm.h" + +class BotanChaCha20Poly1305 : public BotanSymmetricAlgorithm +{ +public: + // Destructor + virtual ~BotanChaCha20Poly1305() { } + + virtual bool wrapKey(const SymmetricKey* /*key*/, const SymWrap::Type /*mode*/, const ByteString& /*in*/, ByteString& /*out*/) { return false; } + virtual bool unwrapKey(const SymmetricKey* /*key*/, const SymWrap::Type /*mode*/, const ByteString& /*in*/, ByteString& /*out*/) { return false; } + + // Return the block size + virtual size_t getBlockSize() const; + +protected: + // Return the right Botan cipher for the operation + virtual std::string getCipher() const; +}; + +#endif // !_SOFTHSM_V2_BOTANCHACHA20POLY1305_H diff --git a/src/lib/crypto/BotanCryptoFactory.cpp b/src/lib/crypto/BotanCryptoFactory.cpp index fbe1dfce3..e3377e139 100644 --- a/src/lib/crypto/BotanCryptoFactory.cpp +++ b/src/lib/crypto/BotanCryptoFactory.cpp @@ -33,6 +33,7 @@ #include "config.h" #include "BotanCryptoFactory.h" #include "BotanAES.h" +#include "BotanChaCha20Poly1305.h" #include "BotanDES.h" #include "BotanDSA.h" #include "BotanDH.h" @@ -119,6 +120,8 @@ SymmetricAlgorithm* BotanCryptoFactory::getSymmetricAlgorithm(SymAlgo::Type algo case SymAlgo::DES: case SymAlgo::DES3: return new BotanDES(); + case SymAlgo::ChaCha20Poly1305: + return new BotanChaCha20Poly1305(); default: // No algorithm implementation is available ERROR_MSG("Unknown algorithm '%i'", algorithm); diff --git a/src/lib/crypto/BotanSymmetricAlgorithm.cpp b/src/lib/crypto/BotanSymmetricAlgorithm.cpp index 1b2a291f5..c530ca11c 100644 --- a/src/lib/crypto/BotanSymmetricAlgorithm.cpp +++ b/src/lib/crypto/BotanSymmetricAlgorithm.cpp @@ -101,7 +101,7 @@ bool BotanSymmetricAlgorithm::encryptInit(const SymmetricKey* key, const SymMode } // Check the IV - if (mode != SymMode::GCM && (IV.size() > 0) && (IV.size() != getBlockSize())) + if (mode != SymMode::GCM && mode != SymMode::ChaCha20Poly1305 && (IV.size() > 0) && (IV.size() != getBlockSize())) { ERROR_MSG("Invalid IV size (%d bytes, expected %d bytes)", IV.size(), getBlockSize()); @@ -188,7 +188,7 @@ bool BotanSymmetricAlgorithm::encryptInit(const SymmetricKey* key, const SymMode cipher->set_key(botanKey); cryption = new Botan::Pipe(cipher); } - else if (mode == SymMode::GCM) + else if (mode == SymMode::GCM || mode == SymMode::ChaCha20Poly1305) { Botan::AEAD_Mode* aead = Botan::get_aead(cipherName, Botan::ENCRYPTION); aead->set_key(botanKey); @@ -336,7 +336,7 @@ bool BotanSymmetricAlgorithm::decryptInit(const SymmetricKey* key, const SymMode } // Check the IV - if (mode != SymMode::GCM && (IV.size() > 0) && (IV.size() != getBlockSize())) + if (mode != SymMode::GCM && mode != SymMode::ChaCha20Poly1305 && (IV.size() > 0) && (IV.size() != getBlockSize())) { ERROR_MSG("Invalid IV size (%d bytes, expected %d bytes)", IV.size(), getBlockSize()); @@ -423,7 +423,7 @@ bool BotanSymmetricAlgorithm::decryptInit(const SymmetricKey* key, const SymMode cipher->set_key(botanKey); cryption = new Botan::Pipe(cipher); } - else if (mode == SymMode::GCM) + else if (mode == SymMode::GCM || mode == SymMode::ChaCha20Poly1305) { Botan::AEAD_Mode* aead = Botan::get_aead(cipherName, Botan::DECRYPTION); aead->set_key(botanKey); @@ -468,7 +468,7 @@ bool BotanSymmetricAlgorithm::decryptUpdate(const ByteString& encryptedData, Byt } // AEAD ciphers should not return decrypted data until final is called - if (currentCipherMode == SymMode::GCM) + if (currentCipherMode == SymMode::GCM || currentCipherMode == SymMode::ChaCha20Poly1305) { data.resize(0); return true; @@ -541,7 +541,7 @@ bool BotanSymmetricAlgorithm::decryptFinal(ByteString& data) return false; } - if (mode == SymMode::GCM) + if (mode == SymMode::GCM || mode == SymMode::ChaCha20Poly1305) { // Write data try diff --git a/src/lib/crypto/CMakeLists.txt b/src/lib/crypto/CMakeLists.txt index 0579a08a3..67a616257 100644 --- a/src/lib/crypto/CMakeLists.txt +++ b/src/lib/crypto/CMakeLists.txt @@ -50,6 +50,7 @@ set(SOURCES AESKey.cpp if(WITH_OPENSSL) list(APPEND SOURCES OSSLAES.cpp + OSSLChaCha20Poly1305.cpp OSSLCMAC.cpp OSSLComp.cpp OSSLCryptoFactory.cpp @@ -110,6 +111,7 @@ endif(WITH_OPENSSL) if(WITH_BOTAN) list(APPEND SOURCES BotanAES.cpp + BotanChaCha20Poly1305.cpp BotanCryptoFactory.cpp BotanDES.cpp BotanDH.cpp diff --git a/src/lib/crypto/Makefile.am b/src/lib/crypto/Makefile.am index 4746a16eb..42ac3e3bf 100644 --- a/src/lib/crypto/Makefile.am +++ b/src/lib/crypto/Makefile.am @@ -57,6 +57,7 @@ EXTRA_DIST = $(srcdir)/CMakeLists.txt \ # Compile with support of OpenSSL if WITH_OPENSSL libsofthsm_crypto_la_SOURCES += OSSLAES.cpp \ + OSSLChaCha20Poly1305.cpp \ OSSLComp.cpp \ OSSLCryptoFactory.cpp \ OSSLDES.cpp \ @@ -117,6 +118,7 @@ endif # Compile with support of Botan if WITH_BOTAN libsofthsm_crypto_la_SOURCES += BotanAES.cpp \ + BotanChaCha20Poly1305.cpp \ BotanCryptoFactory.cpp \ BotanDES.cpp \ BotanDH.cpp \ diff --git a/src/lib/crypto/OSSLChaCha20Poly1305.cpp b/src/lib/crypto/OSSLChaCha20Poly1305.cpp new file mode 100644 index 000000000..94b203004 --- /dev/null +++ b/src/lib/crypto/OSSLChaCha20Poly1305.cpp @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026 SoftHSMv2 contributors + * + * SPDX-License-Identifier: BSD-2-Clause + */ +/***************************************************************************** + OSSLChaCha20Poly1305.cpp + + OpenSSL ChaCha20-Poly1305 implementation + *****************************************************************************/ + +#include "config.h" +#include "OSSLChaCha20Poly1305.h" + +const EVP_CIPHER* OSSLChaCha20Poly1305::getCipher() const +{ + if (currentCipherMode != SymMode::ChaCha20Poly1305) + { + ERROR_MSG("Invalid cipher mode %i for ChaCha20-Poly1305", currentCipherMode); + return NULL; + } + + // ChaCha20-Poly1305 requires a 256-bit key + if (currentKey == NULL || currentKey->getBitLen() != 256) + { + ERROR_MSG("ChaCha20-Poly1305 requires a 256-bit key"); + return NULL; + } + + return EVP_chacha20_poly1305(); +} diff --git a/src/lib/crypto/OSSLChaCha20Poly1305.h b/src/lib/crypto/OSSLChaCha20Poly1305.h new file mode 100644 index 000000000..3f233ed10 --- /dev/null +++ b/src/lib/crypto/OSSLChaCha20Poly1305.h @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2026 SoftHSMv2 contributors + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +/***************************************************************************** + OSSLChaCha20Poly1305.h + + OpenSSL ChaCha20-Poly1305 implementation + *****************************************************************************/ + +#ifndef _SOFTHSM_V2_OSSLCHACHA20POLY1305_H +#define _SOFTHSM_V2_OSSLCHACHA20POLY1305_H + +#include +#include "config.h" +#include "OSSLEVPSymmetricAlgorithm.h" + +class OSSLChaCha20Poly1305 : public OSSLEVPSymmetricAlgorithm +{ +public: + virtual ~OSSLChaCha20Poly1305() { } + + virtual bool wrapKey(const SymmetricKey* /*key*/, const SymWrap::Type /*mode*/, const ByteString& /*in*/, ByteString& /*out*/) { return false; } + virtual bool unwrapKey(const SymmetricKey* /*key*/, const SymWrap::Type /*mode*/, const ByteString& /*in*/, ByteString& /*out*/) { return false; } + + virtual size_t getBlockSize() const { return 1; } + +protected: + virtual const EVP_CIPHER* getCipher() const; +}; + +#endif // !_SOFTHSM_V2_OSSLCHACHA20POLY1305_H diff --git a/src/lib/crypto/OSSLCryptoFactory.cpp b/src/lib/crypto/OSSLCryptoFactory.cpp index b70eec4e7..f20c04186 100644 --- a/src/lib/crypto/OSSLCryptoFactory.cpp +++ b/src/lib/crypto/OSSLCryptoFactory.cpp @@ -35,6 +35,7 @@ #include "OSSLCryptoFactory.h" #include "OSSLRNG.h" #include "OSSLAES.h" +#include "OSSLChaCha20Poly1305.h" #include "OSSLDES.h" #include "OSSLMD5.h" #include "OSSLSHA1.h" @@ -328,6 +329,8 @@ SymmetricAlgorithm* OSSLCryptoFactory::getSymmetricAlgorithm(SymAlgo::Type algor { case SymAlgo::AES: return new OSSLAES(); + case SymAlgo::ChaCha20Poly1305: + return new OSSLChaCha20Poly1305(); case SymAlgo::DES: case SymAlgo::DES3: return new OSSLDES(); diff --git a/src/lib/crypto/OSSLEVPSymmetricAlgorithm.cpp b/src/lib/crypto/OSSLEVPSymmetricAlgorithm.cpp index c3740b103..ffac8a142 100644 --- a/src/lib/crypto/OSSLEVPSymmetricAlgorithm.cpp +++ b/src/lib/crypto/OSSLEVPSymmetricAlgorithm.cpp @@ -116,7 +116,7 @@ bool OSSLEVPSymmetricAlgorithm::encryptInit(const SymmetricKey* key, const SymMo } // Check the IV - if (mode != SymMode::GCM && (IV.size() > 0) && (IV.size() != getBlockSize())) + if (mode != SymMode::GCM && mode != SymMode::ChaCha20Poly1305 && (IV.size() > 0) && (IV.size() != getBlockSize())) { ERROR_MSG("Invalid IV size (%d bytes, expected %d bytes)", IV.size(), getBlockSize()); @@ -166,13 +166,13 @@ bool OSSLEVPSymmetricAlgorithm::encryptInit(const SymmetricKey* key, const SymMo } int rv; - if (mode == SymMode::GCM) + if (mode == SymMode::GCM || mode == SymMode::ChaCha20Poly1305) { rv = EVP_EncryptInit_ex(pCurCTX, cipher, NULL, NULL, NULL); if (rv) { - EVP_CIPHER_CTX_ctrl(pCurCTX, EVP_CTRL_GCM_SET_IVLEN, iv.size(), NULL); + EVP_CIPHER_CTX_ctrl(pCurCTX, EVP_CTRL_AEAD_SET_IVLEN, iv.size(), NULL); rv = EVP_EncryptInit_ex(pCurCTX, NULL, NULL, (unsigned char*) currentKey->getKeyBits().const_byte_str(), iv.byte_str()); } } @@ -195,7 +195,7 @@ bool OSSLEVPSymmetricAlgorithm::encryptInit(const SymmetricKey* key, const SymMo EVP_CIPHER_CTX_set_padding(pCurCTX, padding ? 1 : 0); - if (mode == SymMode::GCM) + if (mode == SymMode::GCM || mode == SymMode::ChaCha20Poly1305) { int outLen = 0; if (aad.size() && !EVP_EncryptUpdate(pCurCTX, NULL, &outLen, (unsigned char*) aad.const_byte_str(), aad.size())) @@ -286,11 +286,11 @@ bool OSSLEVPSymmetricAlgorithm::encryptFinal(ByteString& encryptedData) // Resize the output block encryptedData.resize(outLen); - if (mode == SymMode::GCM) + if (mode == SymMode::GCM || mode == SymMode::ChaCha20Poly1305) { ByteString tag; tag.resize(tagBytes); - EVP_CIPHER_CTX_ctrl(pCurCTX, EVP_CTRL_GCM_GET_TAG, tagBytes, &tag[0]); + EVP_CIPHER_CTX_ctrl(pCurCTX, EVP_CTRL_AEAD_GET_TAG, tagBytes, &tag[0]); encryptedData += tag; } @@ -309,7 +309,7 @@ bool OSSLEVPSymmetricAlgorithm::decryptInit(const SymmetricKey* key, const SymMo } // Check the IV - if (mode != SymMode::GCM && (IV.size() > 0) && (IV.size() != getBlockSize())) + if (mode != SymMode::GCM && mode != SymMode::ChaCha20Poly1305 && (IV.size() > 0) && (IV.size() != getBlockSize())) { ERROR_MSG("Invalid IV size (%d bytes, expected %d bytes)", IV.size(), getBlockSize()); @@ -359,13 +359,13 @@ bool OSSLEVPSymmetricAlgorithm::decryptInit(const SymmetricKey* key, const SymMo } int rv; - if (mode == SymMode::GCM) + if (mode == SymMode::GCM || mode == SymMode::ChaCha20Poly1305) { rv = EVP_DecryptInit_ex(pCurCTX, cipher, NULL, NULL, NULL); if (rv) { - EVP_CIPHER_CTX_ctrl(pCurCTX, EVP_CTRL_GCM_SET_IVLEN, iv.size(), NULL); + EVP_CIPHER_CTX_ctrl(pCurCTX, EVP_CTRL_AEAD_SET_IVLEN, iv.size(), NULL); rv = EVP_DecryptInit_ex(pCurCTX, NULL, NULL, (unsigned char*) currentKey->getKeyBits().const_byte_str(), iv.byte_str()); } } @@ -388,7 +388,7 @@ bool OSSLEVPSymmetricAlgorithm::decryptInit(const SymmetricKey* key, const SymMo EVP_CIPHER_CTX_set_padding(pCurCTX, padding ? 1 : 0); - if (mode == SymMode::GCM) + if (mode == SymMode::GCM || mode == SymMode::ChaCha20Poly1305) { int outLen = 0; if (aad.size() && !EVP_DecryptUpdate(pCurCTX, NULL, &outLen, (unsigned char*) aad.const_byte_str(), aad.size())) @@ -416,7 +416,7 @@ bool OSSLEVPSymmetricAlgorithm::decryptUpdate(const ByteString& encryptedData, B } // AEAD ciphers should not return decrypted data until final is called - if (currentCipherMode == SymMode::GCM) + if (currentCipherMode == SymMode::GCM || currentCipherMode == SymMode::ChaCha20Poly1305) { data.resize(0); return true; @@ -469,7 +469,7 @@ bool OSSLEVPSymmetricAlgorithm::decryptFinal(ByteString& data) } data.resize(0); - if (mode == SymMode::GCM) + if (mode == SymMode::GCM || mode == SymMode::ChaCha20Poly1305) { // Check buffer size if (aeadBuffer.size() < tagBytes) @@ -482,7 +482,7 @@ bool OSSLEVPSymmetricAlgorithm::decryptFinal(ByteString& data) } // Set the tag - EVP_CIPHER_CTX_ctrl(pCurCTX, EVP_CTRL_GCM_SET_TAG, tagBytes, &aeadBuffer[aeadBuffer.size()-tagBytes]); + EVP_CIPHER_CTX_ctrl(pCurCTX, EVP_CTRL_AEAD_SET_TAG, tagBytes, &aeadBuffer[aeadBuffer.size()-tagBytes]); // Prepare the output block data.resize(aeadBuffer.size() - tagBytes + getBlockSize()); @@ -492,6 +492,9 @@ bool OSSLEVPSymmetricAlgorithm::decryptFinal(ByteString& data) { ERROR_MSG("EVP_DecryptUpdate failed: %s", ERR_error_string(ERR_get_error(), NULL)); + // The tag has not been verified yet; do not leave unauthenticated plaintext behind + data.wipe(); + clean(); return false; @@ -511,6 +514,10 @@ bool OSSLEVPSymmetricAlgorithm::decryptFinal(ByteString& data) { ERROR_MSG("EVP_DecryptFinal failed (0x%08X): %s", rv, ERR_error_string(ERR_get_error(), NULL)); + // The tag has not been verified (or verification failed); do not leave + // unauthenticated plaintext behind + data.wipe(); + clean(); return false; diff --git a/src/lib/crypto/SymmetricAlgorithm.cpp b/src/lib/crypto/SymmetricAlgorithm.cpp index 611d5bdeb..cf8e856b7 100644 --- a/src/lib/crypto/SymmetricAlgorithm.cpp +++ b/src/lib/crypto/SymmetricAlgorithm.cpp @@ -121,7 +121,7 @@ bool SymmetricAlgorithm::decryptUpdate(const ByteString& encryptedData, ByteStri } currentBufferSize += encryptedData.size(); - if (currentCipherMode == SymMode::GCM) { + if (currentCipherMode == SymMode::GCM || currentCipherMode == SymMode::ChaCha20Poly1305) { currentAEADBuffer += encryptedData; } @@ -208,6 +208,7 @@ bool SymmetricAlgorithm::isStreamCipher() case SymMode::CTR: case SymMode::GCM: case SymMode::OFB: + case SymMode::ChaCha20Poly1305: return true; default: break; diff --git a/src/lib/crypto/SymmetricAlgorithm.h b/src/lib/crypto/SymmetricAlgorithm.h index 7e060c487..24b69d603 100644 --- a/src/lib/crypto/SymmetricAlgorithm.h +++ b/src/lib/crypto/SymmetricAlgorithm.h @@ -45,7 +45,8 @@ struct SymAlgo Unknown, AES, DES, - DES3 + DES3, + ChaCha20Poly1305 }; }; @@ -59,7 +60,8 @@ struct SymMode CTR, ECB, GCM, - OFB + OFB, + ChaCha20Poly1305 }; }; diff --git a/src/lib/crypto/test/CMakeLists.txt b/src/lib/crypto/test/CMakeLists.txt index b2d72f613..4380bcb4c 100644 --- a/src/lib/crypto/test/CMakeLists.txt +++ b/src/lib/crypto/test/CMakeLists.txt @@ -13,6 +13,7 @@ set(INCLUDE_DIRS ${PROJECT_SOURCE_DIR}/.. set(SOURCES cryptotest.cpp AESTests.cpp + ChaChaTests.cpp DESTests.cpp DHTests.cpp DSATests.cpp diff --git a/src/lib/crypto/test/ChaChaTests.cpp b/src/lib/crypto/test/ChaChaTests.cpp new file mode 100644 index 000000000..26e4debc5 --- /dev/null +++ b/src/lib/crypto/test/ChaChaTests.cpp @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2026 SoftHSMv2 contributors + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +/***************************************************************************** + ChaChaTests.cpp + + Contains test cases to test the ChaCha20-Poly1305 implementation + *****************************************************************************/ + +#include +#include +#include "ChaChaTests.h" +#include "CryptoFactory.h" +#include "SymmetricKey.h" +#include + +CPPUNIT_TEST_SUITE_REGISTRATION(ChaChaTests); + +// Test vectors from RFC 8439: key, nonce, plaintext, AAD, ciphertext || tag +static const char testVectors[2][5][1024] = +{ + // Section 2.8.2 (nonce is the 32-bit constant 07000000 || IV 4041424344454647) + { + "808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9F", + "070000004041424344454647", + "4C616469657320616E642047656E746C656D656E206F662074686520636C617373206F66202739393A204966204920636F756C64206F6666657220796F75206F6E6C79206F6E652074697020666F7220746865206675747572652C2073756E73637265656E20776F756C642062652069742E", + "50515253C0C1C2C3C4C5C6C7", + "D31A8D34648E60DB7B86AFBC53EF7EC2A4ADED51296E08FEA9E2B5A736EE62D63DBEA45E8CA9671282FAFB69DA92728B1A71DE0A9E060B2905D6A5B67ECD3B3692DDBD7F2D778B8C9803AEE328091B58FAB324E4FAD675945585808B4831D7BC3FF4DEF08E4B7A9DE576D26586CEC64B6116" + "1AE10B594F09E26A7E902ECBD0600691" + }, + // Appendix A.5 + { + "1C9240A5EB55D38AF333888604F6B5F0473917C1402B80099DCA5CBC207075C0", + "000000000102030405060708", + "496E7465726E65742D4472616674732061726520647261667420646F63756D656E74732076616C696420666F722061206D6178696D756D206F6620736978206D6F6E74687320616E64206D617920626520757064617465642C207265706C616365642C206F72206F62736F6C65746564206279206F7468657220646F63756D656E747320617420616E792074696D652E20497420697320696E617070726F70726961746520746F2075736520496E7465726E65742D447261667473206173207265666572656E6365206D6174657269616C206F7220746F2063697465207468656D206F74686572207468616E206173202FE2809C776F726B20696E2070726F67726573732E2FE2809D", + "F33388860000000000004E91", + "64A0861575861AF460F062C79BE643BD5E805CFD345CF389F108670AC76C8CB24C6CFC18755D43EEA09EE94E382D26B0BDB7B73C321B0100D4F03B7F355894CF332F830E710B97CE98C8A84ABD0B948114AD176E008D33BD60F982B1FF37C8559797A06EF4F0EF61C186324E2B3506383606907B6A7C02B0F9F6157B53C867E4B9166C767B804D46A59B5216CDE7A4E99040C5A40433225EE282A1B0A06C523EAF4534D7F83FA1155B0047718CBC546A0D072B04B3564EEA1B422273F548271A0BB2316053FA76991955EBD63159434ECEBB4E466DAE5A1073A6727627097A1049E617D91D361094FA68F0FF77987130305BEABA2EDA04DF997B714D6C6F2C29A6AD5CB4022B02709B" + "EEAD9D67890CBB22392336FEA1851F38" + } +}; + +static const size_t tagBytes = 16; + +void ChaChaTests::setUp() +{ + chacha = NULL; + + chacha = CryptoFactory::i()->getSymmetricAlgorithm(SymAlgo::ChaCha20Poly1305); + + // Check the return value + CPPUNIT_ASSERT(chacha != NULL); +} + +void ChaChaTests::tearDown() +{ + if (chacha != NULL) + { + CryptoFactory::i()->recycleSymmetricAlgorithm(chacha); + } + + fflush(stdout); +} + +void ChaChaTests::testBlockSize() +{ + // ChaCha20-Poly1305 is a stream cipher + CPPUNIT_ASSERT(chacha->getBlockSize() == 1); +} + +void ChaChaTests::testRFC8439() +{ + for (int i = 0; i < 2; i++) + { + SymmetricKey key(256); + CPPUNIT_ASSERT(key.setKeyBits(ByteString(testVectors[i][0]))); + + ByteString nonce(testVectors[i][1]); + ByteString plainText(testVectors[i][2]); + ByteString AAD(testVectors[i][3]); + ByteString cipherText(testVectors[i][4]); + + ByteString shsmCipherText; + ByteString shsmPlainText; + ByteString OB; + + // Encrypt and compare with the expected ciphertext and tag + CPPUNIT_ASSERT(chacha->encryptInit(&key, SymMode::ChaCha20Poly1305, nonce, false, 0, AAD, tagBytes)); + + CPPUNIT_ASSERT(chacha->encryptUpdate(plainText, OB)); + shsmCipherText += OB; + + CPPUNIT_ASSERT(chacha->encryptFinal(OB)); + shsmCipherText += OB; + + CPPUNIT_ASSERT(shsmCipherText == cipherText); + + // Decrypt the expected ciphertext and compare with the plaintext + CPPUNIT_ASSERT(chacha->decryptInit(&key, SymMode::ChaCha20Poly1305, nonce, false, 0, AAD, tagBytes)); + + CPPUNIT_ASSERT(chacha->decryptUpdate(cipherText, OB)); + CPPUNIT_ASSERT(OB.size() == 0); + + CPPUNIT_ASSERT(chacha->decryptFinal(OB)); + shsmPlainText += OB; + + CPPUNIT_ASSERT(shsmPlainText == plainText); + } +} + +void ChaChaTests::testRFC8439MultiPart() +{ + // Chunk sizes that do not line up with the 16-byte Poly1305 block + // or the 64-byte ChaCha20 block + const size_t chunkSizes[] = { 1, 7, 16, 63, 64, 100 }; + + for (int i = 0; i < 2; i++) + { + SymmetricKey key(256); + CPPUNIT_ASSERT(key.setKeyBits(ByteString(testVectors[i][0]))); + + ByteString nonce(testVectors[i][1]); + ByteString plainText(testVectors[i][2]); + ByteString AAD(testVectors[i][3]); + ByteString cipherText(testVectors[i][4]); + + for (size_t c = 0; c < sizeof(chunkSizes)/sizeof(chunkSizes[0]); c++) + { + const size_t chunk = chunkSizes[c]; + ByteString shsmCipherText; + ByteString shsmPlainText; + ByteString OB; + + CPPUNIT_ASSERT(chacha->encryptInit(&key, SymMode::ChaCha20Poly1305, nonce, false, 0, AAD, tagBytes)); + for (size_t pos = 0; pos < plainText.size(); pos += chunk) + { + size_t len = plainText.size() - pos < chunk ? plainText.size() - pos : chunk; + CPPUNIT_ASSERT(chacha->encryptUpdate(plainText.substr(pos, len), OB)); + shsmCipherText += OB; + } + CPPUNIT_ASSERT(chacha->encryptFinal(OB)); + shsmCipherText += OB; + + CPPUNIT_ASSERT(shsmCipherText == cipherText); + + CPPUNIT_ASSERT(chacha->decryptInit(&key, SymMode::ChaCha20Poly1305, nonce, false, 0, AAD, tagBytes)); + for (size_t pos = 0; pos < cipherText.size(); pos += chunk) + { + size_t len = cipherText.size() - pos < chunk ? cipherText.size() - pos : chunk; + CPPUNIT_ASSERT(chacha->decryptUpdate(cipherText.substr(pos, len), OB)); + CPPUNIT_ASSERT(OB.size() == 0); + } + CPPUNIT_ASSERT(chacha->decryptFinal(OB)); + shsmPlainText += OB; + + CPPUNIT_ASSERT(shsmPlainText == plainText); + } + } +} + +void ChaChaTests::testRFC8439Tampered() +{ + for (int i = 0; i < 2; i++) + { + SymmetricKey key(256); + CPPUNIT_ASSERT(key.setKeyBits(ByteString(testVectors[i][0]))); + + ByteString nonce(testVectors[i][1]); + ByteString AAD(testVectors[i][3]); + ByteString cipherText(testVectors[i][4]); + ByteString OB; + + // Flipped bit in the tag + ByteString badTag(cipherText); + badTag[badTag.size() - 1] ^= 0x01; + + CPPUNIT_ASSERT(chacha->decryptInit(&key, SymMode::ChaCha20Poly1305, nonce, false, 0, AAD, tagBytes)); + CPPUNIT_ASSERT(chacha->decryptUpdate(badTag, OB)); + CPPUNIT_ASSERT(!chacha->decryptFinal(OB)); + CPPUNIT_ASSERT(OB.size() == 0); + + // Flipped bit in the ciphertext + ByteString badCipherText(cipherText); + badCipherText[0] ^= 0x01; + + CPPUNIT_ASSERT(chacha->decryptInit(&key, SymMode::ChaCha20Poly1305, nonce, false, 0, AAD, tagBytes)); + CPPUNIT_ASSERT(chacha->decryptUpdate(badCipherText, OB)); + CPPUNIT_ASSERT(!chacha->decryptFinal(OB)); + CPPUNIT_ASSERT(OB.size() == 0); + + // Flipped bit in the AAD + ByteString badAAD(AAD); + badAAD[0] ^= 0x01; + + CPPUNIT_ASSERT(chacha->decryptInit(&key, SymMode::ChaCha20Poly1305, nonce, false, 0, badAAD, tagBytes)); + CPPUNIT_ASSERT(chacha->decryptUpdate(cipherText, OB)); + CPPUNIT_ASSERT(!chacha->decryptFinal(OB)); + CPPUNIT_ASSERT(OB.size() == 0); + + // Wrong nonce + ByteString badNonce(nonce); + badNonce[nonce.size() - 1] ^= 0x01; + + CPPUNIT_ASSERT(chacha->decryptInit(&key, SymMode::ChaCha20Poly1305, badNonce, false, 0, AAD, tagBytes)); + CPPUNIT_ASSERT(chacha->decryptUpdate(cipherText, OB)); + CPPUNIT_ASSERT(!chacha->decryptFinal(OB)); + CPPUNIT_ASSERT(OB.size() == 0); + } +} diff --git a/src/lib/crypto/test/ChaChaTests.h b/src/lib/crypto/test/ChaChaTests.h new file mode 100644 index 000000000..9f4a6549d --- /dev/null +++ b/src/lib/crypto/test/ChaChaTests.h @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2026 SoftHSMv2 contributors + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +/***************************************************************************** + ChaChaTests.h + + Contains test cases to test the ChaCha20-Poly1305 implementation + *****************************************************************************/ + +#ifndef _SOFTHSM_V2_CHACHATESTS_H +#define _SOFTHSM_V2_CHACHATESTS_H + +#include +#include "SymmetricAlgorithm.h" + +class ChaChaTests : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(ChaChaTests); + CPPUNIT_TEST(testBlockSize); + CPPUNIT_TEST(testRFC8439); + CPPUNIT_TEST(testRFC8439MultiPart); + CPPUNIT_TEST(testRFC8439Tampered); + CPPUNIT_TEST_SUITE_END(); + +public: + void testBlockSize(); + void testRFC8439(); + void testRFC8439MultiPart(); + void testRFC8439Tampered(); + + void setUp(); + void tearDown(); + +private: + // ChaCha20-Poly1305 instance + SymmetricAlgorithm* chacha; +}; + +#endif // !_SOFTHSM_V2_CHACHATESTS_H diff --git a/src/lib/crypto/test/Makefile.am b/src/lib/crypto/test/Makefile.am index 16be1085e..54dfeab71 100644 --- a/src/lib/crypto/test/Makefile.am +++ b/src/lib/crypto/test/Makefile.am @@ -15,6 +15,7 @@ check_PROGRAMS = cryptotest cryptotest_SOURCES = cryptotest.cpp \ AESTests.cpp \ + ChaChaTests.cpp \ DESTests.cpp \ DHTests.cpp \ DSATests.cpp \ diff --git a/src/lib/test/SymmetricAlgorithmTests.cpp b/src/lib/test/SymmetricAlgorithmTests.cpp index 46f717b30..adc346551 100644 --- a/src/lib/test/SymmetricAlgorithmTests.cpp +++ b/src/lib/test/SymmetricAlgorithmTests.cpp @@ -35,6 +35,7 @@ #include #include #include +#include //#include #include "SymmetricAlgorithmTests.h" @@ -700,6 +701,24 @@ CK_RV SymmetricAlgorithmTests::generateAesKey(CK_SESSION_HANDLE hSession, CK_BBO &hKey) ); } +CK_RV SymmetricAlgorithmTests::generateChaCha20Key(CK_SESSION_HANDLE hSession, CK_BBOOL bToken, CK_BBOOL bPrivate, CK_OBJECT_HANDLE &hKey) +{ + CK_MECHANISM mechanism = { CKM_CHACHA20_KEY_GEN, NULL_PTR, 0 }; + // CK_BBOOL bFalse = CK_FALSE; + CK_BBOOL bTrue = CK_TRUE; + CK_ATTRIBUTE keyAttribs[] = { + { CKA_TOKEN, &bToken, sizeof(bToken) }, + { CKA_PRIVATE, &bPrivate, sizeof(bPrivate) }, + { CKA_ENCRYPT, &bTrue, sizeof(bTrue) }, + { CKA_DECRYPT, &bTrue, sizeof(bTrue) }, + }; + + hKey = CK_INVALID_HANDLE; + return CRYPTOKI_F_PTR( C_GenerateKey(hSession, &mechanism, + keyAttribs, sizeof(keyAttribs)/sizeof(CK_ATTRIBUTE), + &hKey) ); +} + inline CK_RV SymmetricAlgorithmTests::importDesKey(CK_SESSION_HANDLE hSession, CK_BBOOL bToken, CK_BBOOL bPrivate, CK_OBJECT_HANDLE &hKey, const Bytes & vKeyValue ) { @@ -1737,6 +1756,258 @@ void SymmetricAlgorithmTests::testAesEncryptDecrypt() } +void SymmetricAlgorithmTests::testChaCha20Poly1305EncryptDecrypt() +{ + CK_RV rv; + CK_SESSION_HANDLE hSessionRO; + + CK_BYTE chachaNonce[] = { + 0xCA, 0xFE, 0xBA, 0xBE, 0xFA, 0xCE, + 0xDB, 0xAD, 0xDE, 0xCA, 0xF8, 0x88 + }; + CK_BYTE chachaAAD[] = { + 0xFE, 0xED, 0xFA, 0xCE, 0xDE, 0xAD, 0xBE, 0xEF, + 0xFE, 0xED, 0xFA, 0xCE, 0xDE, 0xAD, 0xBE, 0xEF, + 0xAB, 0xAD, 0xDA, 0xD2 + }; + CK_SALSA20_CHACHA20_POLY1305_PARAMS chachaParamsWithAAD = + { + &chachaNonce[0], + sizeof(chachaNonce)*8, + &chachaAAD[0], + sizeof(chachaAAD) + }; + CK_SALSA20_CHACHA20_POLY1305_PARAMS chachaParamsWithoutAAD = + { + &chachaNonce[0], + sizeof(chachaNonce)*8, + NULL_PTR, + 0 + }; + + // Just make sure that we finalize any previous tests + CRYPTOKI_F_PTR( C_Finalize(NULL_PTR) ); + + // Initialize the library and start the test. + rv = CRYPTOKI_F_PTR( C_Initialize(NULL_PTR) ); + CPPUNIT_ASSERT(rv == CKR_OK); + + // Open read-only session + rv = CRYPTOKI_F_PTR( C_OpenSession(m_initializedTokenSlotID, CKF_SERIAL_SESSION, NULL_PTR, NULL_PTR, &hSessionRO) ); + CPPUNIT_ASSERT(rv == CKR_OK); + + // Login USER into the session so we can create a private object + rv = CRYPTOKI_F_PTR( C_Login(hSessionRO,CKU_USER,m_userPin1,m_userPin1Length) ); + CPPUNIT_ASSERT(rv==CKR_OK); + + CK_OBJECT_HANDLE hKey = CK_INVALID_HANDLE; + + rv = generateChaCha20Key(hSessionRO,IN_SESSION,IS_PUBLIC,hKey); + CPPUNIT_ASSERT(rv == CKR_OK); + + // ChaCha20-Poly1305 is a stream cipher; reuse the AES block size purely + // to size the multi-part chunks exercised by the test helper. + const int blockSize(0x10); + + encryptDecrypt({CKM_CHACHA20_POLY1305,&chachaParamsWithAAD,sizeof(chachaParamsWithAAD)},blockSize,hSessionRO,hKey,blockSize*NR_OF_BLOCKS_IN_TEST-1); + encryptDecrypt({CKM_CHACHA20_POLY1305,&chachaParamsWithAAD,sizeof(chachaParamsWithAAD)},blockSize,hSessionRO,hKey,blockSize*NR_OF_BLOCKS_IN_TEST+1); + encryptDecrypt({CKM_CHACHA20_POLY1305,&chachaParamsWithAAD,sizeof(chachaParamsWithAAD)},blockSize,hSessionRO,hKey,blockSize*NR_OF_BLOCKS_IN_TEST); + encryptDecrypt({CKM_CHACHA20_POLY1305,&chachaParamsWithoutAAD,sizeof(chachaParamsWithoutAAD)},blockSize,hSessionRO,hKey,blockSize*NR_OF_BLOCKS_IN_TEST-1); + encryptDecrypt({CKM_CHACHA20_POLY1305,&chachaParamsWithoutAAD,sizeof(chachaParamsWithoutAAD)},blockSize,hSessionRO,hKey,blockSize*NR_OF_BLOCKS_IN_TEST+1); + encryptDecrypt({CKM_CHACHA20_POLY1305,&chachaParamsWithoutAAD,sizeof(chachaParamsWithoutAAD)},blockSize,hSessionRO,hKey,blockSize*NR_OF_BLOCKS_IN_TEST); +} + +static SymmetricAlgorithmTests::Bytes hexToBytes(const char* hex) +{ + SymmetricAlgorithmTests::Bytes out; + for (size_t i = 0; hex[i] != '\0' && hex[i+1] != '\0'; i += 2) + { + char byte[3] = { hex[i], hex[i+1], '\0' }; + out.push_back((CK_BYTE)strtoul(byte, NULL, 16)); + } + return out; +} + +void SymmetricAlgorithmTests::testChaCha20Poly1305KnownAnswer() +{ + // Test vectors from RFC 8439: key, nonce, plaintext, AAD, ciphertext || tag + static const char* testVectors[2][5] = + { + // Section 2.8.2 (nonce is the 32-bit constant 07000000 || IV 4041424344454647) + { + "808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9F", + "070000004041424344454647", + "4C616469657320616E642047656E746C656D656E206F662074686520636C617373206F66202739393A204966204920636F756C64206F6666657220796F75206F6E6C79206F6E652074697020666F7220746865206675747572652C2073756E73637265656E20776F756C642062652069742E", + "50515253C0C1C2C3C4C5C6C7", + "D31A8D34648E60DB7B86AFBC53EF7EC2A4ADED51296E08FEA9E2B5A736EE62D63DBEA45E8CA9671282FAFB69DA92728B1A71DE0A9E060B2905D6A5B67ECD3B3692DDBD7F2D778B8C9803AEE328091B58FAB324E4FAD675945585808B4831D7BC3FF4DEF08E4B7A9DE576D26586CEC64B6116" + "1AE10B594F09E26A7E902ECBD0600691" + }, + // Appendix A.5 + { + "1C9240A5EB55D38AF333888604F6B5F0473917C1402B80099DCA5CBC207075C0", + "000000000102030405060708", + "496E7465726E65742D4472616674732061726520647261667420646F63756D656E74732076616C696420666F722061206D6178696D756D206F6620736978206D6F6E74687320616E64206D617920626520757064617465642C207265706C616365642C206F72206F62736F6C65746564206279206F7468657220646F63756D656E747320617420616E792074696D652E20497420697320696E617070726F70726961746520746F2075736520496E7465726E65742D447261667473206173207265666572656E6365206D6174657269616C206F7220746F2063697465207468656D206F74686572207468616E206173202FE2809C776F726B20696E2070726F67726573732E2FE2809D", + "F33388860000000000004E91", + "64A0861575861AF460F062C79BE643BD5E805CFD345CF389F108670AC76C8CB24C6CFC18755D43EEA09EE94E382D26B0BDB7B73C321B0100D4F03B7F355894CF332F830E710B97CE98C8A84ABD0B948114AD176E008D33BD60F982B1FF37C8559797A06EF4F0EF61C186324E2B3506383606907B6A7C02B0F9F6157B53C867E4B9166C767B804D46A59B5216CDE7A4E99040C5A40433225EE282A1B0A06C523EAF4534D7F83FA1155B0047718CBC546A0D072B04B3564EEA1B422273F548271A0BB2316053FA76991955EBD63159434ECEBB4E466DAE5A1073A6727627097A1049E617D91D361094FA68F0FF77987130305BEABA2EDA04DF997B714D6C6F2C29A6AD5CB4022B02709B" + "EEAD9D67890CBB22392336FEA1851F38" + } + }; + + CK_RV rv; + CK_SESSION_HANDLE hSessionRO; + + // Just make sure that we finalize any previous tests + CRYPTOKI_F_PTR( C_Finalize(NULL_PTR) ); + + // Initialize the library and start the test. + rv = CRYPTOKI_F_PTR( C_Initialize(NULL_PTR) ); + CPPUNIT_ASSERT(rv == CKR_OK); + + // Open read-only session + rv = CRYPTOKI_F_PTR( C_OpenSession(m_initializedTokenSlotID, CKF_SERIAL_SESSION, NULL_PTR, NULL_PTR, &hSessionRO) ); + CPPUNIT_ASSERT(rv == CKR_OK); + + // Login USER into the session so we can create a private object + rv = CRYPTOKI_F_PTR( C_Login(hSessionRO,CKU_USER,m_userPin1,m_userPin1Length) ); + CPPUNIT_ASSERT(rv==CKR_OK); + + for (int i = 0; i < 2; i++) + { + Bytes vKey = hexToBytes(testVectors[i][0]); + Bytes vNonce = hexToBytes(testVectors[i][1]); + Bytes vPlainText = hexToBytes(testVectors[i][2]); + Bytes vAAD = hexToBytes(testVectors[i][3]); + Bytes vCipherText = hexToBytes(testVectors[i][4]); + + // Import the known key + CK_BBOOL bTrue = CK_TRUE; + CK_BBOOL bFalse = CK_FALSE; + CK_OBJECT_CLASS secretClass = CKO_SECRET_KEY; + CK_KEY_TYPE keyType = CKK_CHACHA20; + CK_ATTRIBUTE keyAttribs[] = { + { CKA_TOKEN, &bFalse, sizeof(bFalse) }, + { CKA_PRIVATE, &bTrue, sizeof(bTrue) }, + { CKA_CLASS, &secretClass, sizeof(secretClass) }, + { CKA_KEY_TYPE, &keyType, sizeof(keyType) }, + { CKA_ENCRYPT, &bTrue, sizeof(bTrue) }, + { CKA_DECRYPT, &bTrue, sizeof(bTrue) }, + { CKA_VALUE, vKey.data(), vKey.size() } + }; + CK_OBJECT_HANDLE hKey = CK_INVALID_HANDLE; + rv = CRYPTOKI_F_PTR( C_CreateObject(hSessionRO, keyAttribs, sizeof(keyAttribs)/sizeof(CK_ATTRIBUTE), &hKey) ); + CPPUNIT_ASSERT(rv == CKR_OK); + + CK_SALSA20_CHACHA20_POLY1305_PARAMS params = + { + vNonce.data(), + vNonce.size()*8, + vAAD.data(), + vAAD.size() + }; + CK_MECHANISM mechanism = { CKM_CHACHA20_POLY1305, ¶ms, sizeof(params) }; + CK_ULONG ulLen; + + // Single-part encryption must produce the RFC ciphertext and tag + rv = CRYPTOKI_F_PTR( C_EncryptInit(hSessionRO, &mechanism, hKey) ); + CPPUNIT_ASSERT(rv == CKR_OK); + rv = CRYPTOKI_F_PTR( C_Encrypt(hSessionRO, vPlainText.data(), vPlainText.size(), NULL_PTR, &ulLen) ); + CPPUNIT_ASSERT(rv == CKR_OK); + CPPUNIT_ASSERT(ulLen == vCipherText.size()); + Bytes vEncrypted(ulLen); + rv = CRYPTOKI_F_PTR( C_Encrypt(hSessionRO, vPlainText.data(), vPlainText.size(), vEncrypted.data(), &ulLen) ); + CPPUNIT_ASSERT(rv == CKR_OK); + vEncrypted.resize(ulLen); + CPPUNIT_ASSERT(vEncrypted == vCipherText); + + // Single-part decryption must recover the RFC plaintext + rv = CRYPTOKI_F_PTR( C_DecryptInit(hSessionRO, &mechanism, hKey) ); + CPPUNIT_ASSERT(rv == CKR_OK); + rv = CRYPTOKI_F_PTR( C_Decrypt(hSessionRO, vCipherText.data(), vCipherText.size(), NULL_PTR, &ulLen) ); + CPPUNIT_ASSERT(rv == CKR_OK); + Bytes vDecrypted(ulLen); + rv = CRYPTOKI_F_PTR( C_Decrypt(hSessionRO, vCipherText.data(), vCipherText.size(), vDecrypted.data(), &ulLen) ); + CPPUNIT_ASSERT(rv == CKR_OK); + vDecrypted.resize(ulLen); + CPPUNIT_ASSERT(vDecrypted == vPlainText); + + // Multi-part encryption with chunks that do not line up with the + // 16-byte Poly1305 block or the 64-byte ChaCha20 block + const CK_ULONG chunk = 7; + rv = CRYPTOKI_F_PTR( C_EncryptInit(hSessionRO, &mechanism, hKey) ); + CPPUNIT_ASSERT(rv == CKR_OK); + vEncrypted.clear(); + for (CK_ULONG pos = 0; pos < vPlainText.size(); pos += chunk) + { + CK_ULONG len = std::min(chunk, vPlainText.size() - pos); + Bytes vOut(vCipherText.size()); + ulLen = vOut.size(); + rv = CRYPTOKI_F_PTR( C_EncryptUpdate(hSessionRO, &vPlainText[pos], len, vOut.data(), &ulLen) ); + CPPUNIT_ASSERT(rv == CKR_OK); + vEncrypted.insert(vEncrypted.end(), vOut.begin(), vOut.begin() + ulLen); + } + { + Bytes vOut(vCipherText.size()); + ulLen = vOut.size(); + rv = CRYPTOKI_F_PTR( C_EncryptFinal(hSessionRO, vOut.data(), &ulLen) ); + CPPUNIT_ASSERT(rv == CKR_OK); + vEncrypted.insert(vEncrypted.end(), vOut.begin(), vOut.begin() + ulLen); + } + CPPUNIT_ASSERT(vEncrypted == vCipherText); + + // Multi-part decryption + rv = CRYPTOKI_F_PTR( C_DecryptInit(hSessionRO, &mechanism, hKey) ); + CPPUNIT_ASSERT(rv == CKR_OK); + vDecrypted.clear(); + for (CK_ULONG pos = 0; pos < vCipherText.size(); pos += chunk) + { + CK_ULONG len = std::min(chunk, vCipherText.size() - pos); + Bytes vOut(vCipherText.size()); + ulLen = vOut.size(); + rv = CRYPTOKI_F_PTR( C_DecryptUpdate(hSessionRO, &vCipherText[pos], len, vOut.data(), &ulLen) ); + CPPUNIT_ASSERT(rv == CKR_OK); + vDecrypted.insert(vDecrypted.end(), vOut.begin(), vOut.begin() + ulLen); + } + { + Bytes vOut(vCipherText.size()); + ulLen = vOut.size(); + rv = CRYPTOKI_F_PTR( C_DecryptFinal(hSessionRO, vOut.data(), &ulLen) ); + CPPUNIT_ASSERT(rv == CKR_OK); + vDecrypted.insert(vDecrypted.end(), vOut.begin(), vOut.begin() + ulLen); + } + CPPUNIT_ASSERT(vDecrypted == vPlainText); + + // A tampered tag must be rejected + Bytes vTampered(vCipherText); + vTampered.back() ^= 0x01; + rv = CRYPTOKI_F_PTR( C_DecryptInit(hSessionRO, &mechanism, hKey) ); + CPPUNIT_ASSERT(rv == CKR_OK); + vDecrypted.assign(vTampered.size(), 0); + ulLen = vDecrypted.size(); + rv = CRYPTOKI_F_PTR( C_Decrypt(hSessionRO, vTampered.data(), vTampered.size(), vDecrypted.data(), &ulLen) ); + CPPUNIT_ASSERT(rv == CKR_ENCRYPTED_DATA_INVALID); + + // A tampered AAD must be rejected + Bytes vBadAAD(vAAD); + vBadAAD[0] ^= 0x01; + CK_SALSA20_CHACHA20_POLY1305_PARAMS badParams = + { + vNonce.data(), + vNonce.size()*8, + vBadAAD.data(), + vBadAAD.size() + }; + CK_MECHANISM badMechanism = { CKM_CHACHA20_POLY1305, &badParams, sizeof(badParams) }; + rv = CRYPTOKI_F_PTR( C_DecryptInit(hSessionRO, &badMechanism, hKey) ); + CPPUNIT_ASSERT(rv == CKR_OK); + ulLen = vDecrypted.size(); + rv = CRYPTOKI_F_PTR( C_Decrypt(hSessionRO, vCipherText.data(), vCipherText.size(), vDecrypted.data(), &ulLen) ); + CPPUNIT_ASSERT(rv == CKR_ENCRYPTED_DATA_INVALID); + + rv = CRYPTOKI_F_PTR( C_DestroyObject(hSessionRO, hKey) ); + CPPUNIT_ASSERT(rv == CKR_OK); + } +} + void SymmetricAlgorithmTests::testAesWrapUnwrap() { CK_RV rv; diff --git a/src/lib/test/SymmetricAlgorithmTests.h b/src/lib/test/SymmetricAlgorithmTests.h index 7d60fb576..e04ac7da2 100644 --- a/src/lib/test/SymmetricAlgorithmTests.h +++ b/src/lib/test/SymmetricAlgorithmTests.h @@ -43,6 +43,8 @@ class SymmetricAlgorithmTests : public TestsBase { CPPUNIT_TEST_SUITE(SymmetricAlgorithmTests); CPPUNIT_TEST(testAesEncryptDecrypt); + CPPUNIT_TEST(testChaCha20Poly1305EncryptDecrypt); + CPPUNIT_TEST(testChaCha20Poly1305KnownAnswer); CPPUNIT_TEST(testDesEncryptDecrypt); #ifdef HAVE_AES_KEY_WRAP CPPUNIT_TEST(testAesWrapUnwrap); @@ -61,6 +63,8 @@ class SymmetricAlgorithmTests : public TestsBase using Bytes = std::vector; void testAesEncryptDecrypt(); + void testChaCha20Poly1305EncryptDecrypt(); + void testChaCha20Poly1305KnownAnswer(); void testDesEncryptDecrypt(); void testAesWrapUnwrap(); void testDesWrapUnwrap(); @@ -75,6 +79,7 @@ class SymmetricAlgorithmTests : public TestsBase protected: CK_RV generateGenericKey(CK_SESSION_HANDLE hSession, CK_BBOOL bToken, CK_BBOOL bPrivate, CK_OBJECT_HANDLE &hKey); CK_RV generateAesKey(CK_SESSION_HANDLE hSession, CK_BBOOL bToken, CK_BBOOL bPrivate, CK_OBJECT_HANDLE &hKey); + CK_RV generateChaCha20Key(CK_SESSION_HANDLE hSession, CK_BBOOL bToken, CK_BBOOL bPrivate, CK_OBJECT_HANDLE &hKey); #ifndef WITH_FIPS CK_RV generateDesKey(CK_SESSION_HANDLE hSession, CK_BBOOL bToken, CK_BBOOL bPrivate, CK_OBJECT_HANDLE &hKey); CK_RV generateDes2Key(CK_SESSION_HANDLE hSession, CK_BBOOL bToken, CK_BBOOL bPrivate, CK_OBJECT_HANDLE &hKey);