Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
6e7a320
Initial plan
Copilot Feb 9, 2026
8acc38b
Add Want-Repr-Digest / Repr-Digest support to ledger-chunk endpoints …
Copilot Feb 9, 2026
9efbfed
Check read success before computing repr-digest
Copilot Feb 9, 2026
28ae3e6
Merge branch 'main' into copilot/close-feature-request-7650
achamayou Feb 9, 2026
d57c8e3
Move repr-digest into fill_range_response_from_file; apply brace styl…
Copilot Feb 9, 2026
6bd2559
Add read error checking for digest computation
Copilot Feb 9, 2026
19255f9
fmt
achamayou Feb 9, 2026
f6374eb
Apply suggestion from @achamayou
achamayou Feb 9, 2026
b1b1c5b
Merge branch 'main' into copilot/close-feature-request-7650
achamayou Feb 9, 2026
9ff2e8c
Address review feedback: update comment, gcount check, 200/206 status…
Copilot Feb 9, 2026
f12bc3d
Merge branch 'main' into copilot/close-feature-request-7650
achamayou Feb 10, 2026
43311fa
Merge branch 'main' into copilot/close-feature-request-7650
achamayou Feb 10, 2026
989936d
Merge branch 'main' into copilot/close-feature-request-7650
achamayou Feb 10, 2026
c8fdf43
fmt
achamayou Feb 10, 2026
1eeec77
Default to sha-256 when no supported algorithm matches in Want-Repr-D…
Copilot Feb 10, 2026
5116e08
Return 500 error on HEAD when file read fails with Want-Repr-Digest
Copilot Feb 10, 2026
194e6f5
Move parse_want_repr_digest() to src/http/http_digest.h with unit tests
Copilot Feb 10, 2026
2c95f1e
fmt
achamayou Feb 10, 2026
8452d97
Move http_digest.h include to beginning of http_test.cpp
Copilot Feb 10, 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
### Added

