chio-core 0.1.2

Core types for the Chio protocol
Documentation
use std::collections::HashSet;

use super::error::IdentityNetworkContractError;
use super::types::{
    IdentityArtifactKind, IdentityCredentialFamily, IdentityDidMethod,
    IdentityInteropQualificationMatrix, PublicIdentityProfileArtifact,
    PublicWalletDirectoryEntryArtifact, PublicWalletRoutingManifestArtifact,
    CHIO_IDENTITY_INTEROP_QUALIFICATION_MATRIX_SCHEMA, CHIO_PUBLIC_IDENTITY_PROFILE_SCHEMA,
    CHIO_PUBLIC_WALLET_DIRECTORY_ENTRY_SCHEMA, CHIO_PUBLIC_WALLET_ROUTING_MANIFEST_SCHEMA,
};
use super::validators::{
    contains_non_chio_method, ensure_non_empty, ensure_refs_present, ensure_required_transports,
    ensure_unique_copy_values, ensure_unique_strings, validate_https_url,
    validate_identity_artifact_reference, validate_identity_binding_policy,
    validate_wallet_directory_lookup_guardrails, validate_wallet_routing_guardrails,
};

const IDENTITY_NETWORK_REQUIRED_REQUIREMENTS: [&str; 5] =
    ["IDMAX-01", "IDMAX-02", "IDMAX-03", "IDMAX-04", "IDMAX-05"];

pub fn validate_public_identity_profile(
    profile: &PublicIdentityProfileArtifact,
) -> Result<(), IdentityNetworkContractError> {
    if profile.schema != CHIO_PUBLIC_IDENTITY_PROFILE_SCHEMA {
        return Err(IdentityNetworkContractError::UnsupportedSchema(
            profile.schema.clone(),
        ));
    }
    ensure_non_empty(&profile.profile_id, "profile_id")?;
    ensure_unique_copy_values(
        &profile.supported_subject_methods,
        "supported_subject_methods",
    )?;
    ensure_unique_copy_values(
        &profile.supported_issuer_methods,
        "supported_issuer_methods",
    )?;
    ensure_unique_copy_values(
        &profile.supported_credential_families,
        "supported_credential_families",
    )?;
    ensure_unique_copy_values(
        &profile.supported_proof_families,
        "supported_proof_families",
    )?;
    ensure_unique_copy_values(&profile.supported_transports, "supported_transports")?;
    ensure_refs_present(&profile.basis_refs, "basis_refs")?;
    validate_identity_binding_policy(&profile.binding_policy)?;

    if !profile
        .supported_subject_methods
        .contains(&IdentityDidMethod::DidChio)
        || !profile
            .supported_issuer_methods
            .contains(&IdentityDidMethod::DidChio)
    {
        return Err(IdentityNetworkContractError::InvalidProfile(
            "public identity profiles must retain did:chio provenance in both subject and issuer support".to_string(),
        ));
    }
    if !contains_non_chio_method(&profile.supported_subject_methods)
        && !contains_non_chio_method(&profile.supported_issuer_methods)
    {
        return Err(IdentityNetworkContractError::InvalidProfile(
            "public identity profiles must support at least one non-did:chio method".to_string(),
        ));
    }
    if !profile
        .supported_credential_families
        .contains(&IdentityCredentialFamily::ChioAgentPassportJson)
    {
        return Err(IdentityNetworkContractError::InvalidProfile(
            "public identity profiles must retain chio-agent-passport+json compatibility"
                .to_string(),
        ));
    }
    if !profile.supported_credential_families.iter().any(|family| {
        matches!(
            family,
            IdentityCredentialFamily::DcSdJwt | IdentityCredentialFamily::JwtVcJson
        )
    }) {
        return Err(IdentityNetworkContractError::InvalidProfile(
            "public identity profiles must advertise at least one portable VC family".to_string(),
        ));
    }
    ensure_required_transports(&profile.supported_transports, "supported_transports")?;

    let mut required_kinds = HashSet::from([
        IdentityArtifactKind::PortableTrustProfile,
        IdentityArtifactKind::Oid4vciIssuerMetadata,
        IdentityArtifactKind::Oid4vpVerifierMetadata,
    ]);
    for reference in &profile.basis_refs {
        validate_identity_artifact_reference(reference)?;
        required_kinds.remove(&reference.kind);
    }
    if !required_kinds.is_empty() {
        return Err(IdentityNetworkContractError::InvalidProfile(
            "public identity profiles must reference portable trust, OID4VCI, and OID4VP basis artifacts".to_string(),
        ));
    }

    Ok(())
}

