canic_core/ids/
release_build.rs1use candid::CandidType;
8use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
9use sha2::{Digest, Sha256};
10use std::{fmt, str::FromStr};
11use thiserror::Error as ThisError;
12
13const RELEASE_BUILD_ID_DOMAIN: &[u8] = b"canic:release-build-id\0";
14const RELEASE_BUILD_NONCE_BYTES: u64 = 32;
15
16pub const RELEASE_BUILD_ID_ENV: &str = "CANIC_RELEASE_BUILD_ID";
18
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub struct ReleaseBuildNonce([u8; 32]);
27
28impl ReleaseBuildNonce {
29 #[must_use]
31 pub const fn from_random_bytes(bytes: [u8; 32]) -> Self {
32 Self(bytes)
33 }
34
35 #[must_use]
36 pub const fn as_bytes(&self) -> &[u8; 32] {
37 &self.0
38 }
39}
40
41#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
48pub struct ReleaseBuildId([u8; 32]);
49
50impl ReleaseBuildId {
51 #[must_use]
53 pub fn from_nonce(nonce: ReleaseBuildNonce) -> Self {
54 let mut hasher = Sha256::new();
55 hasher.update(RELEASE_BUILD_ID_DOMAIN);
56 hasher.update(RELEASE_BUILD_NONCE_BYTES.to_be_bytes());
57 hasher.update(nonce.as_bytes());
58 Self(hasher.finalize().into())
59 }
60
61 #[must_use]
62 pub const fn as_bytes(&self) -> &[u8; 32] {
63 &self.0
64 }
65}
66
67impl fmt::Display for ReleaseBuildId {
68 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
69 for byte in self.0 {
70 write!(formatter, "{byte:02x}")?;
71 }
72 Ok(())
73 }
74}
75
76impl CandidType for ReleaseBuildId {
77 fn _ty() -> candid::types::Type {
78 candid::types::TypeInner::Text.into()
79 }
80
81 fn idl_serialize<S>(&self, serializer: S) -> Result<(), S::Error>
82 where
83 S: candid::types::Serializer,
84 {
85 serializer.serialize_text(&self.to_string())
86 }
87}
88
89impl FromStr for ReleaseBuildId {
90 type Err = ReleaseBuildIdParseError;
91
92 fn from_str(value: &str) -> Result<Self, Self::Err> {
93 if value.len() != 64 {
94 return Err(ReleaseBuildIdParseError::Length(value.len()));
95 }
96 if !value
97 .bytes()
98 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
99 {
100 return Err(ReleaseBuildIdParseError::CanonicalHex);
101 }
102
103 let mut bytes = [0; 32];
104 for (index, pair) in value.as_bytes().chunks_exact(2).enumerate() {
105 bytes[index] = (decode_nibble(pair[0]) << 4) | decode_nibble(pair[1]);
106 }
107 Ok(Self(bytes))
108 }
109}
110
111impl Serialize for ReleaseBuildId {
112 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
113 where
114 S: Serializer,
115 {
116 serializer.collect_str(self)
117 }
118}
119
120impl<'de> Deserialize<'de> for ReleaseBuildId {
121 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
122 where
123 D: Deserializer<'de>,
124 {
125 let value = String::deserialize(deserializer)?;
126 value.parse().map_err(de::Error::custom)
127 }
128}
129
130#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
137pub enum ReleaseBuildIdParseError {
138 #[error("release build ID must contain exactly 64 characters, got {0}")]
139 Length(usize),
140
141 #[error("release build ID must contain only lowercase hexadecimal characters")]
142 CanonicalHex,
143}
144
145fn decode_nibble(byte: u8) -> u8 {
146 match byte {
147 b'0'..=b'9' => byte - b'0',
148 b'a'..=b'f' => byte - b'a' + 10,
149 _ => unreachable!("canonical hex was validated before decoding"),
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn release_build_id_uses_the_exact_non_circular_derivation() {
159 let nonce = ReleaseBuildNonce::from_random_bytes([7; 32]);
160 let expected: [u8; 32] = Sha256::digest(
161 [
162 RELEASE_BUILD_ID_DOMAIN,
163 RELEASE_BUILD_NONCE_BYTES.to_be_bytes().as_slice(),
164 nonce.as_bytes(),
165 ]
166 .concat(),
167 )
168 .into();
169
170 assert_eq!(ReleaseBuildId::from_nonce(nonce).as_bytes(), &expected);
171 }
172
173 #[test]
174 fn release_build_id_text_is_exact_and_canonical() {
175 let id = ReleaseBuildId::from_nonce(ReleaseBuildNonce::from_random_bytes([11; 32]));
176 let text = id.to_string();
177
178 assert_eq!(text.len(), 64);
179 assert_eq!(text.parse::<ReleaseBuildId>(), Ok(id));
180 std::assert_matches!(
181 text.to_uppercase().parse::<ReleaseBuildId>(),
182 Err(ReleaseBuildIdParseError::CanonicalHex)
183 );
184 }
185}