Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions identity-wallet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ typetag = "0.2"
unicode-normalization = "0.1"
url = "2.5"
uuid = { version = "1.4", features = ["v4", "fast-rng", "serde"] }
urlencoding = "2.1.3"

[dev-dependencies]
serial_test.workspace = true
Expand Down
3 changes: 2 additions & 1 deletion identity-wallet/bindings/actions/Action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,12 @@ import type { ServiceHealthCheck } from "./ServiceHealthCheck";
import type { SetLocale } from "./SetLocale";
import type { SetPreferredDidMethod } from "./SetPreferredDidMethod";
import type { SetPreferredKeyType } from "./SetPreferredKeyType";
import type { ShareToLinkedIn } from "./ShareToLinkedIn";
import type { ShowDevModeSetting } from "../dev_mode/ShowDevModeSetting";
import type { ToggleTrustListEntry } from "./ToggleTrustListEntry";
import type { UnlockStorage } from "./UnlockStorage";
import type { UpdateCredentialMetadata } from "./UpdateCredentialMetadata";
import type { UpdateProfileSettings } from "./UpdateProfileSettings";
import type { UpdateSortingPreference } from "./UpdateSortingPreference";

export type Action = { "type": "[App] Get state" } | { "type": "[Storage] Unlock", payload: UnlockStorage, } | { "type": "[App] Reset" } | { "type": "[DID] Create new", payload: CreateNew, } | { "type": "[Settings] Set locale", payload: SetLocale, } | { "type": "[Settings] Update profile", payload: UpdateProfileSettings, } | { "type": "[QR Code] Scanned", payload: QrCodeScanned, } | { "type": "[Authenticate] Connection accepted" } | { "type": "[User Flow] Cancel", payload?: CancelUserFlow, } | { "type": "[DEV] Show DEV mode setting", payload: ShowDevModeSetting, } | { "type": "[DEV] Load DEV profile", payload: DevProfile, } | { "type": "[DEV] Toggle DEV mode" } | { "type": "[Authenticate] Credentials selected", payload: CredentialsSelected, } | { "type": "[Credential Offer] Selected", payload: CredentialOffersSelected, } | { "type": "[Credential Offer] Code received", payload: CodeReceived, } | { "type": "[Credential Metadata] Update", payload: UpdateCredentialMetadata, } | { "type": "[Credential] Delete", payload: DeleteCredential, } | { "type": "[Credential] Refresh status", payload: RefreshCredentialStatus, } | { "type": "[Credential] Refresh all statuses" } | { "type": "[User Journey] Cancel" } | { "type": "[Settings] Update sorting preference", payload: UpdateSortingPreference, } | { "type": "[Search] Query", payload: SearchQuery, } | { "type": "[Search] Add recent", payload: AddRecentSearch, } | { "type": "[Search] Delete recent", payload: DeleteRecentSearch, } | { "type": "[DID] Set preferred method", payload: SetPreferredDidMethod, } | { "type": "[Keys] Set preferred key type", payload: SetPreferredKeyType, } | { "type": "[Trust List] Add entry", payload: AddTrustListEntry, } | { "type": "[Trust List] Edit entry", payload: EditTrustListEntry, } | { "type": "[Trust List] Delete entry", payload: DeleteTrustListEntry, } | { "type": "[Trust List] Toggle entry", payload: ToggleTrustListEntry, } | { "type": "[Trust Lists] Add", payload: AddTrustList, } | { "type": "[Trust Lists] Edit", payload: EditTrustList, } | { "type": "[Trust Lists] Delete", payload: DeleteTrustList, } | { "type": "[Biometrics] Enable", payload: EnableBiometrics, } | { "type": "[Storage] Check password", payload: CheckPassword, } | { "type": "[Verified Data] Check service health", payload: ServiceHealthCheck, } | { "type": "[Verified Data] Send verification email", payload: SendVerificationEmail, } | { "type": "[Verified Data] Redeem code", payload: RedeemCode, } | { "type": "[Verified Data] Reset email verification" } | { "type": "[Credential] Self Issue", payload: SelfIssueCredential, };
export type Action = { "type": "[App] Get state" } | { "type": "[Storage] Unlock", payload: UnlockStorage, } | { "type": "[App] Reset" } | { "type": "[DID] Create new", payload: CreateNew, } | { "type": "[Settings] Set locale", payload: SetLocale, } | { "type": "[Settings] Update profile", payload: UpdateProfileSettings, } | { "type": "[QR Code] Scanned", payload: QrCodeScanned, } | { "type": "[Authenticate] Connection accepted" } | { "type": "[User Flow] Cancel", payload?: CancelUserFlow, } | { "type": "[DEV] Show DEV mode setting", payload: ShowDevModeSetting, } | { "type": "[DEV] Load DEV profile", payload: DevProfile, } | { "type": "[DEV] Toggle DEV mode" } | { "type": "[Authenticate] Credentials selected", payload: CredentialsSelected, } | { "type": "[Credential Offer] Selected", payload: CredentialOffersSelected, } | { "type": "[Credential Offer] Code received", payload: CodeReceived, } | { "type": "[Credential Metadata] Update", payload: UpdateCredentialMetadata, } | { "type": "[Credential] Delete", payload: DeleteCredential, } | { "type": "[Credential] Refresh status", payload: RefreshCredentialStatus, } | { "type": "[Credential] Refresh all statuses" } | { "type": "[User Journey] Cancel" } | { "type": "[Settings] Update sorting preference", payload: UpdateSortingPreference, } | { "type": "[Search] Query", payload: SearchQuery, } | { "type": "[Search] Add recent", payload: AddRecentSearch, } | { "type": "[Search] Delete recent", payload: DeleteRecentSearch, } | { "type": "[DID] Set preferred method", payload: SetPreferredDidMethod, } | { "type": "[Keys] Set preferred key type", payload: SetPreferredKeyType, } | { "type": "[Trust List] Add entry", payload: AddTrustListEntry, } | { "type": "[Trust List] Edit entry", payload: EditTrustListEntry, } | { "type": "[Trust List] Delete entry", payload: DeleteTrustListEntry, } | { "type": "[Trust List] Toggle entry", payload: ToggleTrustListEntry, } | { "type": "[Trust Lists] Add", payload: AddTrustList, } | { "type": "[Trust Lists] Edit", payload: EditTrustList, } | { "type": "[Trust Lists] Delete", payload: DeleteTrustList, } | { "type": "[Biometrics] Enable", payload: EnableBiometrics, } | { "type": "[Storage] Check password", payload: CheckPassword, } | { "type": "[Verified Data] Check service health", payload: ServiceHealthCheck, } | { "type": "[Verified Data] Send verification email", payload: SendVerificationEmail, } | { "type": "[Verified Data] Redeem code", payload: RedeemCode, } | { "type": "[Verified Data] Reset email verification" } | { "type": "[Credential] Self Issue", payload: SelfIssueCredential, } | { "type": "[Credential] Share to LinkedIn", payload: ShareToLinkedIn, };
3 changes: 3 additions & 0 deletions identity-wallet/bindings/actions/ShareToLinkedIn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