pub fn validate_public_wallet_directory_entry(
    entry: &PublicWalletDirectoryEntryArtifact,
) -> Result<(), IdentityNetworkContractError> {
    if entry.schema != CHIO_PUBLIC_WALLET_DIRECTORY_ENTRY_SCHEMA {
        return Err(IdentityNetworkContractError::UnsupportedSchema(
            entry.schema.clone(),
        ));
    }
    ensure_non_empty(&entry.entry_id, "entry_id")?;
    ensure_non_empty(&entry.directory_operator_id, "directory_operator_id")?;
    ensure_non_empty(&entry.wallet_id, "wallet_id")?;
    ensure_unique_copy_values(
        &entry.supported_subject_methods,
        "supported_subject_methods",
    )?;
    ensure_unique_copy_values(&entry.supported_issuer_methods, "supported_issuer_methods")?;
    ensure_unique_copy_values(
        &entry.supported_credential_families,
        "supported_credential_families",
    )?;
    ensure_unique_copy_values(&entry.supported_proof_families, "supported_proof_families")?;
    validate_identity_artifact_reference(&entry.discovery_ref)?;
    validate_identity_artifact_reference(&entry.profile_ref)?;
    validate_wallet_directory_lookup_guardrails(&entry.lookup_guardrails)?;
    validate_https_url(&entry.metadata_url, "metadata_url")?;
    validate_https_url(&entry.request_uri_prefix, "request_uri_prefix")?;

    if entry.discovery_ref.kind != IdentityArtifactKind::PublicVerifierDiscovery {
        return Err(IdentityNetworkContractError::InvalidDirectoryEntry(
            "wallet directory discovery_ref must point to public verifier discovery".to_string(),
        ));
    }
    if entry.profile_ref.kind != IdentityArtifactKind::PublicIdentityProfile {
        return Err(IdentityNetworkContractError::InvalidDirectoryEntry(
            "wallet directory profile_ref must point to a public identity profile".to_string(),
        ));
    }
    if !contains_non_chio_method(&entry.supported_subject_methods)
        || !contains_non_chio_method(&entry.supported_issuer_methods)
    {
        return Err(IdentityNetworkContractError::InvalidDirectoryEntry(
            "wallet directory entries must advertise at least one broader subject and issuer method".to_string(),
        ));
    }
    if !entry.supported_credential_families.iter().any(|family| {
        matches!(
            family,
            IdentityCredentialFamily::DcSdJwt | IdentityCredentialFamily::JwtVcJson
        )
    }) {
        return Err(IdentityNetworkContractError::InvalidDirectoryEntry(
            "wallet directory entries must advertise at least one portable credential family"
                .to_string(),
        ));
    }

    Ok(())
}