- `GET` and `HEAD` `/node/ledger-chunk?since={seqno}` and `/node/ledger-chunk/{chunk_name}` endpoints, gated by the `LedgerChunkDownload` RPC interface operator feature. See [documentation](https://microsoft.github.io/CCF/main/operations/ledger_snapshot.html#download-endpoints) for more detail.
- `GET` and `HEAD` `/node/ledger-chunk/{chunk_name}` and `/node/snapshot/{snapshot_name}` now support the `Want-Repr-Digest` request header and return the `Repr-Digest` response header accordingly (RFC 9530). Supported algorithms are `sha-256`, `sha-384`, and `sha-512`. If no supported algorithm is requested, the server defaults to `sha-256` (#7650).

### Fixed

Expand Down
8 changes: 8 additions & 0 deletions doc/operations/ledger_snapshot.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ These endpoints allow downloading a specific ledger chunk by name, where `<chunk
They support the HTTP `Range` header for partial downloads, and the `HEAD` method for clients to query metadata such as the total size without downloading the full chunk.
They also populate the `x-ms-ccf-ledger-chunk-name` response header with the name of the chunk being served.

These endpoints also support the ``Want-Repr-Digest`` request header (`RFC 9530 <https://www.rfc-editor.org/rfc/rfc9530>`_).
When set, the response will include a ``Repr-Digest`` header containing the digest of the full representation of the file.
Supported algorithms are ``sha-256``, ``sha-384``, and ``sha-512``. If the header contains only unsupported or invalid algorithms, the server defaults to ``sha-256`` (as permitted by `RFC 9530 Appendix C.2 <https://www.rfc-editor.org/rfc/rfc9530#appendix-C.2>`_).
For example, a client sending ``Want-Repr-Digest: sha-256=1`` will receive a header such as ``Repr-Digest: sha-256=:AEGPTgUMw5e96wxZuDtpfm23RBU3nFwtgY5fw4NYORo=:`` in the response.
This allows clients to verify the integrity of downloaded files and avoid re-downloading files they already hold by comparing digests.

.. note:: The ``Want-Repr-Digest`` / ``Repr-Digest`` support also applies to the snapshot download endpoints (``/node/snapshot/{snapshot_name}``).

2. :http:GET:`/node/ledger-chunk` and :http:HEAD:`/node/ledger-chunk`, both taking a `seqno` query parameter.

These endpoints can be used by a client to download the next ledger chunk including a given sequence number `<seqno>`.
Expand Down
2 changes: 2 additions & 0 deletions include/ccf/http_consts.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ namespace ccf
static constexpr auto HOST = "host";
static constexpr auto LOCATION = "location";
static constexpr auto RANGE = "range";
static constexpr auto REPR_DIGEST = "repr-digest";
static constexpr auto RETRY_AFTER = "retry-after";
static constexpr auto TRAILER = "trailer";
static constexpr auto WANT_REPR_DIGEST = "want-repr-digest";
static constexpr auto WWW_AUTHENTICATE = "www-authenticate";

static constexpr auto CCF_TX_ID = "x-ms-ccf-transaction-id";
Expand Down
77 changes: 77 additions & 0 deletions src/http/http_digest.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the Apache 2.0 License.
#pragma once

#include "ccf/crypto/md_type.h"
#include "ccf/ds/nonstd.h"

#include <charconv>
#include <string>
#include <utility>

namespace ccf::http
{
// Helper to parse the Want-Repr-Digest request header (RFC 9530) and
// return the best supported algorithm name and MDType. Only sha-256,
// sha-384 and sha-512 are supported. Parsing is best-effort: malformed
// entries are ignored. If no supported algorithm can be matched,
// defaults to sha-256 (permitted by RFC 9530 Appendix C.2).
static std::pair<std::string, ccf::crypto::MDType> parse_want_repr_digest(
const std::string& want_repr_digest)
{
std::string best_algo;
ccf::crypto::MDType best_md = ccf::crypto::MDType::NONE;
int best_pref = 0;

for (const auto& entry : ccf::nonstd::split(want_repr_digest, ","))
{
auto [algo, pref_sv] =
ccf::nonstd::split_1(ccf::nonstd::trim(entry), "=");
auto algo_name = ccf::nonstd::trim(algo);

int pref = 0;
auto pref_trimmed = ccf::nonstd::trim(pref_sv);
if (!pref_trimmed.empty())
{
const auto [p, ec] = std::from_chars(
pref_trimmed.data(), pref_trimmed.data() + pref_trimmed.size(), pref);
if (ec != std::errc() || pref < 1)
{
continue;
}
}
else
{
pref = 1;
}

ccf::crypto::MDType md = ccf::crypto::MDType::NONE;
if (algo_name == "sha-256")
{
md = ccf::crypto::MDType::SHA256;
}
else if (algo_name == "sha-384")
{
md = ccf::crypto::MDType::SHA384;
}
else if (algo_name == "sha-512")
{
md = ccf::crypto::MDType::SHA512;
}

if (md != ccf::crypto::MDType::NONE && pref > best_pref)
{
best_algo = std::string(algo_name);
best_md = md;
best_pref = pref;
}
}

if (best_md == ccf::crypto::MDType::NONE)
{
return std::make_pair("sha-256", ccf::crypto::MDType::SHA256);
}

return std::make_pair(best_algo, best_md);
}
}
135 changes: 135 additions & 0 deletions src/http/test/http_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "ccf/http_query.h"
#include "crypto/openssl/ec_public_key.h"
#include "http/http_builder.h"
#include "http/http_digest.h"
#include "http/http_parser.h"

#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
Expand Down Expand Up @@ -700,4 +701,138 @@ DOCTEST_TEST_CASE("Query parser getters")
"Unable to parse value 'filenotfound' as bool in parameter 'fnf'");
}
}
}

DOCTEST_TEST_CASE("parse_want_repr_digest - single supported algorithm")
{
{
auto [algo, md] = ccf::http::parse_want_repr_digest("sha-256=1");
DOCTEST_CHECK(algo == "sha-256");
DOCTEST_CHECK(md == ccf::crypto::MDType::SHA256);
}

{
auto [algo, md] = ccf::http::parse_want_repr_digest("sha-384=5");
DOCTEST_CHECK(algo == "sha-384");
DOCTEST_CHECK(md == ccf::crypto::MDType::SHA384);
}

{
auto [algo, md] = ccf::http::parse_want_repr_digest("sha-512=10");
DOCTEST_CHECK(algo == "sha-512");
DOCTEST_CHECK(md == ccf::crypto::MDType::SHA512);
}
}

