Skip to content

Commit

Permalink
Feat/status list 2021 (#1273)
Browse files Browse the repository at this point in the history
* StatusList2021

* StatusList2021Credential

* StatusList2021Entry

* Multiple CredentialStatuses

* conversions between statuses, fix test

* StatusList2021Credential deserializable

* test revocation checking with statuslist2021

* WASM bindings

* example

* example enhancement

* wasm bindings example

* License

* docs

* make clippy happy

* clippy again

* fmt

* fmt & clippy

* license template

* revocation feature gate

* review comments

* fix bindings

* dprint fmt

* credential status refactor

* clippy + fmt

* better example

* status list review comment

* status list credential PR comments

* Review comments

* Fix WASM bindings

* Fix merge issue

* dprint fmt

* cargo clippy

* bindings tests

* review comments

* wasm example

* Update bindings/wasm/examples/src/1_advanced/7_status_list_2021.ts

Co-authored-by: Eike Haß <[email protected]>

* review comments

* clippy

* review comments

---------

Co-authored-by: Eike Haß <[email protected]>
  • Loading branch information
UMR1352 and eike-hass authored Jan 24, 2024
1 parent 00a1841 commit 8bc3ab7
Show file tree
Hide file tree
Showing 34 changed files with 2,163 additions and 164 deletions.
2 changes: 1 addition & 1 deletion bindings/wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ wasm-bindgen-futures = { version = "0.4", default-features = false }
version = "1.0.0"
path = "../../identity_iota"
default-features = false
features = ["client", "revocation-bitmap", "resolver", "domain-linkage", "sd-jwt"]
features = ["client", "revocation-bitmap", "resolver", "domain-linkage", "sd-jwt", "status-list-2021"]

[dev-dependencies]
rand = "0.8.5"
Expand Down
11 changes: 11 additions & 0 deletions bindings/wasm/cypress/e2e/1_advanced/7_status_list_2021.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { statusList2021 } from "../../../examples/dist/web/1_advanced/7_status_list_2021";
import { setup } from "../../support/setup";

describe(
"statusList2021",
() => {
it("Status List 2021", async () => {
await setup(statusList2021);
});
},
);
690 changes: 545 additions & 145 deletions bindings/wasm/docs/api-reference.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions bindings/wasm/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ The following advanced examples are available:
| [4_custom_resolution](src/1_advanced/4_custom_resolution.ts) | Demonstrates how to set up a resolver using custom handlers. |
| [5_domain_linkage](src/1_advanced/5_domain_linkage.ts) | Demonstrates how to link a domain and a DID and verify the linkage. |
| [6_sd_jwt](src/1_advanced/6_sd_jwt.ts) | Demonstrates how to create a selective disclosure verifiable credential |
| [7_domain_linkage](src/1_advanced/7_status_list_2021.ts) | Demonstrates how to revoke a credential using `StatusList2021`. |

## Browser

Expand Down
169 changes: 169 additions & 0 deletions bindings/wasm/examples/src/1_advanced/7_status_list_2021.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// Copyright 2020-2023 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import {
Credential,
EdDSAJwsVerifier,
FailFast,
JwkMemStore,
JwsSignatureOptions,
JwtCredentialValidationOptions,
JwtCredentialValidator,
KeyIdMemStore,
StatusCheck,
StatusList2021,
StatusList2021Credential,
StatusList2021CredentialBuilder,
StatusList2021Entry,
StatusPurpose,
Storage,
} from "@iota/identity-wasm/node";
import { Client, MnemonicSecretManager, Utils } from "@iota/sdk-wasm/node";
import { API_ENDPOINT, createDid } from "../util";

export async function statusList2021() {
// ===========================================================================
// Create a Verifiable Credential.
// ===========================================================================

const client = new Client({
primaryNode: API_ENDPOINT,
localPow: true,
});

// Generate a random mnemonic for the issuer.
const issuerSecretManager: MnemonicSecretManager = {
mnemonic: Utils.generateMnemonic(),
};

// Create an identity for the issuer with one verification method `key-1`.
const issuerStorage: Storage = new Storage(
new JwkMemStore(),
new KeyIdMemStore(),
);
let { document: issuerDocument, fragment: issuerFragment } = await createDid(
client,
issuerSecretManager,
issuerStorage,
);

// Generate a random mnemonic for Alice.
const aliceSecretManager: MnemonicSecretManager = {
mnemonic: Utils.generateMnemonic(),
};

// Create an identity for the holder, in this case also the subject.
const aliceStorage: Storage = new Storage(
new JwkMemStore(),
new KeyIdMemStore(),
);
let { document: aliceDocument } = await createDid(
client,
aliceSecretManager,
aliceStorage,
);

// Create a new empty status list. No credentials have been revoked yet.
const statusList = new StatusList2021();

// Create a status list credential so that the status list can be stored anywhere.
// The issuer makes this credential available on `http://example.com/credential/status`.
// For the purposes of this example, the credential will be used directly without fetching.
const statusListCredential = new StatusList2021CredentialBuilder(statusList)
.purpose(StatusPurpose.Revocation)
.subjectId("http://example.com/credential/status")
.issuer(issuerDocument.id().toString())
.build();
const statusListCredentialJSON = statusListCredential.toJSON();
console.log("Status list credential > " + statusListCredential);

// Create a credential subject indicating the degree earned by Alice, linked to their DID.
const subject = {
id: aliceDocument.id(),
name: "Alice",
degreeName: "Bachelor of Science and Arts",
degreeType: "BachelorDegree",
GPA: "4.0",
};

// Create an unsigned `UniversityDegree` credential for Alice.
// The issuer also chooses a unique `StatusList2021` index to be able to revoke it later.
const CREDENTIAL_INDEX = 5;
const status = new StatusList2021Entry(statusListCredential.id(), statusListCredential.purpose(), CREDENTIAL_INDEX)
.toStatus();
const credential = new Credential({
id: "https://example.edu/credentials/3732",
type: "UniversityDegreeCredential",
credentialStatus: status,
issuer: issuerDocument.id(),
credentialSubject: subject,
});

// Create signed JWT credential.
const credentialJwt = await issuerDocument.createCredentialJwt(
issuerStorage,
issuerFragment,
credential,
new JwsSignatureOptions(),
);
console.log(`Credential JWT > ${credentialJwt.toString()}`);

// Validate the credential using the issuer's DID Document.
const validationOptions = new JwtCredentialValidationOptions({ status: StatusCheck.SkipUnsupported });
// The validator has no way of retrieving the status list to check for the
// revocation of the credential. Let's skip that pass and perform the operation manually.
let jwtCredentialValidator = new JwtCredentialValidator(new EdDSAJwsVerifier());

try {
jwtCredentialValidator.validate(
credentialJwt,
issuerDocument,
validationOptions,
FailFast.FirstError,
);
// Check manually for revocation
JwtCredentialValidator.checkStatusWithStatusList2021(
credential,
statusListCredential,
StatusCheck.Strict,
);
} catch (e) {
// This line shouldn't be called as the credential is valid and unrevoked
console.log("Something went wrong: " + e);
}

// ===========================================================================
// Revocation of the Verifiable Credential.
// ===========================================================================

// At a later time, the issuer university found out that Alice cheated in her final exam.
// The issuer will revoke Alice's credential.

// The issuer retrieves the status list credential.
const refetchedStatusListCredential = new StatusList2021Credential(new Credential(statusListCredentialJSON as any));

// Update the status list credential.
// This revokes the credential's unique index.
refetchedStatusListCredential.setCredentialStatus(credential, CREDENTIAL_INDEX, true);

// Credential verification now fails.
try {
jwtCredentialValidator.validate(
credentialJwt,
issuerDocument,
validationOptions,
FailFast.FirstError,
);
/// Since the credential has been revoked, this validation step will throw an error.
JwtCredentialValidator.checkStatusWithStatusList2021(
credential,
refetchedStatusListCredential,
StatusCheck.Strict,
);
// In case the revocation failed for some reason we will hit this point
console.log("Revocation Failed!");
} catch (e) {
/// The credential has been revoked.
console.log("The credential has been successfully revoked.");
}
}
3 changes: 3 additions & 0 deletions bindings/wasm/examples/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { didIssuesTokens } from "./1_advanced/3_did_issues_tokens";
import { customResolution } from "./1_advanced/4_custom_resolution";
import { domainLinkage } from "./1_advanced/5_domain_linkage";
import { sdJwt } from "./1_advanced/6_sd_jwt";
import { statusList2021 } from "./1_advanced/7_status_list_2021";

