diff --git a/implementations/rust/ockam/ockam_api/src/nodes/models/mod.rs b/implementations/rust/ockam/ockam_api/src/nodes/models/mod.rs index 48ed9ce187e..dfc653d72f5 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/models/mod.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/models/mod.rs @@ -6,7 +6,6 @@ pub mod base; pub mod credentials; pub mod flow_controls; pub mod forwarder; -pub mod identity; pub mod policy; pub mod portal; pub mod secure_channel; diff --git a/implementations/rust/ockam/ockam_command/src/credential/issue.rs b/implementations/rust/ockam/ockam_command/src/credential/issue.rs index cd7060ae58c..193c90b5d1a 100644 --- a/implementations/rust/ockam/ockam_command/src/credential/issue.rs +++ b/implementations/rust/ockam/ockam_command/src/credential/issue.rs @@ -8,6 +8,7 @@ use crate::{ }; use clap::Args; +use crate::util::output::CredentialAndPurposeKeyDisplay; use miette::{miette, IntoDiagnostic}; use ockam::identity::utils::AttributesBuilder; use ockam::identity::{ @@ -104,7 +105,10 @@ async fn run_impl( .await .into_diagnostic()?; - print_encodable(credential, &cmd.encode_format)?; + print_encodable( + CredentialAndPurposeKeyDisplay(credential), + &cmd.encode_format, + )?; Ok(()) } diff --git a/implementations/rust/ockam/ockam_command/src/credential/mod.rs b/implementations/rust/ockam/ockam_command/src/credential/mod.rs index af86e2b14c8..7782bef53c4 100644 --- a/implementations/rust/ockam/ockam_command/src/credential/mod.rs +++ b/implementations/rust/ockam/ockam_command/src/credential/mod.rs @@ -10,16 +10,18 @@ use colorful::Colorful; pub(crate) use get::GetCommand; pub(crate) use issue::IssueCommand; pub(crate) use list::ListCommand; -use ockam::identity::Identifier; +use ockam::identity::{Identifier, Identities, Identity}; use ockam_api::cli_state::{CredentialState, StateItemTrait}; pub(crate) use present::PresentCommand; pub(crate) use show::ShowCommand; +use std::sync::Arc; pub(crate) use store::StoreCommand; pub(crate) use verify::VerifyCommand; -use crate::util::output::Output; +use crate::util::output::{CredentialAndPurposeKeyDisplay, Output}; use crate::{CommandGlobalOpts, Result}; use clap::{Args, Subcommand}; +use miette::IntoDiagnostic; use ockam::identity::models::CredentialAndPurposeKey; use ockam_api::cli_state::traits::StateDirTrait; @@ -57,15 +59,29 @@ impl CredentialCommand { } } +pub async fn identities(vault_name: &str, opts: &CommandGlobalOpts) -> Result> { + let vault = opts.state.vaults.get(vault_name)?.get().await?; + let identities = opts.state.get_identities(vault).await?; + + Ok(identities) +} + +pub async fn identity(identity: &str, identities: Arc) -> Result { + let identity_as_bytes = hex::decode(identity)?; + + let identity = identities + .identities_creation() + .import(None, &identity_as_bytes) + .await?; + + Ok(identity) +} + pub async fn validate_encoded_cred( encoded_cred: &[u8], + identities: Arc, issuer: &Identifier, - vault: &str, - opts: &CommandGlobalOpts, ) -> Result<()> { - let vault = opts.state.vaults.get(vault)?.get().await?; - let identities = opts.state.get_identities(vault).await?; - let cred: CredentialAndPurposeKey = minicbor::decode(encoded_cred)?; identities @@ -89,17 +105,19 @@ impl CredentialOutput { vault_name: &str, ) -> Result { let config = state.config(); + + let identities = identities(vault_name, opts).await.into_diagnostic()?; + let is_verified = validate_encoded_cred( &config.encoded_credential, + identities, &config.issuer_identifier, - vault_name, - opts, ) .await .is_ok(); let credential = config.credential()?; - let credential = hex::encode(minicbor::to_vec(credential)?); + let credential = format!("{}", CredentialAndPurposeKeyDisplay(credential)); let output = Self { name: state.name().to_string(), diff --git a/implementations/rust/ockam/ockam_command/src/credential/show.rs b/implementations/rust/ockam/ockam_command/src/credential/show.rs index 84e606015ed..d7fafb037d6 100644 --- a/implementations/rust/ockam/ockam_command/src/credential/show.rs +++ b/implementations/rust/ockam/ockam_command/src/credential/show.rs @@ -1,9 +1,11 @@ use clap::{arg, Args}; use colorful::Colorful; +use miette::IntoDiagnostic; use ockam::Context; use ockam_api::cli_state::{StateDirTrait, StateItemTrait}; -use crate::util::output::Output; +use crate::credential::identities; +use crate::util::output::CredentialAndPurposeKeyDisplay; use crate::{ credential::validate_encoded_cred, util::node_rpc, vault::default_vault_name, CommandGlobalOpts, }; @@ -42,11 +44,20 @@ pub(crate) async fn display_credential( let cred = opts.state.credentials.get(cred_name)?; let cred_config = cred.config(); + let identities = identities(vault_name, opts).await?; + identities + .identities_creation() + .import( + Some(&cred_config.issuer_identifier), + &cred_config.encoded_issuer_change_history, + ) + .await + .into_diagnostic()?; + let is_verified = match validate_encoded_cred( &cred_config.encoded_credential, + identities, &cred_config.issuer_identifier, - vault_name, - opts, ) .await { @@ -56,7 +67,7 @@ pub(crate) async fn display_credential( let cred = cred_config.credential()?; println!("Credential: {cred_name} {is_verified}"); - println!("{}", cred.output()?); + println!("{}", CredentialAndPurposeKeyDisplay(cred)); Ok(()) } diff --git a/implementations/rust/ockam/ockam_command/src/credential/store.rs b/implementations/rust/ockam/ockam_command/src/credential/store.rs index 01e15d7272c..83920e42765 100644 --- a/implementations/rust/ockam/ockam_command/src/credential/store.rs +++ b/implementations/rust/ockam/ockam_command/src/credential/store.rs @@ -6,13 +6,13 @@ use crate::{ terminal::OckamColor, util::{node_rpc, random_name}, vault::default_vault_name, - CommandGlobalOpts, Result, + CommandGlobalOpts, }; use colorful::Colorful; use miette::miette; +use crate::credential::{identities, identity}; use clap::Args; -use ockam::identity::{identities, Identity}; use ockam::Context; use ockam_api::cli_state::{CredentialConfig, StateDirTrait}; use tokio::{sync::Mutex, try_join}; @@ -39,18 +39,6 @@ impl StoreCommand { pub fn run(self, opts: CommandGlobalOpts) { node_rpc(run_impl, (opts, self)); } - - pub async fn identity(&self) -> Result { - let identity_as_bytes = match hex::decode(&self.issuer) { - Ok(b) => b, - Err(e) => return Err(miette!(e).into()), - }; - let identity = identities() - .identities_creation() - .import(None, &identity_as_bytes) - .await?; - Ok(identity) - } } async fn run_impl( @@ -66,7 +54,10 @@ async fn run_impl( let send_req = async { let cred_as_str = match (&cmd.credential, &cmd.credential_path) { - (_, Some(credential_path)) => tokio::fs::read_to_string(credential_path).await?, + (_, Some(credential_path)) => tokio::fs::read_to_string(credential_path) + .await? + .trim() + .to_string(), (Some(credential), _) => credential.to_string(), _ => { *is_finished.lock().await = true; @@ -81,7 +72,15 @@ async fn run_impl( .clone() .unwrap_or_else(|| default_vault_name(&opts.state)); - let issuer = match cmd.identity().await { + let identities = match identities(&vault_name, &opts).await { + Ok(i) => i, + Err(_) => { + *is_finished.lock().await = true; + return Err(miette!("Invalid state").into()); + } + }; + + let issuer = match identity(&cmd.issuer, identities.clone()).await { Ok(i) => i, Err(_) => { *is_finished.lock().await = true; @@ -90,8 +89,7 @@ async fn run_impl( }; let cred = hex::decode(&cred_as_str)?; - if let Err(e) = validate_encoded_cred(&cred, issuer.identifier(), &vault_name, &opts).await - { + if let Err(e) = validate_encoded_cred(&cred, identities, issuer.identifier()).await { *is_finished.lock().await = true; return Err(miette!("Credential is invalid\n{}", e).into()); } diff --git a/implementations/rust/ockam/ockam_command/src/credential/verify.rs b/implementations/rust/ockam/ockam_command/src/credential/verify.rs index 0b846489619..62f9a41ab84 100644 --- a/implementations/rust/ockam/ockam_command/src/credential/verify.rs +++ b/implementations/rust/ockam/ockam_command/src/credential/verify.rs @@ -1,13 +1,13 @@ use std::path::PathBuf; use crate::{ - fmt_err, fmt_log, fmt_ok, util::node_rpc, vault::default_vault_name, CommandGlobalOpts, Result, + fmt_err, fmt_log, fmt_ok, util::node_rpc, vault::default_vault_name, CommandGlobalOpts, }; use miette::miette; +use crate::credential::{identities, identity}; use clap::Args; use colorful::Colorful; -use ockam::identity::{identities, Identity}; use ockam::Context; use tokio::{sync::Mutex, try_join}; @@ -32,18 +32,6 @@ impl VerifyCommand { pub fn run(self, opts: CommandGlobalOpts) { node_rpc(run_impl, (opts, self)); } - - pub async fn issuer(&self) -> Result { - let identity_as_bytes = match hex::decode(&self.issuer) { - Ok(b) => b, - Err(e) => return Err(miette!(e).into()), - }; - let identity = identities() - .identities_creation() - .import(None, &identity_as_bytes) - .await?; - Ok(identity) - } } async fn run_impl( @@ -57,7 +45,10 @@ async fn run_impl( let send_req = async { let cred_as_str = match (&cmd.credential, &cmd.credential_path) { - (_, Some(credential_path)) => tokio::fs::read_to_string(credential_path).await?, + (_, Some(credential_path)) => tokio::fs::read_to_string(credential_path) + .await? + .trim() + .to_string(), (Some(credential), _) => credential.clone(), _ => { *is_finished.lock().await = true; @@ -72,20 +63,27 @@ async fn run_impl( .clone() .unwrap_or_else(|| default_vault_name(&opts.state)); - let issuer = match cmd.issuer().await { + let identities = match identities(&vault_name, &opts).await { + Ok(i) => i, + Err(_) => { + *is_finished.lock().await = true; + return Err(miette!("Invalid state").into()); + } + }; + + let issuer = match identity(&cmd.issuer, identities.clone()).await { Ok(i) => i, Err(_) => { *is_finished.lock().await = true; - return Ok((false, "Issuer is invalid".to_string())); + return Err(miette!("Issuer is invalid").into()); } }; let cred = hex::decode(&cred_as_str)?; - let is_valid = - match validate_encoded_cred(&cred, issuer.identifier(), &vault_name, &opts).await { - Ok(_) => (true, String::new()), - Err(e) => (false, e.to_string()), - }; + let is_valid = match validate_encoded_cred(&cred, identities, issuer.identifier()).await { + Ok(_) => (true, String::new()), + Err(e) => (false, e.to_string()), + }; *is_finished.lock().await = true; Ok(is_valid) diff --git a/implementations/rust/ockam/ockam_command/src/identity/show.rs b/implementations/rust/ockam/ockam_command/src/identity/show.rs index ae57cb33346..b7467f4ddbe 100644 --- a/implementations/rust/ockam/ockam_command/src/identity/show.rs +++ b/implementations/rust/ockam/ockam_command/src/identity/show.rs @@ -1,13 +1,13 @@ use crate::identity::{get_identity_name, initialize_identity_if_default}; -use crate::util::output::Output; +use crate::util::output::{IdentifierDisplay, IdentityDisplay}; use crate::util::{node_rpc, println_output}; -use crate::{docs, CommandGlobalOpts, EncodeFormat, Result}; +use crate::{docs, CommandGlobalOpts, EncodeFormat}; use clap::Args; -use core::fmt::Write; use miette::IntoDiagnostic; +use ockam::identity::Identity; use ockam_api::cli_state::traits::{StateDirTrait, StateItemTrait}; -use ockam_api::nodes::models::identity::{LongIdentityResponse, ShortIdentityResponse}; use ockam_node::Context; +use ockam_vault::Vault; const LONG_ABOUT: &str = include_str!("./static/show/long_about.txt"); const PREVIEW_TAG: &str = include_str!("../static/preview_tag.txt"); @@ -48,45 +48,37 @@ impl ShowCommand { let (opts, cmd) = options; let name = get_identity_name(&opts.state, &cmd.name); let state = opts.state.identities.get(&name)?; + let identifier = state.config().identifier(); if cmd.full { - let identifier = state.config().identifier(); - let identity = opts + let change_history = opts .state .identities .identities_repository() .await? .get_identity(&identifier) .await - .into_diagnostic()? - .export() .into_diagnostic()?; if Some(EncodeFormat::Hex) == cmd.encoding { - println_output(identity, &opts.global_args.output_format)?; + println_output( + hex::encode(change_history.export().into_diagnostic()?), + &opts.global_args.output_format, + )?; } else { - let output = LongIdentityResponse::new(identity); - println_output(output, &opts.global_args.output_format)?; + let identity = Identity::import_from_change_history( + Some(&identifier), + change_history, + Vault::create_verifying_vault(), + ) + .await + .into_diagnostic()?; + let identity_display = IdentityDisplay(identity); + println_output(identity_display, &opts.global_args.output_format)?; } } else { - let output = ShortIdentityResponse::new(state.config().identifier()); - println_output(output, &opts.global_args.output_format)?; + let identifier_display = IdentifierDisplay(identifier); + println_output(identifier_display, &opts.global_args.output_format)?; } Ok(()) } } - -impl Output for LongIdentityResponse { - fn output(&self) -> Result { - let mut w = String::new(); - write!(w, "{}", hex::encode(&self.identity_change_history))?; - Ok(w) - } -} - -impl Output for ShortIdentityResponse { - fn output(&self) -> Result { - let mut w = String::new(); - write!(w, "{}", self.identity_id)?; - Ok(w) - } -} diff --git a/implementations/rust/ockam/ockam_command/src/lib.rs b/implementations/rust/ockam/ockam_command/src/lib.rs index ab703635bd9..42f520ea901 100644 --- a/implementations/rust/ockam/ockam_command/src/lib.rs +++ b/implementations/rust/ockam/ockam_command/src/lib.rs @@ -4,6 +4,8 @@ //! //! For more information please visit the [command guide](https://docs.ockam.io/reference/command) +extern crate core; + mod admin; mod authenticated; mod authority; diff --git a/implementations/rust/ockam/ockam_command/src/project/enroll.rs b/implementations/rust/ockam/ockam_command/src/project/enroll.rs index ca2792c7994..d216a885c1b 100644 --- a/implementations/rust/ockam/ockam_command/src/project/enroll.rs +++ b/implementations/rust/ockam/ockam_command/src/project/enroll.rs @@ -23,7 +23,7 @@ use crate::project::util::create_secure_channel_to_authority; use crate::project::ProjectInfo; use crate::util::api::{CloudOpts, TrustContextOpts}; use crate::util::node_rpc; -use crate::util::output::Output; +use crate::util::output::CredentialAndPurposeKeyDisplay; use crate::{docs, CommandGlobalOpts, Result}; const LONG_ABOUT: &str = include_str!("./static/enroll/long_about.txt"); @@ -210,7 +210,7 @@ pub async fn project_enroll( let credential = client2.credential().await.into_diagnostic()?; println!("---"); - println!("{}", credential.output()?); + println!("{}", CredentialAndPurposeKeyDisplay(credential)); println!("---"); Ok(project.name) } diff --git a/implementations/rust/ockam/ockam_command/src/util/output.rs b/implementations/rust/ockam/ockam_command/src/util/output.rs index 30bed99f712..cbdcf6ca854 100644 --- a/implementations/rust/ockam/ockam_command/src/util/output.rs +++ b/implementations/rust/ockam/ockam_command/src/util/output.rs @@ -1,11 +1,20 @@ +use core::fmt; use core::fmt::Write; +use std::fmt::Formatter; use cli_table::{Cell, Style, Table}; use colorful::Colorful; use miette::miette; use miette::IntoDiagnostic; +use minicbor::Encode; +use ockam::identity::models::{ + CredentialAndPurposeKey, CredentialData, CredentialSigningKey, Ed25519PublicKey, + P256ECDSAPublicKey, PurposeKeyAttestation, PurposeKeyAttestationData, PurposePublicKey, + X25519PublicKey, +}; +use ockam::identity::{Credential, Identifier, Identity, TimestampInSeconds}; +use serde::{Serialize, Serializer}; -use ockam::identity::models::CredentialAndPurposeKey; use ockam_api::cli_state::{StateItemTrait, VaultState}; use ockam_api::cloud::project::Project; use ockam_api::cloud::space::Space; @@ -59,6 +68,18 @@ impl Output for &O { } } +impl Output for String { + fn output(&self) -> Result { + Ok(self.clone()) + } +} + +impl Output for &str { + fn output(&self) -> Result { + Ok(self.to_string()) + } +} + impl Output for Space { fn output(&self) -> Result { let mut w = String::new(); @@ -283,14 +304,6 @@ From {} to {}"#, } } -impl Output for CredentialAndPurposeKey { - fn output(&self) -> Result { - let s = minicbor::to_vec(self)?; - let s = hex::encode(s); - Ok(s) - } -} - impl Output for Vec { fn output(&self) -> Result { Ok(hex::encode(self)) @@ -381,3 +394,301 @@ impl Output for VaultState { Ok(output) } } + +fn human_readable_time(time: TimestampInSeconds) -> String { + use time::format_description::well_known::iso8601::*; + use time::Error::Format; + use time::OffsetDateTime; + + match OffsetDateTime::from_unix_timestamp(*time as i64) { + Ok(time) => { + match time.format( + &Iso8601::< + { + Config::DEFAULT + .set_time_precision(TimePrecision::Second { + decimal_digits: None, + }) + .encode() + }, + >, + ) { + Ok(now_iso) => now_iso, + Err(_) => { + Format(time::error::Format::InvalidComponent("timestamp error")).to_string() + } + } + } + Err(_) => Format(time::error::Format::InvalidComponent( + "unix time is invalid", + )) + .to_string(), + } +} + +pub struct X25519PublicKeyDisplay(pub X25519PublicKey); + +impl fmt::Display for X25519PublicKeyDisplay { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "X25519: {}", hex::encode(self.0 .0)) + } +} + +pub struct Ed25519PublicKeyDisplay(pub Ed25519PublicKey); + +impl fmt::Display for Ed25519PublicKeyDisplay { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "Ed25519: {}", hex::encode(self.0 .0)) + } +} + +pub struct P256PublicKeyDisplay(pub P256ECDSAPublicKey); + +impl fmt::Display for P256PublicKeyDisplay { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "P256: {}", hex::encode(self.0 .0)) + } +} + +pub struct PurposePublicKeyDisplay(pub PurposePublicKey); + +impl fmt::Display for PurposePublicKeyDisplay { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match &self.0 { + PurposePublicKey::SecureChannelStaticKey(key) => { + writeln!( + f, + "Secure Channel Key -> {}", + X25519PublicKeyDisplay(key.clone()) + )?; + } + PurposePublicKey::CredentialSigningKey(key) => match key { + CredentialSigningKey::Ed25519PublicKey(key) => { + writeln!( + f, + "Credentials Key -> {}", + Ed25519PublicKeyDisplay(key.clone()) + )?; + } + CredentialSigningKey::P256ECDSAPublicKey(key) => { + writeln!( + f, + "Credentials Key -> {}", + P256PublicKeyDisplay(key.clone()) + )?; + } + }, + } + + Ok(()) + } +} + +pub struct CredentialDisplay(pub Credential); + +impl fmt::Display for CredentialDisplay { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let versioned_data = match self.0.get_versioned_data() { + Ok(versioned_data) => versioned_data, + Err(_) => { + writeln!(f, "Invalid VersionedData")?; + return Ok(()); + } + }; + + writeln!(f, "Version: {}", versioned_data.version)?; + + let credential_data = match CredentialData::get_data(&versioned_data) { + Ok(credential_data) => credential_data, + Err(_) => { + writeln!(f, "Invalid CredentialData")?; + return Ok(()); + } + }; + + if let Some(subject) = &credential_data.subject { + writeln!(f, "Subject: {}", subject)?; + } + + if let Some(subject_latest_change_hash) = &credential_data.subject_latest_change_hash { + writeln!( + f, + "Subject Latest Change Hash: {}", + subject_latest_change_hash + )?; + } + + writeln!( + f, + "Created: {}", + human_readable_time(credential_data.created_at) + )?; + writeln!( + f, + "Expires: {}", + human_readable_time(credential_data.expires_at) + )?; + + writeln!(f, "Attributes: ")?; + + write!( + f, + " Schema: {}; ", + credential_data.subject_attributes.schema.0 + )?; + + f.debug_map() + .entries(credential_data.subject_attributes.map.iter().map(|(k, v)| { + ( + std::str::from_utf8(k).unwrap_or("**binary**"), + std::str::from_utf8(v).unwrap_or("**binary**"), + ) + })) + .finish()?; + + Ok(()) + } +} + +pub struct PurposeKeyDisplay(pub PurposeKeyAttestation); + +impl fmt::Display for PurposeKeyDisplay { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let versioned_data = match self.0.get_versioned_data() { + Ok(versioned_data) => versioned_data, + Err(_) => { + writeln!(f, "Invalid VersionedData")?; + return Ok(()); + } + }; + + writeln!(f, "Version: {}", versioned_data.version)?; + + let purpose_key_attestation_data = + match PurposeKeyAttestationData::get_data(&versioned_data) { + Ok(purpose_key_attestation_data) => purpose_key_attestation_data, + Err(_) => { + writeln!(f, "Invalid PurposeKeyAttestationData")?; + return Ok(()); + } + }; + + writeln!( + f, + "Subject: {}", + purpose_key_attestation_data.subject + )?; + + writeln!( + f, + "Subject Latest Change Hash: {}", + purpose_key_attestation_data.subject_latest_change_hash + )?; + + writeln!( + f, + "Created: {}", + human_readable_time(purpose_key_attestation_data.created_at) + )?; + writeln!( + f, + "Expires: {}", + human_readable_time(purpose_key_attestation_data.expires_at) + )?; + + writeln!( + f, + "Public Key -> {}", + PurposePublicKeyDisplay(purpose_key_attestation_data.public_key.clone()) + )?; + + Ok(()) + } +} + +#[derive(Encode)] +#[cbor(transparent)] +pub struct CredentialAndPurposeKeyDisplay(#[n(0)] pub CredentialAndPurposeKey); + +impl Output for CredentialAndPurposeKeyDisplay { + fn output(&self) -> Result { + Ok(format!("{}", self)) + } +} + +impl fmt::Display for CredentialAndPurposeKeyDisplay { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + // TODO: Could borrow using a lifetime + writeln!(f, "Credential:")?; + writeln!(f, "{}", CredentialDisplay(self.0.credential.clone()))?; + writeln!(f)?; + writeln!(f, "Purpose key:")?; + writeln!( + f, + "{}", + PurposeKeyDisplay(self.0.purpose_key_attestation.clone()) + )?; + + Ok(()) + } +} + +#[derive(Serialize)] +#[serde(transparent)] +pub struct IdentifierDisplay(pub Identifier); + +impl fmt::Display for IdentifierDisplay { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl Output for IdentifierDisplay { + fn output(&self) -> Result { + Ok(self.to_string()) + } +} + +pub struct IdentityDisplay(pub Identity); + +impl Serialize for IdentityDisplay { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + use serde::ser::Error; + serializer.serialize_bytes(&self.0.export().map_err(Error::custom)?) + } +} + +impl fmt::Display for IdentityDisplay { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + writeln!(f, "Identifier: {}", self.0.identifier())?; + for (i_num, change) in self.0.changes().iter().enumerate() { + writeln!(f, " Change[{}]:", i_num)?; + writeln!( + f, + " identifier: {}", + hex::encode(change.change_hash()) + )?; + writeln!( + f, + " primary_public_key: {}", + change.primary_public_key() + )?; + writeln!( + f, + " revoke_all_purpose_keys: {}", + change.revoke_all_purpose_keys() + )?; + } + + Ok(()) + } +} + +impl Output for IdentityDisplay { + fn output(&self) -> Result { + Ok(format!("{}", self)) + } +}