Skip to content

Expose all service public keys from the past via NetworkIdentity subsystem#7690

Merged
maxtropets merged 24 commits intomainfrom
copilot/extend-network-identity-interface
Mar 4, 2026
Merged

Expose all service public keys from the past via NetworkIdentity subsystem#7690
maxtropets merged 24 commits intomainfrom
copilot/extend-network-identity-interface

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 2, 2026

  • Identify 4 files with spurious whitespace changes from Ubuntu clang-format
  • Revert those whitespace changes
  • Move JWK-to-DER key construction helper into tests/infra/crypto.py as pub_key_der_from_jwk()
  • Refactor tests/recovery.py to use the new helper, removing inline key-construction details
  • Fix kid in trusted_keys endpoint to use SHA-256 hash of public key DER (matching COSE receipts)
  • Move kid_from_key() to src/crypto/public_key.h / src/crypto/public_key.cpp as a standalone function accepting DER bytes (not a class method, not in public API); update callers in history.h and logging.cpp
  • Fix kid_from_key() doc comment in src/crypto/public_key.h to describe the function API without unnecessary context
  • Add doxygen comments to FetchStatus, TrustedKeys, and NetworkIdentitySubsystemInterface and expose via sphinx
  • Add doxygen comments to RawCoseEndorsement, CoseEndorsementsChain, and all interface methods; expose type aliases in sphinx RST
  • Document exception throwing behavior (which methods throw, exception types, conditions) for all interface methods
Original prompt

Summary

Extend the NetworkIdentitySubsystemInterface to expose all trusted keys (the full trusted_keys map), and add a logging app endpoint + recovery test verification.

Changes Required

1. Interface: include/ccf/network_identity_interface.h

Add a new pure virtual method to NetworkIdentitySubsystemInterface:

using TrustedKeys = std::map<ccf::SeqNo, ccf::crypto::ECPublicKeyPtr>;

[[nodiscard]] virtual TrustedKeys get_trusted_keys() const = 0;

You'll need to add #include <map> and forward-declare or include ccf/tx_id.h for SeqNo (it's likely already available via ec_public_key.h or node_subsystem_interface.h). Check what's already included. SeqNo is int64_t, already used elsewhere in the file.

2. Implementation: src/node/rpc/network_identity_subsystem.h

In the NetworkIdentitySubsystem class, add the override:

[[nodiscard]] TrustedKeys get_trusted_keys() const override
{
  return trusted_keys;
}

The member trusted_keys is std::map<SeqNo, ccf::crypto::ECPublicKeyPtr> and is protected, so this just returns a copy. Make sure TrustedKeys type alias matches.

3. Logging app endpoint: samples/apps/logging/logging.cpp

Add a new no-auth, read-only endpoint /log/public/trusted_keys that returns a JWKS (JSON Web Key Set). Place it near the existing /log/public/cose_endorsements endpoint (around line 2110).

The endpoint should:

  • Get the NetworkIdentitySubsystemInterface subsystem from context
  • Call get_trusted_keys() on it
  • Build a JSON response with {"keys": [...]} where each key is the JWK representation (public_key_jwk()) of the ECPublicKeyPtr, with kid set to the string of the SeqNo
  • Return HTTP 200 with the JSON

Concise implementation:

auto get_trusted_keys = [&](ccf::endpoints::ReadOnlyEndpointContext& ctx) {
  auto network_identity_subsystem =
    context.get_subsystem<ccf::NetworkIdentitySubsystemInterface>();
  if (network_identity_subsystem == nullptr)
  {
    ctx.rpc_ctx->set_error(
      HTTP_STATUS_INTERNAL_SERVER_ERROR,
      ccf::errors::InternalError,
      "Network identity subsystem not available");
    return;
  }

  auto keys = network_identity_subsystem->get_trusted_keys();
  nlohmann::json jwks = nlohmann::json::object();
  auto keys_array = nlohmann::json::array();
  for (const auto& [seqno, key_ptr] : keys)
  {
    keys_array.push_back(key_ptr->public_key_jwk(std::to_string(seqno)));
  }
  jwks["keys"] = keys_array;

  ctx.rpc_ctx->set_response_json(jwks, HTTP_STATUS_OK);
};
make_read_only_endpoint(
  "/log/public/trusted_keys",
  HTTP_GET,
  get_trusted_keys,
  ccf::no_auth_required)
  .set_auto_schema<void, void>()
  .set_forwarding_required(ccf::endpoints::ForwardingRequired::Never)
  .install();