async function main() {
// Extract example name.
Expand Down Expand Up @@ -55,6 +56,8 @@ async function main() {
return await domainLinkage();
case "6_sd_jwt":
return await sdJwt();
case "7_status_list_2021":
return await statusList2021();
default:
throw "Unknown example name: '" + argument + "'";
}
Expand Down
8 changes: 8 additions & 0 deletions bindings/wasm/examples/src/tests/7_status_list_2021.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { statusList2021 } from "../1_advanced/7_status_list_2021";

// Only verifies that no uncaught exceptions are thrown, including syntax errors etc.
describe("Test node examples", function() {
it("StatusList2021", async () => {
await statusList2021();
});
});
1 change: 1 addition & 0 deletions bindings/wasm/src/credential/credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ use crate::error::Result;
use crate::error::WasmResult;

#[wasm_bindgen(js_name = Credential, inspectable)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WasmCredential(pub(crate) Credential);

#[wasm_bindgen(js_class = Credential)]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2020-2023 IOTA Stiftung
// Copyright 2020-2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

use identity_iota::core::Object;
Expand All @@ -13,6 +13,7 @@ use crate::common::ImportedDocumentLock;
use crate::common::ImportedDocumentReadGuard;
use crate::common::WasmTimestamp;
use crate::credential::options::WasmStatusCheck;
use crate::credential::revocation::status_list_2021::WasmStatusList2021Credential;
use crate::credential::WasmCredential;
use crate::credential::WasmDecodedJwtCredential;
use crate::credential::WasmFailFast;
Expand Down Expand Up @@ -170,6 +171,21 @@ impl WasmJwtCredentialValidator {
JwtCredentialValidatorUtils::check_status(&credential.0, &trusted_issuers, status_check).wasm_result()
}

/// Checks wheter the credential status has been revoked using `StatusList2021`.
#[wasm_bindgen(js_name = checkStatusWithStatusList2021)]
pub fn check_status_with_status_list_2021(
credential: &WasmCredential,
status_list: &WasmStatusList2021Credential,
status_check: WasmStatusCheck,
) -> Result<()> {
JwtCredentialValidatorUtils::check_status_with_status_list_2021(
&credential.0,
&status_list.inner,
status_check.into(),
)
.wasm_result()
}

/// Utility for extracting the issuer field of a {@link Credential} as a DID.
///
/// ### Errors
Expand Down
2 changes: 2 additions & 0 deletions bindings/wasm/src/credential/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub use self::options::WasmFailFast;
pub use self::options::WasmSubjectHolderRelationship;
pub use self::presentation::*;
pub use self::proof::WasmProof;
pub use self::revocation::*;
pub use self::types::*;

mod credential;
Expand All @@ -29,4 +30,5 @@ mod linked_domain_service;
mod options;
mod presentation;
mod proof;
mod revocation;
mod types;
4 changes: 4 additions & 0 deletions bindings/wasm/src/credential/revocation/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Copyright 2020-2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

pub mod status_list_2021;
Loading

0 comments on commit 8bc3ab7

Please sign in to comment.