Skip to content

Commit dc40657

Browse files
authored
feat: CRP-2797 Add functions to access the production master key (#171)
This allows performing key derivation completely offline, without having to interact with the IC.
1 parent b439199 commit dc40657

File tree

8 files changed

+183
-18
lines changed

8 files changed

+183
-18
lines changed

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/rs/ic_vetkeys/CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Change Log
22

3+
## [0.3.1] - Not Yet Released
4+
5+
### Added
6+
7+
- Added MasterPublicKey::production_key which allows accessing the production public keys
8+
39
## [0.3.0] - 2025-06-30
410

511
### Added
@@ -27,4 +33,4 @@
2733

2834
## [0.1.0] - 2025-05-27
2935

30-
Initial release
36+
Initial release

backend/rs/ic_vetkeys/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ ic_bls12_381 = { version = "0.10.1", default-features = false, features = [
3535
] }
3636
hkdf = { version = "0.12" }
3737
futures = "0.3.31"
38-
hex = { workspace = true }
38+
hex-literal = { version = "1" }
3939
ic-cdk = { workspace = true }
4040
ic-cdk-macros = { workspace = true }
4141
ic-stable-structures = { workspace = true }

backend/rs/ic_vetkeys/src/utils/mod.rs

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#![warn(rust_2018_idioms)]
77
#![forbid(missing_docs)]
88

9+
use hex_literal::hex;
910
use ic_bls12_381::{
1011
hash_to_curve::{ExpandMsgXmd, HashToCurve},
1112
G1Affine, G1Projective, G2Affine, G2Prepared, Gt, Scalar,
@@ -17,8 +18,15 @@ use std::array::TryFromSliceError;
1718
use std::ops::Neg;
1819
use zeroize::{Zeroize, ZeroizeOnDrop};
1920

21+
const MASTER_PUBLIC_KEY_BYTES_KEY_1 : [u8; 96] = hex!("a9caf9ae8af0c7c7272f8a122133e2e0c7c0899b75e502bda9e109ca8193ded3ef042ed96db1125e1bdaad77d8cc60d917e122fe2501c45b96274f43705edf0cfd455bc66c3c060faa2fcd15486e76351edf91fecb993797273bbc8beaa47404");
22+
23+
const MASTER_PUBLIC_KEY_BYTES_TEST_KEY_1 : [u8; 96] = hex!("ad86e8ff845912f022a0838a502d763fdea547c9948f8cb20ea7738dd52c1c38dcb4c6ca9ac29f9ac690fc5ad7681cb41922b8dffbd65d94bff141f5fb5b6624eccc03bf850f222052df888cf9b1e47203556d7522271cbb879b2ef4b8c2bfb1");
24+
2025
lazy_static::lazy_static! {
2126
static ref G2PREPARED_NEG_G : G2Prepared = G2Affine::generator().neg().into();
27+
28+
static ref G2_KEY_1: G2Affine = G2Affine::from_compressed(&MASTER_PUBLIC_KEY_BYTES_KEY_1).expect("Hardcoded master public key not a valid point");
29+
static ref G2_TEST_KEY_1: G2Affine = G2Affine::from_compressed(&MASTER_PUBLIC_KEY_BYTES_TEST_KEY_1).expect("Hardcoded master public key not a valid point");
2230
}
2331

2432
const G1AFFINE_BYTES: usize = 48; // Size of compressed form
@@ -146,6 +154,15 @@ pub enum PublicKeyDeserializationError {
146154
InvalidPublicKey,
147155
}
148156

157+
/// Enumeration identifying the production master public key
158+
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
159+
pub enum MasterPublicKeyId {
160+
/// The production key created in June 2025
161+
Key1,
162+
/// The test key created in May 2025
163+
TestKey1,
164+
}
165+
149166
#[derive(Clone, Debug, Eq, PartialEq)]
150167
/// A master VetKD public key
151168
pub struct MasterPublicKey {
@@ -155,9 +172,6 @@ pub struct MasterPublicKey {
155172
impl MasterPublicKey {
156173
const BYTES: usize = G2AFFINE_BYTES;
157174

158-
// TODO(CRP-2797) add
159-
// pub fn production_key(key_id: SomeEnum) -> Self
160-
161175
/// Deserializes a (derived) public key.
162176
///
163177
/// Only compressed points are supported.
@@ -200,6 +214,18 @@ impl MasterPublicKey {
200214
pub fn serialize(&self) -> Vec<u8> {
201215
self.point.to_compressed().to_vec()
202216
}
217+
218+
/// Return the hardcoded master public key used on IC
219+
///
220+
/// This allows performing public key derivation offline
221+
pub fn production_key(key_id: MasterPublicKeyId) -> Self {
222+
match key_id {
223+
MasterPublicKeyId::Key1 => Self { point: *G2_KEY_1 },
224+
MasterPublicKeyId::TestKey1 => Self {
225+
point: *G2_TEST_KEY_1,
226+
},
227+
}
228+
}
203229
}
204230

205231
#[derive(Clone, Debug, Eq, PartialEq)]

backend/rs/ic_vetkeys/tests/utils.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use ic_bls12_381::*;
22
use ic_vetkeys::*;
33
use ic_vetkeys_test_utils::*;
44
use rand::Rng;
5+
use hex_literal::hex;
56

67
#[test]
78
fn test_hkdf_test_vector() {
@@ -76,6 +77,52 @@ fn test_bls_signature_verification_using_identity() {
7677
assert!(!verify_bls_signature(&dpk, msg, &signature));
7778
}
7879

80+
#[test]
81+
fn test_derivation_using_test_key_1() {
82+
// This test data was generated on mainnet using test_key_1
83+
84+
let test_key1 = MasterPublicKey::production_key(MasterPublicKeyId::TestKey1);
85+
86+
let canister_id = hex!("0000000000c0a0d00101");
87+
88+
let canister_key = test_key1.derive_canister_key(&canister_id);
89+
90+
assert_eq!(
91+
hex::encode(canister_key.serialize()),
92+
"8b961f06d392367e84136088971c4808b434e5d6b928b60fa6177f811db9930e4f2a911ef517db40f7e7897588ae0e2316500dbef3abf08ad7f63940af0cf816c2c1c234943c9bb6f4d53da121dceed093d118d0bd5552740da315eac3b59b0f",
93+
);
94+
95+
let derived_key = canister_key.derive_sub_key(b"context-string");
96+
97+
assert_eq!(
98+
hex::encode(derived_key.serialize()),
99+
"958a2700438db39cf848f99c80d4d1c0f42b5e6783c35abffe5acda4fdb09548a025fdf85aad8980fcf6e20c1082596310c2612a3f3034c56445ddfc32a0c3cd34a7d0fea8df06a2996c54e21e3f8361a6e633d706ff58e979858fe436c7edf3",
100+
);
101+
}
102+
103+
#[test]
104+
fn test_derivation_using_production_key() {
105+
// This test data was generated on mainnet using key_1
106+
107+
let key1 = MasterPublicKey::production_key(MasterPublicKeyId::Key1);
108+
109+
let canister_id = hex!("0000000000c0a0d00101");
110+
111+
let canister_key = key1.derive_canister_key(&canister_id);
112+
113+
assert_eq!(
114+
hex::encode(canister_key.serialize()),
115+
"a4df5fb733dc53ba0b3f8dab3f7538b2f345052072f69a5749d630d9c2b2b1c4b00af09fa1d993e1ce533996961575ad027e058e2a279ab05271c115ef27d750b6b233f12bc9f1973b203e338d43b6a7617be58d5c7195dfb809d756413bc006",
116+
);
117+
118+
let derived_key = canister_key.derive_sub_key(b"context-string");
119+
120+
assert_eq!(
121+
hex::encode(derived_key.serialize()),
122+
"aa45fccb82432315e39fedb1b1f150d2e895fb1f7399cc593b826ac151b519f0966b92aef49a89efe60570ef325f0f7e1974ac3519d2e127a52c013e246aedbff2158bdd0bb9f26c763c88c0b8ec796f401d057eab276d0a34384a8a97b1937f",
123+
);
124+
}
125+
79126
#[test]
80127
fn test_second_level_public_key_derivation() {
81128
let canister_key = DerivedPublicKey::deserialize(&hex::decode("8bf165ea580742abf5fd5123eb848aa116dcf75c3ddb3cd3540c852cf99f0c5394e72dfc2f25dbcb5f9220f251cd04040a508a0bcb8b2543908d6626b46f09d614c924c5deb63a9949338ae4f4ac436bd77f8d0a392fd29de0f392a009fa61f3").unwrap()).unwrap();

frontend/ic_vetkeys/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Change Log
22

3+
## [0.3.1] - Not Yet Released
4+
5+
### Added
6+
7+
- Added MasterPublicKey.productionKey which allows accessing the production public keys
8+
39
## [0.3.0] - 2025-06-30
410

511
### Changed

frontend/ic_vetkeys/src/utils/utils.test.ts

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
IbeIdentity,
55
IbeCiphertext,
66
MasterPublicKey,
7+
MasterPublicKeyId,
78
IbeSeed,
89
TransportSecretKey,
910
VetKey,
@@ -74,20 +75,49 @@ test("parsing DerivedPublicKey", () => {
7475
assertEqual(valid, key.publicKeyBytes());
7576
});
7677

77-
test("MasterPublicKey derivation", () => {
78-
const masterKey = MasterPublicKey.deserialize(
79-
hexToBytes(
80-
"9183b871aa141d15ba2efc5bc58a49cb6a167741364804617f48dfe11e0285696b7018f172dad1a87ed81abf27ea4c320995041e2ee4a47b2226a2439d92a38557a7e2acc72fd157283b20f1f37ba872be235214c6a9cbba1eb2ef39deec72a5",
81-
),
78+
test("MasterPublicKey derivation using test key", () => {
79+
const masterKey = MasterPublicKey.productionKey(
80+
MasterPublicKeyId.TEST_KEY_1,
81+
);
82+
83+
const canisterId = hexToBytes("0000000000c0a0d00101");
84+
85+
const canisterKey = masterKey.deriveCanisterKey(canisterId);
86+
87+
assertEqual(
88+
bytesToHex(canisterKey.publicKeyBytes()),
89+
"8b961f06d392367e84136088971c4808b434e5d6b928b60fa6177f811db9930e4f2a911ef517db40f7e7897588ae0e2316500dbef3abf08ad7f63940af0cf816c2c1c234943c9bb6f4d53da121dceed093d118d0bd5552740da315eac3b59b0f",
90+
);
91+
92+
const derivedKey = canisterKey.deriveSubKey(
93+
new TextEncoder().encode("context-string"),
94+
);
95+
96+
assertEqual(
97+
bytesToHex(derivedKey.publicKeyBytes()),
98+
"958a2700438db39cf848f99c80d4d1c0f42b5e6783c35abffe5acda4fdb09548a025fdf85aad8980fcf6e20c1082596310c2612a3f3034c56445ddfc32a0c3cd34a7d0fea8df06a2996c54e21e3f8361a6e633d706ff58e979858fe436c7edf3",
8299
);
100+
});
101+
102+
test("MasterPublicKey derivation using prod key", () => {
103+
const masterKey = MasterPublicKey.productionKey();
83104

84-
const canisterId = new TextEncoder().encode("test-canister-id");
105+
const canisterId = hexToBytes("0000000000c0a0d00101");
85106

86-
const derivedKey = masterKey.deriveKey(canisterId);
107+
const canisterKey = masterKey.deriveCanisterKey(canisterId);
108+
109+
assertEqual(
110+
bytesToHex(canisterKey.publicKeyBytes()),
111+
"a4df5fb733dc53ba0b3f8dab3f7538b2f345052072f69a5749d630d9c2b2b1c4b00af09fa1d993e1ce533996961575ad027e058e2a279ab05271c115ef27d750b6b233f12bc9f1973b203e338d43b6a7617be58d5c7195dfb809d756413bc006",
112+
);
113+
114+
const derivedKey = canisterKey.deriveSubKey(
115+
new TextEncoder().encode("context-string"),
116+
);
87117

88118
assertEqual(
89119
bytesToHex(derivedKey.publicKeyBytes()),
90-
"af78a908589d332fc8b9d042807c483e73872e2aea7620bdb985b9289d5a99ebfd5ac0ec4844a4c542f6d0f12a716d941674953cef4f38dde601ce9792db8832557eaa051733c5541fa5017465d69b62cc4d93f2079fb8c050b4bd735ef75859",
120+
"aa45fccb82432315e39fedb1b1f150d2e895fb1f7399cc593b826ac151b519f0966b92aef49a89efe60570ef325f0f7e1974ac3519d2e127a52c013e246aedbff2158bdd0bb9f26c763c88c0b8ec796f401d057eab276d0a34384a8a97b1937f",
91121
);
92122
});
93123

@@ -100,7 +130,7 @@ test("DerivedPublicKey subderivation", () => {
100130

101131
const context = new TextEncoder().encode("test-context");
102132

103-
const derivedKey = canisterKey.deriveKey(context);
133+
const derivedKey = canisterKey.deriveSubKey(context);
104134

105135
assertEqual(
106136
bytesToHex(derivedKey.publicKeyBytes()),

frontend/ic_vetkeys/src/utils/utils.ts

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,27 @@ function prefixWithLen(input: Uint8Array): Uint8Array {
110110
return result;
111111
}
112112

113+
/**
114+
* Enumeration identifying possible master public keys
115+
*/
116+
export enum MasterPublicKeyId {
117+
/** The production key generated in June 2025 */
118+
KEY_1 = "key_1",
119+
/** The test key generated in May 2025 */
120+
TEST_KEY_1 = "test_key_1",
121+
}
122+
123+
/**
124+
* @internal helper to perform hex decoding
125+
*/
126+
function hexToBytes(hex: string): Uint8Array {
127+
const bytes = new Uint8Array(hex.length / 2);
128+
for (let i = 0; i < hex.length; i += 2) {
129+
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
130+
}
131+
return bytes;
132+
}
133+
113134
/**
114135
* VetKD master key
115136
*
@@ -141,7 +162,7 @@ export class MasterPublicKey {
141162
* plus the canister identity. This avoids having to interact with the IC for performing this
142163
* computation.
143164
*/
144-
deriveKey(canisterId: Uint8Array): DerivedPublicKey {
165+
deriveCanisterKey(canisterId: Uint8Array): DerivedPublicKey {
145166
const dst = "ic-vetkd-bls12-381-g2-canister-id";
146167
const pkbytes = this.publicKeyBytes();
147168
const randomOracleInput = new Uint8Array([
@@ -161,9 +182,31 @@ export class MasterPublicKey {
161182
}
162183

163184
/**
164-
* TODO CRP-2797 add getter for the production subnet key once this has been
165-
* generated.
185+
* Return the hardcoded master public key used on IC
186+
*
187+
* This allows performing public key derivation offline
166188
*/
189+
static productionKey(
190+
keyId: MasterPublicKeyId = MasterPublicKeyId.KEY_1,
191+
): MasterPublicKey {
192+
if (keyId == MasterPublicKeyId.KEY_1) {
193+
return MasterPublicKey.deserialize(
194+
hexToBytes(
195+
"a9caf9ae8af0c7c7272f8a122133e2e0c7c0899b75e502bda9e109ca8193ded3ef042ed96db1125e1bdaad77d8cc60d917e122fe2501c45b96274f43705edf0cfd455bc66c3c060faa2fcd15486e76351edf91fecb993797273bbc8beaa47404",
196+
),
197+
);
198+
} else if (keyId == MasterPublicKeyId.TEST_KEY_1) {
199+
return MasterPublicKey.deserialize(
200+
hexToBytes(
201+
"ad86e8ff845912f022a0838a502d763fdea547c9948f8cb20ea7738dd52c1c38dcb4c6ca9ac29f9ac690fc5ad7681cb41922b8dffbd65d94bff141f5fb5b6624eccc03bf850f222052df888cf9b1e47203556d7522271cbb879b2ef4b8c2bfb1",
202+
),
203+
);
204+
} else {
205+
throw new Error(
206+
"Unknown MasterPublicKeyId value for productionKey",
207+
);
208+
}
209+
}
167210

168211
/**
169212
* @internal constructor
@@ -210,7 +253,7 @@ export class DerivedPublicKey {
210253
* If `context` is empty, then this simply returns the underlying key. This matches the behavior
211254
* of `vetkd_public_key`
212255
*/
213-
deriveKey(context: Uint8Array): DerivedPublicKey {
256+
deriveSubKey(context: Uint8Array): DerivedPublicKey {
214257
if (context.length === 0) {
215258
return this;
216259
} else {

0 commit comments

Comments
 (0)