You'll likely need #include <string> (already there) for std::to_string. The public_key_jwk() method is on ECPublicKey and is already virtual. The JsonWebKeyECPublic struct serializes to JSON automatically via DECLARE_JSON_TYPE_WITH_BASE.

4. Recovery test: tests/recovery.py

After the existing block (around line 528):

for tx in txids[4:]:
    response = query_endorsements_chain(primary, tx)
    assert response.status_code == http.HTTPStatus.NOT_FOUND, response

Add the following verification:

# Verify trusted keys from the endpoint match endorsement keys
with primary.client() as cli:
    r = cli.get("/log/public/trusted_keys")
    assert r.status_code == http.HTTPStatus.OK, r
    jwks = r.body.json()
    assert "keys" in jwks, jwks
    # There should be at least 3 trusted keys (original + 2 recoveries)
    assert len(jwks["keys"]) >= 3, f"Expected at least 3 trusted keys, got {len(jwks['keys'])}"

This is intentionally concise. The endpoint is called, status is checked, and we verify the JWKS contains at least 3 keys (the original service identity + one per recovery, we did 2 recoveries).

Key points

  • Keep changes minimal/KISS
  • The TrustedKeys type alias should be defined inside the interface class or in the ccf namespace in network_identity_interface.h
  • The endpoint is no-auth and returns JSON in JWKS format
  • The test just verifies the endpoint works and returns the expected number of keys
  • #include <map> may be needed in network_identity_interface.h if not already transitively included

This pull request was created from Copilot chat.


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

@maxtropets
Copy link
Copy Markdown
Collaborator

@copilot don't forget to add a changelog

Co-authored-by: maxtropets <16566519+maxtropets@users.noreply.github.com>
Copilot AI changed the title [WIP] Extend NetworkIdentitySubsystemInterface to expose all trusted keys Expose all trusted keys via NetworkIdentitySubsystemInterface Mar 2, 2026
Co-authored-by: maxtropets <16566519+maxtropets@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 2, 2026

@copilot don't forget to add a changelog

Added changelog entries in 4d41cac under the [7.0.0-dev12] section.

@achamayou
Copy link
Copy Markdown
Member

Do we have a nice way to get a COSE_Key from a ECPublicKeyPtr using EverCBOR?

@maxtropets
Copy link
Copy Markdown
Collaborator

Do we have a nice way to get a COSE_Key from a ECPublicKeyPtr using EverCBOR?

I don't understand what EverCBOR has to do with COSE keys, it has no notion of COSE key mapping.

What is the motivation here? For SCITT ledger, they are going to be converted to JWKs anyway, aren't they?

Co-authored-by: maxtropets <16566519+maxtropets@users.noreply.github.com>
…orsement keys in test

Co-authored-by: maxtropets <16566519+maxtropets@users.noreply.github.com>
@achamayou
Copy link
Copy Markdown
Member

Do we have a nice way to get a COSE_Key from a ECPublicKeyPtr using EverCBOR?

I don't understand what EverCBOR has to do with COSE keys, it has no notion of COSE key mapping.

What is the motivation here? For SCITT ledger, they are going to be converted to JWKs anyway, aren't they?

EverCBOR is a way to serialise CBOR, and COSE_Key are CBOR objects.

Keys are exposed as JWKs today, and will continue to be for a while for backwards compatibility reasons, but going forward they are going to be COSE_Key: https://ietf-wg-scitt.github.io/draft-ietf-scitt-scrapi/draft-ietf-scitt-scrapi.html#name-transparency-service-keys

Co-authored-by: maxtropets <16566519+maxtropets@users.noreply.github.com>
Co-authored-by: maxtropets <16566519+maxtropets@users.noreply.github.com>
…inspired context

Co-authored-by: maxtropets <16566519+maxtropets@users.noreply.github.com>
@achamayou
Copy link
Copy Markdown
Member