export interface ShareToLinkedIn { id: string, }
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

export interface CredentialMetadata { is_favorite: boolean, date_added: string, date_issued: string, icon?: string, }
export interface CredentialMetadata { is_favorite: boolean, date_added: string, date_issued: string, expiration_date: string | null, icon?: string, }
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ import type { CredentialMetadata } from "./CredentialMetadata";
import type { CredentialStatus } from "./CredentialStatus";
import type { DisplayClaim } from "./DisplayClaim";

export interface DisplayCredential { id: string, format: { format: string }, issuer_name: string, data: any, display_claims: Array<DisplayClaim>, metadata: CredentialMetadata, connection_id?: string, display_name: string, credential_status?: CredentialStatus, }
export interface DisplayCredential { id: string, format: { format: string }, issuer_name: string, data: any, display_claims: Array<DisplayClaim>, metadata: CredentialMetadata, connection_id?: string, display_name: string, credential_status?: CredentialStatus, public_link: string | null, }
4 changes: 3 additions & 1 deletion identity-wallet/src/state/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ mod bindings {
authorization_code_received::CodeReceived, credential_offers_selected::CredentialOffersSelected,
credentials_selected::CredentialsSelected, delete_credential::DeleteCredential,
refresh_credential_status::RefreshCredentialStatus, self_issue_credential::SelfIssueCredential,
update_credential_metadata::UpdateCredentialMetadata,
share_to_linkedin::ShareToLinkedIn, update_credential_metadata::UpdateCredentialMetadata,
},
dev_mode::actions::{dev_profile::DevProfile, show_setting::ShowDevModeSetting},
did::actions::{set_preferred_keytype::SetPreferredKeyType, set_preferred_method::SetPreferredDidMethod},
Expand Down Expand Up @@ -181,5 +181,7 @@ mod bindings {
ResetEmailVerification,
#[serde(rename = "[Credential] Self Issue")]
SelfIssueCredential { payload: SelfIssueCredential },
#[serde(rename = "[Credential] Share to LinkedIn")]
ShareToLinkedIn { payload: ShareToLinkedIn },
}
}
4 changes: 2 additions & 2 deletions identity-wallet/src/state/core_utils/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ pub async fn validate_jwt_vc_json(
.ok_or(AppError::Error("Failed to resolve issuer DID".to_string()))?;

validator
.validate::<_, Value>(&credential_jwt, &issuer_document, &options, FailFast::FirstError)
.map_err(|_| AppError::Error("Invalid jwt_vc_json".to_string()))
.validate::<_, Value>(&credential_jwt, &issuer_document, &options, FailFast::AllErrors)
.map_err(|e| AppError::Error(format!("Invalid jwt_vc_json: {e}")))
}

