Skip to main content

auths_sdk/
platform.rs

1use base64::Engine;
2use base64::engine::general_purpose::URL_SAFE_NO_PAD;
3use serde::{Deserialize, Serialize};
4
5use auths_core::ports::clock::ClockProvider;
6use auths_core::signing::{PassphraseProvider, SecureSigner};
7use auths_core::storage::keychain::KeyAlias;
8
9use crate::error::SetupError;
10
11/// A signed platform claim linking a DID to a platform username.
12///
13/// Usage:
14/// ```ignore
15/// let claim: PlatformClaim = serde_json::from_str(&claim_json)?;
16/// assert_eq!(claim.platform, "github");
17/// ```
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct PlatformClaim {
20    /// The claim type identifier (always `"platform_claim"`).
21    #[serde(rename = "type")]
22    pub claim_type: String,
23    /// The platform name (e.g. `"github"`, `"gitlab"`).
24    pub platform: String,
25    /// The username on the platform.
26    pub namespace: String,
27    /// The controller DID (e.g. `"did:keri:E..."`).
28    pub did: String,
29    /// ISO-8601 timestamp of when the claim was created.
30    pub timestamp: String,
31    /// Base64url-encoded Ed25519 signature over the canonicalized claim.
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub signature: Option<String>,
34}
35
36/// Creates a signed platform claim linking a DID to a platform username.
37///
38/// The claim is JSON-canonicalized (RFC 8785) before signing, ensuring
39/// deterministic verification without the original OAuth token.
40///
41/// Args:
42/// * `platform`: Platform name (e.g., "github").
43/// * `namespace`: Username on the platform.
44/// * `did`: The controller DID (e.g., "did:keri:E...").
45/// * `key_alias`: Keychain alias for the signing key.
46/// * `signer`: Secure signer for creating the claim signature.
47/// * `passphrase_provider`: Provider for key decryption passphrase.
48///
49/// Usage:
50/// ```ignore
51/// let claim_json = create_platform_claim("github", "octocat", "did:keri:E...", "main", &signer, &provider)?;
52/// ```
53pub fn create_platform_claim(
54    platform: &str,
55    namespace: &str,
56    did: &str,
57    key_alias: &KeyAlias,
58    signer: &dyn SecureSigner,
59    passphrase_provider: &dyn PassphraseProvider,
60    clock: &dyn ClockProvider,
61) -> Result<String, SetupError> {
62    let mut claim = PlatformClaim {
63        claim_type: "platform_claim".to_string(),
64        platform: platform.to_string(),
65        namespace: namespace.to_string(),
66        did: did.to_string(),
67        timestamp: clock.now().to_rfc3339(),
68        signature: None,
69    };
70
71    let unsigned_json = serde_json::to_value(&claim)
72        .map_err(|e| SetupError::PlatformVerificationFailed(format!("serialize claim: {e}")))?;
73    let canonical = json_canon::to_string(&unsigned_json)
74        .map_err(|e| SetupError::PlatformVerificationFailed(format!("canonicalize claim: {e}")))?;
75
76    let signature_bytes = signer
77        .sign_with_alias(key_alias, passphrase_provider, canonical.as_bytes())
78        .map_err(|e| SetupError::PlatformVerificationFailed(format!("sign claim: {e}")))?;
79
80    claim.signature = Some(URL_SAFE_NO_PAD.encode(&signature_bytes));
81
82    serde_json::to_string_pretty(&claim)
83        .map_err(|e| SetupError::PlatformVerificationFailed(format!("serialize signed claim: {e}")))
84}