#![deny(unsafe_code)]
use crate::errors::OpenVTCError;
#[cfg(feature = "openpgp-card")]
use ::openpgp_card::ocard::KeyType;
use affinidi_tdk::{
didcomm::Message,
messaging::{ATM, profiles::ATMProfile},
};
use serde::{Deserialize, Serialize};
use std::{fmt, sync::Arc};
pub mod agent_name;
pub mod bip32;
pub mod capabilities;
pub mod community_access;
pub mod config;
pub mod context_probe;
pub mod credential_sync;
pub mod devices;
pub mod diagnostics;
pub mod didcomm;
pub mod display;
pub mod dtg;
pub mod errors;
pub mod git_ns;
pub mod health;
pub mod identity;
pub mod issued_credential;
pub mod join;
pub mod logs;
pub mod members;
pub mod messaging;
mod net_guard;
#[cfg(feature = "openpgp-card")]
pub mod openpgp_card;
pub mod operational;
pub mod persona;
pub mod personhood;
pub mod presentation;
pub mod process_lock;
pub mod proof_check;
pub mod rebuild;
pub mod rebuild_apply;
pub mod relationships;
pub mod renewal;
pub mod secure_store;
pub mod status_list;
pub mod tasks;
pub mod trust_task_doc;
pub mod tsp;
pub mod tsp_store;
pub mod vetting;
pub mod vrc;
pub const LF_PUBLIC_MEDIATOR_DID: &str =
"did:webvh:QmetnhxzJXTJ9pyXR1BbZ2h6DomY6SB1ZbzFPrjYyaEq9V:fpp.storm.ws:public-mediator";
pub const LF_ORG_DID: &str =
"did:webvh:QmXkYcFCbvFFcYZf2q5gNk8Vp4b4vMbVKWbbc7oivcdZHK:fpp.storm.ws";
pub fn mediator_did(override_did: Option<&str>) -> String {
if let Some(did) = override_did {
if did.starts_with("did:") {
return did.to_string();
}
tracing::warn!(
"mediator DID override '{}' is not a valid DID (must start with 'did:'), using default",
did
);
}
LF_PUBLIC_MEDIATOR_DID.to_string()
}
pub fn org_did(override_did: Option<&str>) -> String {
if let Some(did) = override_did {
if did.starts_with("did:") {
return did.to_string();
}
tracing::warn!(
"org DID override '{}' is not a valid DID (must start with 'did:'), using default",
did
);
}
LF_ORG_DID.to_string()
}
pub async fn pack_and_send(
atm: &ATM,
profile: &Arc<ATMProfile>,
msg: &Message,
from: &str,
to: &str,
mediator: &str,
) -> Result<(), errors::OpenVTCError> {
let (packed, _) = atm.pack_encrypted(msg, to, Some(from), None).await?;
atm.forward_and_send_message(
profile, false, &packed, None, mediator, to, None, None, false,
)
.await?;
Ok(())
}
pub fn require_from(msg: &Message) -> Result<String, errors::OpenVTCError> {
msg.from
.as_deref()
.map(String::from)
.ok_or_else(|| errors::OpenVTCError::Config("Message has no 'from' address".to_string()))
}
pub mod protocol_urls {
pub const RELATIONSHIP_REQUEST: &str =
"https://linuxfoundation.org/openvtc/1.0/relationship-request";
pub const RELATIONSHIP_REQUEST_REJECT: &str =
"https://linuxfoundation.org/openvtc/1.0/relationship-request-reject";
pub const RELATIONSHIP_REQUEST_ACCEPT: &str =
"https://linuxfoundation.org/openvtc/1.0/relationship-request-accept";
pub const RELATIONSHIP_REQUEST_FINALIZE: &str =
"https://linuxfoundation.org/openvtc/1.0/relationship-request-finalize";
pub const TRUST_PING: &str = "https://didcomm.org/trust-ping/2.0/ping";
pub const TRUST_PONG: &str = "https://didcomm.org/trust-ping/2.0/ping-response";
pub const VRC_REQUEST: &str = "https://firstperson.network/vrc/1.0/request";
pub const VRC_REJECTED: &str = "https://firstperson.network/vrc/1.0/rejected";
pub const VRC_ISSUED: &str = "https://firstperson.network/vrc/1.0/issued";
pub const MESSAGEPICKUP_STATUS: &str = "https://didcomm.org/messagepickup/3.0/status";
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[non_exhaustive]
pub enum MessageType {
RelationshipRequest,
RelationshipRequestRejected,
RelationshipRequestAccepted,
RelationshipRequestFinalize,
TrustPing,
TrustPong,
VRCRequest,
VRCRequestRejected,
VRCIssued,
}
impl MessageType {
pub fn friendly_name(&self) -> String {
match self {
MessageType::RelationshipRequest => "Relationship Request",
MessageType::RelationshipRequestRejected => "Relationship Request Rejected",
MessageType::RelationshipRequestAccepted => "Relationship Request Accepted",
MessageType::RelationshipRequestFinalize => "Relationship Request Finalize",
MessageType::TrustPing => "Trust Ping (Send)",
MessageType::TrustPong => "Trust Pong (Receive)",
MessageType::VRCRequest => "VRC Request",
MessageType::VRCRequestRejected => "VRC Request Rejected",
MessageType::VRCIssued => "VRC Issued",
}
.to_string()
}
}
impl From<MessageType> for String {
fn from(value: MessageType) -> Self {
use protocol_urls::*;
match value {
MessageType::RelationshipRequest => RELATIONSHIP_REQUEST,
MessageType::RelationshipRequestRejected => RELATIONSHIP_REQUEST_REJECT,
MessageType::RelationshipRequestAccepted => RELATIONSHIP_REQUEST_ACCEPT,
MessageType::RelationshipRequestFinalize => RELATIONSHIP_REQUEST_FINALIZE,
MessageType::TrustPing => TRUST_PING,
MessageType::TrustPong => TRUST_PONG,
MessageType::VRCRequest => VRC_REQUEST,
MessageType::VRCRequestRejected => VRC_REJECTED,
MessageType::VRCIssued => VRC_ISSUED,
}
.to_string()
}
}
impl TryFrom<&str> for MessageType {
type Error = OpenVTCError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
use protocol_urls::*;
match value {
RELATIONSHIP_REQUEST => Ok(MessageType::RelationshipRequest),
RELATIONSHIP_REQUEST_REJECT => Ok(MessageType::RelationshipRequestRejected),
RELATIONSHIP_REQUEST_ACCEPT => Ok(MessageType::RelationshipRequestAccepted),
RELATIONSHIP_REQUEST_FINALIZE => Ok(MessageType::RelationshipRequestFinalize),
TRUST_PING => Ok(MessageType::TrustPing),
TRUST_PONG => Ok(MessageType::TrustPong),
VRC_REQUEST => Ok(MessageType::VRCRequest),
VRC_REJECTED => Ok(MessageType::VRCRequestRejected),
VRC_ISSUED => Ok(MessageType::VRCIssued),
_ => Err(OpenVTCError::InvalidMessage(value.to_string())),
}
}
}
impl TryFrom<&Message> for MessageType {
type Error = OpenVTCError;
fn try_from(value: &Message) -> Result<Self, Self::Error> {
value.typ.as_str().try_into()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[non_exhaustive]
pub enum CredentialKind {
Membership,
Role,
CommunityVetting,
}
impl CredentialKind {
pub const ALL: &'static [CredentialKind] = &[
CredentialKind::Membership,
CredentialKind::Role,
CredentialKind::CommunityVetting,
];
pub fn vc_type(self) -> &'static str {
match self {
CredentialKind::Membership => "MembershipCredential",
CredentialKind::Role => "AuthorityCredential",
CredentialKind::CommunityVetting => "StatementCredential",
}
}
pub fn config_key(self) -> &'static str {
match self {
CredentialKind::Membership => "Membership",
CredentialKind::Role => "Role",
CredentialKind::CommunityVetting => "CommunityVetting",
}
}
pub fn activates_membership(self) -> bool {
matches!(self, CredentialKind::Membership)
}
pub fn from_config_key(key: &str) -> Option<CredentialKind> {
CredentialKind::ALL
.iter()
.copied()
.find(|k| k.config_key() == key)
}
pub fn from_credential(credential: &serde_json::Value) -> Option<CredentialKind> {
let parsed = dtg::parse_conformant(credential).ok()?;
match parsed.type_() {
dtg_credentials::DTGCredentialType::Membership => Some(CredentialKind::Membership),
dtg_credentials::DTGCredentialType::Authority
if dtg::community_roles(&parsed).is_some() =>
{
Some(CredentialKind::Role)
}
dtg_credentials::DTGCredentialType::Statement if dtg::is_community_vetting(&parsed) => {
Some(CredentialKind::CommunityVetting)
}
_ => None,
}
}
}
impl Serialize for CredentialKind {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.config_key())
}
}
impl<'de> Deserialize<'de> for CredentialKind {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let key = String::deserialize(deserializer)?;
CredentialKind::from_config_key(&key)
.ok_or_else(|| serde::de::Error::custom(format!("unknown credential kind {key:?}")))
}
}
#[derive(Default, Debug, Clone, Copy, PartialEq)]
pub enum KeyPurpose {
Signing,
Authentication,
Encryption,
#[default]
Unknown,
}
impl fmt::Display for KeyPurpose {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
KeyPurpose::Signing => write!(f, "Signing"),
KeyPurpose::Authentication => write!(f, "Authentication"),
KeyPurpose::Encryption => write!(f, "Encryption"),
KeyPurpose::Unknown => write!(f, "Unknown"),
}
}
}
#[cfg(feature = "openpgp-card")]
impl From<KeyType> for KeyPurpose {
fn from(kt: KeyType) -> Self {
match kt {
KeyType::Signing => KeyPurpose::Signing,
KeyType::Authentication => KeyPurpose::Authentication,
KeyType::Decryption => KeyPurpose::Encryption,
_ => KeyPurpose::Unknown,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn all_message_types() -> [MessageType; 9] {
[
MessageType::RelationshipRequest,
MessageType::RelationshipRequestRejected,
MessageType::RelationshipRequestAccepted,
MessageType::RelationshipRequestFinalize,
MessageType::TrustPing,
MessageType::TrustPong,
MessageType::VRCRequest,
MessageType::VRCRequestRejected,
MessageType::VRCIssued,
]
}
#[test]
fn test_message_type_try_from_valid() {
use protocol_urls::*;
let cases = vec![
(RELATIONSHIP_REQUEST, "RelationshipRequest"),
(RELATIONSHIP_REQUEST_REJECT, "RelationshipRequestRejected"),
(RELATIONSHIP_REQUEST_ACCEPT, "RelationshipRequestAccepted"),
(RELATIONSHIP_REQUEST_FINALIZE, "RelationshipRequestFinalize"),
(TRUST_PING, "TrustPing"),
(TRUST_PONG, "TrustPong"),
(VRC_REQUEST, "VRCRequest"),
(VRC_REJECTED, "VRCRequestRejected"),
(VRC_ISSUED, "VRCIssued"),
];
for (url, expected_debug_contains) in cases {
let mt = MessageType::try_from(url);
assert!(mt.is_ok(), "Should parse URL '{}' into a MessageType", url);
let debug_str = format!("{:?}", mt.unwrap());
assert_eq!(debug_str, expected_debug_contains);
}
}
#[test]
fn credential_kind_registry_is_self_consistent() {
for kind in CredentialKind::ALL {
let cred = match kind {
CredentialKind::Membership => crate::dtg::fixtures::grant("did:ex:c", "did:ex:m"),
CredentialKind::Role => {
crate::dtg::fixtures::role_vac("did:ex:c", "did:ex:m", "member")
}
CredentialKind::CommunityVetting => {
crate::dtg::fixtures::community_vetting("did:ex:c", "did:ex:m")
}
};
assert_eq!(cred["type"][2], kind.vc_type());
assert_eq!(
CredentialKind::from_credential(&cred),
Some(*kind),
"{kind:?} must be classified from its vc_type",
);
assert_eq!(
CredentialKind::from_config_key(kind.config_key()),
Some(*kind),
"{kind:?} config_key must round-trip",
);
}
assert_eq!(
CredentialKind::from_credential(&serde_json::json!({ "type": ["Other"] })),
None,
);
assert_eq!(
CredentialKind::from_credential(
&serde_json::json!({ "type": ["VerifiableCredential", "MembershipCredential"] })
),
None,
);
assert_eq!(
CredentialKind::from_credential(&crate::dtg::fixtures::vetted_statement(
"did:ex:v",
dtg_credentials::IssuerScope::Directed,
"did:ex:m",
"did:ex:c",
true,
)),
None,
);
assert_eq!(
CredentialKind::from_credential(&crate::dtg::fixtures::retired_role_endorsement(
"did:ex:c", "did:ex:m"
)),
None,
);
assert_eq!(CredentialKind::from_config_key("Nope"), None);
}
#[test]
fn test_message_type_try_from_unknown_yields_invalid_message() {
let unknown = "https://example.com/not-a-real-openvtc-type";
let err = MessageType::try_from(unknown).unwrap_err();
match err {
errors::OpenVTCError::InvalidMessage(s) => assert_eq!(s, unknown),
other => panic!("expected InvalidMessage, got {other:?}"),
}
}
#[test]
fn test_message_type_string_roundtrip_all_variants() {
for ty in all_message_types() {
let url: String = ty.clone().into();
let parsed = MessageType::try_from(url.as_str()).unwrap_or_else(|e| {
panic!("try_from failed for variant url {url:?}: {e:?}");
});
let again: String = parsed.into();
assert_eq!(url, again, "From<MessageType> and TryFrom drift");
}
}
#[test]
fn test_message_type_try_from_message() {
let msg = Message::build(
"test-id".to_string(),
String::from(MessageType::TrustPing),
serde_json::json!({}),
)
.finalize();
let parsed = MessageType::try_from(&msg).expect("valid message type");
assert_eq!(String::from(parsed), String::from(MessageType::TrustPing));
}
#[test]
fn test_message_type_friendly_names() {
let cases = [
(MessageType::RelationshipRequest, "Relationship Request"),
(
MessageType::RelationshipRequestRejected,
"Relationship Request Rejected",
),
(
MessageType::RelationshipRequestAccepted,
"Relationship Request Accepted",
),
(
MessageType::RelationshipRequestFinalize,
"Relationship Request Finalize",
),
(MessageType::TrustPing, "Trust Ping (Send)"),
(MessageType::TrustPong, "Trust Pong (Receive)"),
(MessageType::VRCRequest, "VRC Request"),
(MessageType::VRCRequestRejected, "VRC Request Rejected"),
(MessageType::VRCIssued, "VRC Issued"),
];
for (ty, want) in cases {
assert_eq!(ty.friendly_name(), want);
}
}
#[test]
fn test_mediator_did_default() {
let did = mediator_did(None);
assert_eq!(did, LF_PUBLIC_MEDIATOR_DID);
assert!(
did.starts_with("did:webvh:"),
"Mediator DID should start with did:webvh:"
);
}
#[test]
fn test_org_did_default() {
let did = org_did(None);
assert_eq!(did, LF_ORG_DID);
assert!(
did.starts_with("did:webvh:"),
"Org DID should start with did:webvh:"
);
}
#[test]
fn test_mediator_did_valid_override() {
let custom = "did:web:example.com:mediator";
let did = mediator_did(Some(custom));
assert_eq!(did, custom);
}
#[test]
fn test_mediator_did_invalid_override_falls_back() {
let did = mediator_did(Some("not-a-did"));
assert_eq!(
did, LF_PUBLIC_MEDIATOR_DID,
"Invalid override value should fall back to default"
);
}
#[test]
fn test_org_did_valid_override() {
let custom = "did:web:example.com:org";
let did = org_did(Some(custom));
assert_eq!(did, custom);
}
#[test]
fn test_org_did_invalid_override_falls_back() {
let did = org_did(Some("bogus-value"));
assert_eq!(
did, LF_ORG_DID,
"Invalid override value should fall back to default"
);
}
#[test]
fn test_key_purpose_display() {
assert_eq!(format!("{}", KeyPurpose::Signing), "Signing");
assert_eq!(format!("{}", KeyPurpose::Authentication), "Authentication");
assert_eq!(format!("{}", KeyPurpose::Encryption), "Encryption");
assert_eq!(format!("{}", KeyPurpose::Unknown), "Unknown");
}
#[test]
fn test_key_purpose_default() {
let kp = KeyPurpose::default();
assert_eq!(kp, KeyPurpose::Unknown);
}
}