Skip to main content

uselesskey_core/srp/
identity.rs

1//! Core identity and derivation primitives for uselesskey.
2//!
3//! Defines `ArtifactId` — the `(domain, label, spec_fingerprint, variant,
4//! derivation_version)` tuple that uniquely identifies each generated artifact.
5//! Provides deterministic seed derivation from a master seed and artifact id.
6
7use alloc::string::String;
8
9use crate::srp::hash::Hasher;
10pub use crate::srp::hash::{hash32, write_len_prefixed};
11pub use crate::srp::seed::Seed;
12
13/// Domain strings are used to separate unrelated fixture types.
14pub type ArtifactDomain = &'static str;
15
16/// Version tag for the derivation scheme.
17#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
18pub struct DerivationVersion(pub u16);
19
20impl DerivationVersion {
21    /// The initial (and currently only) derivation scheme version.
22    pub const V1: Self = Self(1);
23}
24
25/// Identifier used for deterministic artifact cache entries.
26///
27/// Each field contributes to the derived seed, so two artifacts with the
28/// same `ArtifactId` are guaranteed to be identical across runs.
29#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
30pub struct ArtifactId {
31    /// Namespace that separates unrelated fixture types (e.g. `"rsa"`, `"ecdsa"`).
32    pub domain: ArtifactDomain,
33    /// User-supplied label for this fixture (e.g. `"issuer"`, `"audience"`).
34    pub label: String,
35    /// BLAKE3 hash of the spec's stable byte representation.
36    pub spec_fingerprint: [u8; 32],
37    /// Variant tag (e.g. `"default"`, `"mismatch"`, `"corrupt:bad-header"`).
38    pub variant: String,
39    /// Which derivation algorithm version to use.
40    pub derivation_version: DerivationVersion,
41}
42
43impl ArtifactId {
44    /// Create a new artifact identifier by hashing `spec_bytes` into a fingerprint.
45    pub fn new(
46        domain: ArtifactDomain,
47        label: impl Into<String>,
48        spec_bytes: &[u8],
49        variant: impl Into<String>,
50        derivation_version: DerivationVersion,
51    ) -> Self {
52        Self {
53            domain,
54            label: label.into(),
55            spec_fingerprint: *hash32(spec_bytes).as_bytes(),
56            variant: variant.into(),
57            derivation_version,
58        }
59    }
60}
61
62/// Derive a per-artifact seed from the master seed and the artifact identifier.
63pub fn derive_seed(master: &Seed, id: &ArtifactId) -> Seed {
64    match id.derivation_version.0 {
65        1 => derive_seed_v1(master, id),
66        other => {
67            #[cfg(feature = "std")]
68            eprintln!("uselesskey-core-id: unknown derivation version {other}, using v1");
69            #[cfg(not(feature = "std"))]
70            let _ = other;
71            derive_seed_v1(master, id)
72        }
73    }
74}
75
76fn derive_seed_v1(master: &Seed, id: &ArtifactId) -> Seed {
77    let mut hasher = Hasher::new_keyed(master.bytes());
78
79    hasher.update(&id.derivation_version.0.to_be_bytes());
80    write_len_prefixed(&mut hasher, id.domain.as_bytes());
81    write_len_prefixed(&mut hasher, id.label.as_bytes());
82    write_len_prefixed(&mut hasher, id.variant.as_bytes());
83    hasher.update(&id.spec_fingerprint);
84
85    let out = hasher.finalize();
86    Seed::new(*out.as_bytes())
87}
88
89#[cfg(all(test, feature = "std"))]
90mod tests {
91    use super::{ArtifactId, DerivationVersion, Seed, derive_seed, hash32};
92    use uselesskey_test_support::{TestResult, require_ok};
93
94    #[test]
95    fn artifact_id_fingerprints_spec_bytes() {
96        let spec = [1u8, 2, 3, 4, 5];
97        let id = ArtifactId::new(
98            "domain:test",
99            "label",
100            &spec,
101            "variant",
102            DerivationVersion::V1,
103        );
104
105        let expected = *hash32(&spec).as_bytes();
106        assert_eq!(id.spec_fingerprint, expected);
107    }
108
109    #[test]
110    fn artifact_id_preserves_fields() {
111        let id = ArtifactId::new(
112            "domain:test",
113            "my-label",
114            b"spec",
115            "my-variant",
116            DerivationVersion::V1,
117        );
118
119        assert_eq!(id.domain, "domain:test");
120        assert_eq!(id.label, "my-label");
121        assert_eq!(id.variant, "my-variant");
122        assert_eq!(id.derivation_version, DerivationVersion::V1);
123    }
124
125    #[test]
126    fn derive_seed_unknown_version_is_deterministic() {
127        let master = Seed::new([9u8; 32]);
128        let id = ArtifactId::new(
129            "domain:test",
130            "label",
131            b"spec",
132            "variant",
133            DerivationVersion(999),
134        );
135
136        let first = derive_seed(&master, &id);
137        let second = derive_seed(&master, &id);
138        assert_eq!(first.bytes(), second.bytes());
139    }
140
141    #[test]
142    fn derive_seed_version_affects_output() {
143        let master = Seed::new([3u8; 32]);
144        let id_v1 = ArtifactId::new(
145            "domain:test",
146            "label",
147            b"spec",
148            "variant",
149            DerivationVersion::V1,
150        );
151        let id_v2 = ArtifactId::new(
152            "domain:test",
153            "label",
154            b"spec",
155            "variant",
156            DerivationVersion(2),
157        );
158
159        let v1 = derive_seed(&master, &id_v1);
160        let v2 = derive_seed(&master, &id_v2);
161        assert_ne!(v1.bytes(), v2.bytes());
162    }
163
164    #[test]
165    fn seed_reexport_matches_core_seed() -> TestResult<()> {
166        let seed = require_ok(
167            Seed::from_env_value("core-id-seed"),
168            "core-id-seed must parse via the re-export",
169        )?;
170        let expected = require_ok(
171            crate::srp::seed::Seed::from_env_value("core-id-seed"),
172            "core-id-seed must parse via the underlying core-seed crate",
173        )?;
174        assert_eq!(seed.bytes(), expected.bytes());
175        Ok(())
176    }
177
178    #[test]
179    fn derive_seed_label_affects_output() {
180        let master = Seed::new([5u8; 32]);
181        let id_a = ArtifactId::new("d", "label-a", b"spec", "v", DerivationVersion::V1);
182        let id_b = ArtifactId::new("d", "label-b", b"spec", "v", DerivationVersion::V1);
183        assert_ne!(
184            derive_seed(&master, &id_a).bytes(),
185            derive_seed(&master, &id_b).bytes()
186        );
187    }
188
189    #[test]
190    fn derive_seed_domain_affects_output() {
191        let master = Seed::new([6u8; 32]);
192        let id_a = ArtifactId::new("domain:a", "lbl", b"spec", "v", DerivationVersion::V1);
193        let id_b = ArtifactId::new("domain:b", "lbl", b"spec", "v", DerivationVersion::V1);
194        assert_ne!(
195            derive_seed(&master, &id_a).bytes(),
196            derive_seed(&master, &id_b).bytes()
197        );
198    }
199
200    #[test]
201    fn derive_seed_variant_affects_output() {
202        let master = Seed::new([7u8; 32]);
203        let id_a = ArtifactId::new("d", "lbl", b"spec", "good", DerivationVersion::V1);
204        let id_b = ArtifactId::new("d", "lbl", b"spec", "bad", DerivationVersion::V1);
205        assert_ne!(
206            derive_seed(&master, &id_a).bytes(),
207            derive_seed(&master, &id_b).bytes()
208        );
209    }
210
211    #[test]
212    fn derive_seed_spec_affects_output() {
213        let master = Seed::new([8u8; 32]);
214        let id_a = ArtifactId::new("d", "lbl", b"RS256", "v", DerivationVersion::V1);
215        let id_b = ArtifactId::new("d", "lbl", b"RS384", "v", DerivationVersion::V1);
216        assert_ne!(
217            derive_seed(&master, &id_a).bytes(),
218            derive_seed(&master, &id_b).bytes()
219        );
220    }
221
222    #[test]
223    fn derive_seed_master_affects_output() {
224        let id = ArtifactId::new("d", "lbl", b"spec", "v", DerivationVersion::V1);
225        let a = derive_seed(&Seed::new([1u8; 32]), &id);
226        let b = derive_seed(&Seed::new([2u8; 32]), &id);
227        assert_ne!(a.bytes(), b.bytes());
228    }
229
230    #[test]
231    fn artifact_id_empty_fields() {
232        let id = ArtifactId::new("d", "", b"", "", DerivationVersion::V1);
233        assert_eq!(id.label, "");
234        assert_eq!(id.variant, "");
235        assert_eq!(id.spec_fingerprint, *hash32(b"").as_bytes());
236    }
237
238    #[test]
239    fn artifact_id_ordering() {
240        let a = ArtifactId::new("a", "lbl", b"spec", "v", DerivationVersion::V1);
241        let b = ArtifactId::new("b", "lbl", b"spec", "v", DerivationVersion::V1);
242        assert!(a < b, "ArtifactId ordering should be by domain first");
243    }
244
245    #[test]
246    fn artifact_id_clone_equals_original() {
247        let id = ArtifactId::new("d", "lbl", b"spec", "v", DerivationVersion::V1);
248        let cloned = id.clone();
249        assert_eq!(id, cloned);
250    }
251
252    #[test]
253    fn derivation_version_copy_and_hash() {
254        use core::hash::{Hash, Hasher};
255        let v = DerivationVersion::V1;
256        let copy = v;
257        assert_eq!(v, copy);
258
259        // Verify Hash is implemented.
260        let mut h = std::collections::hash_map::DefaultHasher::new();
261        v.hash(&mut h);
262        let hash1 = h.finish();
263
264        let mut h2 = std::collections::hash_map::DefaultHasher::new();
265        copy.hash(&mut h2);
266        assert_eq!(hash1, h2.finish());
267    }
268
269    #[test]
270    fn derivation_version_debug() {
271        let dbg = format!("{:?}", DerivationVersion::V1);
272        assert!(dbg.contains("1"), "Debug should contain the version number");
273    }
274}