@copilot add NetworkIdentitySubsystemInterface and TrustedKeys to the sphinx documentation via doxygen directives to expose them to users. If they do not have a doxygen string in the header in which they live, please create an appropriate one so they get picked up by doxygen correctly.

…rface and TrustedKeys

Co-authored-by: achamayou <4016369+achamayou@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 4, 2026

@copilot add NetworkIdentitySubsystemInterface and TrustedKeys to the sphinx documentation via doxygen directives to expose them to users. If they do not have a doxygen string in the...

Done in 862626b. Added /// doxygen comments to FetchStatus, TrustedKeys, and NetworkIdentitySubsystemInterface in include/ccf/network_identity_interface.h, and added a new "Network Identity" section to doc/build_apps/api.rst with doxygenenum, doxygentypedef, and doxygenclass directives for all three.

@maxtropets
Copy link
Copy Markdown
Collaborator

@copilot add NetworkIdentitySubsystemInterface and TrustedKeys to the sphinx documentation via doxygen directives to expose them to users. If they do not have a doxygen string in the...

Done in 862626b. Added /// doxygen comments to FetchStatus, TrustedKeys, and NetworkIdentitySubsystemInterface in include/ccf/network_identity_interface.h, and added a new "Network Identity" section to doc/build_apps/api.rst with doxygenenum, doxygentypedef, and doxygenclass directives for all three.

@copilot also document the rest of the network identity interface and it's methods:

using RawCoseEndorsement = std::vector<uint8_t>;
using CoseEndorsementsChain = std::vector;
[[nodiscard]] virtual FetchStatus endorsements_fetching_status() const = 0;
[[nodiscard]] virtual std::optional get_cose_endorsements_chain(ccf::SeqNo seqno) const = 0;
[[nodiscard]] virtual ccf::crypto::ECPublicKeyPtr get_trusted_identity_for(ccf::SeqNo seqno) const = 0;
[[nodiscard]] virtual TrustedKeys get_trusted_keys() const = 0;

…methods

Co-authored-by: maxtropets <16566519+maxtropets@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 4, 2026

@copilot add NetworkIdentitySubsystemInterface and TrustedKeys to the sphinx documentation via doxygen directives to expose them to users. If they do not have a doxygen string in t...

Done in 591ccc9. Added /// doxygen comments to RawCoseEndorsement, CoseEndorsementsChain, get(), endorsements_fetching_status(), get_cose_endorsements_chain(), get_trusted_identity_for(), and get_trusted_keys(). Also added doxygentypedef directives for RawCoseEndorsement and CoseEndorsementsChain to the sphinx RST. The class directive already has :members: so all documented methods are now exposed.

@maxtropets
Copy link
Copy Markdown
Collaborator

@copilot add NetworkIdentitySubsystemInterface and TrustedKeys to the sphinx documentation via doxygen directives to expose them to users. If they do not have a doxygen string in t...

Done in 591ccc9. Added /// doxygen comments to RawCoseEndorsement, CoseEndorsementsChain, get(), endorsements_fetching_status(), get_cose_endorsements_chain(), get_trusted_identity_for(), and get_trusted_keys(). Also added doxygentypedef directives for RawCoseEndorsement and CoseEndorsementsChain to the sphinx RST. The class directive already has :members: so all documented methods are now exposed.

@copilot For all the methods you've documented, make it clear which of them throw and in what cases, so the end user is expecting that, also mention the type of exception to expect.

…rface methods

Co-authored-by: maxtropets <16566519+maxtropets@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 4, 2026

@copilot add NetworkIdentitySubsystemInterface and TrustedKeys to the sphinx documentation via doxygen directives to expose them to users. If they do not have a doxygen string ...

Done in daa2f3a. Added @throws std::logic_error documentation to get_trusted_identity_for() and get_trusted_keys() with the specific conditions. Also clarified that get_cose_endorsements_chain() returns std::nullopt rather than throwing when fetching hasn't completed.

@maxtropets maxtropets enabled auto-merge (squash) March 4, 2026 22:04
@maxtropets maxtropets merged commit 71c64f5 into main Mar 4, 2026
17 checks passed
@maxtropets maxtropets deleted the copilot/extend-network-identity-interface branch March 4, 2026 22:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants