pic_continuity/artifacts/
transition.rs1use super::check_profile;
21use crate::authority::indexed::KvTuple;
22use crate::cose::CoseSigned;
23use crate::error::RejectReason;
24use serde::{Deserialize, Serialize};
25
26mod optional_bytes {
28 use serde::{Deserialize, Deserializer, Serialize, Serializer};
29
30 pub fn serialize<S>(value: &Option<Vec<u8>>, serializer: S) -> Result<S::Ok, S::Error>
31 where
32 S: Serializer,
33 {
34 match value {
35 Some(bytes) => serde_bytes::Bytes::new(bytes).serialize(serializer),
36 None => serializer.serialize_none(),
37 }
38 }
39
40 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error>
41 where
42 D: Deserializer<'de>,
43 {
44 let opt: Option<serde_bytes::ByteBuf> = Option::deserialize(deserializer)?;
45 Ok(opt.map(|b| b.into_vec()))
46 }
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52pub struct Predecessor {
53 #[serde(rename = "type")]
56 pub predecessor_type: String,
57 #[serde(with = "serde_bytes")]
60 pub hash: Vec<u8>,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66pub struct TransitionChallenge {
67 #[serde(with = "serde_bytes")]
69 pub previous_challenge: Vec<u8>,
70 #[serde(with = "serde_bytes")]
73 pub next_challenge: Vec<u8>,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80pub struct ProofOfRelationship {
81 #[serde(rename = "type")]
84 pub por_type: String,
85 #[serde(with = "serde_bytes")]
88 pub evidence: Vec<u8>,
89}
90
91impl ProofOfRelationship {
92 pub fn sd_jwt(presentation: &str) -> Self {
97 Self {
98 por_type: crate::POR_TYPE_SD_JWT.to_string(),
99 evidence: presentation.as_bytes().to_vec(),
100 }
101 }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
106pub struct BitmapAttenuation {
107 #[serde(with = "serde_bytes")]
110 pub remove_bitmap: Vec<u8>,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
116pub struct ContractAdditions {
117 pub additions: Vec<KvTuple>,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
124pub struct AttenuationsWire {
125 #[serde(skip_serializing_if = "Option::is_none")]
127 pub identity_context: Option<BitmapAttenuation>,
128 #[serde(skip_serializing_if = "Option::is_none")]
130 pub invariants: Option<BitmapAttenuation>,
131 #[serde(skip_serializing_if = "Option::is_none")]
133 pub execution_contract: Option<ContractAdditions>,
134}
135
136impl AttenuationsWire {
137 pub fn is_empty(&self) -> bool {
140 self.identity_context.is_none()
141 && self.invariants.is_none()
142 && self.execution_contract.is_none()
143 }
144
145 pub fn parse(&self) -> Result<crate::authority::attenuation::Attenuations, RejectReason> {
149 use crate::authority::attenuation::Attenuations;
150 use crate::authority::bitmap::RemoveBitmap;
151 Ok(Attenuations {
152 identity_context: self
153 .identity_context
154 .as_ref()
155 .map(|b| RemoveBitmap::from_bytes(&b.remove_bitmap))
156 .transpose()?,
157 invariants: self
158 .invariants
159 .as_ref()
160 .map(|b| RemoveBitmap::from_bytes(&b.remove_bitmap))
161 .transpose()?,
162 execution_contract_additions: self
163 .execution_contract
164 .as_ref()
165 .map(|a| a.additions.clone())
166 .unwrap_or_default(),
167 })
168 }
169}
170
171#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
176pub struct PicTransitionPayload {
177 pub profile: String,
179 pub position: u64,
181 pub predecessor: Predecessor,
183 pub challenge: TransitionChallenge,
185 #[serde(skip_serializing_if = "Option::is_none")]
188 pub attenuations: Option<AttenuationsWire>,
189 pub proof_of_relationship: ProofOfRelationship,
191 #[serde(
194 default,
195 skip_serializing_if = "Option::is_none",
196 with = "optional_bytes"
197 )]
198 pub request_digest: Option<Vec<u8>>,
199 #[serde(skip_serializing_if = "Option::is_none")]
202 pub executor_evidence: Option<ciborium::Value>,
203}
204
205impl PicTransitionPayload {
206 pub fn check_profile(&self) -> Result<(), RejectReason> {
208 check_profile("pic-continuity-transition+cose", &self.profile)
209 }
210}
211
212pub type PicTransitionCose = CoseSigned<PicTransitionPayload>;
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[test]
220 fn transition_cbor_roundtrip() {
221 let t = PicTransitionPayload {
222 profile: crate::PROFILE_0_2.into(),
223 position: 1,
224 predecessor: Predecessor {
225 predecessor_type: crate::PREDECESSOR_TYPE_PCA.into(),
226 hash: vec![0xAA; 32],
227 },
228 challenge: TransitionChallenge {
229 previous_challenge: b"c0".to_vec(),
230 next_challenge: b"c1".to_vec(),
231 },
232 attenuations: Some(AttenuationsWire {
233 invariants: Some(BitmapAttenuation {
234 remove_bitmap: vec![0x01],
235 }),
236 ..Default::default()
237 }),
238 proof_of_relationship: ProofOfRelationship {
239 por_type: crate::POR_TYPE_SD_JWT.into(),
240 evidence: b"<sd-jwt presentation bytes>".to_vec(),
241 },
242 request_digest: None,
243 executor_evidence: None,
244 };
245
246 let mut buf = Vec::new();
247 ciborium::into_writer(&t, &mut buf).unwrap();
248 let decoded: PicTransitionPayload = ciborium::from_reader(buf.as_slice()).unwrap();
249 assert_eq!(t, decoded);
250 let parsed = decoded.attenuations.unwrap().parse().unwrap();
251 assert_eq!(parsed.invariants.unwrap().indices(), vec![0]);
252 }
253
254 #[test]
255 fn sd_jwt_constructor_keeps_exact_bytes() {
256 let presentation = "<issuer-signed jwt>~<disclosure>~";
257 let por = ProofOfRelationship::sd_jwt(presentation);
258 assert_eq!(por.por_type, crate::POR_TYPE_SD_JWT);
259 assert_eq!(por.evidence, presentation.as_bytes());
260 }
261}