DOCTEST_TEST_CASE(
"parse_want_repr_digest - multiple algorithms with priorities")
{
{
auto [algo, md] =
ccf::http::parse_want_repr_digest("sha-256=1, sha-512=10");
DOCTEST_CHECK(algo == "sha-512");
DOCTEST_CHECK(md == ccf::crypto::MDType::SHA512);
}

{
auto [algo, md] =
ccf::http::parse_want_repr_digest("sha-512=3, sha-256=7, sha-384=5");
DOCTEST_CHECK(algo == "sha-256");
DOCTEST_CHECK(md == ccf::crypto::MDType::SHA256);
}

{
auto [algo, md] =
ccf::http::parse_want_repr_digest("sha-384=10, sha-256=10");
// Equal preference - first one wins
DOCTEST_CHECK(algo == "sha-384");
DOCTEST_CHECK(md == ccf::crypto::MDType::SHA384);
}
}

DOCTEST_TEST_CASE("parse_want_repr_digest - unknown algorithms are ignored")
{
{
auto [algo, md] = ccf::http::parse_want_repr_digest("md5=10, sha-256=1");
DOCTEST_CHECK(algo == "sha-256");
DOCTEST_CHECK(md == ccf::crypto::MDType::SHA256);
}

{
auto [algo, md] =
ccf::http::parse_want_repr_digest("crc32=5, sha-384=3, unknown=10");
DOCTEST_CHECK(algo == "sha-384");
DOCTEST_CHECK(md == ccf::crypto::MDType::SHA384);
}
}

DOCTEST_TEST_CASE("parse_want_repr_digest - defaults to sha-256 when no match")
{
{
auto [algo, md] = ccf::http::parse_want_repr_digest("md5=10");
DOCTEST_CHECK(algo == "sha-256");
DOCTEST_CHECK(md == ccf::crypto::MDType::SHA256);
}

{
auto [algo, md] = ccf::http::parse_want_repr_digest("unknown=5");
DOCTEST_CHECK(algo == "sha-256");
DOCTEST_CHECK(md == ccf::crypto::MDType::SHA256);
}

{
auto [algo, md] = ccf::http::parse_want_repr_digest("");
DOCTEST_CHECK(algo == "sha-256");
DOCTEST_CHECK(md == ccf::crypto::MDType::SHA256);
}
}

DOCTEST_TEST_CASE("parse_want_repr_digest - malformed entries are skipped")
{
{
// Preference of 0 is invalid (must be >= 1)
auto [algo, md] = ccf::http::parse_want_repr_digest("sha-256=0");
DOCTEST_CHECK(algo == "sha-256");
DOCTEST_CHECK(md == ccf::crypto::MDType::SHA256);
}

{
// Negative preference is invalid
auto [algo, md] = ccf::http::parse_want_repr_digest("sha-512=-1");
DOCTEST_CHECK(algo == "sha-256");
DOCTEST_CHECK(md == ccf::crypto::MDType::SHA256);
}

{
// Non-numeric preference is skipped, but valid entry is used
auto [algo, md] =
ccf::http::parse_want_repr_digest("sha-256=abc, sha-384=5");
DOCTEST_CHECK(algo == "sha-384");
DOCTEST_CHECK(md == ccf::crypto::MDType::SHA384);
}
}

DOCTEST_TEST_CASE("parse_want_repr_digest - whitespace handling")
{
{
auto [algo, md] = ccf::http::parse_want_repr_digest(" sha-256 = 1 ");
DOCTEST_CHECK(algo == "sha-256");
DOCTEST_CHECK(md == ccf::crypto::MDType::SHA256);
}

{
auto [algo, md] =
ccf::http::parse_want_repr_digest("sha-256=1 , sha-512=10");
DOCTEST_CHECK(algo == "sha-512");
DOCTEST_CHECK(md == ccf::crypto::MDType::SHA512);
}
}

DOCTEST_TEST_CASE(
"parse_want_repr_digest - algorithm without explicit preference")
{
// No "=" means preference defaults to 1
auto [algo, md] = ccf::http::parse_want_repr_digest("sha-512");
DOCTEST_CHECK(algo == "sha-512");
DOCTEST_CHECK(md == ccf::crypto::MDType::SHA512);
}
Loading
Loading