Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/core/common/config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,18 @@
#define AOS_CONFIG_TYPES_FILE_CHUNK_SIZE 64 * 1024
#endif

/**
* Chunk size used when streaming ciphertext into a multi-part PKCS11 decrypt operation (see
* aos::sm::imagemanager::BlobDecryptor). Deliberately smaller than AOS_CONFIG_TYPES_FILE_CHUNK_SIZE: some
* PKCS11 modules (e.g. a TEE-backed one with a limited shared memory budget) reject a single
* C_DecryptUpdate call above a certain size with CKR_DEVICE_MEMORY - empirically, one such module accepted
* up to ~34 KiB and rejected 36 KiB+. Override per platform if a target's PKCS11 module is known to allow
* more (or needs less).
*/
#ifndef AOS_CONFIG_IMAGEMANAGER_DECRYPT_CHUNK_SIZE
#define AOS_CONFIG_IMAGEMANAGER_DECRYPT_CHUNK_SIZE 16 * 1024
#endif

/**
* File system mount type len.
*/
Expand Down
111 changes: 110 additions & 1 deletion src/core/common/crypto/itf/privkey.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
#define AOS_CORE_COMMON_CRYPTO_ITF_PRIVKEY_HPP_

#include <core/common/config.hpp>
#include <core/common/tools/array.hpp>
#include <core/common/tools/enum.hpp>
#include <core/common/tools/string.hpp>
#include <core/common/tools/variant.hpp>

#include "aes.hpp"
#include "hash.hpp"

