Skip to content

Commit

Permalink
Use JWT credentials for Domain Linkage (#1180)
Browse files Browse the repository at this point in the history
* Issuer/credentialSubject.id must be a DID

* Validate origin as a domain

* Use JSON Web Token Proof Format in Domain Linkage

* Rename fixture files

* Update Domain Linkage example

* Replace linked data proof format mentions

* Update Wasm Domain Linkage bindings

* Update Wasm Domain Linkage example

* Expose `memstore` feature via `identity_iota`

* Feature-gate domain linkage config & builder

* Use MemStores in Wasm domain linkage example

* Update api-reference
  • Loading branch information
PhilippGackstatter authored Jun 12, 2023
1 parent 36606a8 commit 68055e2
Show file tree
Hide file tree
Showing 29 changed files with 737 additions and 369 deletions.
172 changes: 130 additions & 42 deletions bindings/wasm/docs/api-reference.md

Large diffs are not rendered by default.

38 changes: 24 additions & 14 deletions bindings/wasm/examples/src/1_advanced/6_domain_linkage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { Client, MnemonicSecretManager } from "@iota/client-wasm/node";
import { Bip39 } from "@iota/crypto.js";
import {
CoreDID,
Credential,
CredentialValidationOptions,
DIDUrl,
Expand All @@ -13,12 +14,17 @@ import {
IotaDID,
IotaDocument,
IotaIdentityClient,
JwkMemStore,
JwsSignatureOptions,
JwtCredentialValidationOptions,
KeyIdMemStore,
LinkedDomainService,
ProofOptions,
Storage,
Timestamp,
} from "@iota/identity-wasm/node";
import { IAliasOutput, IRent, TransactionHelper } from "@iota/iota.js";
import { API_ENDPOINT, createDid } from "../util";
import { API_ENDPOINT, createDid, createDidStorage } from "../util";

/**
* Demonstrates how to link a domain and a DID and verify the linkage.
Expand All @@ -35,8 +41,10 @@ export async function domainLinkage() {
mnemonic: Bip39.randomMnemonic(),
};

const storage: Storage = new Storage(new JwkMemStore(), new KeyIdMemStore());

// Creates a new wallet and identity (see "0_create_did" example).
let { document, keypair } = await createDid(client, secretManager);
let { document, fragment } = await createDidStorage(client, secretManager, storage);
const did: IotaDID = document.id();

// =====================================================
Expand Down Expand Up @@ -74,19 +82,19 @@ export async function domainLinkage() {
});

// Sign the credential.
domainLinkageCredential = document.signCredential(
const credentialJwt = await document.createCredentialJwt(
storage,
fragment,
domainLinkageCredential,
keypair.private(),
"#key-1",
ProofOptions.default(),
new JwsSignatureOptions(),
);

// Create the DID Configuration Resource which wraps the Domain Linkage credential.
let configurationResource: DomainLinkageConfiguration = new DomainLinkageConfiguration([domainLinkageCredential]);
let configurationResource: DomainLinkageConfiguration = new DomainLinkageConfiguration([credentialJwt]);

// The DID Configuration resource can be made available on `https://foo.example.com/.well-known/did-configuration.json`.
let configurationResourceJson = configurationResource.toJSON();
console.log("Configuration Resource:", JSON.stringify(configurationResource.toJSON(), null, 2));
console.log("Configuration Resource:", JSON.stringify(configurationResourceJson, null, 2));

// Now the DID Document links to the Domains through the service, and the Foo domain links to the DID
// through the DID Configuration resource. A bidirectional linkage is established.
Expand Down Expand Up @@ -114,16 +122,16 @@ export async function domainLinkage() {

// Retrieve the issuers of the Domain Linkage Credentials which correspond to the possibly linked DIDs.
// Note that in this example only the first entry in the credential is validated.
let issuers: Array<string> = fetchedConfigurationResource.issuers();
const issuerDocument: IotaDocument = await didClient.resolveDid(IotaDID.parse(issuers[0]));
let issuers: Array<CoreDID> = fetchedConfigurationResource.issuers();
const issuerDocument: IotaDocument = await didClient.resolveDid(IotaDID.parse(issuers[0].toString()));

// Validate the linkage between the Domain Linkage Credential in the configuration and the provided issuer DID.
// Validation succeeds when no error is thrown.
DomainLinkageValidator.validateLinkage(
new DomainLinkageValidator().validateLinkage(
issuerDocument,
fetchedConfigurationResource,
domainFoo,
CredentialValidationOptions.default(),
JwtCredentialValidationOptions.default(),
);

// =====================================================
Expand Down Expand Up @@ -153,12 +161,14 @@ export async function domainLinkage() {

// Validate the linkage between the Domain Linkage Credential in the configuration and the provided issuer DID.
// Validation succeeds when no error is thrown.
DomainLinkageValidator.validateLinkage(
new DomainLinkageValidator().validateLinkage(
didDocument,
fetchedConfigurationResource,
domains[0],
CredentialValidationOptions.default(),
JwtCredentialValidationOptions.default(),
);

console.log("Successfully validated Domain Linkage!");
}

async function publishDocument(
Expand Down
50 changes: 50 additions & 0 deletions bindings/wasm/examples/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ import {
IotaDID,
IotaDocument,
IotaIdentityClient,
JwkMemStore,
JwsAlgorithm,
KeyPair,
KeyType,
MethodScope,
Storage,
VerificationMethod,
} from "@iota/identity-wasm/node";
import { AddressTypes, Bech32Helper, IAliasOutput } from "@iota/iota.js";
Expand All @@ -15,6 +18,53 @@ export const FAUCET_ENDPOINT = "http://localhost:8091/api/enqueue";

/** Creates a DID Document and publishes it in a new Alias Output.
Its functionality is equivalent to the "create DID" example
and exists for convenient calling from the other examples. */
export async function createDidStorage(client: Client, secretManager: SecretManager, storage: Storage): Promise<{
address: AddressTypes;
document: IotaDocument;
fragment: string;
}> {
const didClient = new IotaIdentityClient(client);
const networkHrp: string = await didClient.getNetworkHrp();

const walletAddressBech32 = (await client.generateAddresses(secretManager, {
accountIndex: 0,
range: {
start: 0,
end: 1,
},
}))[0];
console.log("Wallet address Bech32:", walletAddressBech32);

await ensureAddressHasFunds(client, walletAddressBech32);

const address = Bech32Helper.addressFromBech32(walletAddressBech32, networkHrp);

// Create a new DID document with a placeholder DID.
// The DID will be derived from the Alias Id of the Alias Output after publishing.
const document = new IotaDocument(networkHrp);

const fragment = await document.generateMethod(
storage,
JwkMemStore.ed25519KeyType(),
JwsAlgorithm.EdDSA,
"#jwk",
MethodScope.AssertionMethod(),
);

// Construct an Alias Output containing the DID document, with the wallet address
// set as both the state controller and governor.
const aliasOutput: IAliasOutput = await didClient.newDidOutput(address, document);

// Publish the Alias Output and get the published DID document.
const published = await didClient.publishDidOutput(secretManager, aliasOutput);

return { address, document: published, fragment };
}

/** Creates a DID Document and publishes it in a new Alias Output.
Its functionality is equivalent to the "create DID" example
and exists for convenient calling from the other examples. */
export async function createDid(client: Client, secretManager: SecretManager): Promise<{
Expand Down
44 changes: 25 additions & 19 deletions bindings/wasm/src/credential/domain_linkage_configuration.rs
Original file line number Diff line number Diff line change
@@ -1,60 +1,66 @@
// Copyright 2020-2023 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

use crate::common::ArrayString;
use crate::credential::ArrayCredential;
use crate::credential::WasmCredential;
use crate::credential::ArrayCoreDID;
use crate::credential::WasmJwt;
use crate::did::WasmCoreDID;
use crate::error::Result;
use crate::error::WasmResult;
use identity_iota::credential::Credential;

use identity_iota::credential::DomainLinkageConfiguration;
use identity_iota::credential::Jwt;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;

use super::ArrayJwt;

/// DID Configuration Resource which contains Domain Linkage Credentials.
/// It can be placed in an origin's `.well-known` directory to prove linkage between the origin and a DID.
/// See: <https://identity.foundation/.well-known/resources/did-configuration/#did-configuration-resource>
///
/// Note:
/// - Only [Linked Data Proof Format](https://identity.foundation/.well-known/resources/did-configuration/#linked-data-proof-format)
/// is supported.
/// - Only the [JSON Web Token Proof Format](https://identity.foundation/.well-known/resources/did-configuration/#json-web-token-proof-format)
#[wasm_bindgen(js_name = DomainLinkageConfiguration, inspectable)]
pub struct WasmDomainLinkageConfiguration(pub(crate) DomainLinkageConfiguration);

#[wasm_bindgen(js_class = DomainLinkageConfiguration)]
impl WasmDomainLinkageConfiguration {
/// Constructs a new `DomainLinkageConfiguration`.
#[wasm_bindgen(constructor)]
pub fn new(linked_dids: ArrayCredential) -> Result<WasmDomainLinkageConfiguration> {
let wasm_credentials: Vec<Credential> = linked_dids.into_serde().wasm_result()?;
pub fn new(linked_dids: &ArrayJwt) -> Result<WasmDomainLinkageConfiguration> {
let wasm_credentials: Vec<Jwt> = linked_dids.into_serde().wasm_result()?;
Ok(Self(DomainLinkageConfiguration::new(wasm_credentials)))
}

/// List of the Domain Linkage Credentials.
#[wasm_bindgen(js_name = linkedDids)]
pub fn linked_dids(&self) -> ArrayCredential {
pub fn linked_dids(&self) -> ArrayJwt {
self
.0
.linked_dids()
.iter()
.cloned()
.map(WasmCredential::from)
.map(WasmJwt::from)
.map(JsValue::from)
.collect::<js_sys::Array>()
.unchecked_into::<ArrayCredential>()
.unchecked_into::<ArrayJwt>()
}

/// List of the issuers of the Domain Linkage Credentials.
#[wasm_bindgen]
pub fn issuers(&self) -> ArrayString {
self
.0
.issuers()
.map(|url| url.to_string())
.map(JsValue::from)
.collect::<js_sys::Array>()
.unchecked_into::<ArrayString>()
pub fn issuers(&self) -> Result<ArrayCoreDID> {
Ok(
self
.0
.issuers()
.wasm_result()?
.into_iter()
.map(WasmCoreDID::from)
.map(JsValue::from)
.collect::<js_sys::Array>()
.unchecked_into::<ArrayCoreDID>(),
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::error::WasmResult;
use identity_iota::core::Timestamp;
use identity_iota::core::Url;
use identity_iota::credential::DomainLinkageCredentialBuilder;
use identity_iota::credential::Issuer;
use identity_iota::did::CoreDID;
use proc_typescript::typescript;
use wasm_bindgen::prelude::*;

Expand Down Expand Up @@ -49,8 +49,8 @@ extern "C" {
#[typescript(name = "IDomainLinkageCredential", readonly, optional)]
struct IDomainLinkageCredentialHelper {
/// A reference to the issuer of the `Credential`.
#[typescript(optional = false, type = "string | CoreDID | IotaDID | Issuer")]
issuer: Option<Issuer>,
#[typescript(optional = false, type = "CoreDID | IotaDID")]
issuer: Option<CoreDID>,
/// A timestamp of when the `Credential` becomes valid. Defaults to the current datetime.
#[typescript(name = "issuanceDate", type = "Timestamp")]
issuance_date: Option<Timestamp>,
Expand Down
44 changes: 34 additions & 10 deletions bindings/wasm/src/credential/domain_linkage_validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,45 @@

use crate::common::ImportedDocumentLock;
use crate::credential::WasmDomainLinkageConfiguration;
use crate::credential::WasmJwtCredentialValidationOptions;
use crate::did::IToCoreDocument;
use crate::error::Result;
use crate::error::WasmResult;
use crate::verification::IJwsVerifier;
use crate::verification::WasmJwsVerifier;
use identity_iota::core::Url;
use identity_iota::credential::DomainLinkageValidator;
use wasm_bindgen::prelude::wasm_bindgen;

use super::WasmCredential;
use super::WasmCredentialValidationOptions;
use super::WasmJwt;

/// A validator for a Domain Linkage Configuration and Credentials.
#[wasm_bindgen(js_name = DomainLinkageValidator)]
pub struct WasmDomainLinkageValidator;
pub struct WasmDomainLinkageValidator {
validator: DomainLinkageValidator<WasmJwsVerifier>,
}

#[wasm_bindgen(js_class = DomainLinkageValidator)]
impl WasmDomainLinkageValidator {
/// Creates a new `DomainLinkageValidator`. If a `signatureVerifier` is provided it will be used when
/// verifying decoded JWS signatures, otherwise the default which is only capable of handling the `EdDSA`
/// algorithm will be used.
#[wasm_bindgen(constructor)]
#[allow(non_snake_case)]
pub fn new(signatureVerifier: Option<IJwsVerifier>) -> WasmDomainLinkageValidator {
let signature_verifier = WasmJwsVerifier::new(signatureVerifier);
WasmDomainLinkageValidator {
validator: DomainLinkageValidator::with_signature_verifier(signature_verifier),
}
}

/// Validates the linkage between a domain and a DID.
/// [`DomainLinkageConfiguration`] is validated according to [DID Configuration Resource Verification](https://identity.foundation/.well-known/resources/did-configuration/#did-configuration-resource-verification).
///
/// Linkage is valid if no error is thrown.
///
/// # Note:
/// - Only [Linked Data Proof Format](https://identity.foundation/.well-known/resources/did-configuration/#linked-data-proof-format)
/// is supported.
/// - Only the [JSON Web Token Proof Format](https://identity.foundation/.well-known/resources/did-configuration/#json-web-token-proof-format)
/// - Only the Credential issued by `issuer` is verified.
///
/// # Errors
Expand All @@ -35,29 +50,38 @@ impl WasmDomainLinkageValidator {
/// - Validation of the matched Domain Linkage Credential fails.
#[wasm_bindgen(js_name = validateLinkage)]
pub fn validate_linkage(
&self,
issuer: &IToCoreDocument,
configuration: &WasmDomainLinkageConfiguration,
domain: &str,
options: &WasmCredentialValidationOptions,
options: &WasmJwtCredentialValidationOptions,
) -> Result<()> {
let domain = Url::parse(domain).wasm_result()?;
let doc = ImportedDocumentLock::from(issuer);
let doc_guard = doc.blocking_read();
DomainLinkageValidator::validate_linkage(&doc_guard, &configuration.0, &domain, &options.0).wasm_result()
self
.validator
.validate_linkage(&doc_guard, &configuration.0, &domain, &options.0)
.wasm_result()
}

/// Validates a [Domain Linkage Credential](https://identity.foundation/.well-known/resources/did-configuration/#domain-linkage-credential).
/// Error will be thrown in case the validation fails.
#[wasm_bindgen(js_name = validateCredential)]
#[allow(non_snake_case)]
pub fn validate_credential(
&self,
issuer: &IToCoreDocument,
credential: &WasmCredential,
credentialJwt: &WasmJwt,
domain: &str,
options: &WasmCredentialValidationOptions,
options: &WasmJwtCredentialValidationOptions,
) -> Result<()> {
let domain = Url::parse(domain).wasm_result()?;
let doc = ImportedDocumentLock::from(issuer);
let doc_guard = doc.blocking_read();
DomainLinkageValidator::validate_credential(&doc_guard, &credential.0, &domain, &options.0).wasm_result()
self
.validator
.validate_credential(&doc_guard, &credentialJwt.0, &domain, &options.0)
.wasm_result()
}
}
15 changes: 15 additions & 0 deletions bindings/wasm/src/credential/jwt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,18 @@ impl WasmJwt {
self.0.as_str().to_owned()
}
}

impl_wasm_json!(WasmJwt, Jwt);
impl_wasm_clone!(WasmJwt, Jwt);

impl From<Jwt> for WasmJwt {
fn from(value: Jwt) -> Self {
WasmJwt(value)
}
}

impl From<WasmJwt> for Jwt {
fn from(value: WasmJwt) -> Self {
value.0
}
}
Loading

0 comments on commit 68055e2

Please sign in to comment.