pub fn validate_public_wallet_routing_manifest(
    manifest: &PublicWalletRoutingManifestArtifact,
) -> Result<(), IdentityNetworkContractError> {
    if manifest.schema != CHIO_PUBLIC_WALLET_ROUTING_MANIFEST_SCHEMA {
        return Err(IdentityNetworkContractError::UnsupportedSchema(
            manifest.schema.clone(),
        ));
    }
    ensure_non_empty(&manifest.route_id, "route_id")?;
    ensure_non_empty(&manifest.verifier_id, "verifier_id")?;
    validate_identity_artifact_reference(&manifest.directory_entry_ref)?;
    validate_https_url(&manifest.verifier_id, "verifier_id")?;
    validate_https_url(&manifest.response_uri_prefix, "response_uri_prefix")?;
    validate_https_url(&manifest.relay_url, "relay_url")?;
    ensure_required_transports(&manifest.transport_modes, "transport_modes")?;
    validate_wallet_routing_guardrails(&manifest.routing_guardrails)?;

    if manifest.directory_entry_ref.kind != IdentityArtifactKind::PublicWalletDirectoryEntry {
        return Err(IdentityNetworkContractError::InvalidRouting(
            "wallet routing manifest directory_entry_ref must point to a wallet directory entry"
                .to_string(),
        ));
    }
    if !manifest.requires_signed_request_object {
        return Err(IdentityNetworkContractError::InvalidRouting(
            "wallet routing manifests must require signed request objects".to_string(),
        ));
    }
    if !manifest.requires_replay_anchors {
        return Err(IdentityNetworkContractError::InvalidRouting(
            "wallet routing manifests must require replay anchors".to_string(),
        ));
    }

    Ok(())
}

pub fn validate_identity_interop_qualification_matrix(
    matrix: &IdentityInteropQualificationMatrix,
) -> Result<(), IdentityNetworkContractError> {
    if matrix.schema != CHIO_IDENTITY_INTEROP_QUALIFICATION_MATRIX_SCHEMA {
        return Err(IdentityNetworkContractError::UnsupportedSchema(
            matrix.schema.clone(),
        ));
    }
    validate_identity_artifact_reference(&matrix.profile_ref)?;
    validate_identity_artifact_reference(&matrix.directory_entry_ref)?;
    validate_identity_artifact_reference(&matrix.routing_manifest_ref)?;
    if matrix.profile_ref.kind != IdentityArtifactKind::PublicIdentityProfile {
        return Err(IdentityNetworkContractError::InvalidQualificationCase(
            "qualification matrix profile_ref must point to a public identity profile".to_string(),
        ));
    }
    if matrix.directory_entry_ref.kind != IdentityArtifactKind::PublicWalletDirectoryEntry {
        return Err(IdentityNetworkContractError::InvalidQualificationCase(
            "qualification matrix directory_entry_ref must point to a wallet directory entry"
                .to_string(),
        ));
    }
    if matrix.routing_manifest_ref.kind != IdentityArtifactKind::PublicWalletRoutingManifest {
        return Err(IdentityNetworkContractError::InvalidQualificationCase(
            "qualification matrix routing_manifest_ref must point to a wallet routing manifest"
                .to_string(),
        ));
    }
    if matrix.cases.is_empty() {
        return Err(IdentityNetworkContractError::MissingField("cases"));
    }

    let mut case_ids = HashSet::new();
    let mut covered_requirements = HashSet::new();
    for case in &matrix.cases {
        ensure_non_empty(&case.id, "case.id")?;
        ensure_non_empty(&case.name, "case.name")?;
        if !case_ids.insert(case.id.as_str()) {
            return Err(IdentityNetworkContractError::DuplicateValue(format!(
                "case.id:{}",
                case.id
            )));
        }
        if case.expected_outcome != case.observed_outcome {
            return Err(IdentityNetworkContractError::InvalidQualificationCase(
                format!(
                    "case `{}` expected and observed outcomes must match",
                    case.id
                ),
            ));
        }
        ensure_unique_strings(&case.requirement_ids, "case.requirement_ids")?;
        for requirement_id in &case.requirement_ids {
            covered_requirements.insert(requirement_id.as_str());
        }
        for note in &case.notes {
            ensure_non_empty(note, "case.notes")?;
        }
    }

    for requirement_id in IDENTITY_NETWORK_REQUIRED_REQUIREMENTS {
        if !covered_requirements.contains(requirement_id) {
            return Err(IdentityNetworkContractError::InvalidQualificationCase(
                format!("qualification matrix must cover `{requirement_id}`"),
            ));
        }
    }

    Ok(())
}