Skip to content

Commit b31741f

Browse files
authored
Keytool Storage (#1612)
* keytool signer refactor: made it sync and updated code structure * KeytoolStorage * rename feature keytool-signer to keytool * keytool insert key * keytool storage implementation * test keytool storage signing and verification * KeytoolStorage WASM bindings * dprint fmt * add missing license header * yet another missing license header * WASM KeytoolStorage JwkStorage & KeyIdStorage impl * clippy * fix bindgen toJSON impl * review comments * keytool storage example * fix example by requesting funds for the new address * TS Keytool storage example * add missing license header * remove unneeded code * fix web build * Added missing examples to tests, format * Add inline docs for KeytoolSigner bindings * link PublicKey docs through URL
1 parent b3bd2ea commit b31741f

File tree

44 files changed

+1451
-209
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1451
-209
lines changed

bindings/wasm/identity_wasm/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,4 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(wasm_bindgen_unstable_test
8181
[features]
8282
default = ["dummy-client"]
8383
dummy-client = ["dep:iota_interaction_ts"]
84+
keytool = ["dep:iota_interaction_ts", "iota_interaction_ts/keytool", "identity_iota/keytool"]

bindings/wasm/identity_wasm/cypress/app/src/identity.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import url from "@iota/identity-wasm/web/identity_wasm_bg.wasm?url";
22

33
import { init } from "@iota/identity-wasm/web";
4-
import { main } from "../../../examples/dist/web/main";
4+
import { main } from "../../../examples/dist/web/web-main";
55

66
export const runTest = async (example: string) => {
77
try {
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright 2020-2025 IOTA Stiftung
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import {
5+
IdentityClient,
6+
IdentityClientReadOnly,
7+
IotaDocument,
8+
JwkKeytoolStore,
9+
JwsAlgorithm,
10+
KeyIdKeytoolStore,
11+
KeytoolStorage,
12+
MethodScope,
13+
Storage,
14+
} from "@iota/identity-wasm/node";
15+
import { IotaClient } from "@iota/iota-sdk/client";
16+
import { IOTA_IDENTITY_PKG_ID, NETWORK_URL, requestFunds } from "../util";
17+
18+
export async function iotaKeytoolIntegration() {
19+
// For starter we access the local IOTA Keytool executable to create a new keypair.
20+
const keytool = new KeytoolStorage();
21+
// We generate a new Ed25519 key handled by the keytool, that we will use to interact with the ledger
22+
// throughout this example.
23+
const [pk, alias] = keytool.generateKey("ed25519");
24+
const address = pk.toIotaAddress();
25+
console.log(`Created new address ${address} with alias ${alias}!`);
26+
27+
// Let's request some funds for our new address.
28+
await requestFunds(address);
29+
30+
// Let's use the newly generated key to build the signer that will power our identity client.
31+
const iotaClient = new IotaClient({ url: NETWORK_URL });
32+
const readOnlyClient = await IdentityClientReadOnly.createWithPkgId(iotaClient, IOTA_IDENTITY_PKG_ID);
33+
const signer = keytool.signer(address);
34+
// A signer that relies on IOTA Keytool may also be built with:
35+
// const signer = new KeytoolSigner(address);
36+
const identityClient = await IdentityClient.create(readOnlyClient, signer);
37+
38+
// Let's create a new DID Document, with a verification method
39+
// that has its secret key stored in the Keytool.
40+
41+
// Firstly, we create a storage instance from our Keytool.
42+
const storage = new Storage(new JwkKeytoolStore(keytool), new KeyIdKeytoolStore(keytool));
43+
// Then we start building our DID Document.
44+
const didDocument = new IotaDocument(identityClient.network());
45+
const _vmFragment = await didDocument.generateMethod(
46+
storage,
47+
"secp256r1",
48+
JwsAlgorithm.ES256,
49+
null,
50+
MethodScope.VerificationMethod(),
51+
);
52+
53+
// Let's publish our new DID Document.
54+
let publishedDidDocument = await identityClient
55+
.publishDidDocument(didDocument)
56+
.execute(identityClient)
57+
.then(res => res.output);
58+
59+
console.log(`Here is our published DID document: ${JSON.stringify(publishedDidDocument, null, 2)}`);
60+
}

bindings/wasm/identity_wasm/examples/src/main.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { createVC } from "./0_basic/5_create_vc";
1010
import { createVP } from "./0_basic/6_create_vp";
1111
import { revokeVC } from "./0_basic/7_revoke_vc";
1212
import { sdJwtVc } from "./1_advanced/10_sd_jwt_vc";
13+
import { iotaKeytoolIntegration } from "./1_advanced/12_iota_keytool_integration";
1314
import { customResolution } from "./1_advanced/4_custom_resolution";
1415
import { domainLinkage } from "./1_advanced/5_domain_linkage";
1516
import { sdJwt } from "./1_advanced/6_sd_jwt";
@@ -55,6 +56,8 @@ export async function main(example?: string) {
5556
return await zkp_revocation();
5657
case "10_sd_jwt_vc":
5758
return await sdJwtVc();
59+
case "12_iota_keytool_integration":
60+
return await iotaKeytoolIntegration();
5861
default:
5962
throw "Unknown example name: '" + argument + "'";
6063
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { sdJwtVc } from "../1_advanced/10_sd_jwt_vc";
2+
3+
// Only verifies that no uncaught exceptions are thrown, including syntax errors etc.
4+
describe("Test node examples", function() {
5+
it("SD-JWT VC", async () => {
6+
await sdJwtVc();
7+
});
8+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { iotaKeytoolIntegration } from "../1_advanced/12_iota_keytool_integration";
2+
3+
// Only verifies that no uncaught exceptions are thrown, including syntax errors etc.
4+
describe("Test node examples", function() {
5+
it("IOTA Keytool Integration", async () => {
6+
await iotaKeytoolIntegration();
7+
});
8+
});

bindings/wasm/identity_wasm/examples/src/util.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ export async function createDocumentForNetwork(storage: Storage, network: string
4343
return [unpublished, verificationMethodFragment];
4444
}
4545

46+
export async function requestFunds(address: string) {
47+
await requestIotaFromFaucetV0({
48+
host: getFaucetHost(NETWORK_NAME_FAUCET),
49+
recipient: address,
50+
});
51+
}
52+
4653
export async function getFundedClient(storage: Storage): Promise<IdentityClient> {
4754
if (!IOTA_IDENTITY_PKG_ID) {
4855
throw new Error(`IOTA_IDENTITY_PKG_ID env variable must be provided to run the examples`);
@@ -68,10 +75,7 @@ export async function getFundedClient(storage: Storage): Promise<IdentityClient>
6875
let signer = new StorageSigner(storage, keyId, publicKeyJwk);
6976
const identityClient = await IdentityClient.create(identityClientReadOnly, signer);
7077

71-
await requestIotaFromFaucetV0({
72-
host: getFaucetHost(NETWORK_NAME_FAUCET),
73-
recipient: identityClient.senderAddress(),
74-
});
78+
await requestFunds(identityClient.senderAddress());
7579

7680
const balance = await iotaClient.getBalance({ owner: identityClient.senderAddress() });
7781
if (balance.totalBalance === "0") {
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright 2020-2025 IOTA Stiftung
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { createIdentity } from "./0_basic/0_create_did";
5+
import { updateIdentity } from "./0_basic/1_update_did";
6+
import { resolveIdentity } from "./0_basic/2_resolve_did";
7+
import { deactivateIdentity } from "./0_basic/3_deactivate_did";
8+
import { deleteIdentityDID } from "./0_basic/4_delete_did";
9+
import { createVC } from "./0_basic/5_create_vc";
10+
import { createVP } from "./0_basic/6_create_vp";
11+
import { revokeVC } from "./0_basic/7_revoke_vc";
12+
import { sdJwtVc } from "./1_advanced/10_sd_jwt_vc";
13+
import { customResolution } from "./1_advanced/4_custom_resolution";
14+
import { domainLinkage } from "./1_advanced/5_domain_linkage";
15+
import { sdJwt } from "./1_advanced/6_sd_jwt";
16+
import { statusList2021 } from "./1_advanced/7_status_list_2021";
17+
import { zkp } from "./1_advanced/8_zkp";
18+
import { zkp_revocation } from "./1_advanced/9_zkp_revocation";
19+
20+
export async function main(example?: string) {
21+
// Extract example name.
22+
const argument = example ?? process.argv?.[2]?.toLowerCase();
23+
if (!argument) {
24+
throw "Please specify an example name, e.g. '0_create_did'";
25+
}
26+
27+
switch (argument) {
28+
case "0_create_did":
29+
return await createIdentity();
30+
case "1_update_did":
31+
return await updateIdentity();
32+
case "2_resolve_did":
33+
return await resolveIdentity();
34+
case "3_deactivate_did":
35+
return await deactivateIdentity();
36+
case "4_delete_did":
37+
return await deleteIdentityDID();
38+
case "5_create_vc":
39+
return await createVC();
40+
case "6_create_vp":
41+
return await createVP();
42+
case "7_revoke_vc":
43+
return await revokeVC();
44+
case "4_custom_resolution":
45+
return await customResolution();
46+
case "5_domain_linkage":
47+
return await domainLinkage();
48+
case "6_sd_jwt":
49+
return await sdJwt();
50+
case "7_status_list_2021":
51+
return await statusList2021();
52+
case "8_zkp":
53+
return await zkp();
54+
case "9_zkp_revocation":
55+
return await zkp_revocation();
56+
case "10_sd_jwt_vc":
57+
return await sdJwtVc();
58+
default:
59+
throw "Unknown example name: '" + argument + "'";
60+
}
61+
}
62+
63+
main()
64+
.catch((error) => {
65+
console.log("Example error:", error);
66+
});

bindings/wasm/identity_wasm/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@
1414
},
1515
"scripts": {
1616
"build:src": "cargo build --lib --release --target wasm32-unknown-unknown --target-dir ../target",
17+
"build:src:nodejs": "cargo build --lib --release --target wasm32-unknown-unknown --target-dir ../target --features keytool",
1718
"prebundle:nodejs": "rimraf node",
1819
"bundle:nodejs": "wasm-bindgen ../target/wasm32-unknown-unknown/release/identity_wasm.wasm --typescript --weak-refs --target nodejs --out-dir node && node ../build/node identity_wasm && tsc --project ./lib/tsconfig.json && node ../build/replace_paths ./lib/tsconfig.json node identity_wasm",
1920
"prebundle:web": "rimraf web",
2021
"bundle:web": "wasm-bindgen ../target/wasm32-unknown-unknown/release/identity_wasm.wasm --typescript --target web --out-dir web && node ../build/web identity_wasm && tsc --project ./lib/tsconfig.web.json && node ../build/replace_paths ./lib/tsconfig.web.json web identity_wasm",
21-
"build:nodejs": "npm run build:src && npm run bundle:nodejs && wasm-opt -O node/identity_wasm_bg.wasm -o node/identity_wasm_bg.wasm",
22+
"build:nodejs": "npm run build:src:nodejs && npm run bundle:nodejs && wasm-opt -O node/identity_wasm_bg.wasm -o node/identity_wasm_bg.wasm",
2223
"build:web": "npm run build:src && npm run bundle:web && wasm-opt -O web/identity_wasm_bg.wasm -o web/identity_wasm_bg.wasm",
2324
"build:docs": "typedoc && npm run fix_docs",
2425
"build:examples:web": "tsc --project ./examples/tsconfig.web.json || node ../build/replace_paths ./tsconfig.web.json dist identity_wasm/examples resolve",
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Copyright 2020-2025 IOTA Stiftung
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use identity_iota::storage::JwkStorage;
5+
use identity_iota::storage::KeyId;
6+
use identity_iota::storage::KeyIdStorage;
7+
use identity_iota::storage::KeyType;
8+
use iota_interaction_ts::bindings::keytool::WasmKeytoolStorage;
9+
use wasm_bindgen::prelude::wasm_bindgen;
10+
11+
use super::WasmJwkGenOutput;
12+
use super::WasmMethodDigest;
13+
use crate::error::Result;
14+
use crate::error::WasmResult;
15+
use crate::jose::WasmJwk;
16+
17+
/// Implementation of {@link JwkStorage} for {@link KeytoolStorage}.
18+
#[wasm_bindgen(js_name = JwkKeytoolStore)]
19+
pub struct WasmJwkKeytoolStore(pub(crate) WasmKeytoolStorage);
20+
21+
#[wasm_bindgen(js_class = JwkKeytoolStore)]
22+
impl WasmJwkKeytoolStore {
23+
#[wasm_bindgen(constructor)]
24+
pub fn new(keytool: &WasmKeytoolStorage) -> Self {
25+
Self(keytool.clone())
26+
}
27+
28+
// JwkStorage implementation
29+
30+
#[wasm_bindgen]
31+
pub async fn generate(&self, key_type: &str, algorithm: &str) -> Result<WasmJwkGenOutput> {
32+
let key_type = KeyType::new(key_type);
33+
let jws_alg = algorithm.parse().wasm_result()?;
34+
self
35+
.0
36+
.as_ref()
37+
.generate(key_type, jws_alg)
38+
.await
39+
.wasm_result()
40+
.map(WasmJwkGenOutput)
41+
}
42+
43+
#[wasm_bindgen]
44+
pub async fn insert(&self, jwk: WasmJwk) -> Result<String> {
45+
self
46+
.0
47+
.as_ref()
48+
.insert(jwk.0.clone())
49+
.await
50+
.wasm_result()
51+
.map(|key_id| key_id.to_string())
52+
}
53+
54+
#[wasm_bindgen]
55+
pub async fn sign(&self, key_id: &str, data: &[u8], public_key: &WasmJwk) -> Result<Vec<u8>> {
56+
let key_id = KeyId::new(key_id);
57+
self.0.as_ref().sign(&key_id, data, &public_key.0).await.wasm_result()
58+
}
59+
60+
#[wasm_bindgen]
61+
pub async fn delete(&self, key_id: &str) -> Result<()> {
62+
let key_id = KeyId::new(key_id);
63+
self.0.as_ref().delete(&key_id).await.wasm_result()
64+
}
65+
66+
#[wasm_bindgen]
67+
pub async fn exists(&self, key_id: &str) -> Result<bool> {
68+
let key_id = KeyId::new(key_id);
69+
self.0.as_ref().exists(&key_id).await.wasm_result()
70+
}
71+
}
72+
73+
/// Implementation of interface {@link KeyIdStorage} for {@link KeytoolStorage}.
74+
#[wasm_bindgen(js_name = KeyIdKeytoolStore)]
75+
pub struct WasmKeyIdKeytoolStore(pub(crate) WasmKeytoolStorage);
76+
77+
#[wasm_bindgen(js_class = KeyIdKeytoolStore)]
78+
impl WasmKeyIdKeytoolStore {
79+
#[wasm_bindgen(constructor)]
80+
pub fn new(keytool: &WasmKeytoolStorage) -> Self {
81+
Self(keytool.clone())
82+
}
83+
84+
// KeyIdStorage implementation
85+
86+
#[wasm_bindgen(js_name = insertKeyId)]
87+
pub async fn insert_key_id(&self, method_digest: WasmMethodDigest, key_id: &str) -> Result<()> {
88+
let key_id = KeyId::new(key_id);
89+
self
90+
.0
91+
.as_ref()
92+
.insert_key_id(method_digest.0, key_id)
93+
.await
94+
.wasm_result()
95+
}
96+
97+
#[wasm_bindgen(js_name = getKeyId)]
98+
pub async fn get_key_id(&self, method_digest: &WasmMethodDigest) -> Result<String> {
99+
self
100+
.0
101+
.as_ref()
102+
.get_key_id(&method_digest.0)
103+
.await
104+
.wasm_result()
105+
.map(|key_id| key_id.to_string())
106+
}
107+
108+
#[wasm_bindgen(js_name = deleteKeyId)]
109+
pub async fn delete_key_id(&self, method_digest: &WasmMethodDigest) -> Result<()> {
110+
self.0.as_ref().delete_key_id(&method_digest.0).await.wasm_result()
111+
}
112+
}

bindings/wasm/identity_wasm/src/storage/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ mod jwk_storage;
77
mod jwk_storage_bbs_plus_ext;
88
mod jwt_presentation_options;
99
mod key_id_storage;
10+
#[cfg(feature = "keytool")]
11+
mod keytool_storage;
1012
mod method_digest;
1113
mod signature_options;
1214
mod wasm_storage;
@@ -18,6 +20,8 @@ pub use jwk_gen_output::*;
1820
pub use jwk_storage::*;
1921
pub use jwt_presentation_options::*;
2022
pub use key_id_storage::*;
23+
#[cfg(feature = "keytool")]
24+
pub use keytool_storage::*;
2125
pub use method_digest::*;
2226
pub use signature_options::*;
2327
pub use wasm_storage::*;

bindings/wasm/iota_interaction_ts/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,5 @@ instant = { version = "0.1", default-features = false, features = ["wasm-bindgen
5353
empty_docs = "allow"
5454

5555
[features]
56-
keytool-signer = ["identity_iota_interaction/keytool-signer"]
56+
default = []
57+
keytool = ["identity_iota_interaction/keytool"]

bindings/wasm/iota_interaction_ts/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
},
1212
"scripts": {
1313
"build:src": "cargo build --lib --release --target wasm32-unknown-unknown --target-dir ../target",
14-
"build:src:node": "cargo build --lib --release --target wasm32-unknown-unknown --features keytool-signer --target-dir ../target",
14+
"build:src:node": "cargo build --lib --release --target wasm32-unknown-unknown --features keytool --target-dir ../target",
1515
"prebundle:nodejs": "rimraf node",
1616
"bundle:nodejs": "wasm-bindgen ../target/wasm32-unknown-unknown/release/iota_interaction_ts.wasm --typescript --weak-refs --target nodejs --out-dir node && node ../build/node iota_interaction_ts && tsc --project ./lib/tsconfig.json && node ../build/replace_paths ./lib/tsconfig.json node iota_interaction_ts",
1717
"prebundle:web": "rimraf web",
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Copyright 2020-2025 IOTA Stiftung
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
mod signer;
5+
mod storage;
6+
7+
pub use signer::*;
8+
pub use storage::*;

0 commit comments

Comments
 (0)