Skip to content

Commit 019e5b4

Browse files
Copilotachamayou
andcommitted
Merge latest main and adapt ETag code for 200/206 status
- Resolve merge conflicts with Want-Repr-Digest (#7651) changes - Fix ETag check to handle both HTTP 200 OK (no Range) and 206 Partial Content (with Range) from fill_range_response_from_file() - Merge CHANGELOG, docs, and header constant additions Co-authored-by: achamayou <4016369+achamayou@users.noreply.github.com>
1 parent 38df08c commit 019e5b4

File tree

7 files changed

+607
-59
lines changed

7 files changed

+607
-59
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1212
### Added
1313

1414
- `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.
15+
- `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).
1516
- `ETag` and `If-None-Match` support on `GET /node/ledger-chunk/{chunk_name}`, using SHA-256 by default for the `ETag` response header. Clients can supply `If-None-Match` with `sha-256`, `sha-384`, or `sha-512` digest ETags to avoid re-downloading unchanged content (#7652).
1617

1718
### Fixed

doc/operations/ledger_snapshot.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ These endpoints allow downloading a specific ledger chunk by name, where `<chunk
5656
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.
5757
They also populate the `x-ms-ccf-ledger-chunk-name` response header with the name of the chunk being served.
5858

59+
These endpoints also support the ``Want-Repr-Digest`` request header (`RFC 9530 <https://www.rfc-editor.org/rfc/rfc9530>`_).
60+
When set, the response will include a ``Repr-Digest`` header containing the digest of the full representation of the file.
61+
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>`_).
62+
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.
63+
This allows clients to verify the integrity of downloaded files and avoid re-downloading files they already hold by comparing digests.
64+
65+
.. note:: The ``Want-Repr-Digest`` / ``Repr-Digest`` support also applies to the snapshot download endpoints (``/node/snapshot/{snapshot_name}``).
66+
5967
ETag and If-None-Match
6068
^^^^^^^^^^^^^^^^^^^^^^
6169

include/ccf/http_consts.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ namespace ccf
2323
static constexpr auto RANGE = "range";
2424
static constexpr auto ETAG = "etag";
2525
static constexpr auto IF_NONE_MATCH = "if-none-match";
26+
static constexpr auto REPR_DIGEST = "repr-digest";
2627
static constexpr auto RETRY_AFTER = "retry-after";
2728
static constexpr auto TRAILER = "trailer";
29+
static constexpr auto WANT_REPR_DIGEST = "want-repr-digest";
2830
static constexpr auto WWW_AUTHENTICATE = "www-authenticate";
2931

3032
static constexpr auto CCF_TX_ID = "x-ms-ccf-transaction-id";

src/http/http_digest.h

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the Apache 2.0 License.
3+
#pragma once
4+
5+
#include "ccf/crypto/md_type.h"
6+
#include "ccf/ds/nonstd.h"
7+
8+
#include <charconv>
9+
#include <string>
10+
#include <utility>
11+
12+
namespace ccf::http
13+
{
14+
// Helper to parse the Want-Repr-Digest request header (RFC 9530) and
15+
// return the best supported algorithm name and MDType. Only sha-256,
16+
// sha-384 and sha-512 are supported. Parsing is best-effort: malformed
17+
// entries are ignored. If no supported algorithm can be matched,
18+
// defaults to sha-256 (permitted by RFC 9530 Appendix C.2).
19+
static std::pair<std::string, ccf::crypto::MDType> parse_want_repr_digest(
20+
const std::string& want_repr_digest)
21+
{
22+
std::string best_algo;
23+
ccf::crypto::MDType best_md = ccf::crypto::MDType::NONE;
24+
int best_pref = 0;
25+
26+
for (const auto& entry : ccf::nonstd::split(want_repr_digest, ","))
27+
{
28+
auto [algo, pref_sv] =
29+
ccf::nonstd::split_1(ccf::nonstd::trim(entry), "=");
30+
auto algo_name = ccf::nonstd::trim(algo);
31+
32+
int pref = 0;
33+
auto pref_trimmed = ccf::nonstd::trim(pref_sv);
34+
if (!pref_trimmed.empty())
35+
{
36+
const auto [p, ec] = std::from_chars(
37+
pref_trimmed.data(), pref_trimmed.data() + pref_trimmed.size(), pref);
38+
if (ec != std::errc() || pref < 1)
39+
{
40+
continue;
41+
}
42+
}
43+
else
44+
{
45+
pref = 1;
46+
}
47+
48+
ccf::crypto::MDType md = ccf::crypto::MDType::NONE;
49+
if (algo_name == "sha-256")
50+
{
51+
md = ccf::crypto::MDType::SHA256;
52+
}
53+
else if (algo_name == "sha-384")
54+
{
55+
md = ccf::crypto::MDType::SHA384;
56+
}
57+
else if (algo_name == "sha-512")
58+
{
59+
md = ccf::crypto::MDType::SHA512;
60+
}
61+
62+
if (md != ccf::crypto::MDType::NONE && pref > best_pref)
63+
{
64+
best_algo = std::string(algo_name);
65+
best_md = md;
66+
best_pref = pref;
67+
}
68+
}
69+
70+
if (best_md == ccf::crypto::MDType::NONE)
71+
{
72+
return std::make_pair("sha-256", ccf::crypto::MDType::SHA256);
73+
}
74+
75+
return std::make_pair(best_algo, best_md);
76+
}
77+
}

src/http/test/http_test.cpp

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include "ccf/http_query.h"
66
#include "crypto/openssl/ec_public_key.h"
77
#include "http/http_builder.h"
8+
#include "http/http_digest.h"
89
#include "http/http_parser.h"
910

1011
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
@@ -700,4 +701,138 @@ DOCTEST_TEST_CASE("Query parser getters")
700701
"Unable to parse value 'filenotfound' as bool in parameter 'fnf'");
701702
}
702703
}
704+
}
705+
706+
DOCTEST_TEST_CASE("parse_want_repr_digest - single supported algorithm")
707+
{
708+
{
709+
auto [algo, md] = ccf::http::parse_want_repr_digest("sha-256=1");
710+
DOCTEST_CHECK(algo == "sha-256");
711+
DOCTEST_CHECK(md == ccf::crypto::MDType::SHA256);
712+
}
713+
714+
{
715+
auto [algo, md] = ccf::http::parse_want_repr_digest("sha-384=5");
716+
DOCTEST_CHECK(algo == "sha-384");
717+
DOCTEST_CHECK(md == ccf::crypto::MDType::SHA384);
718+
}
719+
720+
{
721+
auto [algo, md] = ccf::http::parse_want_repr_digest("sha-512=10");
722+
DOCTEST_CHECK(algo == "sha-512");
723+
DOCTEST_CHECK(md == ccf::crypto::MDType::SHA512);
724+
}
725+
}
726+
727+
DOCTEST_TEST_CASE(
728+
"parse_want_repr_digest - multiple algorithms with priorities")
729+
{
730+
{
731+
auto [algo, md] =
732+
ccf::http::parse_want_repr_digest("sha-256=1, sha-512=10");
733+
DOCTEST_CHECK(algo == "sha-512");
734+
DOCTEST_CHECK(md == ccf::crypto::MDType::SHA512);
735+
}
736+
737+
{
738+
auto [algo, md] =
739+
ccf::http::parse_want_repr_digest("sha-512=3, sha-256=7, sha-384=5");
740+
DOCTEST_CHECK(algo == "sha-256");
741+
DOCTEST_CHECK(md == ccf::crypto::MDType::SHA256);
742+
}
743+
744+
{
745+
auto [algo, md] =
746+
ccf::http::parse_want_repr_digest("sha-384=10, sha-256=10");
747+
// Equal preference - first one wins
748+
DOCTEST_CHECK(algo == "sha-384");
749+
DOCTEST_CHECK(md == ccf::crypto::MDType::SHA384);
750+
}
751+
}
752+
753+
DOCTEST_TEST_CASE("parse_want_repr_digest - unknown algorithms are ignored")
754+
{
755+
{
756+
auto [algo, md] = ccf::http::parse_want_repr_digest("md5=10, sha-256=1");
757+
DOCTEST_CHECK(algo == "sha-256");
758+
DOCTEST_CHECK(md == ccf::crypto::MDType::SHA256);
759+
}
760+
761+
{
762+
auto [algo, md] =
763+
ccf::http::parse_want_repr_digest("crc32=5, sha-384=3, unknown=10");
764+
DOCTEST_CHECK(algo == "sha-384");
765+
DOCTEST_CHECK(md == ccf::crypto::MDType::SHA384);
766+
}
767+
}
768+
769+
DOCTEST_TEST_CASE("parse_want_repr_digest - defaults to sha-256 when no match")
770+
{
771+
{
772+
auto [algo, md] = ccf::http::parse_want_repr_digest("md5=10");
773+
DOCTEST_CHECK(algo == "sha-256");
774+
DOCTEST_CHECK(md == ccf::crypto::MDType::SHA256);
775+
}
776+
777+
{
778+
auto [algo, md] = ccf::http::parse_want_repr_digest("unknown=5");
779+
DOCTEST_CHECK(algo == "sha-256");
780+
DOCTEST_CHECK(md == ccf::crypto::MDType::SHA256);
781+
}
782+
783+
{
784+
auto [algo, md] = ccf::http::parse_want_repr_digest("");
785+
DOCTEST_CHECK(algo == "sha-256");
786+
DOCTEST_CHECK(md == ccf::crypto::MDType::SHA256);
787+
}
788+
}
789+
790+
DOCTEST_TEST_CASE("parse_want_repr_digest - malformed entries are skipped")
791+
{
792+
{
793+
// Preference of 0 is invalid (must be >= 1)
794+
auto [algo, md] = ccf::http::parse_want_repr_digest("sha-256=0");
795+
DOCTEST_CHECK(algo == "sha-256");
796+
DOCTEST_CHECK(md == ccf::crypto::MDType::SHA256);
797+
}
798+
799+
{
800+
// Negative preference is invalid
801+
auto [algo, md] = ccf::http::parse_want_repr_digest("sha-512=-1");
802+
DOCTEST_CHECK(algo == "sha-256");
803+
DOCTEST_CHECK(md == ccf::crypto::MDType::SHA256);
804+
}
805+
806+
{
807+
// Non-numeric preference is skipped, but valid entry is used
808+
auto [algo, md] =
809+
ccf::http::parse_want_repr_digest("sha-256=abc, sha-384=5");
810+
DOCTEST_CHECK(algo == "sha-384");
811+
DOCTEST_CHECK(md == ccf::crypto::MDType::SHA384);
812+
}
813+
}
814+
815+
DOCTEST_TEST_CASE("parse_want_repr_digest - whitespace handling")
816+
{
817+
{
818+
auto [algo, md] = ccf::http::parse_want_repr_digest(" sha-256 = 1 ");
819+
DOCTEST_CHECK(algo == "sha-256");
820+
DOCTEST_CHECK(md == ccf::crypto::MDType::SHA256);
821+
}
822+
823+
{
824+
auto [algo, md] =
825+
ccf::http::parse_want_repr_digest("sha-256=1 , sha-512=10");
826+
DOCTEST_CHECK(algo == "sha-512");
827+
DOCTEST_CHECK(md == ccf::crypto::MDType::SHA512);
828+
}
829+
}
830+
831+
DOCTEST_TEST_CASE(
832+
"parse_want_repr_digest - algorithm without explicit preference")
833+
{
834+
// No "=" means preference defaults to 1
835+
auto [algo, md] = ccf::http::parse_want_repr_digest("sha-512");
836+
DOCTEST_CHECK(algo == "sha-512");
837+
DOCTEST_CHECK(md == ccf::crypto::MDType::SHA512);
703838
}

0 commit comments

Comments
 (0)