namespace aos::crypto {
Expand Down Expand Up @@ -77,10 +79,90 @@ struct OAEPDecryptionOptions {
Hash mHash;
};

/**
* AES-GCM decryption options.
*/
struct GCMDecryptionOptions {
/**
* GCM initialization vector (nonce): AESCipherItf::cGCMIVSize (12) bytes.
*/
StaticArray<uint8_t, AESCipherItf::cGCMIVSize> mIV;
};

/**
* AES-CTR decryption options.
*/
struct CTRDecryptionOptions {
/**
* Initial counter block: AESCipherItf::cBlockSize (16) bytes, incremented as a single big-endian 128-bit
* value for each subsequent block.
*/
StaticArray<uint8_t, AESCipherItf::cBlockSize> mCounter;
};

/**
* Decryption options.
*/
using DecryptionOptions = Variant<PKCS1v15DecryptionOptions, OAEPDecryptionOptions>;
using DecryptionOptions
= Variant<PKCS1v15DecryptionOptions, OAEPDecryptionOptions, GCMDecryptionOptions, CTRDecryptionOptions>;

/**
* Supplies data chunks on demand to a streaming operation (see PrivateKeyItf::StreamDecrypt), so the
* whole input never needs to be held in memory at once. The provider owns the chunk buffer itself (sized
* and allocated however its own implementation sees fit) rather than requiring the caller to supply one:
* on a stack-constrained target, a caller-supplied buffer sized for a whole chunk (tens of KiB) can blow a
* function's stack budget, whereas a concrete provider (e.g. one backed by a file) can size its buffer via
* whatever AllocatorItf it already has.
*/
class ChunkProviderItf {
public:
/**
* Returns the next chunk of data. The returned array is only valid until the next call to
* NextChunk or until this provider is destroyed - callers must consume it before calling again.
*
* @return RetWithError<Array<uint8_t>>. ErrorEnum::eEOF (with an empty array) once no more data
* remains; a call that delivers the last chunk returns ErrorEnum::eNone, even if that chunk is
* short.
*/
virtual RetWithError<Array<uint8_t>> NextChunk() = 0;

/**
* Destroys object instance.
*/
virtual ~ChunkProviderItf() = default;
};

/**
* Receives data chunks produced by a streaming operation (see PrivateKeyItf::StreamDecrypt), so the caller
* can handle output (e.g. write it to a file) as it comes back instead of collecting it into a single array.
* Like ChunkProviderItf, the receiver owns the buffer output is written into: the streaming operation writes
* each produced chunk into GetBuffer() and then hands it back via OnChunk.
*/
class ChunkReceiverItf {
public:
/**
* Returns the buffer the next output chunk is written into. Its MaxSize bounds how much output a single
* step of the operation may produce: many PKCS11 tokens only release AEAD-decrypted data all at once,
* at the very end, so for those it must have capacity for the whole output.
*
* @return Array<uint8_t>&.
*/
virtual Array<uint8_t>& GetBuffer() = 0;

/**
* Handles the next output chunk. chunk is a view into GetBuffer() and is only valid until this call
* returns. Never called with an empty chunk.
*
* @param chunk output chunk.
* @return Error. Any error aborts the streaming operation and is returned to its caller.
*/
virtual Error OnChunk(const Array<uint8_t>& chunk) = 0;

/**
* Destroys object instance.
*/
virtual ~ChunkReceiverItf() = default;
};

/**
* Public key interface.
Expand Down Expand Up @@ -141,6 +223,33 @@ class PrivateKeyItf {
virtual Error Decrypt(const Array<uint8_t>& cipher, const DecryptionOptions& options, Array<uint8_t>& result) const
= 0;

/**
* Decrypts a cipher message supplied incrementally by chunkProvider, so the whole ciphertext
* doesn't need to be held in memory at once - only whatever chunk size chunkProvider hands back
* at a time. Decrypted data is handed to chunkReceiver as it becomes available. This doesn't
* necessarily bound *output* memory the same way: many PKCS11 tokens only release AEAD-decrypted
* data once the authentication tag has been verified, all at once, at the very end, so
* chunkReceiver's buffer must still have capacity for the whole plaintext regardless of how the
* input was chunked (see pkcs11::AESPrivateKey for the concrete behavior). Note that, for tokens
* that do release data early, chunkReceiver may get plaintext before the tag is verified: it must
* not trust it until StreamDecrypt returns successfully. Default implementation returns
* ErrorEnum::eNotSupported; only override it where streaming input actually helps.
*
* @param chunkProvider supplies the cipher message in chunks.
* @param options decryption options.
* @param chunkReceiver receives the decoded message in chunks.
* @return Error.
*/
virtual Error StreamDecrypt(
ChunkProviderItf& chunkProvider, const DecryptionOptions& options, ChunkReceiverItf& chunkReceiver) const
{
(void)chunkProvider;
(void)options;
(void)chunkReceiver;

return ErrorEnum::eNotSupported;
}

/**
* Destroys object instance.
*/
Expand Down
14 changes: 14 additions & 0 deletions src/core/common/crypto/mbedtls/cryptoprovider.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1874,6 +1874,20 @@ Error MbedTLSCryptoProvider::MbedTLSRSAPrivKey::Decrypt(
return ErrorEnum::eNone;
}

Error Visit(const GCMDecryptionOptions& opts) const
{
(void)opts;

return AOS_ERROR_WRAP(ErrorEnum::eNotSupported);
}

Error Visit(const CTRDecryptionOptions& opts) const
{
(void)opts;

return AOS_ERROR_WRAP(ErrorEnum::eNotSupported);
}

mbedtls_pk_context* mPrivKey = nullptr;
mbedtls_ctr_drbg_context* mDRBG = nullptr;

Expand Down
14 changes: 14 additions & 0 deletions src/core/common/crypto/openssl/cryptoprovider.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2790,6 +2790,20 @@ Error OpenSSLCryptoProvider::OpenSSLRSAPrivKey::Decrypt(
return ErrorEnum::eNone;
}

Error Visit(const GCMDecryptionOptions& opts) const
{
(void)opts;

return AOS_ERROR_WRAP(ErrorEnum::eNotSupported);
}

Error Visit(const CTRDecryptionOptions& opts) const
{
(void)opts;

return AOS_ERROR_WRAP(ErrorEnum::eNotSupported);
}

private:
EVP_PKEY* mPrivKey = nullptr;
const Array<uint8_t>& mCipher;
Expand Down
83 changes: 83 additions & 0 deletions src/core/common/crypto/tests/certloader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <gtest/gtest.h>

#include <core/common/crypto/certloader.hpp>
#include <core/common/pkcs11/pkcs11.hpp>
#include <core/common/tests/crypto/providers/cryptofactory.hpp>
#include <core/common/tests/crypto/softhsmenv.hpp>
#include <core/common/tests/utils/log.hpp>
Expand Down Expand Up @@ -70,6 +71,49 @@ class CertloaderTest : public Test {
.IsNone());
}

