1use ed25519_dalek::{Signature, VerifyingKey};
4use prost::Message;
5use sha2::{Digest, Sha256};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
8pub enum Reject {
9 #[error("unsupported format")]
10 Version,
11 #[error("noncanonical or missing field")]
12 Canonical,
13 #[error("size or count bound exceeded")]
14 Bounds,
15 #[error("invalid signature")]
16 Signature,
17 #[error("independent authority/root mismatch")]
18 Root,
19 #[error("semantically invalid record")]
20 Semantic,
21 #[error("generation rollback or equivocation")]
22 HighWater,
23 #[error("illegal signer lifecycle transition")]
24 Transition,
25 #[error("job key cannot be a witness")]
26 JobAsWitness,
27 #[error("key roles overlap")]
28 KeyRole,
29 #[error("authority is not valid at verification time")]
30 Expired,
31 #[error("delegation scope violation")]
32 Scope,
33 #[error("prepared frozen fields changed")]
34 PreparedFields,
35 #[error("preparation refused: {0:?}")]
36 PreparationRefused(crate::heddle::api::v1alpha2::ImportPreparationRefusalReason),
37 #[error("observe mode lacks its signed disclosure")]
38 RefDisclosure,
39 #[error("known commit must be pinned exactly")]
40 RefPinning,
41 #[error("missing or mismatched provider source selection")]
42 SourceSelection,
43 #[error("ImportSource requires CommitImportJob")]
44 ImportSourceRequiresCommit,
45 #[error("client operation ID was reused with changed inputs")]
46 OperationIdReused,
47 #[error("Commit must return its created pending operation")]
48 PendingOperation,
49 #[error("delegation window exceeds prepared host bounds")]
50 ValidityBounds,
51 #[error("genesis binding does not match prepared branch")]
52 GenesisBinding,
53 #[error("missing typed owner import permission")]
54 ImportPermission,
55 #[error("renewal forks the logical job or widens remaining scope")]
56 RenewalFork,
57 #[error("committed manifest changed before renewal")]
58 StaleManifest,
59 #[error("replacement includes an already committed slot")]
60 CommittedSlot,
61 #[error("context is stale at mutation boundary")]
62 StaleContext,
63 #[error("missing or invalid exact retirement inclusion proof")]
64 Proof,
65 #[error("witness is revoked")]
66 Revoked,
67 #[error("committed slot conflict")]
68 SlotConflict,
69 #[error("missing or mismatched exact boundary acceptance binding")]
70 BoundaryAcceptance,
71 #[error("incompatible peer")]
72 Protocol,
73}
74
75pub trait Canonical {
76 fn write(&self, out: &mut Vec<u8>) -> Result<(), Reject>;
77}
78pub fn canonical<T: Canonical>(value: &T) -> Result<Vec<u8>, Reject> {
79 let mut out = Vec::new();
80 value.write(&mut out)?;
81 if out.len() > 1024 * 1024 {
82 return Err(Reject::Bounds);
83 }
84 Ok(out)
85}
86pub fn counted(out: &mut Vec<u8>, value: &[u8]) -> Result<(), Reject> {
87 if value.len() > 1024 * 1024
88 || out.len().saturating_add(value.len()).saturating_add(4) > 1024 * 1024
89 {
90 return Err(Reject::Bounds);
91 }
92 out.extend_from_slice(&(value.len() as u32).to_be_bytes());
93 out.extend_from_slice(value);
94 Ok(())
95}
96pub fn hash(parts: &[&[u8]]) -> Vec<u8> {
97 let mut hash = Sha256::new();
98 for part in parts {
99 hash.update(part);
100 }
101 hash.finalize().to_vec()
102}
103pub fn signing_digest<T: Canonical>(domain: &str, value: &T) -> Result<Vec<u8>, Reject> {
104 Ok(hash(&[domain.as_bytes(), &canonical(value)?]))
105}
106pub fn key_id(public_key: &[u8]) -> Vec<u8> {
107 hash(&[b"heddle-key-v1", &1_u32.to_be_bytes(), public_key])
108}
109pub fn width(value: &[u8], expected: usize) -> Result<(), Reject> {
110 if value.len() != expected {
111 return Err(Reject::Canonical);
112 }
113 Ok(())
114}
115pub fn verify(public_key: &[u8], input: &[u8], signature: &[u8]) -> Result<(), Reject> {
116 let key: &[u8; 32] = public_key.try_into().map_err(|_| Reject::Canonical)?;
117 let signature: &[u8; 64] = signature.try_into().map_err(|_| Reject::Canonical)?;
118 VerifyingKey::from_bytes(key)
119 .map_err(|_| Reject::Signature)?
120 .verify_strict(input, &Signature::from_bytes(signature))
121 .map_err(|_| Reject::Signature)
122}
123pub fn strict_decode<T: Message + Default>(bytes: &[u8], max: usize) -> Result<T, Reject> {
127 if bytes.is_empty() || bytes.len() > max {
128 return Err(Reject::Bounds);
129 }
130 let value = T::decode(bytes).map_err(|_| Reject::Canonical)?;
131 if value.encode_to_vec() != bytes {
132 return Err(Reject::Canonical);
133 }
134 Ok(value)
135}
136
137macro_rules! field {
138 ($out:ident, $v:expr, b) => {
139 $crate::hybrid_codec::counted($out, &$v)?
140 };
141 ($out:ident, $v:expr, s) => {
142 $crate::hybrid_codec::counted($out, $v.as_bytes())?
143 };
144 ($out:ident, $v:expr, u) => {
145 $out.extend_from_slice(&$v.to_be_bytes())
146 };
147 ($out:ident, $v:expr, e) => {
148 $out.extend_from_slice(
149 &u32::try_from($v)
150 .map_err(|_| $crate::hybrid_codec::Reject::Canonical)?
151 .to_be_bytes(),
152 )
153 };
154 ($out:ident, $v:expr, m) => {
155 $crate::hybrid_codec::Canonical::write(
156 $v.as_ref().ok_or($crate::hybrid_codec::Reject::Canonical)?,
157 $out,
158 )?
159 };
160 ($out:ident, $v:expr, o) => {
161 $out.extend_from_slice(&u32::from($v.is_some()).to_be_bytes());
162 if let Some(value) = &$v {
163 $crate::hybrid_codec::Canonical::write(value, $out)?;
164 }
165 };
166 ($out:ident, $v:expr, l) => {
167 if $v.len() > 4096 {
168 return Err($crate::hybrid_codec::Reject::Bounds);
169 }
170 $out.extend_from_slice(&($v.len() as u32).to_be_bytes());
171 for value in &$v {
172 $crate::hybrid_codec::Canonical::write(value, $out)?;
173 }
174 };
175 ($out:ident, $v:expr, p) => {
176 if $v.len() > 4 {
177 return Err($crate::hybrid_codec::Reject::Bounds);
178 }
179 $out.extend_from_slice(&($v.len() as u32).to_be_bytes());
180 for value in &$v {
181 $out.extend_from_slice(
182 &u32::try_from(*value)
183 .map_err(|_| $crate::hybrid_codec::Reject::Canonical)?
184 .to_be_bytes(),
185 );
186 }
187 };
188 ($out:ident, $v:expr, h) => {
189 if $v.len() > 128 {
190 return Err($crate::hybrid_codec::Reject::Bounds);
191 }
192 $out.extend_from_slice(&($v.len() as u32).to_be_bytes());
193 for value in &$v {
194 $crate::hybrid_codec::counted($out, value)?;
195 }
196 };
197 ($out:ident, $v:expr, q) => {
198 if $v.len() > 64 {
199 return Err($crate::hybrid_codec::Reject::Bounds);
200 }
201 $out.extend_from_slice(&($v.len() as u32).to_be_bytes());
202 for value in &$v {
203 $crate::hybrid_codec::counted($out, value)?;
204 }
205 };
206}
207macro_rules! record {
208 ($ty:ty, $($field:ident : $kind:ident),+ $(,)?) => {
209 impl $crate::hybrid_codec::Canonical for $ty {
210 fn write(&self, out: &mut Vec<u8>) -> Result<(), $crate::hybrid_codec::Reject> {
211 $(field!(out, self.$field, $kind);
212 if out.len() > 1024 * 1024 { return Err($crate::hybrid_codec::Reject::Bounds); })+
213 Ok(())
214 }
215 }
216 };
217}
218pub(crate) use {field, record};