/// Validate supported credential types against their corresponding JSON Schema.
Expand Down
1 change: 1 addition & 0 deletions identity-wallet/src/state/credentials/actions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ pub mod delete_credential;
pub mod refresh_all_credential_statuses;
pub mod refresh_credential_status;
pub mod self_issue_credential;
pub mod share_to_linkedin;
pub mod update_credential_metadata;
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use crate::reducer;
use crate::state::credentials::reducers::share_to_linkedin::share_to_linkedin;
use crate::state::{actions::ActionTrait, Reducer};

use serde::{Deserialize, Serialize};
use ts_rs::TS;

#[derive(Serialize, Deserialize, Debug, TS, Clone)]
#[ts(export, export_to = "bindings/actions/ShareToLinkedIn.ts")]
pub struct ShareToLinkedIn {
#[ts(type = "string")]
pub id: String,
}

#[typetag::serde(name = "[Credential] Share to LinkedIn")]
impl ActionTrait for ShareToLinkedIn {
fn reducers<'a>(&self) -> Vec<Reducer<'a>> {
vec![reducer!(share_to_linkedin)]
}
}
206 changes: 206 additions & 0 deletions identity-wallet/src/state/credentials/create_public_link.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
use std::str::FromStr;

use crate::error::AppError::{self, *};
use crate::state::core_utils::helpers::get_unverified_jwt_claims;
use crate::state::credentials::VerifiableCredentialRecord;
use crate::state::did::validate_linked_verifiable_presentations::validate_linked_verifiable_presentations;
use crate::state::AppState;
use chrono::{Duration, Utc};
use did_manager::Resolver;
use identity_iota::core::ToJson;
use jsonwebtoken::Header;
use log::{info, warn};
use oid4vc::oid4vc_core::jwt::encode;
use serde::Serialize;
use serde_json::{json, Value};
use url::Url;
use uuid::Uuid;

