Skip to main content

uselesskey_core_id/
lib.rs

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