Skip to main content

ignition_core/deploy/
uuid.rs

1//! Deterministic `attributes.uuid` for resources that render creates
2//! (Phase 21, DRND-06, decision D-03).
3//!
4//! The uuid is RFC 9562 **version 8** (custom): the first 16 bytes of
5//! `SHA-256(UUID_DOMAIN NUL gateway NUL module "/" type "/" name)`, with the
6//! version nibble set to 8 and the variant bits set to `10`. D-03 chose
7//! `sha2` (already a dependency) over a `sha1`/UUIDv5 or `uuid` crate.
8//!
9//! - **CD1 live check.** Decision CD1 in `21-LIVE-CAPTURES.md` records an
10//!   8.3.6 gateway loading a hand-written resource carrying the uuid this
11//!   function derives for `ign-deploy-rig` / `ignition` / `database-translator`
12//!   / `CAP21_V8` (see `name_based_uuid_matches_live_vector`), so a version-8
13//!   uuid is accepted and the derivation is frozen.
14//! - **The environment is not an input.** One resource keeps one identity
15//!   across a gateway's environments.
16//! - **The gateway is an input.** Two gateway trees never share ids.
17//! - **Inputs are exact UTF-8 bytes.** No normalization and no case folding:
18//!   `Pump` and `pump`, or NFC and NFD spellings, are different resources.
19//! - **Existing uuids are never recomputed.** Render only derives a uuid for
20//!   a resource that has none.
21
22use sha2::{Digest, Sha256};
23
24/// Domain separation prefix hashed before the inputs. Changing it changes
25/// every minted uuid, so it carries a version.
26pub const UUID_DOMAIN: &str = "ign-deploy:uuid:v1";
27
28/// The uuid render derives for resource `module/ty/name` of `gateway`, as
29/// lowercase 8-4-4-4-12 hex.
30pub fn name_based_uuid(gateway: &str, module: &str, ty: &str, name: &str) -> String {
31    let mut hasher = Sha256::new();
32    hasher.update(UUID_DOMAIN.as_bytes());
33    hasher.update([0u8]);
34    hasher.update(gateway.as_bytes());
35    hasher.update([0u8]);
36    hasher.update(module.as_bytes());
37    hasher.update(b"/");
38    hasher.update(ty.as_bytes());
39    hasher.update(b"/");
40    hasher.update(name.as_bytes());
41    let digest = hasher.finalize();
42    let mut b = [0u8; 16];
43    b.copy_from_slice(&digest[..16]);
44    b[6] = (b[6] & 0x0f) | 0x80;
45    b[8] = (b[8] & 0x3f) | 0x80;
46    let hex: String = b.iter().map(|x| format!("{x:02x}")).collect();
47    format!(
48        "{}-{}-{}-{}-{}",
49        &hex[0..8],
50        &hex[8..12],
51        &hex[12..16],
52        &hex[16..20],
53        &hex[20..32]
54    )
55}
56
57/// Whether `s` is exactly 36 characters: hex digits (either case) with
58/// hyphens at positions 8, 13, 18 and 23.
59pub fn is_canonical_uuid(s: &str) -> bool {
60    s.len() == 36
61        && s.bytes().enumerate().all(|(i, c)| match i {
62            8 | 13 | 18 | 23 => c == b'-',
63            _ => c.is_ascii_hexdigit(),
64        })
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn name_based_uuid_matches_live_vector() {
73        // CD1 (21-LIVE-CAPTURES.md): this uuid was hand-written into
74        // CAP21_V8/resource.json and the live 8.3.6 gateway loaded it.
75        assert_eq!(
76            name_based_uuid(
77                "ign-deploy-rig",
78                "ignition",
79                "database-translator",
80                "CAP21_V8"
81            ),
82            "4516ad9c-d6b0-8d86-89b9-802c96e33a9e"
83        );
84        // CD1 stale-signature capture used the same derivation.
85        assert_eq!(
86            name_based_uuid(
87                "ign-deploy-rig",
88                "ignition",
89                "database-translator",
90                "CAP21_STALE"
91            ),
92            "1caa175b-6703-89d8-8ead-c9b7c6bdfb96"
93        );
94    }
95
96    #[test]
97    fn uuid_is_version_8_and_rfc_variant() {
98        for name in ["a", "b", "Pump 1", "é", "x/y"] {
99            let u = name_based_uuid("g", "ignition", "schedule", name);
100            assert!(is_canonical_uuid(&u), "{u}");
101            assert_eq!(u, u.to_lowercase());
102            let c: Vec<char> = u.chars().collect();
103            assert_eq!(c[14], '8', "version nibble of {u}");
104            assert!("89ab".contains(c[19]), "variant nibble of {u}");
105        }
106    }
107
108    #[test]
109    fn uuid_is_deterministic_and_input_sensitive() {
110        let base = name_based_uuid("g", "m", "t", "n");
111        assert_eq!(base, name_based_uuid("g", "m", "t", "n"));
112        for other in [
113            name_based_uuid("g2", "m", "t", "n"),
114            name_based_uuid("g", "m2", "t", "n"),
115            name_based_uuid("g", "m", "t2", "n"),
116            name_based_uuid("g", "m", "t", "n2"),
117        ] {
118            assert_ne!(base, other);
119        }
120        assert_ne!(
121            name_based_uuid("g", "m", "t", "Pump"),
122            name_based_uuid("g", "m", "t", "pump")
123        );
124        // NFC (one code point) and NFD (e + combining acute) differ
125        assert_ne!(
126            name_based_uuid("g", "m", "t", "\u{e9}"),
127            name_based_uuid("g", "m", "t", "e\u{301}")
128        );
129    }
130
131    #[test]
132    fn uuid_fields_are_separated() {
133        assert_ne!(
134            name_based_uuid("ab", "c", "t", "n"),
135            name_based_uuid("a", "bc", "t", "n")
136        );
137        assert_ne!(
138            name_based_uuid("g", "m", "ab", "c"),
139            name_based_uuid("g", "m", "a", "bc")
140        );
141    }
142
143    #[test]
144    fn canonical_uuid_shape() {
145        assert!(is_canonical_uuid("4516ad9c-d6b0-8d86-89b9-802c96e33a9e"));
146        assert!(is_canonical_uuid("4516AD9C-D6B0-8D86-89B9-802C96E33A9E"));
147        for bad in [
148            "",
149            "4516ad9cd6b08d8689b9802c96e33a9e",
150            "4516ad9c-d6b0-8d86-89b9-802c96e33a9",
151            "4516ad9c-d6b0-8d86-89b9-802c96e33a9ee",
152            "4516ad9c-d6b0-8d86-89b9-802c96e33a9g",
153            "{4516ad9c-d6b0-8d86-89b9-802c96e33a9e}",
154            "4516ad9c_d6b0-8d86-89b9-802c96e33a9e",
155        ] {
156            assert!(!is_canonical_uuid(bad), "{bad:?}");
157        }
158    }
159}