// TODO: this should actually be the reducer and the sharing to LinkedIn or whatever platform should be the helper, since that part doesnt update the AppState.
pub async fn create_public_link(state: &AppState, credential_id: &str) -> Result<Url, AppError> {
// Get the VerifiableCredentialRecord belonging to the Id from Stronghold
let id = Uuid::from_str(credential_id).map_err(|e| AppError::Error(e.to_string()))?;
let stronghold_manager = state
.core_utils
.managers
.lock()
.await
.stronghold_manager
.as_ref()
.ok_or(MissingManagerError("stronghold"))?
.clone();
let vcr_bytes = stronghold_manager
.get(id)
.map_err(|e| AppError::Error(e.to_string()))?
.ok_or(AppError::Error(
"Failed to get VerifiableCredentialRecord bytes from Stronghold".to_string(),
))?;
let vcr: VerifiableCredentialRecord =
serde_json::from_slice(&vcr_bytes).map_err(|e| AppError::Error(e.to_string()))?;

// Get the UniMe did
let did_method = state
.profile_settings
.preferred_did_methods
.first()
.ok_or(AppError::Error("Failed to get a preferred did method".to_string()))?;

let issuer_did = state.dids.get(did_method).ok_or(AppError::Error(
"Failed to get the did for the preferred did method".to_string(),
))?;

// Get preferred key type and convert it to jsonwebtoken::Algorithm
let key_type = state
.profile_settings
.preferred_key_types
.first()
.ok_or(AppError::Error("Failed to get a preferred key type".to_string()))?
.as_str();
let algorithm = match key_type {
"EdDSA" => jsonwebtoken::Algorithm::EdDSA,
"ES256" => jsonwebtoken::Algorithm::ES256,
_ => return Err(AppError::Error("Unsupported key type".to_string())),
};

// Get kid
let managers = state.core_utils.managers.lock().await;
let subject = managers
.identity_manager
.as_ref()
.ok_or(MissingManagerError("identity"))?
.subject
.clone();

let kid = subject
.key_id(did_method, algorithm)
.await
.ok_or(AppError::Error("Failed to create a key id".to_string()))?;

// Compose the JWT header
let header = Header {
alg: algorithm,
typ: Some("JWT".to_string()),
kid: Some(kid.clone()),
..Default::default()
};

// Get the credential's issuer DID from the credential data
let jwt = &mut vcr.verifiable_credential.clone();
let jwt_data = get_unverified_jwt_claims(jwt)?;
let credential_issuer_did = jwt_data
.get("iss")
.and_then(|v| v.as_str())
.ok_or(AppError::Error("Issuer (iss) not found".to_string()))?;

// Get the JTI claim from the credential data
let jti = jwt_data
.get("jti")
.and_then(|v| v.as_str())
.ok_or(AppError::Error("JTI not found".to_string()))?;

let now = Utc::now();
let exp = now + Duration::days(365);

let claims = PublicLinkTokenClaims {
iss: issuer_did.to_string(),
sub: jti.to_string(),
aud: credential_issuer_did.to_string(),
iat: now.timestamp(),
nbf: now.timestamp(),
exp: exp.timestamp(),
status: "active".to_string(), // TODO: implement TSL revocation
};

let public_link_jwt = encode(subject, header, claims, did_method)
.await
.map_err(|e| AppError::Error(e.to_string()))?;

// Extract the Issuer DID from the `aud` claim of the token
let public_verifier_endpoint_url =
get_trusted_verifier_public_verification_endpoint(state, credential_issuer_did).await?;

let public_link = format!(
"{}?public-credential-token={}",
public_verifier_endpoint_url, public_link_jwt
);

let public_link_url =
Url::parse(&public_link).map_err(|e| AppError::Error(format!("Invalid public link URL: {}", e)))?;

info!("Succesfully generated public link URL: {}", public_link_url);

Ok(public_link_url)
}

// Through the issuer DID Document find the ecosystem leader and its public verification endpoint, who will be the trusted verifier.
// Step 1: get the issuer DID Document
// Step 2: find the Linked VP service
// Step 3: get the issuer DID Document of the Linked VP credential
// Step 4: find the Public Verification Endpoint service
pub async fn get_trusted_verifier_public_verification_endpoint(
_state: &AppState,
issuer_did: &str,
) -> Result<String, AppError> {
let resolver = Resolver::new().await;

// TODO: hardcoded logic that only selects the first Linked VP
let linked_vp = validate_linked_verifiable_presentations(&resolver, issuer_did)
.await
.iter()
.flatten()
.next()
.ok_or(AppError::Error(
"No Linked VP found for issuer of credential requested for public sharing".to_string(),
))?
.clone();

info!("Linked VP: {linked_vp:#?}");

let linked_vp_claims = get_unverified_jwt_claims(&json!(&linked_vp.data))?;
let linked_vp_issuer_did = linked_vp_claims
.get("iss")
.and_then(|v| v.as_str())
.ok_or(AppError::Error("Issuer (iss) not found in Linked VP".to_string()))?;
let linked_vp_issuer_document = resolver
.resolve(linked_vp_issuer_did)
.await
.map_err(|_| AppError::Error("Failed to resolve linked VP issuer did".to_string()))?;

info!("Linked VP Issuer DID Document: {linked_vp_issuer_document:#?}");

let public_verification_endpoint = linked_vp_issuer_document.service().iter().find_map(|service| {
service
.type_()
.contains("PublicVerificationEndpoint")
.then(|| {
info!("Found Public Verification Endpoint: {service:#?}");
service.service_endpoint()
})
.and_then(|service_endpoint| service_endpoint.to_json_value().ok())
.and_then(|endpoint_value| match endpoint_value {
Value::String(url) => Some(url),
Value::Object(obj) => obj
.get("url")
.or_else(|| obj.get("uri"))
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
_ => {
warn!("Unexpected service endpoint format: {endpoint_value:#?}");
None
}
})
});

public_verification_endpoint
.ok_or_else(|| AppError::Error("No public verification endpoint found in issuer DID document".to_string()))
}
#[derive(Serialize, Debug)]
struct PublicLinkTokenClaims {
iss: String,
sub: String,
aud: String,
iat: i64,
nbf: i64,
exp: i64,
status: String, // TODO: impl TSL revocation
}
Loading
Loading