// Provisions a non-extractable CKO_SECRET_KEY (AES) object: the posture LoadPrivKeyByURL/FindPrivateKey
// actually search for (alongside CKO_PRIVATE_KEY), and the only one production provisioning is expected
// to create. The key's own value is never read back by anything under test here (crypto::PrivateKeyItf
// never exposes it).
void WriteSecretKeyObject(const Array<uint8_t>& id, const String& label, const Array<uint8_t>& value)
{
Error err = ErrorEnum::eNone;
SharedPtr<pkcs11::SessionContext> session;

Tie(session, err) = mSoftHSMEnv.OpenUserSession(mPIN, true);
ASSERT_TRUE(err.IsNone());

CK_OBJECT_CLASS keyClass = CKO_SECRET_KEY;
CK_KEY_TYPE keyType = CKK_AES;
CK_BBOOL trueVal = CK_TRUE;
CK_BBOOL falseVal = CK_FALSE;

StaticArray<pkcs11::ObjectAttribute, 10> templ;

auto pushBytes = [&](pkcs11::AttributeType type, const void* data, size_t size) {
ASSERT_TRUE(
templ.PushBack({type, Array<uint8_t>(reinterpret_cast<uint8_t*>(const_cast<void*>(data)), size)})
.IsNone());
};

pushBytes(CKA_CLASS, &keyClass, sizeof(keyClass));
pushBytes(CKA_KEY_TYPE, &keyType, sizeof(keyType));
pushBytes(CKA_TOKEN, &trueVal, sizeof(trueVal));
pushBytes(CKA_PRIVATE, &trueVal, sizeof(trueVal));
pushBytes(CKA_EXTRACTABLE, &falseVal, sizeof(falseVal));
pushBytes(CKA_SENSITIVE, &trueVal, sizeof(trueVal));
pushBytes(CKA_ID, id.Get(), id.Size());
pushBytes(CKA_LABEL, label.Get(), label.Size());
ASSERT_TRUE(
templ.PushBack({CKA_VALUE, Array<uint8_t>(const_cast<uint8_t*>(value.Get()), value.Size())}).IsNone());

pkcs11::ObjectHandle handle = 0;
Error createErr = ErrorEnum::eNone;

Tie(handle, createErr) = session->CreateObject(templ);
ASSERT_TRUE(createErr.IsNone());
}

void GeneratePrivateKey(const Array<uint8_t>& id)
{
Error err;
Expand Down Expand Up @@ -339,4 +383,43 @@ TEST_F(CertloaderTest, FindCertificatesFromFile)
EXPECT_EQ(std::string(issuer.CStr()), std::string("CN=Aos Cloud"));
}

TEST_F(CertloaderTest, FindPKCS11SecretKey)
{
constexpr uint8_t id[] = {0xDD, 0xEE, 0xFF};
constexpr uint8_t value[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20};

WriteSecretKeyObject(Array(id, ArraySize(id)), "aos-layer-key", Array(value, ArraySize(value)));

const auto url = "pkcs11:token=cryptoutils;object=aos-layer-key;id=%DD%EE%FF?module-path=" SOFTHSM2_LIB
"&pin-source="
+ std::string(mPINSource);

auto [key, err] = mCertLoader.LoadPrivKeyByURL(url.c_str());

// the key's own value is never exposed: success and a non-null handle is all there is to check here.
// Actually decrypting with it is covered by the pkcs11/sm/imagemanager round-trip tests.
ASSERT_TRUE(err.IsNone());
EXPECT_TRUE(key);
}

