1use std::collections::BTreeMap;
9
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13use crate::{Sha256Digest, ValidationError, deserialize_schema_version, validate_schema_version};
14
15pub const ENTITLEMENT_SIGNATURE_DOMAIN: &[u8] = b"leanctx/entitlement/v1\0";
16pub const MAX_ENTITLEMENT_BYTES: usize = 32 * 1024;
17pub const MAX_ENTITLEMENT_CAPABILITIES: usize = 256;
18pub const MAX_ENTITLEMENT_CAPABILITY_BYTES: usize = 128;
19pub const MAX_ENTITLEMENT_IDENTIFIER_BYTES: usize = 256;
20pub const MAX_ENTITLEMENT_TIMESTAMP: u64 = 9_007_199_254_740_991;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum EntitlementKindV1 {
25 OnlineSubscription,
26 OfflineEnterprise,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(rename_all = "snake_case")]
32pub enum EntitlementPlanV1 {
33 Community,
34 Pro,
35 Team,
36 Enterprise,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
40#[serde(rename_all = "snake_case")]
41pub enum EntitlementDeploymentV1 {
42 Hosted,
43 SelfHosted,
44 AirGapped,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
49#[serde(deny_unknown_fields)]
50pub struct EntitlementSignerV1 {
51 pub algorithm: String,
52 pub key_id: String,
53 pub public_key_digest: Sha256Digest,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(deny_unknown_fields)]
58pub struct EntitlementEnvelopeV1 {
59 #[serde(deserialize_with = "deserialize_schema_version")]
60 pub schema_version: u32,
61 pub entitlement_id: String,
62 pub kind: EntitlementKindV1,
63 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub account_id: Option<String>,
65 pub plan: EntitlementPlanV1,
66 pub seats: u32,
67 pub capabilities: Vec<String>,
68 pub issued_at: u64,
69 pub not_before: u64,
70 pub expires_at: u64,
71 pub grace_until: u64,
72 pub allowed_deployments: Vec<EntitlementDeploymentV1>,
73 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub deployment_id: Option<String>,
75 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub org_id: Option<String>,
77 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub workspace_id: Option<String>,
79 pub signer: EntitlementSignerV1,
80 pub signature: String,
81}
82
83impl EntitlementEnvelopeV1 {
84 pub const SCHEMA_VERSION: u32 = 1;
85
86 pub fn from_canonical_bytes(bytes: &[u8]) -> Result<Self, ValidationError> {
90 if bytes.len() > MAX_ENTITLEMENT_BYTES {
91 return Err(ValidationError::new("entitlement exceeds 32 KiB"));
92 }
93 let envelope: Self = serde_json::from_slice(bytes)
96 .map_err(|_| ValidationError::new("invalid entitlement JSON or field shape"))?;
97 if envelope.canonical_bytes()? != bytes {
98 return Err(ValidationError::new("entitlement JSON is not canonical"));
99 }
100 Ok(envelope)
101 }
102
103 pub fn from_bytes(bytes: &[u8]) -> Result<Self, ValidationError> {
104 Self::from_canonical_bytes(bytes)
105 }
106
107 pub fn canonical_bytes(&self) -> Result<Vec<u8>, ValidationError> {
109 self.validate()?;
110 canonical_value(self, false)
111 }
112
113 pub fn unsigned_canonical_bytes(&self) -> Result<Vec<u8>, ValidationError> {
115 self.validate_unsigned()?;
116 canonical_value(self, true)
117 }
118
119 pub fn signing_bytes(&self) -> Result<Vec<u8>, ValidationError> {
121 let unsigned = self.unsigned_canonical_bytes()?;
122 let mut bytes = Vec::with_capacity(ENTITLEMENT_SIGNATURE_DOMAIN.len() + unsigned.len());
123 bytes.extend_from_slice(ENTITLEMENT_SIGNATURE_DOMAIN);
124 bytes.extend_from_slice(&unsigned);
125 Ok(bytes)
126 }
127
128 pub fn validate(&self) -> Result<(), ValidationError> {
129 self.validate_unsigned()?;
130 let bytes = self.signature.as_bytes();
131 if bytes.len() != 88
134 || &bytes[86..] != b"=="
135 || !bytes[..86]
136 .iter()
137 .all(|b| b.is_ascii_alphanumeric() || *b == b'+' || *b == b'/')
138 || !matches!(bytes[85], b'A' | b'Q' | b'g' | b'w')
139 {
140 return Err(ValidationError::new(
141 "entitlement signature must be canonical base64 of 64 Ed25519 bytes",
142 ));
143 }
144 Ok(())
145 }
146
147 fn validate_unsigned(&self) -> Result<(), ValidationError> {
148 validate_schema_version(self.schema_version)?;
149 validate_id(
150 &self.entitlement_id,
151 "entitlement_id",
152 MAX_ENTITLEMENT_IDENTIFIER_BYTES,
153 )?;
154 for (field, value) in [
155 ("account_id", &self.account_id),
156 ("deployment_id", &self.deployment_id),
157 ("org_id", &self.org_id),
158 ("workspace_id", &self.workspace_id),
159 ] {
160 if let Some(value) = value {
161 validate_id(value, field, MAX_ENTITLEMENT_IDENTIFIER_BYTES)?;
162 }
163 }
164 if self.seats == 0 {
165 return Err(ValidationError::new("entitlement seats must be positive"));
166 }
167 if self.capabilities.len() > MAX_ENTITLEMENT_CAPABILITIES {
168 return Err(ValidationError::new(
169 "entitlement has more than 256 capabilities",
170 ));
171 }
172 for capability in &self.capabilities {
173 validate_id(capability, "capability", MAX_ENTITLEMENT_CAPABILITY_BYTES)?;
174 }
175 if self.capabilities.windows(2).any(|pair| pair[0] >= pair[1]) {
176 return Err(ValidationError::new(
177 "entitlement capabilities must be sorted and unique",
178 ));
179 }
180 if [
181 self.issued_at,
182 self.not_before,
183 self.expires_at,
184 self.grace_until,
185 ]
186 .into_iter()
187 .any(|time| time > MAX_ENTITLEMENT_TIMESTAMP)
188 {
189 return Err(ValidationError::new(
190 "entitlement timestamp exceeds exact JSON integer range",
191 ));
192 }
193 if !(self.not_before <= self.issued_at
194 && self.issued_at < self.expires_at
195 && self.expires_at <= self.grace_until)
196 {
197 return Err(ValidationError::new("entitlement time window is invalid"));
198 }
199 if self.allowed_deployments.is_empty()
200 || self.allowed_deployments.len() > 3
201 || self
202 .allowed_deployments
203 .iter()
204 .enumerate()
205 .any(|(i, value)| self.allowed_deployments[..i].contains(value))
206 {
207 return Err(ValidationError::new(
208 "allowed deployments must be nonempty and unique",
209 ));
210 }
211 match self.kind {
212 EntitlementKindV1::OnlineSubscription if self.account_id.is_none() => {
213 return Err(ValidationError::new(
214 "online entitlement requires account_id",
215 ));
216 }
217 EntitlementKindV1::OfflineEnterprise
218 if self.plan != EntitlementPlanV1::Enterprise
219 || self.deployment_id.is_none()
220 || self
221 .allowed_deployments
222 .contains(&EntitlementDeploymentV1::Hosted) =>
223 {
224 return Err(ValidationError::new(
225 "offline entitlement requires Enterprise, deployment_id and no hosted deployment",
226 ));
227 }
228 _ => {}
229 }
230 if self.signer.algorithm != "ed25519" {
231 return Err(ValidationError::new(
232 "entitlement signer algorithm must be ed25519",
233 ));
234 }
235 validate_id(
236 &self.signer.key_id,
237 "key_id",
238 MAX_ENTITLEMENT_IDENTIFIER_BYTES,
239 )?;
240 if canonical_value(self, true)?.len() + b",\"signature\":\"\"".len() + 88
243 > MAX_ENTITLEMENT_BYTES
244 {
245 return Err(ValidationError::new("entitlement exceeds 32 KiB"));
246 }
247 Ok(())
248 }
249}
250
251fn validate_id(value: &str, field: &str, max_bytes: usize) -> Result<(), ValidationError> {
252 if value.is_empty() || value.len() > max_bytes || !value.bytes().all(|b| b.is_ascii_graphic()) {
253 return Err(ValidationError::new(format!(
254 "{field} must be 1..={max_bytes} printable ASCII bytes without whitespace"
255 )));
256 }
257 Ok(())
258}
259
260fn canonical_value(
261 envelope: &EntitlementEnvelopeV1,
262 unsigned: bool,
263) -> Result<Vec<u8>, ValidationError> {
264 let mut value = serde_json::to_value(envelope)
265 .map_err(|_| ValidationError::new("cannot serialize entitlement"))?;
266 if unsigned {
267 let Value::Object(object) = &mut value else {
268 return Err(ValidationError::new("entitlement must be an object"));
269 };
270 object.remove("signature");
271 }
272 serde_json::to_vec(&sort_json(value))
273 .map_err(|_| ValidationError::new("cannot canonicalize entitlement"))
274}
275
276pub(crate) fn sort_json(value: Value) -> Value {
279 match value {
280 Value::Object(object) => Value::Object(
281 object
282 .into_iter()
283 .map(|(key, value)| (key, sort_json(value)))
284 .collect::<BTreeMap<_, _>>()
285 .into_iter()
286 .collect(),
287 ),
288 Value::Array(values) => Value::Array(values.into_iter().map(sort_json).collect()),
289 scalar => scalar,
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use super::{
296 ENTITLEMENT_SIGNATURE_DOMAIN, EntitlementDeploymentV1, EntitlementEnvelopeV1,
297 EntitlementKindV1, EntitlementPlanV1, EntitlementSignerV1, MAX_ENTITLEMENT_BYTES,
298 MAX_ENTITLEMENT_TIMESTAMP,
299 };
300 use crate::Sha256Digest;
301
302 fn envelope() -> EntitlementEnvelopeV1 {
303 EntitlementEnvelopeV1 {
304 schema_version: 1,
305 entitlement_id: "ent-1".into(),
306 kind: EntitlementKindV1::OnlineSubscription,
307 account_id: Some("account-1".into()),
308 plan: EntitlementPlanV1::Pro,
309 seats: 1,
310 capabilities: vec!["context.autopilot".into(), "execution.autopilot".into()],
311 issued_at: 100,
312 not_before: 99,
313 expires_at: 200,
314 grace_until: 210,
315 allowed_deployments: vec![EntitlementDeploymentV1::Hosted],
316 deployment_id: None,
317 org_id: None,
318 workspace_id: None,
319 signer: EntitlementSignerV1 {
320 algorithm: "ed25519".into(),
321 key_id: "key-1".into(),
322 public_key_digest: Sha256Digest::new(format!("sha256:{}", "a".repeat(64))).unwrap(),
323 },
324 signature: "A".repeat(86) + "==",
325 }
326 }
327
328 #[test]
329 fn golden_canonical_unsigned_domain_and_round_trip() {
330 let envelope = envelope();
331 let expected = concat!(
332 "{\"account_id\":\"account-1\",\"allowed_deployments\":[\"hosted\"],",
333 "\"capabilities\":[\"context.autopilot\",\"execution.autopilot\"],",
334 "\"entitlement_id\":\"ent-1\",\"expires_at\":200,\"grace_until\":210,",
335 "\"issued_at\":100,\"kind\":\"online_subscription\",\"not_before\":99,",
336 "\"plan\":\"pro\",\"schema_version\":1,\"seats\":1,",
337 "\"signer\":{\"algorithm\":\"ed25519\",\"key_id\":\"key-1\",",
338 "\"public_key_digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}}"
339 );
340 assert_eq!(
341 envelope.unsigned_canonical_bytes().unwrap(),
342 expected.as_bytes()
343 );
344 let mut signing = b"leanctx/entitlement/v1\0".to_vec();
345 signing.extend_from_slice(expected.as_bytes());
346 assert_eq!(envelope.signing_bytes().unwrap(), signing);
347 assert_eq!(ENTITLEMENT_SIGNATURE_DOMAIN.last(), Some(&0));
348 let bytes = envelope.canonical_bytes().unwrap();
349 assert_eq!(EntitlementEnvelopeV1::from_bytes(&bytes).unwrap(), envelope);
350 }
351
352 #[test]
353 fn duplicate_unknown_nested_and_optional_fields_are_rejected() {
354 let bytes = envelope().canonical_bytes().unwrap();
355 let canonical = String::from_utf8(bytes).unwrap();
356 for bad in [
357 canonical.replacen("{", "{\"schema_version\":1,", 1),
358 canonical.replacen("{", "{\"future\":true,", 1),
359 canonical.replace("\"algorithm\":", "\"algorithm\":\"ed25519\",\"algorithm\":"),
360 canonical.replace("\"algorithm\":", "\"unknown\":0,\"algorithm\":"),
361 canonical.replacen("{", "{\"org_id\":null,\"org_id\":null,", 1),
362 canonical.replacen("{", "{\"account_id\":null,", 1),
363 ] {
364 assert!(EntitlementEnvelopeV1::from_bytes(bad.as_bytes()).is_err());
365 assert!(serde_json::from_str::<EntitlementEnvelopeV1>(&bad).is_err());
368 }
369 }
370
371 #[test]
372 fn strict_decode_rejects_noncanonical_utf8_trailing_and_oversize_input() {
373 let canonical = envelope().canonical_bytes().unwrap();
374 let mut spaced = canonical.clone();
375 spaced.push(b'\n');
376 let mut trailing = canonical.clone();
377 trailing.extend_from_slice(b"{}");
378 for invalid in [
379 spaced,
380 trailing,
381 vec![0xff],
382 vec![b' '; MAX_ENTITLEMENT_BYTES + 1],
383 ] {
384 assert!(EntitlementEnvelopeV1::from_bytes(&invalid).is_err());
385 }
386 let text = String::from_utf8(canonical).unwrap();
387 for invalid in [
388 text.replace("account-1", "account\\u002d1"),
389 text.replacen("{", "{\"org_id\":null,", 1),
390 text.replace("\"seats\":1", "\"seats\":1.0"),
391 text.replace("\"issued_at\":100", "\"issued_at\":1e2"),
392 text.replace("\"issued_at\":100", "\"issued_at\":-1"),
393 text.replace("\"schema_version\":1", "\"schema_version\":1.0"),
394 ] {
395 assert!(EntitlementEnvelopeV1::from_bytes(invalid.as_bytes()).is_err());
396 }
397 }
398
399 #[test]
400 fn only_four_canonical_plans_and_two_kinds_are_decoded() {
401 for plan in [
402 EntitlementPlanV1::Community,
403 EntitlementPlanV1::Pro,
404 EntitlementPlanV1::Team,
405 EntitlementPlanV1::Enterprise,
406 ] {
407 let mut value = envelope();
408 value.plan = plan;
409 assert_eq!(
410 EntitlementEnvelopeV1::from_bytes(&value.canonical_bytes().unwrap())
411 .unwrap()
412 .plan,
413 plan
414 );
415 }
416 let bytes = String::from_utf8(envelope().canonical_bytes().unwrap()).unwrap();
417 for plan in ["business", "biz", "supporter", "sponsor", "Pro", "unknown"] {
418 assert!(
419 EntitlementEnvelopeV1::from_bytes(
420 bytes.replace("\"pro\"", &format!("\"{plan}\"")).as_bytes()
421 )
422 .is_err()
423 );
424 }
425 assert!(
426 EntitlementEnvelopeV1::from_bytes(
427 bytes.replace("online_subscription", "trial").as_bytes()
428 )
429 .is_err()
430 );
431 }
432
433 #[test]
434 fn online_and_offline_bindings_are_distinct() {
435 let mut value = envelope();
436 value.account_id = None;
437 assert!(value.validate().is_err());
438 value.kind = EntitlementKindV1::OfflineEnterprise;
439 value.plan = EntitlementPlanV1::Enterprise;
440 value.deployment_id = Some("deployment-1".into());
441 assert!(value.validate().is_err()); value.allowed_deployments = vec![
443 EntitlementDeploymentV1::SelfHosted,
444 EntitlementDeploymentV1::AirGapped,
445 ];
446 value.validate().unwrap();
447 assert_eq!(
448 EntitlementEnvelopeV1::from_bytes(&value.canonical_bytes().unwrap()).unwrap(),
449 value
450 );
451 value.plan = EntitlementPlanV1::Team;
452 assert!(value.validate().is_err());
453 value.plan = EntitlementPlanV1::Enterprise;
454 value.deployment_id = None;
455 assert!(value.validate().is_err());
456 }
457
458 #[test]
459 fn deployment_sets_are_nonempty_unique_and_known() {
460 let mut value = envelope();
461 value.allowed_deployments.clear();
462 assert!(value.validate().is_err());
463 value.allowed_deployments = vec![EntitlementDeploymentV1::Hosted; 2];
464 assert!(value.validate().is_err());
465 let bytes = String::from_utf8(envelope().canonical_bytes().unwrap()).unwrap();
466 assert!(
467 EntitlementEnvelopeV1::from_bytes(
468 bytes.replace("\"hosted\"", "\"anything\"").as_bytes()
469 )
470 .is_err()
471 );
472 }
473
474 #[test]
475 fn identifier_and_capability_ascii_length_order_bounds() {
476 for invalid in [
477 "".to_string(),
478 "a b".into(),
479 "a\n".into(),
480 "é".into(),
481 "\u{7f}".into(),
482 "x".repeat(257),
483 ] {
484 for field in 0..6 {
485 let mut value = envelope();
486 match field {
487 0 => value.entitlement_id = invalid.clone(),
488 1 => value.account_id = Some(invalid.clone()),
489 2 => value.deployment_id = Some(invalid.clone()),
490 3 => value.org_id = Some(invalid.clone()),
491 4 => value.workspace_id = Some(invalid.clone()),
492 _ => value.signer.key_id = invalid.clone(),
493 }
494 assert!(value.validate().is_err());
495 }
496 }
497 let mut value = envelope();
498 value.entitlement_id = "x".repeat(256);
499 value.capabilities = vec!["x".repeat(128)];
500 value.validate().unwrap();
501 for invalid in [
502 vec!["x".repeat(129)],
503 vec!["é".into()],
504 vec!["".into()],
505 vec!["a b".into()],
506 vec!["a".into(), "a".into()],
507 vec!["z".into(), "a".into()],
508 ] {
509 value.capabilities = invalid;
510 assert!(value.validate().is_err());
511 }
512 value.capabilities = (0..256).map(|i| format!("cap-{i:03}")).collect();
513 value.validate().unwrap();
514 value.capabilities.push("cap-256".into());
515 assert!(value.validate().is_err());
516 value.capabilities.clear(); value.validate().unwrap();
518 }
519
520 #[test]
521 fn combined_byte_bound_applies_even_with_individually_valid_capabilities() {
522 let mut value = envelope();
523 value.capabilities = (0..256)
524 .map(|i| format!("{i:03}{}", "x".repeat(125)))
525 .collect();
526 assert!(value.validate().is_err());
527 assert!(value.signing_bytes().is_err());
528 assert!(value.canonical_bytes().is_err());
529 }
530
531 #[test]
532 fn exact_full_envelope_byte_boundary_includes_signature_member() {
533 let mut value = envelope();
534 value.capabilities = (0..256).map(|i| format!("{i:03}")).collect();
535 let mut remaining = MAX_ENTITLEMENT_BYTES - value.canonical_bytes().unwrap().len();
536 for capability in &mut value.capabilities {
537 let extra = remaining.min(128 - capability.len());
538 capability.push_str(&"x".repeat(extra));
539 remaining -= extra;
540 }
541 assert_eq!(remaining, 0);
542 let bytes = value.canonical_bytes().unwrap();
543 assert_eq!(bytes.len(), MAX_ENTITLEMENT_BYTES);
544 assert_eq!(EntitlementEnvelopeV1::from_bytes(&bytes).unwrap(), value);
545 value
546 .capabilities
547 .iter_mut()
548 .find(|capability| capability.len() < 128)
549 .unwrap()
550 .push('x');
551 assert!(value.validate().is_err());
552 assert!(value.unsigned_canonical_bytes().is_err());
553 }
554
555 #[test]
556 fn timestamps_seats_and_schema_fail_closed() {
557 let mut value = envelope();
558 value.not_before = 0;
559 value.issued_at = 0;
560 value.expires_at = MAX_ENTITLEMENT_TIMESTAMP;
561 value.grace_until = MAX_ENTITLEMENT_TIMESTAMP;
562 value.validate().unwrap();
563 for field in 0..4 {
564 let mut invalid = envelope();
565 match field {
566 0 => invalid.issued_at = MAX_ENTITLEMENT_TIMESTAMP + 1,
567 1 => invalid.not_before = MAX_ENTITLEMENT_TIMESTAMP + 1,
568 2 => invalid.expires_at = MAX_ENTITLEMENT_TIMESTAMP + 1,
569 _ => invalid.grace_until = MAX_ENTITLEMENT_TIMESTAMP + 1,
570 }
571 assert!(invalid.validate().is_err());
572 }
573 for (not_before, issued_at, expires_at, grace_until) in [
574 (101, 100, 200, 210),
575 (99, 100, 100, 210),
576 (99, 100, 200, 199),
577 ] {
578 let mut invalid = envelope();
579 invalid.not_before = not_before;
580 invalid.issued_at = issued_at;
581 invalid.expires_at = expires_at;
582 invalid.grace_until = grace_until;
583 assert!(invalid.validate().is_err());
584 }
585 value.seats = 0;
586 assert!(value.validate().is_err());
587 value.seats = 1;
588 value.schema_version = 2;
589 assert!(value.validate().is_err());
590 }
591
592 #[test]
593 fn canonical_signature_length_alphabet_and_pad_bits_are_checked_without_trust_claim() {
594 for signature in [
595 "".to_string(),
596 "A".repeat(88),
597 "A".repeat(85) + "B==",
598 "A".repeat(85) + "_==",
599 "A".repeat(85) + "-==",
600 "A".repeat(86) + "=",
601 "é".repeat(43) + "==",
602 ] {
603 let mut invalid = envelope();
604 invalid.signature = signature;
605 assert!(invalid.validate().is_err());
606 assert!(invalid.signing_bytes().is_ok());
607 }
608 for last in ['A', 'Q', 'g', 'w'] {
609 let mut value = envelope();
610 value.signature = format!("{}{last}==", "A".repeat(85));
611 value.validate().unwrap(); }
613 let mut value = envelope();
614 value.signer.algorithm = "ed25519ph".into();
615 assert!(value.validate().is_err());
616 }
617
618 #[test]
619 fn signature_coverage_includes_all_other_fields() {
620 let original = envelope();
621 let expected = original.signing_bytes().unwrap();
622 let original_json = serde_json::to_value(&original).unwrap();
623 let mutations = [
624 ("entitlement_id", serde_json::json!("ent-2")),
625 ("account_id", serde_json::json!("account-2")),
626 ("plan", serde_json::json!("team")),
627 ("seats", serde_json::json!(2)),
628 ("capabilities", serde_json::json!(["context.autopilot"])),
629 ("issued_at", serde_json::json!(101)),
630 ("not_before", serde_json::json!(98)),
631 ("expires_at", serde_json::json!(201)),
632 ("grace_until", serde_json::json!(211)),
633 ("allowed_deployments", serde_json::json!(["self_hosted"])),
634 ("deployment_id", serde_json::json!("deployment-1")),
635 ("org_id", serde_json::json!("org-1")),
636 ("workspace_id", serde_json::json!("workspace-1")),
637 ];
638 for (field, mutation) in mutations {
639 let mut json = original_json.clone();
640 json[field] = mutation;
641 let value: EntitlementEnvelopeV1 = serde_json::from_value(json).unwrap();
642 assert_ne!(value.signing_bytes().unwrap(), expected, "{field}");
643 }
644 for field in ["key_id", "public_key_digest"] {
645 let mut json = original_json.clone();
646 json["signer"][field] = if field == "key_id" {
647 serde_json::json!("key-2")
648 } else {
649 serde_json::json!(format!("sha256:{}", "b".repeat(64)))
650 };
651 let value: EntitlementEnvelopeV1 = serde_json::from_value(json).unwrap();
652 assert_ne!(value.signing_bytes().unwrap(), expected);
653 }
654 let mut value = original.clone();
655 value.signature.replace_range(..1, "B");
656 assert_eq!(value.signing_bytes().unwrap(), expected);
657 assert_ne!(
658 value.canonical_bytes().unwrap(),
659 original.canonical_bytes().unwrap()
660 );
661 }
662}