Skip to main content

auths_cli/services/
platform_claim.rs

1use anyhow::{Context, Result};
2use base64::Engine;
3use base64::engine::general_purpose::URL_SAFE_NO_PAD;
4use serde::{Deserialize, Serialize};
5
6use auths_core::signing::{SecureSigner, StorageSigner};
7use auths_core::storage::keychain::{KeyAlias, get_platform_keychain};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct PlatformClaim {
11    #[serde(rename = "type")]
12    pub claim_type: String,
13    pub platform: String,
14    pub namespace: String,
15    pub did: String,
16    pub timestamp: String,
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub signature: Option<String>,
19}
20
21/// Creates a signed platform claim linking a DID to a platform username.
22///
23/// The claim is JSON-canonicalized (RFC 8785) before signing, ensuring
24/// deterministic verification without the original OAuth token.
25///
26/// Args:
27/// * `platform`: Platform name (e.g., "github").
28/// * `namespace`: Username on the platform.
29/// * `did`: The controller DID (e.g., "did:keri:E...").
30/// * `key_alias`: Keychain alias for the signing key.
31/// * `passphrase_provider`: Provider for key decryption passphrase.
32///
33/// Usage:
34/// ```ignore
35/// let claim_json = create_signed_platform_claim("github", "octocat", "did:keri:E...", "main", provider)?;
36/// ```
37pub fn create_signed_platform_claim(
38    platform: &str,
39    namespace: &str,
40    did: &str,
41    key_alias: &str,
42    passphrase_provider: &dyn auths_core::signing::PassphraseProvider,
43) -> Result<String> {
44    let mut claim = PlatformClaim {
45        claim_type: "platform_claim".to_string(),
46        platform: platform.to_string(),
47        namespace: namespace.to_string(),
48        did: did.to_string(),
49        timestamp: chrono::Utc::now().to_rfc3339(),
50        signature: None,
51    };
52
53    // Canonicalize the unsigned claim for signing
54    let unsigned_json = serde_json::to_value(&claim).context("Failed to serialize claim")?;
55    let canonical =
56        json_canon::to_string(&unsigned_json).context("Failed to canonicalize claim")?;
57
58    // Sign with the identity key
59    let signer = StorageSigner::new(get_platform_keychain().map_err(|e| anyhow::anyhow!(e))?);
60    let alias_typed = KeyAlias::new_unchecked(key_alias);
61    let signature_bytes = signer
62        .sign_with_alias(&alias_typed, passphrase_provider, canonical.as_bytes())
63        .map_err(|e| anyhow::anyhow!("Failed to sign platform claim: {e}"))?;
64
65    claim.signature = Some(URL_SAFE_NO_PAD.encode(&signature_bytes));
66
67    serde_json::to_string_pretty(&claim).context("Failed to serialize signed claim")
68}