Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
53a6898
Some fixes from ai security audit
May 20, 2026
47715fe
Fixed cdoc-tool index usage
May 22, 2026
fef2389
Update cdoc/cdoc-tool.cpp
lauris71 May 25, 2026
8848542
Update cdoc/cdoc-tool.cpp
lauris71 May 25, 2026
bf5ca62
Some more AI issue fixes
May 26, 2026
0a6b12b
Hardcode SSL timeout
May 27, 2026
a4dafb9
Fixed xstream uint overflow and tool logging
May 27, 2026
8c9b789
Some more fixes
May 27, 2026
e9a8a6e
Secure tool key handling, use explicid compile time definitions for k…
May 27, 2026
fa288ee
Fixed potential tar size overflow and secured proxy password
May 27, 2026
b89e4f1
Windows build fix
May 27, 2026
d0e0c90
Revert proxy password for now
May 27, 2026
87d1547
Make proxy password string_view
May 29, 2026
3631991
Disable potential Bleichenbacher attack for CDoc1 RSA encryption
Jun 1, 2026
7e6cdaa
Moved fix to main decryptRSA method
Jun 1, 2026
f01510a
Added ct.h
Jun 1, 2026
0aec4a1
Bleichenbacher fix for NCrypt backend
Jun 1, 2026
6f37761
Some cleanups
Jun 1, 2026
cc19133
All C,H & M fixes from Caludo Opus review
Jun 5, 2026
6bd12a8
Merge branch 'open-eid:master' into master
lauris71 Jun 5, 2026
8102224
Update cdoc-tool
Jun 5, 2026
c3d8dd2
Merge branch 'open-eid:master' into master
lauris71 Jun 11, 2026
bb9945c
Merge branch 'master' into ai-security
Jun 11, 2026
08ca9e5
Make default KDF iter 600000
Jun 11, 2026
004ce57
Added std_string_view.i
Jun 16, 2026
212825a
Fixed label parsing on Ubuntu 22
Jun 16, 2026
6db3372
Include <string.h>
Jun 17, 2026
8a2f494
Use explicit_bzero on glibc
Jun 17, 2026
573f147
Use SecureZeroMemory on windows
Jun 17, 2026
2d552cb
Use OPENSSL_cleanse for secure cleanup
Jun 17, 2026
f63ed77
Fixed inverted constant-time comparison
Jun 17, 2026
a6fc79a
Merge branch 'open-eid:master' into master
lauris71 Jun 17, 2026
88f1fa7
Merge commit 'a6fc79ad2e3c4d9d34157742f84db1c0c25faca3' into ai-security
Jun 17, 2026
c0626a0
Update cdoc/CDocCipher.cpp
metsma Jul 2, 2026
0e544fc
Fixed PKCS11 and NCrypt RSA handling and check all random() calls for…
Jul 2, 2026
3e62abe
Fixed PKCS11 public key loading
Jul 3, 2026
68cc522
Merge branch 'open-eid:master' into master
lauris71 Jul 3, 2026
5769447
Fixed label UTF-8 escaping if locale is not C
Jul 15, 2026
a7b7c6b
Merge branch 'open-eid:master' into master
lauris71 Jul 15, 2026
1225355
Fixed label generation for non-C locales
Jul 15, 2026
4ba85f1
Merge branch 'master' of github.com:lauris71/libcdoc
Jul 15, 2026
e5ac2a3
Merge branch 'open-eid:master' into master
lauris71 Jul 24, 2026
463bfd5
Merge branch 'master' into ai-security
Jul 24, 2026
dc23ffa
Added SecureTarget class
Jul 24, 2026
139d1a7
Fixed N1 (out-of-bounds read) from 2026-07 report
Jul 27, 2026
e37902c
Added Utils.cpp to test build
Jul 27, 2026
e7ad099
Delete reader and writer in TestPasswordWithlabel
Jul 27, 2026
5743645
Clean up z_stream on zlib errors
Jul 27, 2026
6ae5ab9
Fixed N3 (base64 decode throws on errors) from 202607 report
Jul 27, 2026
e2d7828
Minor fixes: removed AES-CBC, N4, N5
Aug 17, 2026
b47770c
fixed N7 (zstream max size), N8 (PBKDF limits), N9 (logging), N10 (th…
Aug 17, 2026
662da29
Added comment about N12 (OpenSSL fast-path timing), fixed N13 (decode…
Aug 17, 2026
311be5b
Fixed N17 (time parsing errors) and N18 (clean FMK in CDocCipher)
Aug 17, 2026
fb37985
Fixed N20 (handle exactly 100 byte tar filenames), N21 (treat PKCS11 …
Aug 18, 2026
0892d09
Minor logging cleanups
Aug 18, 2026
9743447
fixed libcoc::LOG...
Aug 18, 2026
11bfdad
Remove apostrofes from numberl literals
Aug 18, 2026
dce0136
Use swig workaround for new configuration keys
Aug 18, 2026
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
14 changes: 14 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,20 @@ jobs:
with:
name: ${{ matrix.target }}
path: ${{ env.DEST }}
sanitizers:
name: Run unit tests with ASan+UBSan
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install dependencies
run: sudo apt update -qq && sudo apt install --no-install-recommends -y ninja-build libboost-test-dev ${UBUNTU_DEPS}
- name: Configure
run: cmake --preset sanitize
- name: Build
run: cmake --build --preset sanitize --target unittests
- name: Test
run: ctest --test-dir build/sanitize --output-on-failure
windows:
name: Build on Windows
runs-on: ${{ matrix.image }}
Expand Down
19 changes: 19 additions & 0 deletions CMakePresets.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,29 @@
"CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
"VCPKG_TARGET_TRIPLET": "$env{PLATFORM}-windows-static-md"
}
},
{
"name": "sanitize",
"displayName": "Sanitizers (ASan+UBSan)",
"description": "Debug build with AddressSanitizer and UndefinedBehaviorSanitizer for running the unit tests (system dependencies, no vcpkg; requires boost-test, openssl, libxml2, zlib and flatbuffers development packages)",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/${presetName}",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_CXX_FLAGS": "-fsanitize=address,undefined -fno-omit-frame-pointer",
"CMAKE_EXE_LINKER_FLAGS": "-fsanitize=address,undefined",
"CMAKE_SHARED_LINKER_FLAGS": "-fsanitize=address,undefined",
"CMAKE_DISABLE_FIND_PACKAGE_SWIG": "YES",
"CMAKE_DISABLE_FIND_PACKAGE_Doxygen": "YES"
}
}
],
"buildPresets":
[
{
"name": "sanitize",
"configurePreset": "sanitize"
},
{
"name": "macos",
"configurePreset": "macos"
Expand Down
72 changes: 47 additions & 25 deletions cdoc/CDoc1Reader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include "CDoc1Reader.h"

#include "Certificate.h"
#include "Configuration.h"
#include "Crypto.h"
#include "CryptoBackend.h"
#include "DDocReader.h"
Expand All @@ -28,7 +29,9 @@
#include "utils/memory.h"

#include <openssl/evp.h>
#include <openssl/sha.h>

#include <array>
#include <map>
#include <span>

Expand All @@ -38,8 +41,11 @@ constexpr std::string_view MIME_ZLIB = "http://www.isi.edu/in-noes/iana/assignme
constexpr std::string_view MIME_DDOC = "http://www.sk.ee/DigiDoc/v1.3.0/digidoc.xsd";
constexpr std::string_view MIME_DDOC_OLD = "http://www.sk.ee/DigiDoc/1.3.0/digidoc.xsd";

// CDoc 1.0 used AES-CBC; CDoc 1.1 switched to AES-GCM and all CDoc 1.0
// containers have long since expired. AES-CBC also has no authentication,
// which makes it a poor fit for the "reject at the body" FMK-oracle
// defence documented below. We therefore do not accept CBC containers.
constexpr std::array SUPPORTED_METHODS {
libcdoc::Crypto::AES128CBC_MTH, libcdoc::Crypto::AES192CBC_MTH, libcdoc::Crypto::AES256CBC_MTH,
libcdoc::Crypto::AES128GCM_MTH, libcdoc::Crypto::AES192GCM_MTH, libcdoc::Crypto::AES256GCM_MTH
};

Expand All @@ -64,6 +70,11 @@ struct CDoc1Reader::Private
int64_t f_pos = -1;
std::unique_ptr<libcdoc::VectorSource> src;

// N10: RSA oracle throttle state. Set during getFMK when the lock is
// RSA; empty otherwise (no throttle for ECC/AES-wrap paths).
bool is_rsa = false;
std::string throttle_key_id;

~Private()
{
if (src_owned) delete dsrc;
Expand Down Expand Up @@ -115,10 +126,10 @@ CDoc1Reader::getFMK(std::vector<uint8_t>& fmk, unsigned int lock_idx)
setLastError({});

// Determine the FMK length from the container's body cipher. The CDoc1
// body uses AES-128/192/256 in CBC or GCM mode, so the FMK is 16, 24
// or 32 bytes long. We pin this length up-front and pass it to the RSA
// decrypt path so that an attacker observing this function cannot
// distinguish between
// body uses AES-128/192/256 in GCM mode (CBC was only used by CDoc 1.0,
// which we no longer accept), so the FMK is 16, 24 or 32 bytes long.
// We pin this length up-front and pass it to the RSA decrypt path so
// that an attacker observing this function cannot distinguish between
// (a) RSA padding failed
// (b) RSA padding succeeded but the resulting length was wrong
// (c) a wholly different recipient was used to derive a wrong key.
Expand All @@ -127,9 +138,7 @@ CDoc1Reader::getFMK(std::vector<uint8_t>& fmk, unsigned int lock_idx)
// candidate FMK of the right length, and the eventual AES decrypt at
// the container body level either authenticates that FMK (success) or
// rejects it. CDoc1 has no header HMAC, so the AES-GCM tag is the
// only bit of authentication we can rely on. AES-CBC containers
// therefore retain a residual oracle (PKCS#7 stripping); using GCM
// when re-encrypting with libcdoc is strongly preferred.
// only bit of authentication we can rely on.
size_t expected_fmk_len = 0;
if (const EVP_CIPHER *c = libcdoc::Crypto::cipher(d->method); c) {
expected_fmk_len = size_t(EVP_CIPHER_key_length(c));
Expand Down Expand Up @@ -163,23 +172,34 @@ CDoc1Reader::getFMK(std::vector<uint8_t>& fmk, unsigned int lock_idx)
// Even on "OK" the contents may be synthetic - that is the point.
// The downstream AES decrypt at the body level is what tells
// success from failure.
//
// N10: mark this lock as RSA and compute a stable key identifier
// for the oracle throttle. The identifier is a SHA-256 hash of the
// recipient's public key, so different RSA keys have independent
// throttle intervals and no cross-tenant DoS is possible.
d->is_rsa = true;
std::array<uint8_t, 32> hash{};
SHA256(lock.getBytes(Lock::Params::RCPT_KEY).data(),
lock.getBytes(Lock::Params::RCPT_KEY).size(), hash.data());
d->throttle_key_id = toHex(hash);
} else {
std::vector<uint8_t> key;
int result = crypto->deriveConcatKDF(key,
d->is_rsa = false;
d->throttle_key_id.clear();
SecureTarget key;
int result = crypto->deriveConcatKDF(key.getTarget(),
lock.getBytes(Lock::Params::KEY_MATERIAL),
lock.getString(Lock::Params::CONCAT_DIGEST),
lock.getBytes(Lock::Params::ALGORITHM_ID),
lock.getBytes(Lock::Params::PARTY_UINFO),
lock.getBytes(Lock::Params::PARTY_VINFO),
lock_idx);
if (result < 0) {
libcdoc::cleanse(key);
setLastError(FAIL_MSG);
LOG_ERROR("{}", last_error);
return libcdoc::CRYPTO_ERROR;
}
fmk = libcdoc::Crypto::AESWrap(key, lock.encrypted_fmk, false);
libcdoc::cleanse(key);
key.cleanse();
// AESWrap returns {} on failure. Pad the candidate to expected
// length so the failure shape matches the RSA path; the bytes
// are arbitrary because the body decrypt is going to reject
Expand Down Expand Up @@ -399,7 +419,6 @@ CDoc1Reader::isCDoc1File(libcdoc::DataSource *src)
result_t CDoc1Reader::decryptData(const std::vector<uint8_t>& fmk,
const std::function<libcdoc::result_t(libcdoc::DataSource &src, const std::string &mime)>& f)
{
setLastError({});
if (fmk.empty()) {
setLastError("FMK is missing");
return libcdoc::WRONG_ARGUMENTS;
Expand All @@ -414,9 +433,10 @@ result_t CDoc1Reader::decryptData(const std::vector<uint8_t>& fmk,
return libcdoc::WORKFLOW_ERROR;
}
if (auto result = d->dsrc->seek(0); result != libcdoc::OK) {
LOG_ERROR("{}", d->src->getLastErrorStr(result));
LOG_ERROR("{}", d->dsrc->getLastErrorStr(result));
return result;
}
setLastError({});

std::vector<unsigned char> b64;
XMLReader reader(*d->dsrc);
Expand All @@ -442,16 +462,14 @@ result_t CDoc1Reader::decryptData(const std::vector<uint8_t>& fmk,
return libcdoc::IO_ERROR;
}

// Treat any post-FMK decrypt error - including AES-CBC PKCS#7 stripping
// failures and AES-GCM tag mismatches - as the same "container body
// decrypt failed" event. This is the single bit of information an
// attacker can extract per submission of a tampered CDoc1, and we
// rate-limit it. A per-process exponential backoff turns a remote
// Bleichenbacher campaign of 2^20+ queries into hours/days of
// wall-clock cost without penalising legitimate single-shot use.
constexpr auto THROTTLE_SCOPE = "cdoc1-rsa-decrypt";
// N10: the RSA oracle throttle is keyed by a hash of the recipient's
// public key, and only fires for RSA locks (set in getFMK). ECC/
// AES-wrap locks have no Bleichenbacher-style oracle to protect, so
// they skip the throttle entirely.
auto report_failure = [&]{
libcdoc::Crypto::rsaOracleThrottleOnFailure(THROTTLE_SCOPE);
if (d->is_rsa && !d->throttle_key_id.empty()) {
libcdoc::Crypto::rsaOracleThrottle(d->throttle_key_id);
}
};

VectorSource src(b64);
Expand All @@ -463,7 +481,12 @@ result_t CDoc1Reader::decryptData(const std::vector<uint8_t>& fmk,
}
libcdoc::result_t inner_rv = libcdoc::OK;
if (d->mime == MIME_ZLIB) {
libcdoc::ZSource zsrc(&dec);
// N7: cap decompressed size to prevent decompression bombs.
// CDoc1 buffers whole files in memory, so a smaller default (2 GiB)
// is used compared to CDoc2's streaming default (20 GiB).
static constexpr int64_t DEFAULT_MAX = 2LL * 1024 * 1024 * 1024;
int64_t max_size = conf ? conf->getInt64(libcdoc::Configuration::CDOC1_MAX_DECOMPRESSED_SIZE, DEFAULT_MAX) : DEFAULT_MAX;
libcdoc::ZSource zsrc(&dec, false, max_size);
inner_rv = f(zsrc, d->properties["OriginalMimeType"]);
} else {
inner_rv = f(dec, d->mime);
Expand All @@ -482,6 +505,5 @@ result_t CDoc1Reader::decryptData(const std::vector<uint8_t>& fmk,
report_failure();
return close_rv;
}
libcdoc::Crypto::rsaOracleThrottleOnSuccess(THROTTLE_SCOPE);
return libcdoc::OK;
}
Loading
Loading