Skip to content

feat(crypto): support the mainnet secp256r1 ECDSA keys in ic-pub-key - #11894

Merged
eichhorl merged 9 commits into
masterfrom
eichhorl/ic-pub-key-secp256r1
Oct 23, 2026
Merged

eichhorl merged 9 commits into
masterfrom
eichhorl/ic-pub-key-secp256r1

Conversation

@eichhorl

@eichhorl eichhorl commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Why

The mainnet ECDSA keys key_1 and test_key_1 now also exist over secp256r1 (P-256), the curve #11573 added, and PocketIC holds secp256r1 keys too (#11741). ic-pub-key derives canister public keys offline from hardcoded master keys, but for ECDSA only over secp256k1: any other curve returns AlgorithmNotSupported.

ic-secp256r1 0.3.0 (#11895) has the secp256r1 master keys, and the workspace's ic-management-canister-types 0.11.0 (#11909) has EcdsaCurve::Secp256r1. Both are published.

What

  • A new secp256r1 feature, on by default, derives keys for key_1, test_key_1, pocketic_key_1, pocketic_test_key_1 and dfx_test_key, following the secp256k1 code paths. It requires ic-secp256r1 0.3.0.
  • TryFrom<&EcdsaKeyId> checks each curve in its own feature-gated block, like TryFrom<&SchnorrKeyId>, instead of depending on the order of its checks.
  • serialize and chain_code share one implementation on DerivedPublicKeyInner, instead of one each in CanisterMasterKey and DerivedPublicKey.
  • The crate docs and README describe the features, including default-features = false, and derive_ecdsa_key names the feature each curve needs.
  • The changelog adds the secp256r1 support and the changes since the published 0.3.0 that weren't recorded yet: ic-vetkeys 0.6.0 → 0.7.0 and the ic-cdk-management-canister dependency of the vetkeys feature. chore: bump ic-management-canister-types to 0.11.0 #11909 already recorded ic-management-canister-types 0.5.0 → 0.11.0, which makes the next release a breaking one.

Tests

  • Known-answer tests for all five secp256r1 keys, using the same canisters and path shape as the secp256k1 tests.
  • All ECDSA tests now also check the chain code, and that unknown key names are rejected.
  • All expected public keys and chain codes are what the replica's derive_threshold_public_key, the function that answers ecdsa_public_key, returns for these master keys. A one-off check, not committed, confirmed this for all 11 ECDSA vectors:
    • ic-pub-key matches the replica on both public key and chain code.
    • The replica reproduces the existing secp256k1 vectors.
  • The offline derivation also matches mainnet itself: for a canister on mainnet, ecdsa_public_key returned exactly the public keys ic-pub-key derives offline, for key_1 and test_key_1 on both curves, each with an empty and a 7-element path. Mainnet doesn't expose chain codes, so those rest on the replica check.
  • The test helpers follow the Schnorr ones and are feature-gated. With a single feature, the tests build without warnings, and so does the library, except with only vetkeys: the two warnings master already has in CanisterMasterKey::derive_key remain. This PR removes a third, in TryFrom<&SchnorrKeyId>.
  • Locally, in the dev container, all of these pass: cargo check (default and each single feature), rust-lint.sh, bazel build //... --nobuild, //bazel:buildifier.check, and the 3 affected tests.

🤖 Generated with Claude Code

The mainnet ECDSA keys `key_1` and `test_key_1` now also exist over
secp256r1, but `ic-pub-key` derives keys offline only for secp256k1.

* `ic-secp256r1` gains `PublicKey::mainnet_key` with the master public
  keys of `ecdsa:Secp256r1:key_1` and `ecdsa:Secp256r1:test_key_1`, read
  from the CUPs of subnets uzr34 and fuqsr.
* `ic-pub-key` derives keys for both behind a new default `secp256r1`
  feature, and moves from the workspace's `ic-management-canister-types`
  0.8.0 to the in-repo 0.11.0, the first version with
  `EcdsaCurve::Secp256r1`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Mainnet compatibility needs independent validation because the new expected keys were generated using the same derivation implementation.

1 open issue
What changed in this PR

Adds offline derivation for mainnet P-256 ECDSA keys, extending the curve support introduced in #11573.

Changes:

  • Embeds both mainnet master keys in ic-secp256r1.
  • Enables P-256 derivation by default in ic-pub-key.
  • Updates dependencies, build targets, tests, and changelogs.
File Description
packages/​ic-secp256r1/​tests/​tests.rs Checks master-key round trips.
packages/​ic-secp256r1/​src/​lib.rs Adds mainnet master-key lookup.
packages/​ic-secp256r1/​CHANGELOG.md Documents the new API.
packages/​ic-secp256r1/​Cargo.toml Adds runtime hex-literal dependency.
packages/​ic-secp256r1/​BUILD.bazel Mirrors the dependency addition.
packages/​ic-pub-key/​tests/​tests.rs Adds offline P-256 derivation vectors.
packages/​ic-pub-key/​src/​lib.rs Implements P-256 derivation and serialization.
packages/​ic-pub-key/​CHANGELOG.md Documents support and re-export changes.
packages/​ic-pub-key/​Cargo.toml Enables P-256 and updates management types.
packages/​ic-pub-key/​BUILD.bazel Updates features and dependencies.
Cargo.lock Records dependency changes.

Review effort: Balanced

Comment thread packages/ic-pub-key/src/lib.rs
PocketIC holds secp256r1 keys for `key_1`, `test_key_1` and
`dfx_test_key`, as it does for the other algorithms `ic-pub-key`
supports.

* `ic-secp256r1` gains `PublicKey::pocketic_key` with their master
  public keys, and a test that derives them the way PocketIC does.
* `ic-pub-key` derives keys for `pocketic_key_1`, `pocketic_test_key_1`
  and `dfx_test_key` over secp256r1.
* The expected keys of the secp256r1 tests in `ic-pub-key` are what the
  replica's `derive_threshold_public_key` returns for these master keys,
  which a one-off check confirmed for the mainnet ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The hardcoded production master keys need human verification against authoritative subnet catch-up packages.

1 open issue

Review effort: Balanced

`ic-secp256k1` and `ic-ed25519` ship the mainnet and PocketIC master
public keys of their algorithms, which `ic-pub-key` uses for offline key
derivation. The mainnet ECDSA keys `key_1` and `test_key_1` now also
exist over secp256r1, and PocketIC holds secp256r1 keys too, but
`ic-secp256r1` has none of them.

* `PublicKey::mainnet_key` returns the master public keys of
  `ecdsa:Secp256r1:key_1` and `ecdsa:Secp256r1:test_key_1`, read from
  the CUPs of subnets uzr34 and fuqsr.
* `PublicKey::pocketic_key` returns the ones PocketIC uses for `key_1`,
  `test_key_1` and `dfx_test_key`, and a test derives them the way
  PocketIC does.
* The version moves to 0.3.0, to be released before `ic-pub-key` can
  depend on these keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
eichhorl and others added 3 commits October 20, 2026 12:03
* Add `PublicKey::derive_mainnet_key` and `PublicKey::derive_pocketic_key`,
  and re-export `ic_principal::Principal` as `CanisterId` for them, as
  `ic-secp256k1` and `ic-ed25519` do. Their tests expect what the
  replica's `derive_threshold_public_key` returns for the same master
  keys, canisters and paths.
* Test the SLIP-10 retry for nist256p1 (index 33941), the only branch of
  the derivation the tests didn't reach. It runs when iL >= n, about once
  in 2^32 steps on P-256.
* Rename `mainnet_keys_are_the_ones_in_the_cups` to
  `mainnet_keys_round_trip`, which is what it checks.
* Name the two new enums in the changelog, drop the duplicate
  `hex-literal` dev-dependency, and list `ic-secp256r1` among the crates
  with hardcoded master keys in the `cup_explorer` README.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-key-secp256r1

ic-pub-key needs the ic-secp256r1 0.3.0 of #11895: take its version of
ic-secp256r1 and raise the requirement to 0.3.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* `TryFrom<&EcdsaKeyId>` has one feature-gated block per curve, like
  `TryFrom<&SchnorrKeyId>`, instead of depending on the order of its
  checks.
* `serialize` and `chain_code` of `CanisterMasterKey` and
  `DerivedPublicKey` share one implementation on `DerivedPublicKeyInner`.
* The ECDSA tests also check the chain code, and that unknown key names
  are rejected. All expected values are what the replica's
  `derive_threshold_public_key` returns. The test helpers follow the
  Schnorr ones and are feature-gated, so single-feature builds have no
  unused code.
* The crate docs and README describe the features and
  `default-features = false`, and `derive_ecdsa_key` names the feature
  each curve needs.
* The changelog describes the dependency changes since the published
  0.3.0, and `Cargo.toml` explains why `ic-management-canister-types` is
  the in-repo crate.
* The BUILD file lists the features once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Production master-key additions and the breaking public-type dependency upgrade warrant final human validation.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

@eichhorl
eichhorl changed the base branch from master to eichhorl/ic-secp256r1-master-keys October 20, 2026 12:13
@eichhorl
eichhorl added this pull request to stack #11902 October 20, 2026 12:13
Base automatically changed from eichhorl/ic-secp256r1-master-keys to master October 21, 2026 06:37
eichhorl and others added 2 commits October 23, 2026 13:33
Resolve two conflicts with #11909:
- CHANGELOG.md: keep #11909's line for ic-management-canister-types
  0.5.0 -> 0.11.0 once, next to this PR's entries.
- Cargo.lock: master's, with cargo re-applying this PR's change to the
  ic-pub-key entry.

The ic-secp256r1 changes this branch carried from #11895 are identical to
master's squash commit, so they drop out of the diff.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#11909 moved the workspace to ic-management-canister-types 0.11.0, which
has EcdsaCurve::Secp256r1, so ic-pub-key no longer needs the in-repo crate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No blocking issues remain; feature wiring and derivation follow existing patterns, and all five P-256 key and chain-code vectors were independently verified.

No open issues 🎉

Review effort: Balanced

@eichhorl
eichhorl marked this pull request as ready for review October 23, 2026 13:37
@eichhorl
eichhorl requested a review from a team as a code owner October 23, 2026 13:37
basvandijk pushed a commit to Ocheretovich/ic that referenced this pull request Oct 23, 2026
dfinity#11895)

### Why

`ic-secp256k1` and `ic-ed25519` ship the mainnet and PocketIC master
public keys of their algorithms, and `ic-pub-key` uses them to derive
keys offline. The mainnet ECDSA keys `key_1` and `test_key_1` now also
exist over secp256r1 (P-256), and PocketIC holds secp256r1 keys too
(dfinity#11741). But `ic-secp256r1` has none of them.

This PR adds the keys and bumps the version, so the crate can be
released. `ic-pub-key` can only use the keys once that release is out
(dfinity#11894). That's the same order as dfinity#7148 before dfinity#7194 for the secp256k1
and Ed25519 PocketIC keys.

### What

* `PublicKey::mainnet_key(MasterPublicKeyId)` returns `key_1` and
`test_key_1`.
* `PublicKey::pocketic_key(PocketIcMasterPublicKeyId)` returns the keys
PocketIC uses for `key_1`, `test_key_1` and `dfx_test_key`.
* `PublicKey::derive_mainnet_key` and `PublicKey::derive_pocketic_key`
derive a canister's public key from those master keys. They are offline
equivalents of `ecdsa_public_key`. For their `&CanisterId` argument, the
crate re-exports `ic_principal::Principal` as `CanisterId`, as
`ic-secp256k1` and `ic-ed25519` do.
* `hex-literal` becomes a regular dependency, to embed the keys, and
`ic_principal` a new one.
* The version goes from 0.2.0 to 0.3.0, with a dated changelog entry,
for a release after merging. In-repo dependents use the crate by path
without a version, so nothing else changes.
* The `cup_explorer` README now lists `ic-secp256r1` among the crates
with hardcoded master keys.

Where the keys come from:

* **Mainnet:** the CUPs of subnets uzr34 (`key_1`) and fuqsr
(`test_key_1`). In that same output, the secp256k1, BIP340, Ed25519 and
VetKD master keys match the values `ic-secp256k1`, `ic-ed25519` and
`ic-vetkeys` already ship.
* **PocketIC:** derived the way the StateMachine does it, from
`PrivateKey::generate_insecure_key_for_testing(42)` and the key name.

### Tests

* `mainnet_keys_round_trip`: the mainnet keys round-trip.
* A test derives the PocketIC keys the StateMachine's way and compares
them with the hardcoded ones.
* `derive_mainnet_key` and `derive_pocketic_key` have known-answer tests
for all five keys. The expected keys are what the replica's
`derive_threshold_public_key` (the function that answers
`ecdsa_public_key`) returns for the same master key, canister and path.
* The SLIP-10 retry for nist256p1 (index 33941) is now tested. It's the
only branch of the derivation the existing tests didn't reach: it runs
when iL >= n, about once in 2^32 steps on P-256. The vector is the one
the replica's own key derivation test uses.
* Locally, in the dev container, all of these pass: `cargo check`,
`cargo fmt`, `rust-lint.sh`, `bazel build //... --nobuild`,
`//bazel:buildifier.check`, and the 37 tests from the depth-2 rdeps
query (this package, `ic-pub-key`, `rs/crypto`, the P-256 basic-sig
crate, `standalone-sig-verifier`, `rs/validator`).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
basvandijk pushed a commit to Ocheretovich/ic that referenced this pull request Oct 23, 2026
Bump the workspace dependency `ic-management-canister-types` from 0.8.0
to 0.11.0. This is needed for dfinity#11894 (offline derivation of secp256r1
keys in `ic-pub-key`).

Between 0.8.0 and 0.11.0 nothing was removed or renamed. The additions
still break Rust code in places, because none of these types is
`#[non_exhaustive]`:
- `CanisterSettings` and `DefiniteCanisterSettings` gained the fields
`minimum_incoming_canister_call_cycles` and `status_visibility`, along
with the new `StatusVisibility` type.
- `HttpRequestArgs` gained `pricing_version`, and `HttpMethod` gained
`PATCH`.
- New types for flexible HTTP outcalls and subnet metrics.
- `EcdsaCurve` gained `Secp256r1`.

Changes that follow from the bump:
- `canister_test`: one struct literal lists every `CanisterSettings`
field, so it now sets the two new fields as well.
- CMC: `cmc.did` declares the two new `CanisterSettings` fields, because
`test_candid_interface_compatibility` requires it. The CMC now forwards
these two settings in `create_canister` and `notify_create_canister`.
Before, it dropped them. `unreleased_changelog.md` records this. dfinity#10756
did the same for `snapshot_visibility`.
- ckDOGE minter: `ckdoge_minter.did` declares the two new
`DefiniteCanisterSettings` fields, which `get_canister_status` now
returns. The replica returns both fields unconditionally since dfinity#10398
and dfinity#10667, and all mainnet GuestOS versions in
`mainnet-icos-revisions.json` contain both PRs. So decoding
`canister_status` with the new types also works on mainnet.
- Changelogs: `pocket-ic` and `ic-pub-key` re-export these types, so
both record the bump.

The older copies left in `Cargo.lock` come from `ic-utils` and
`evm_rpc_types`, which have no newer release, and from the cdk, which a
follow-up PR bumps.

This does not yet remove the secp256r1 workarounds in the system tests
(`cast_ecdsa_key_id`, the signer canister) and in the PocketIC test
canister. Those are cdk canisters, and the workspace's
`ic-cdk-management-canister` 0.1.1 re-exports the 0.7.1 types. Its 0.3.0
requires `ic-management-canister-types` ^0.11, so this bump comes first.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread packages/ic-pub-key/src/lib.rs
Comment thread packages/ic-pub-key/tests/tests.rs Outdated
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The integration follows existing derivation patterns, preserves other algorithms, and has known-answer coverage with no unresolved blocking findings.

No open issues 🎉

Review effort: Balanced

@eichhorl
eichhorl enabled stacked auto-merge October 23, 2026 16:12
@eichhorl
eichhorl added this pull request to the merge queue Oct 23, 2026
Merged via the queue into master with commit ee9f72c Oct 23, 2026
41 checks passed
@eichhorl
eichhorl deleted the eichhorl/ic-pub-key-secp256r1 branch October 23, 2026 16:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants