From 09a66e14fd76f26a061a0c22dd42d33073b6dc5b Mon Sep 17 00:00:00 2001 From: dumblepy <39766805+dumblepy@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:08:53 +0900 Subject: [PATCH 1/6] rm #main --- nicp_cdk.nimble | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nicp_cdk.nimble b/nicp_cdk.nimble index 1e918a2..2b9b7ee 100644 --- a/nicp_cdk.nimble +++ b/nicp_cdk.nimble @@ -17,7 +17,7 @@ requires "nim >= 2.2.2" requires "cligen >= 1.8.3" requires "illwill >= 0.4.1" requires "base32 >= 0.1.3" -requires "https://github.com/dumblepy/nim-rustcrypto#main" +requires "https://github.com/dumblepy/nim-rustcrypto" task test, "Run tests": exec """testament p "tests/test_*.nim" """ From 4d85f4eeb5fd0021daaaf400e1ab59f8e015a14e Mon Sep 17 00:00:00 2001 From: dumblepy <39766805+dumblepy@users.noreply.github.com> Date: Thu, 20 Aug 2026 00:21:09 +0900 Subject: [PATCH 2/6] nicp cHeaders --- runTest.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/runTest.sh b/runTest.sh index dd30874..a9727ca 100755 --- a/runTest.sh +++ b/runTest.sh @@ -19,6 +19,7 @@ trap cleanup_icp_state EXIT nimble uninstall nicp_cdk -iy >/dev/null 2>&1 || true nimble install -y +nicp cHeaders free_icp_gateway_port cleanup_icp_state From c7ba8e5c8874b535de7d926c84651625d7e63c2e Mon Sep 17 00:00:00 2001 From: itsumura-h Date: Wed, 19 Aug 2026 15:55:51 +0000 Subject: [PATCH 3/6] =?UTF-8?q?canister.yaml=E3=81=A8icp.yaml=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=E3=81=97=E3=80=81icp=5Fnetwork.nim=E3=81=AE?= =?UTF-8?q?=E3=82=B3=E3=83=9E=E3=83=B3=E3=83=89=E3=82=92nicp=E3=81=AB?= =?UTF-8?q?=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/t_ecdsa/backend/canister.yaml | 8 ++++++++ examples/t_ecdsa/icp.yaml | 16 ++++++++++++++++ tests/icp_network.nim | 2 +- 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 examples/t_ecdsa/backend/canister.yaml create mode 100644 examples/t_ecdsa/icp.yaml diff --git a/examples/t_ecdsa/backend/canister.yaml b/examples/t_ecdsa/backend/canister.yaml new file mode 100644 index 0000000..ca41c05 --- /dev/null +++ b/examples/t_ecdsa/backend/canister.yaml @@ -0,0 +1,8 @@ +# yaml-language-server: $schema=https://github.com/dfinity/icp-cli/raw/refs/tags/v0.1.0/docs/schemas/canister-yaml-schema.json + +name: backend +build: + steps: + - type: script + commands: + - bash -c 'if [ "${DFX_NETWORK:-local}" = "local" ]; then nicp developmentBuild; else nicp productionBuild; fi' diff --git a/examples/t_ecdsa/icp.yaml b/examples/t_ecdsa/icp.yaml new file mode 100644 index 0000000..d4f08d0 --- /dev/null +++ b/examples/t_ecdsa/icp.yaml @@ -0,0 +1,16 @@ +# yaml-language-server: $schema=https://github.com/dfinity/icp-cli/raw/refs/tags/v0.1.0/docs/schemas/icp-yaml-schema.json + +canisters: + - backend + +networks: + - name: local + mode: managed + gateway: + bind: "0.0.0.0" + port: 8000 + ii: true + +environments: + - name: local + network: local diff --git a/tests/icp_network.nim b/tests/icp_network.nim index e6e6e2c..44225f1 100644 --- a/tests/icp_network.nim +++ b/tests/icp_network.nim @@ -23,7 +23,7 @@ proc stopIcpNetwork*(projectDir: string) = proc startIcpNetwork*(projectDir: string) = var lastCode = -1 for attempt in 0..2: - lastCode = runIcpCommand(projectDir, "icp network start -d >/dev/null 2>&1") + lastCode = runIcpCommand(projectDir, "nicp network >/dev/null 2>&1") if lastCode == 0: return sleep(1000) From ece3a2917ce25dca926f571ac90b048558c2afa9 Mon Sep 17 00:00:00 2001 From: itsumura-h Date: Wed, 19 Aug 2026 17:37:34 +0000 Subject: [PATCH 4/6] =?UTF-8?q?canister.yaml=E3=81=AE=E4=BE=9D=E5=AD=98?= =?UTF-8?q?=E9=96=A2=E4=BF=82=E3=82=92@dfinity/asset-canister@v2.2.1?= =?UTF-8?q?=E3=81=AB=E6=9B=B4=E6=96=B0=E3=81=97=E3=80=81t=5Fecdsa.nim?= =?UTF-8?q?=E3=81=A7=E3=81=AE=E3=82=B3=E3=82=B9=E3=83=88=E6=8E=A8=E5=AE=9A?= =?UTF-8?q?=E5=87=A6=E7=90=86=E3=82=92=E6=94=B9=E5=96=84=E3=80=82test=5Fec?= =?UTF-8?q?dsa.nim=E3=81=AB=E7=BD=B2=E5=90=8D=E7=B5=90=E6=9E=9C=E3=81=AE?= =?UTF-8?q?=E5=87=BA=E5=8A=9B=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=97=E3=80=81?= =?UTF-8?q?test=5Fvetkey.nim=E3=81=A7ICP=E3=81=AE=E5=AD=98=E5=9C=A8?= =?UTF-8?q?=E3=83=81=E3=82=A7=E3=83=83=E3=82=AF=E3=82=92=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/vetkey/frontend/canister.yaml | 2 +- .../canisters/management_canister/t_ecdsa.nim | 80 +++++++++---------- tests/management_canister/test_ecdsa.nim | 1 + tests/management_canister/test_vetkey.nim | 8 +- 4 files changed, 46 insertions(+), 45 deletions(-) diff --git a/examples/vetkey/frontend/canister.yaml b/examples/vetkey/frontend/canister.yaml index 6e15aca..255d2e7 100644 --- a/examples/vetkey/frontend/canister.yaml +++ b/examples/vetkey/frontend/canister.yaml @@ -4,7 +4,7 @@ name: frontend recipe: # https://github.com/dfinity/icp-cli-recipes/blob/main/recipes/asset-canister/README.md - type: "@dfinity/asset-canister@v2.1.0" + type: "@dfinity/asset-canister@v2.2.1" configuration: build: - pnpm install diff --git a/src/nicp_cdk/canisters/management_canister/t_ecdsa.nim b/src/nicp_cdk/canisters/management_canister/t_ecdsa.nim index 06d8d6e..587c89b 100644 --- a/src/nicp_cdk/canisters/management_canister/t_ecdsa.nim +++ b/src/nicp_cdk/canisters/management_canister/t_ecdsa.nim @@ -2,7 +2,6 @@ import std/options import std/asyncfutures import std/asyncdispatch import std/tables -import std/strutils import ../../ic0/ic0 import ../../ic_types/candid_types import ../../ic_types/ic_principal @@ -10,10 +9,12 @@ import ../../ic_types/ic_record import ../../ic_types/candid_message/candid_encode import ../../ic_types/candid_message/candid_decode import ../../ic_types/candid_message/candid_message_types -import ./estimateGas import ./management_canister_type import ../../ic_api +when defined(release): + import ./estimateGas + # ================================================================================ # Utilities @@ -71,19 +72,22 @@ type # Constants # ================================================================================ const - # ECDSA: フォールバック推定(サイズベース)の係数 - # - 動的推定(ic0_cost_sign_with_ecdsa)が使えない環境向けの保守的な概算 - EcdsaFallbackBaseCycles = 18_000_000_000'u64 - EcdsaFallbackPerPayloadByteCycles = 30_000_000'u64 - -proc estimateEcdsaCostFallback(payload: seq[uint8]): uint64 = - let payloadSize = payload.len.uint64 - var cost = EcdsaFallbackBaseCycles - cost = addCap(cost, mulCap(payloadSize, EcdsaFallbackPerPayloadByteCycles)) - let finalCost = addMargin20(cost) - devEcho "📊 Estimated ECDSA cost (fallback): ", cost, " cycles + 20% margin = ", finalCost, - " (payload size: ", payloadSize, " bytes)" - finalCost + # 動的コスト API を利用できない開発ビルド用の、公式価格表に基づく最低額。 + # https://docs.internetcomputer.org/references/cycle-costs/ + # 署名料金は Candid ペイロードの大きさではなく、鍵を保持するサブネットで決まる。 + EcdsaTestKeySigningCycles = 10_000_000_000'u64 + EcdsaProductionKeySigningCycles = 26_153_846_153'u64 + +proc estimateEcdsaCostFallback(keyId: EcdsaKeyId): uint64 = + ## 開発ビルドでは System API の動的見積りを使えないため、公開済みの鍵別料金を使う。 + ## 未知の鍵には、本番用 key_1 と同じ保守的な料金を付与する。 + let cost = if keyId.name == "test_key_1": + EcdsaTestKeySigningCycles + else: + EcdsaProductionKeySigningCycles + devEcho "📊 Estimated ECDSA cost (fallback): ", cost, + " cycles (key name: ", keyId.name, ")" + cost # ================================================================================ @@ -101,16 +105,19 @@ when defined(release): except: return false - proc estimateEcdsaCostDynamic(keyId: EcdsaKeyId, payload: seq[uint8]): Option[uint64] = + proc estimateEcdsaCostDynamic(keyId: EcdsaKeyId): Option[uint64] = ## ic0_cost_sign_with_ecdsa APIを使用した動的なcycle計算 + ## API の src / size には Candid payload ではなく key_id.name を渡す。 ## 成功時は計算されたcycle量を返し、失敗時はnoneを返す try: + if keyId.name.len == 0: + return none(uint64) let curveValue = uint32(keyId.curve.ord) var costBuffer: array[16, uint8] # 128bit for cycles let apiResult = ic0_cost_sign_with_ecdsa( - ptrToInt(addr payload[0]), # ペイロードの先頭アドレス - payload.len, # ペイロードのサイズ + ptrToInt(addr keyId.name[0]), # 鍵名の先頭アドレス + keyId.name.len, # 鍵名のバイト長 curveValue, # ECDSA曲線タイプ ptrToInt(addr costBuffer[0]) # 結果を格納するバッファ ) @@ -127,39 +134,31 @@ when defined(release): devEcho "⚠️ ic0_cost_sign_with_ecdsa returned 0 cycles" return none(uint64) - # 20%の安全マージンを追加 - let finalCost = addMargin20(exactCost) - devEcho "📊 Estimated ECDSA cost (dynamic): ", exactCost, " cycles + 20% margin = ", finalCost, " cycles" - return some(finalCost) + devEcho "📊 Estimated ECDSA cost (dynamic): ", exactCost, " cycles" + return some(exactCost) - except Exception as e: - devEcho "⚠️ Failed to estimate ECDSA cost dynamically: ", e.msg + except Exception: + devEcho "⚠️ Failed to estimate ECDSA cost dynamically" return none(uint64) -proc estimateEcdsaCost(keyId: EcdsaKeyId, payload: seq[uint8]): uint64 = +proc estimateEcdsaCost(keyId: EcdsaKeyId): uint64 = ## ECDSAのサイクル使用量を計算 ## keyId: 使用する鍵の情報 - ## payload: Candidエンコードされた引数データ - ## - ## コンパイル時フラグ `-d:enableEcdsaDynamicCost` を指定すると、 - ## レプリカ環境で動的計算を試行します。 - ## デフォルトではフォールバック値を使用します(ローカルレプリカで安全)。 + ## リリースビルドでは、レプリカ環境で System API により現在の料金を取得する。 - # 動的計算の有効化フラグ(デフォルト: 無効) - when defined(enableEcdsaDynamicCost) and defined(release): + when defined(release): # メインネット/テストネット用: 動的計算を試行 try: if isReplicatedExecution(): devEcho "🔍 Attempting dynamic ECDSA cost estimation..." - let dynamicCost = estimateEcdsaCostDynamic(keyId, payload) + let dynamicCost = estimateEcdsaCostDynamic(keyId) if dynamicCost.isSome: return dynamicCost.get - except Exception as e: - devEcho "⚠️ Dynamic cost estimation failed: ", e.msg + except Exception: + devEcho "⚠️ Dynamic cost estimation failed" # フォールバックへ続行 - # デフォルト: サイズベースのフォールバック推定(ローカルレプリカ対応) - return estimateEcdsaCostFallback(payload) + return estimateEcdsaCostFallback(keyId) # ================================================================================ @@ -279,11 +278,6 @@ proc publicKey*(_:type ManagementCanister, arg: EcdsaPublicKeyArgs): Future[Ecds let candidValue = newCandidRecord(arg) let encoded = encodeCandidMessage(@[candidValue]) - # cycle量を計算して追加 - let requiredCycles = estimateEcdsaCost(arg.key_id, encoded) - devEcho "Adding cycles for ECDSA public_key: ", requiredCycles - ic0_call_cycles_add128(0, requiredCycles) - ## 3. Execute call ic0_call_data_append(ptrToInt(addr encoded[0]), encoded.len) let err = ic0_call_perform() @@ -322,7 +316,7 @@ proc sign*(_:type ManagementCanister, arg: EcdsaSignArgs): Future[SignWithEcdsaR let encoded = encodeCandidMessage(@[candidValue]) # cycle量を計算して追加 - let requiredCycles = estimateEcdsaCost(arg.key_id, encoded) + let requiredCycles = estimateEcdsaCost(arg.key_id) devEcho "Adding cycles for ECDSA sign: ", requiredCycles ic0_call_cycles_add128(0, requiredCycles) diff --git a/tests/management_canister/test_ecdsa.nim b/tests/management_canister/test_ecdsa.nim index 7c075d7..06b4945 100644 --- a/tests/management_canister/test_ecdsa.nim +++ b/tests/management_canister/test_ecdsa.nim @@ -116,6 +116,7 @@ withIcpNetwork(T_ECDSA_DIR): # メッセージに署名 let testMessage = "\"Hello, ICP ECDSA!\"" let signResult = callCanisterFunction("signWithEcdsa", testMessage) + echo "=== signResult: ", signResult # 署名が正常に生成されることを確認 check signResult.contains("\"") and signResult.len > 10 diff --git a/tests/management_canister/test_vetkey.nim b/tests/management_canister/test_vetkey.nim index b385a6d..765c205 100644 --- a/tests/management_canister/test_vetkey.nim +++ b/tests/management_canister/test_vetkey.nim @@ -34,6 +34,12 @@ const GOOD_TRANSPORT_PUBLIC_KEY_HEX = "a7e75af9dd4d868a41ad2f5a5b021d653e31084261724fb40ae2f1b1c31c778d3b9464502d599cf6720723ec5c68b59d" +when isMainModule: + if findExe(ICP_PATH).len == 0: + echo "Skipping test_vetkey because icp is unavailable in this environment." + quit(0) + + # 統合テスト用: icp CLI のラッパと `icp canister call` 向け Candid 引数の組み立て。 var alicePrincipal = "" @@ -269,7 +275,7 @@ suite "vetKD Candid tests": check record["transport_public_key"].getBlob() == @[9'u8, 10'u8] -withRestartedIcpNetwork(VETKEY_DIR): +withIcpNetwork(VETKEY_DIR): # examples/vetkey をデプロイし、identity 付きの canister call でサンプル API の振る舞いを検証する。 suite "vetKD integration tests": test "Prepare identities and deploy vetkey canister": From ae499240d80cf9cb61cdb2c70ecbf8578bddd894 Mon Sep 17 00:00:00 2001 From: itsumura-h Date: Wed, 19 Aug 2026 17:45:35 +0000 Subject: [PATCH 5/6] fix vetkey test --- tests/management_canister/test_vetkey.nim | 356 ++-------------------- 1 file changed, 29 insertions(+), 327 deletions(-) diff --git a/tests/management_canister/test_vetkey.nim b/tests/management_canister/test_vetkey.nim index 765c205..edcf809 100644 --- a/tests/management_canister/test_vetkey.nim +++ b/tests/management_canister/test_vetkey.nim @@ -7,7 +7,7 @@ discard """ # vetKD 周りの検証。 # - vetKD Candid tests: CDK の Candid エンコード/デコードが型どおりか(icp 不要)。 # - vetKD integration tests: `icp` が PATH にあるときのみ。ローカル ICP 上の examples/vetkey -# キャニスターを CLI 経由で呼び、プライベート/共有ノートと ACL・エラー応答を確認する。 +# キャニスターを CLI 経由で呼び、caller と key version に応じた Private KV envelope を確認する。 # Private KV 往復の client 側暗号は `vetkey_roundtrip_crypto.nim`(rustcrypto BLS / HKDF / AES-GCM)で行う。 import std/unittest @@ -30,8 +30,6 @@ const CANISTER_NAME = "backend" ALICE_IDENTITY = "vetkey-alice" BOB_IDENTITY = "vetkey-bob" - CAROL_IDENTITY = "vetkey-carol" - GOOD_TRANSPORT_PUBLIC_KEY_HEX = "a7e75af9dd4d868a41ad2f5a5b021d653e31084261724fb40ae2f1b1c31c778d3b9464502d599cf6720723ec5c68b59d" when isMainModule: @@ -44,7 +42,6 @@ when isMainModule: var alicePrincipal = "" bobPrincipal = "" - carolPrincipal = "" backendCanisterId = "" @@ -92,10 +89,6 @@ proc quotedText(value: string): string = "\"" & value.replace("\\", "\\\\").replace("\"", "\\\"") & "\"" -proc principalLiteral(value: string): string = - "principal " & quotedText(value) - - proc tupleArgs(args: seq[string]): string = "(" & args.join(", ") & ")" @@ -104,41 +97,6 @@ proc nat64Literal(value: uint64): string = $value & ":nat64" -proc privateNoteArgs(noteId: string, keyVersion: uint64): string = - tupleArgs(@[ - quotedText(noteId), - quotedText("ciphertext:" & noteId), - quotedText("nonce:" & noteId), - quotedText("aad:" & noteId), - nat64Literal(keyVersion) - ]) - - -proc sharedNoteArgs(noteId: string, keyVersion: uint64): string = - tupleArgs(@[ - quotedText(noteId), - quotedText("shared-ciphertext:" & noteId), - quotedText("shared-nonce:" & noteId), - quotedText("shared-aad:" & noteId), - nat64Literal(keyVersion) - ]) - - -proc privateOwnerArgs(ownerPrincipal, noteId: string, transportPublicKey: string): string = - tupleArgs(@[ - principalLiteral(ownerPrincipal), - quotedText(noteId), - quotedText(transportPublicKey) - ]) - - -proc sharedNoteKeyArgs(noteId, transportPublicKey: string): string = - tupleArgs(@[ - quotedText(noteId), - quotedText(transportPublicKey) - ]) - - proc callCanisterFunction( projectDir: string, canisterName: string, @@ -174,19 +132,6 @@ proc callCanisterSuccess( output.strip() -proc callCanisterFailure( - projectDir: string, - canisterName: string, - functionName: string, - args: string = "", - identityName: string = "" -): string = - let (output, code) = callCanisterFunction(projectDir, canisterName, functionName, args, true, identityName) - let lower = output.toLowerAscii() - check code != 0 or lower.contains("reject") or lower.contains("error") or lower.contains("failed") - output.strip() - - proc deploy(projectDir: string, canisterName: string) = let originalDir = getCurrentDir() try: @@ -276,296 +221,57 @@ suite "vetKD Candid tests": withIcpNetwork(VETKEY_DIR): - # examples/vetkey をデプロイし、identity 付きの canister call でサンプル API の振る舞いを検証する。 + # 現在の examples/vetkey は、キャニスタに暗号文を保存しない Private KV の + # envelope API だけを公開している。identity ごとの context/input 分離と、 + # client 側の暗号化・復号を検証する。 suite "vetKD integration tests": test "Prepare identities and deploy vetkey canister": - # Alice/Bob/Carol の identity と principal を用意し、backend キャニスタをデプロイする前提整備。 ensureIdentity(VETKEY_DIR, ALICE_IDENTITY) ensureIdentity(VETKEY_DIR, BOB_IDENTITY) - ensureIdentity(VETKEY_DIR, CAROL_IDENTITY) alicePrincipal = identityPrincipal(VETKEY_DIR, ALICE_IDENTITY) bobPrincipal = identityPrincipal(VETKEY_DIR, BOB_IDENTITY) - carolPrincipal = identityPrincipal(VETKEY_DIR, CAROL_IDENTITY) deploy(VETKEY_DIR, CANISTER_NAME) check backendCanisterId.len > 0 sleep(2000) - test "Private notes keep caller-specific context and resource-specific input": - # 別ユーザー・別ノートで describe が principal 付きメタを返し、derive の応答に - # ノート別ラベルと public_key / encrypted_key が含まれること。Alice と Bob のエンベロープは一致しないこと。 - let aliceNote = "alice-note" - let bobNote = "bob-note" - - discard callCanisterSuccess( - VETKEY_DIR, - CANISTER_NAME, - "createPrivateNote", - privateNoteArgs(aliceNote, 1), - ALICE_IDENTITY - ) - discard callCanisterSuccess( - VETKEY_DIR, - CANISTER_NAME, - "createPrivateNote", - privateNoteArgs(bobNote, 1), - BOB_IDENTITY - ) - - let aliceSummary = callCanisterSuccess( - VETKEY_DIR, - CANISTER_NAME, - "describePrivateNote", - tupleArgs(@[principalLiteral(alicePrincipal), quotedText(aliceNote)]), - ALICE_IDENTITY - ) - let bobSummary = callCanisterSuccess( - VETKEY_DIR, - CANISTER_NAME, - "describePrivateNote", - tupleArgs(@[principalLiteral(bobPrincipal), quotedText(bobNote)]), - BOB_IDENTITY - ) - check aliceSummary.contains("ciphertext_len") - check aliceSummary.contains("note_id") - check aliceSummary.contains(alicePrincipal) - check bobSummary.contains("ciphertext_len") - check bobSummary.contains("note_id") - check bobSummary.contains(bobPrincipal) - - let aliceEnvelope = callCanisterSuccess( - VETKEY_DIR, - CANISTER_NAME, - "derivePrivateNoteKey", - privateOwnerArgs(alicePrincipal, aliceNote, GOOD_TRANSPORT_PUBLIC_KEY_HEX), - ALICE_IDENTITY - ) - let bobEnvelope = callCanisterSuccess( - VETKEY_DIR, - CANISTER_NAME, - "derivePrivateNoteKey", - privateOwnerArgs(bobPrincipal, bobNote, GOOD_TRANSPORT_PUBLIC_KEY_HEX), - BOB_IDENTITY - ) - check aliceEnvelope.contains("encrypted-notes-v1|owner=") - check bobEnvelope.contains("encrypted-notes-v1|owner=") - check aliceEnvelope.contains("note:alice-note:key:v1") - check bobEnvelope.contains("note:bob-note:key:v1") - check aliceEnvelope != bobEnvelope - check aliceEnvelope.contains("public_key") - check aliceEnvelope.contains("encrypted_key") - - test "Private note access control rejects a different caller": - # 所有者以外(Bob)が Alice のプライベートノートで derivePrivateNoteKey すると拒否されること。 - let unauthorized = callCanisterFailure( - VETKEY_DIR, - CANISTER_NAME, - "derivePrivateNoteKey", - privateOwnerArgs(alicePrincipal, "alice-note", GOOD_TRANSPORT_PUBLIC_KEY_HEX), - BOB_IDENTITY - ) - check unauthorized.contains("Unauthorized private note access") - check unauthorized.contains(alicePrincipal) - check unauthorized.contains(bobPrincipal) - - test "Resource isolation changes the private note input label": - # 同一オーナーでも note_id が異なれば鍵導出ラベルとエンベロープが別物になること(リソース分離)。 - let firstNote = "alice-resource-1" - let secondNote = "alice-resource-2" - - discard callCanisterSuccess( + test "Private KV envelope separates callers and key versions": + let (_, transportPublicHex) = generateVetkeyTransportKeyPair() + let aliceV1 = callCanisterSuccess( VETKEY_DIR, CANISTER_NAME, - "createPrivateNote", - privateNoteArgs(firstNote, 1), - ALICE_IDENTITY - ) - discard callCanisterSuccess( - VETKEY_DIR, - CANISTER_NAME, - "createPrivateNote", - privateNoteArgs(secondNote, 1), - ALICE_IDENTITY - ) - - let firstEnvelope = callCanisterSuccess( - VETKEY_DIR, - CANISTER_NAME, - "derivePrivateNoteKey", - privateOwnerArgs(alicePrincipal, firstNote, GOOD_TRANSPORT_PUBLIC_KEY_HEX), - ALICE_IDENTITY - ) - let secondEnvelope = callCanisterSuccess( - VETKEY_DIR, - CANISTER_NAME, - "derivePrivateNoteKey", - privateOwnerArgs(alicePrincipal, secondNote, GOOD_TRANSPORT_PUBLIC_KEY_HEX), - ALICE_IDENTITY - ) - check firstEnvelope.contains("note:alice-resource-1:key:v1") - check secondEnvelope.contains("note:alice-resource-2:key:v1") - check firstEnvelope != secondEnvelope - - test "Key rotation changes the input label and keeps the note metadata versioned": - # rotatePrivateNoteKey 後は derive のラベルが v2 に変わり、describe に key_version が反映されること。 - let rotatedNote = "alice-rotation" - - discard callCanisterSuccess( - VETKEY_DIR, - CANISTER_NAME, - "createPrivateNote", - privateNoteArgs(rotatedNote, 1), - ALICE_IDENTITY - ) - let beforeRotation = callCanisterSuccess( - VETKEY_DIR, - CANISTER_NAME, - "derivePrivateNoteKey", - privateOwnerArgs(alicePrincipal, rotatedNote, GOOD_TRANSPORT_PUBLIC_KEY_HEX), - ALICE_IDENTITY - ) - discard callCanisterSuccess( - VETKEY_DIR, - CANISTER_NAME, - "rotatePrivateNoteKey", - tupleArgs(@[principalLiteral(alicePrincipal), quotedText(rotatedNote), nat64Literal(2)]), - ALICE_IDENTITY - ) - let afterRotation = callCanisterSuccess( - VETKEY_DIR, - CANISTER_NAME, - "describePrivateNote", - tupleArgs(@[principalLiteral(alicePrincipal), quotedText(rotatedNote)]), - ALICE_IDENTITY - ) - let rotatedEnvelope = callCanisterSuccess( - VETKEY_DIR, - CANISTER_NAME, - "derivePrivateNoteKey", - privateOwnerArgs(alicePrincipal, rotatedNote, GOOD_TRANSPORT_PUBLIC_KEY_HEX), - ALICE_IDENTITY - ) - check beforeRotation.contains("note:alice-rotation:key:v1") - check rotatedEnvelope.contains("note:alice-rotation:key:v2") - check rotatedEnvelope != beforeRotation - check afterRotation.contains("key_version") - check afterRotation.contains("2") - - test "Shared notes allow authorized callers and reject revoked callers": - # 付与された二者の deriveSharedNoteKey が同一応答。revoke 後は ACL と derive が Bob を拒否すること。 - let sharedNote = "shared-team-note" - - discard callCanisterSuccess( - VETKEY_DIR, - CANISTER_NAME, - "createSharedNote", - sharedNoteArgs(sharedNote, 1), - ALICE_IDENTITY - ) - discard callCanisterSuccess( - VETKEY_DIR, - CANISTER_NAME, - "grantSharedNoteAccess", - tupleArgs(@[quotedText(sharedNote), principalLiteral(bobPrincipal)]), - ALICE_IDENTITY - ) - - let aliceShared = callCanisterSuccess( - VETKEY_DIR, - CANISTER_NAME, - "deriveSharedNoteKey", - sharedNoteKeyArgs(sharedNote, GOOD_TRANSPORT_PUBLIC_KEY_HEX), - ALICE_IDENTITY - ) - let bobShared = callCanisterSuccess( - VETKEY_DIR, - CANISTER_NAME, - "deriveSharedNoteKey", - sharedNoteKeyArgs(sharedNote, GOOD_TRANSPORT_PUBLIC_KEY_HEX), - BOB_IDENTITY - ) - check aliceShared.contains("shared-note-v1|note=shared-team-note") - check aliceShared.contains("content-key:v1") - check aliceShared == bobShared - - let sharedSummary = callCanisterSuccess( - VETKEY_DIR, - CANISTER_NAME, - "describeSharedNote", - tupleArgs(@[quotedText(sharedNote)]), - ALICE_IDENTITY - ) - check sharedSummary.contains(alicePrincipal) - check sharedSummary.contains(bobPrincipal) - check sharedSummary.contains("acl") - - discard callCanisterSuccess( - VETKEY_DIR, - CANISTER_NAME, - "revokeSharedNoteAccess", - tupleArgs(@[quotedText(sharedNote), principalLiteral(bobPrincipal)]), - ALICE_IDENTITY - ) - let revokedSummary = callCanisterSuccess( - VETKEY_DIR, - CANISTER_NAME, - "describeSharedNote", - tupleArgs(@[quotedText(sharedNote)]), + "derivePrivateKvEnvelope", + tupleArgs(@[quotedText(transportPublicHex), nat64Literal(1)]), ALICE_IDENTITY ) - check revokedSummary.contains(alicePrincipal) - check not revokedSummary.contains(bobPrincipal) - - let revokedFailure = callCanisterFailure( + let bobV1 = callCanisterSuccess( VETKEY_DIR, CANISTER_NAME, - "deriveSharedNoteKey", - sharedNoteKeyArgs(sharedNote, GOOD_TRANSPORT_PUBLIC_KEY_HEX), + "derivePrivateKvEnvelope", + tupleArgs(@[quotedText(transportPublicHex), nat64Literal(1)]), BOB_IDENTITY ) - check revokedFailure.contains("Unauthorized shared note access") - check revokedFailure.contains(bobPrincipal) - - test "Missing resources and invalid transport keys fail cleanly": - # 存在しない共有ノートと不正な transport 公開鍵で、期待するエラーメッセージ/失敗になること。 - let missingResource = callCanisterFailure( + let aliceV2 = callCanisterSuccess( VETKEY_DIR, CANISTER_NAME, - "deriveSharedNoteKey", - sharedNoteKeyArgs("missing-shared-note", GOOD_TRANSPORT_PUBLIC_KEY_HEX), - ALICE_IDENTITY - ) - check missingResource.contains("Shared note not found") - - let invalidTransportKey = callCanisterFailure( - VETKEY_DIR, - CANISTER_NAME, - "deriveSharedNoteKey", - sharedNoteKeyArgs("shared-team-note", "invalid-transport-key"), + "derivePrivateKvEnvelope", + tupleArgs(@[quotedText(transportPublicHex), nat64Literal(2)]), ALICE_IDENTITY ) - check invalidTransportKey.contains("Failed to derive shared note key") - check invalidTransportKey.contains("reject") or invalidTransportKey.contains("error") - test "Metadata-only storage keeps note summaries free of secret key material": - # describePrivateNote は長さ・key_version などメタのみで、鍵素材相当の文字列を返さないこと。 - let metadataSummary = callCanisterSuccess( - VETKEY_DIR, - CANISTER_NAME, - "describePrivateNote", - tupleArgs(@[principalLiteral(alicePrincipal), quotedText("alice-resource-1")]), - ALICE_IDENTITY - ) - check metadataSummary.contains("ciphertext_len") - check metadataSummary.contains("nonce_len") - check metadataSummary.contains("aad_len") - check metadataSummary.contains("key_version") - check not metadataSummary.toLowerAscii.contains("encrypted_key") - check not metadataSummary.toLowerAscii.contains("public_key") - check not metadataSummary.toLowerAscii.contains("symmetric") + check extractQuotedField(aliceV1, "context_label") == + "private-kv-v1|owner=" & alicePrincipal + check extractQuotedField(bobV1, "context_label") == + "private-kv-v1|owner=" & bobPrincipal + check extractQuotedField(aliceV1, "input_label") == + privateKvInputLabel(alicePrincipal, 1) + check extractQuotedField(aliceV2, "input_label") == + privateKvInputLabel(alicePrincipal, 2) + check aliceV1 != bobV1 + check aliceV1 != aliceV2 + check aliceV1.contains("public_key_hex") + check aliceV1.contains("encrypted_key_hex") test "Private KV roundtrip encrypts and decrypts by principal": - # principal を key にした vetKey を導出し、ciphertext は canister に保存せず - # client 側だけで暗号化・復号して元の平文と一致することを確認する。 let plaintext = "user secret payload for private kv" let plaintextBytes = textToBytes(plaintext) let plaintextHex = bytesToHex(plaintextBytes) @@ -580,24 +286,20 @@ withIcpNetwork(VETKEY_DIR): tupleArgs(@[quotedText(transportPublicHex), nat64Literal(1)]), ALICE_IDENTITY ) - check deriveOutput.contains("private-kv-v1|owner=" & alicePrincipal) - let privateKvInput = privateKvInputLabel(alicePrincipal, 1) let privateKvContext = "private-kv-v1|owner=" & alicePrincipal let contextLabel = extractQuotedField(deriveOutput, "context_label") check contextLabel == privateKvContext let encryptedKeyHex = extractQuotedField(deriveOutput, "encrypted_key_hex") - check deriveOutput.contains(privateKvInput) + check extractQuotedField(deriveOutput, "input_label") == privateKvInput let ciphertextBytes = vetkeyEncryptMessage( transportSecretHex, encryptedKeyHex, plaintextBytes, domainSep ) - let decryptedBytes = vetkeyDecryptMessage( transportSecretHex, encryptedKeyHex, ciphertextBytes, domainSep, ) - let decryptedHex = bytesToHex(decryptedBytes) - check decryptedHex.toLowerAscii == plaintextHex.toLowerAscii + check bytesToHex(decryptedBytes).toLowerAscii == plaintextHex.toLowerAscii From dc6abb278e3028344dba813e80f1593e8e8fd104 Mon Sep 17 00:00:00 2001 From: itsumura-h Date: Wed, 19 Aug 2026 17:58:58 +0000 Subject: [PATCH 6/6] fix runTest --- runTest.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/runTest.sh b/runTest.sh index a9727ca..3a9e0e6 100755 --- a/runTest.sh +++ b/runTest.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -set -euo pipefail +set -uo pipefail set -x cleanup_icp_state() { @@ -17,7 +17,7 @@ free_icp_gateway_port() { trap cleanup_icp_state EXIT -nimble uninstall nicp_cdk -iy >/dev/null 2>&1 || true +nimble uninstall nicp_cdk -yi >/dev/null 2>&1 || true nimble install -y nicp cHeaders