Skip to main content

auths_sdk/workflows/
auth.rs

1use auths_core::error::AuthsErrorInfo;
2use thiserror::Error;
3
4/// Result of signing an authentication challenge.
5///
6/// Args:
7/// * `signature_hex`: Hex-encoded Ed25519 signature over the canonical payload.
8/// * `public_key_hex`: Hex-encoded Ed25519 public key.
9/// * `did`: The identity's DID (e.g. `"did:keri:EPREFIX"`).
10///
11/// Usage:
12/// ```ignore
13/// let msg = build_auth_challenge_message("abc123", "auths.dev")?;
14/// let (sig, pubkey, _curve) = sign_with_key(&keychain, &alias, &provider, msg.as_bytes())?;
15/// let result = SignedAuthChallenge {
16///     signature_hex: hex::encode(sig),
17///     public_key_hex: hex::encode(pubkey),
18///     did: "did:keri:E...".to_string(),
19/// };
20/// ```
21#[derive(Debug, Clone)]
22pub struct SignedAuthChallenge {
23    /// Hex-encoded Ed25519 signature over the canonical JSON payload.
24    pub signature_hex: String,
25    /// Hex-encoded Ed25519 public key of the signer.
26    pub public_key_hex: String,
27    /// The signer's identity DID (e.g. `"did:keri:EPREFIX"`).
28    pub did: String,
29}
30
31/// Errors from the auth challenge signing workflow.
32#[derive(Debug, Error)]
33#[non_exhaustive]
34pub enum AuthChallengeError {
35    /// The nonce was empty.
36    #[error("nonce must not be empty")]
37    EmptyNonce,
38
39    /// The domain was empty.
40    #[error("domain must not be empty")]
41    EmptyDomain,
42
43    /// Canonical JSON serialization failed.
44    #[error("canonical JSON serialization failed: {0}")]
45    Canonicalization(String),
46}
47
48impl AuthsErrorInfo for AuthChallengeError {
49    fn error_code(&self) -> &'static str {
50        match self {
51            Self::EmptyNonce => "AUTHS-E6001",
52            Self::EmptyDomain => "AUTHS-E6002",
53            Self::Canonicalization(_) => "AUTHS-E6003",
54        }
55    }
56
57    fn suggestion(&self) -> Option<&'static str> {
58        match self {
59            Self::EmptyNonce => Some("Provide the nonce from the authentication challenge"),
60            Self::EmptyDomain => Some("Provide the domain (e.g. auths.dev)"),
61            Self::Canonicalization(_) => {
62                Some("This is an internal error; please report it as a bug")
63            }
64        }
65    }
66}
67
68/// Builds the canonical JSON message for an auth challenge signature.
69///
70/// The auth-server verifies the signature over exactly these bytes. Callers
71/// that sign through a keychain (SE-safe) should use this helper to get the
72/// message, then feed it into `keychain::sign_with_key`.
73///
74/// Args:
75/// * `nonce`: The challenge nonce from the authentication server.
76/// * `domain`: The domain requesting authentication (e.g. `"auths.dev"`).
77///
78/// Usage:
79/// ```ignore
80/// let msg = build_auth_challenge_message("abc123", "auths.dev")?;
81/// let (sig, pubkey, _curve) = sign_with_key(&keychain, &alias, &provider, msg.as_bytes())?;
82/// ```
83pub fn build_auth_challenge_message(
84    nonce: &str,
85    domain: &str,
86) -> Result<String, AuthChallengeError> {
87    if nonce.is_empty() {
88        return Err(AuthChallengeError::EmptyNonce);
89    }
90    if domain.is_empty() {
91        return Err(AuthChallengeError::EmptyDomain);
92    }
93    let payload = serde_json::json!({
94        "domain": domain,
95        "nonce": nonce,
96    });
97    json_canon::to_string(&payload).map_err(|e| AuthChallengeError::Canonicalization(e.to_string()))
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn canonical_json_sorts_keys_alphabetically() {
106        let payload = serde_json::json!({
107            "nonce": "abc",
108            "domain": "xyz",
109        });
110        let canonical = json_canon::to_string(&payload).expect("canonical");
111        assert_eq!(canonical, r#"{"domain":"xyz","nonce":"abc"}"#);
112    }
113
114    #[test]
115    fn build_auth_challenge_message_produces_canonical_json() {
116        let msg = build_auth_challenge_message("abc123", "auths.dev").unwrap();
117        assert_eq!(msg, r#"{"domain":"auths.dev","nonce":"abc123"}"#);
118    }
119
120    #[test]
121    fn build_auth_challenge_message_rejects_empty_nonce() {
122        let err = build_auth_challenge_message("", "auths.dev").unwrap_err();
123        assert!(matches!(err, AuthChallengeError::EmptyNonce));
124    }
125
126    #[test]
127    fn build_auth_challenge_message_rejects_empty_domain() {
128        let err = build_auth_challenge_message("abc", "").unwrap_err();
129        assert!(matches!(err, AuthChallengeError::EmptyDomain));
130    }
131}