Skip to main content

confium_store/
identity.rs

1//! Identity types used to index the public compartment.
2//!
3//! The public compartment is identity-indexed: a `(module_id, app_id)`
4//! pair scopes a namespace, and within it each entry is addressed by an
5//! `Identity`. Identities are deliberately a small sum type so the Store
6//! can validate shape before touching the backend.
7//!
8//! Concretely the variants mirror the README's identity-based signature
9//! scheme: the public key is the user's unique info (e.g. email), and
10//! `put_public` requires the caller to supply a detached signature over
11//! the identity so verifiers can check authenticity before trusting the
12//! key.
13
14use std::fmt;
15
16/// One of the supported identity shapes. Stored as the key of the public
17/// compartment's HashMap; serialised to a canonical string for FFI and
18/// backend persistence.
19#[derive(Debug, Clone, PartialEq, Eq, Hash)]
20pub enum Identity {
21    /// RFC 5321 mailbox, e.g. `alice@example.com`.
22    Email(String),
23    /// Opaque key identifier (hex, base32, fingerprint — the Store does
24    /// not impose a format; it only uses the bytes for lookup).
25    KeyId(String),
26    /// Raw cryptographic hash of an identity document, hex-encoded.
27    Hash(String),
28}
29
30impl Identity {
31    /// Parse a `(kind, value)` pair coming from the FFI into a typed
32    /// identity. `kind` is ASCII, case-sensitive. Unknown kinds return
33    /// the raw string as a [`Identity::Hash`] so legacy callers keep
34    /// working — a future stricter revision can promote this to an error.
35    pub fn from_kind(kind: &str, value: &str) -> Self {
36        match kind {
37            "email" => Identity::Email(value.to_string()),
38            "key-id" => Identity::KeyId(value.to_string()),
39            _ => Identity::Hash(value.to_string()),
40        }
41    }
42
43    /// Canonical string form used as the HashMap key inside backends.
44    /// The scheme prefix keeps Email / KeyId / Hash namespaces disjoint
45    /// even when their textual values collide.
46    pub fn canonical(&self) -> String {
47        match self {
48            Identity::Email(v) => format!("email:{v}"),
49            Identity::KeyId(v) => format!("key-id:{v}"),
50            Identity::Hash(v) => format!("hash:{v}"),
51        }
52    }
53
54    /// The bare identity value without the scheme prefix. Useful for
55    /// logging or when the caller already knows the kind.
56    pub fn value(&self) -> &str {
57        match self {
58            Identity::Email(v) | Identity::KeyId(v) | Identity::Hash(v) => v,
59        }
60    }
61}
62
63impl fmt::Display for Identity {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        f.write_str(&self.canonical())
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn email_canonicalises_with_prefix() {
75        let id = Identity::Email("alice@example.com".to_string());
76        assert_eq!(id.canonical(), "email:alice@example.com");
77        assert_eq!(id.value(), "alice@example.com");
78    }
79
80    #[test]
81    fn from_kind_routes_known_prefixes() {
82        assert_eq!(
83            Identity::from_kind("email", "b@b"),
84            Identity::Email("b@b".to_string())
85        );
86        assert_eq!(
87            Identity::from_kind("key-id", "deadbeef"),
88            Identity::KeyId("deadbeef".to_string())
89        );
90    }
91
92    #[test]
93    fn from_kind_defaults_unknown_to_hash() {
94        assert_eq!(
95            Identity::from_kind("fingerprint", "abcd"),
96            Identity::Hash("abcd".to_string())
97        );
98    }
99
100    #[test]
101    fn distinct_kinds_are_distinct_keys() {
102        // Same textual value, different kinds — must not collide as map
103        // keys. This is the invariant the public compartment relies on.
104        let a = Identity::Email("x".to_string());
105        let b = Identity::KeyId("x".to_string());
106        assert_ne!(a.canonical(), b.canonical());
107    }
108}