TEST_F(CertloaderTest, FindPKCS11SecretKeyNotFound)
{
constexpr uint8_t id[] = {0xDD, 0xEE, 0xFF};
constexpr uint8_t value[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20};

WriteSecretKeyObject(Array(id, ArraySize(id)), "aos-layer-key", Array(value, ArraySize(value)));

// matches neither the secret key above (wrong label) nor any RSA/ECDSA key pair.
const auto url = "pkcs11:token=cryptoutils;object=no-such-label;id=%DD%EE%FF?module-path=" SOFTHSM2_LIB
"&pin-source="
+ std::string(mPINSource);

auto [key, err] = mCertLoader.LoadPrivKeyByURL(url.c_str());

EXPECT_TRUE(err.Is(ErrorEnum::eNotFound));
EXPECT_FALSE(key);
}

} // namespace aos::crypto
20 changes: 20 additions & 0 deletions src/core/common/crypto/tests/cryptoprovider.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1256,6 +1256,26 @@ TEST_P(CryptoProviderTest, VerifyCACert)
ASSERT_TRUE(ValidateCACert(cert).IsNone());
}

TEST_P(CryptoProviderTest, RSAPrivKeyRejectsAESDecryptionOptions)
{
StaticString<cPrivKeyPEMLen> pem;

ASSERT_TRUE(fs::ReadFileToString(TEST_CERTIFICATES_DIR "/client.key", pem).IsNone());

auto [key, err] = mCryptoProvider->PEMToX509PrivKey(pem);
ASSERT_TRUE(err.IsNone());
ASSERT_TRUE(key);

StaticArray<uint8_t, 256> cipher;
StaticArray<uint8_t, 256> result;

ASSERT_TRUE(cipher.Resize(cipher.MaxSize(), 0).IsNone());

// AES-GCM/AES-CTR options only apply to a symmetric key: an RSA key rejects them without decrypting anything.
EXPECT_TRUE(key->Decrypt(cipher, DecryptionOptions {GCMDecryptionOptions {}}, result).Is(ErrorEnum::eNotSupported));
EXPECT_TRUE(key->Decrypt(cipher, DecryptionOptions {CTRDecryptionOptions {}}, result).Is(ErrorEnum::eNotSupported));
}

TEST_P(CryptoProviderTest, VerifyLeafCert)
{
StaticString<cCertPEMLen> buff;
Expand Down
1 change: 1 addition & 0 deletions src/core/common/ocispec/itf/imagespec.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ constexpr auto cRootfsTypeLen = AOS_CONFIG_OCISPEC_ROOTFS_TYPE_LEN;
*/
constexpr auto cMediaTypeLayerTar = "application/vnd.oci.image.layer.v1.tar";
constexpr auto cMediaTypeLayerTarGZip = "application/vnd.oci.image.layer.v1.tar+gzip";
constexpr auto cMediaTypeLayerTarGZipEncrypted = "application/vnd.aos.image.layer.enc.v1.aes256gcm+tar+gz";
constexpr auto cMediaTypeEmptyBlob = "application/vnd.oci.empty.v1+json";
constexpr auto cMediaTypeComponentFullTarGZip = "application/vnd.aos.image.component.full.v1+gzip";
constexpr auto cMediaTypeComponentFullSquashfs = "application/vnd.aos.image.component.full.v1+squashfs";
Expand Down
5 changes: 5 additions & 0 deletions src/core/common/pkcs11/cryptoki/pkcs11.h
Original file line number Diff line number Diff line change
Expand Up @@ -877,6 +877,11 @@ typedef struct CK_GCM_PARAMS {
unsigned long ulTagBits;
} CK_GCM_PARAMS;

typedef struct CK_AES_CTR_PARAMS {
unsigned long ulCounterBits;
unsigned char cb[16];
} CK_AES_CTR_PARAMS;

typedef unsigned long ck_rv_t;


Expand Down
Loading
Loading