1use serde::{Deserialize, Serialize};
2
3use crate::id::{Blake3Hex, PilotId};
4use crate::identity::{DidKey, DidKeyError};
5use crate::store::PublicationMode;
6use crate::util::is_canonical_utc_timestamp;
7
8const PILOT_AUTH_DID_RECORD_SCHEMA: &str = "igc-net/pilot-auth-did-record";
9const PILOT_AUTH_DID_RECORD_VERSION: u8 = 1;
10const PRIVATE_ACCESS_ROTATION_RECORD_SCHEMA: &str = "igc-net/private-access-rotation-record";
11const PRIVATE_ACCESS_ROTATION_RECORD_VERSION: u8 = 1;
12const OWNER_CLAIM_RECORD_SCHEMA: &str = "igc-net/claim";
13const OWNER_CLAIM_RECORD_VERSION: u8 = 1;
14const OWNER_CLAIM_TYPE: &str = "owner";
15const CLAIM_APPROVAL_RECORD_SCHEMA: &str = "igc-net/claim-approval";
16const CLAIM_APPROVAL_RECORD_VERSION: u8 = 1;
17const CLAIM_CHALLENGE_RECORD_SCHEMA: &str = "igc-net/claim-challenge";
18const CLAIM_CHALLENGE_RECORD_VERSION: u8 = 1;
19const CLAIM_RESOLUTION_RECORD_SCHEMA: &str = "igc-net/claim-resolution";
20const CLAIM_RESOLUTION_RECORD_VERSION: u8 = 1;
21const IDENTITY_RECOVERY_RECORD_SCHEMA: &str = "igc-net/identity-recovery";
22const IDENTITY_RECOVERY_RECORD_VERSION: u8 = 1;
23const ROSTER_UPDATE_RECORD_SCHEMA: &str = "igc-net/roster-update";
24const ROSTER_UPDATE_RECORD_VERSION: u8 = 1;
25const PUBLICATION_MODE_RECORD_SCHEMA: &str = "igc-net/publication-mode-record";
26const PUBLICATION_MODE_RECORD_VERSION: u8 = 1;
27const DELETION_REQUEST_RECORD_SCHEMA: &str = "igc-net/deletion-request";
28const DELETION_REQUEST_RECORD_VERSION: u8 = 1;
29
30#[derive(Debug, thiserror::Error)]
31pub enum PilotAuthDidRecordError {
32 #[error("JSON: {0}")]
33 Json(#[from] serde_json::Error),
34 #[error("identifier: {0}")]
35 Identifier(#[from] crate::id::IdentifierError),
36 #[error("did:key: {0}")]
37 DidKey(#[from] DidKeyError),
38 #[error("schema must be {PILOT_AUTH_DID_RECORD_SCHEMA:?}, got {0:?}")]
39 Schema(String),
40 #[error("schema_version must be {PILOT_AUTH_DID_RECORD_VERSION}, got {0}")]
41 SchemaVersion(u8),
42 #[error("created_at is not canonical UTC RFC3339 seconds format: {0:?}")]
43 CreatedAt(String),
44 #[error("signature must be 128 lowercase hex chars")]
45 SignatureEncoding,
46 #[error("pilot_id does not contain a valid Ed25519 public key: {0}")]
47 PilotIdPublicKey(String),
48 #[error("record_id mismatch: expected {expected}, found {found}")]
49 RecordIdMismatch {
50 expected: Blake3Hex,
51 found: Blake3Hex,
52 },
53 #[error("signature verification failed")]
54 SignatureVerification,
55}
56
57#[derive(Debug, thiserror::Error)]
58pub enum PrivateAccessRotationRecordError {
59 #[error("JSON: {0}")]
60 Json(#[from] serde_json::Error),
61 #[error("identifier: {0}")]
62 Identifier(#[from] crate::id::IdentifierError),
63 #[error("schema must be {PRIVATE_ACCESS_ROTATION_RECORD_SCHEMA:?}, got {0:?}")]
64 Schema(String),
65 #[error("schema_version must be {PRIVATE_ACCESS_ROTATION_RECORD_VERSION}, got {0}")]
66 SchemaVersion(u8),
67 #[error("created_at is not canonical UTC RFC3339 seconds format: {0:?}")]
68 CreatedAt(String),
69 #[error("private_access_public_key must be 64 lowercase hex chars")]
70 PrivateAccessPublicKeyEncoding,
71 #[error("signature must be 128 lowercase hex chars")]
72 SignatureEncoding,
73 #[error("pilot_id does not contain a valid Ed25519 public key: {0}")]
74 PilotIdPublicKey(String),
75 #[error("record_id mismatch: expected {expected}, found {found}")]
76 RecordIdMismatch {
77 expected: Blake3Hex,
78 found: Blake3Hex,
79 },
80 #[error("signature verification failed")]
81 SignatureVerification,
82}
83
84#[derive(Debug, thiserror::Error)]
85pub enum FlightGovernanceRecordError {
86 #[error("JSON: {0}")]
87 Json(#[from] serde_json::Error),
88 #[error("identifier: {0}")]
89 Identifier(#[from] crate::id::IdentifierError),
90 #[error("schema must be one of the supported flight governance schemas, got {0:?}")]
91 Schema(String),
92 #[error("schema_version is unsupported: {0}")]
93 SchemaVersion(u8),
94 #[error("claim_type must be {OWNER_CLAIM_TYPE:?}, got {0:?}")]
95 ClaimType(String),
96 #[error("resolution value is unsupported: {0:?}")]
97 Resolution(String),
98 #[error("created_at is not canonical UTC RFC3339 seconds format: {0:?}")]
99 CreatedAt(String),
100 #[error("signature must be 128 lowercase hex chars")]
101 SignatureEncoding,
102 #[error("resolver_id must be 64 lowercase hex chars: {0:?}")]
103 ResolverIdEncoding(String),
104 #[error("challenger_resolver_id must be 64 lowercase hex chars: {0:?}")]
105 ChallengerResolverIdEncoding(String),
106 #[error("signer_id must be 64 lowercase hex chars: {0:?}")]
107 SignerIdEncoding(String),
108 #[error("resolver_profile must be present for add and absent for remove")]
109 RosterProfilePresence,
110 #[error("old_pilot_id and new_pilot_id must be distinct")]
111 IdentityRecoverySamePilot,
112 #[error("protected_hash presence does not match publication_mode")]
113 ProtectedHashPresence,
114 #[error("pilot_id does not contain a valid Ed25519 public key: {0}")]
115 PilotIdPublicKey(String),
116 #[error("record_id mismatch: expected {expected}, found {found}")]
117 RecordIdMismatch {
118 expected: Blake3Hex,
119 found: Blake3Hex,
120 },
121 #[error("signature verification failed")]
122 SignatureVerification,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
126pub struct PilotAuthDidRecord {
127 pub schema: String,
128 pub schema_version: u8,
129 pub record_id: Blake3Hex,
130 pub pilot_id: PilotId,
131 pub pilot_auth_did: DidKey,
132 pub supersedes: Option<Blake3Hex>,
133 pub created_at: String,
134 pub signature: String,
135}
136
137impl PilotAuthDidRecord {
138 pub fn issue(
139 pilot_id_secret_key: &iroh::SecretKey,
140 pilot_auth_did: DidKey,
141 supersedes: Option<Blake3Hex>,
142 created_at: impl Into<String>,
143 ) -> Result<Self, PilotAuthDidRecordError> {
144 let created_at = created_at.into();
145 if !is_canonical_utc_timestamp(&created_at) {
146 return Err(PilotAuthDidRecordError::CreatedAt(created_at));
147 }
148
149 let pilot_id = PilotId::from_public_key(pilot_id_secret_key.public());
150 let record_id = derive_record_id(&pilot_id, &pilot_auth_did, &supersedes, &created_at)?;
151 let signature_bytes = signing_payload(
152 &pilot_id,
153 &record_id,
154 &pilot_auth_did,
155 &supersedes,
156 &created_at,
157 )?;
158 let signature = hex::encode(pilot_id_secret_key.sign(&signature_bytes).to_bytes());
159
160 let record = Self {
161 schema: PILOT_AUTH_DID_RECORD_SCHEMA.to_string(),
162 schema_version: PILOT_AUTH_DID_RECORD_VERSION,
163 record_id,
164 pilot_id,
165 pilot_auth_did,
166 supersedes,
167 created_at,
168 signature,
169 };
170 record.validate()?;
171 Ok(record)
172 }
173
174 pub fn validate(&self) -> Result<(), PilotAuthDidRecordError> {
175 if self.schema != PILOT_AUTH_DID_RECORD_SCHEMA {
176 return Err(PilotAuthDidRecordError::Schema(self.schema.clone()));
177 }
178 if self.schema_version != PILOT_AUTH_DID_RECORD_VERSION {
179 return Err(PilotAuthDidRecordError::SchemaVersion(self.schema_version));
180 }
181 if !is_canonical_utc_timestamp(&self.created_at) {
182 return Err(PilotAuthDidRecordError::CreatedAt(self.created_at.clone()));
183 }
184
185 let expected_record_id = derive_record_id(
186 &self.pilot_id,
187 &self.pilot_auth_did,
188 &self.supersedes,
189 &self.created_at,
190 )?;
191 if self.record_id != expected_record_id {
192 return Err(PilotAuthDidRecordError::RecordIdMismatch {
193 expected: expected_record_id,
194 found: self.record_id.clone(),
195 });
196 }
197
198 let signature = decode_signature_hex(&self.signature)?;
199 let signing_bytes = signing_payload(
200 &self.pilot_id,
201 &self.record_id,
202 &self.pilot_auth_did,
203 &self.supersedes,
204 &self.created_at,
205 )?;
206 pilot_id_public_key(&self.pilot_id)?
207 .verify(&signing_bytes, &signature)
208 .map_err(|_| PilotAuthDidRecordError::SignatureVerification)?;
209
210 Ok(())
211 }
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
215pub struct PrivateAccessRotationRecord {
216 pub schema: String,
217 pub schema_version: u8,
218 pub record_id: Blake3Hex,
219 pub pilot_id: PilotId,
220 pub private_access_public_key: String,
221 pub supersedes: Option<Blake3Hex>,
222 pub created_at: String,
223 pub signature: String,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
227pub struct OwnerClaimRecord {
228 pub schema: String,
229 pub schema_version: u8,
230 pub record_id: Blake3Hex,
231 pub raw_igc_hash: Blake3Hex,
232 pub claim_type: String,
233 pub pilot_id: PilotId,
234 pub signature: String,
235 pub created_at: String,
236 pub evidence: Vec<serde_json::Value>,
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
240pub struct ClaimApprovalRecord {
241 pub schema: String,
242 pub schema_version: u8,
243 pub record_id: Blake3Hex,
244 pub claim_record_id: Blake3Hex,
245 pub raw_igc_hash: Blake3Hex,
246 pub resolver_id: String,
247 pub signature: String,
248 pub created_at: String,
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
252pub struct ClaimChallengeRecord {
253 pub schema: String,
254 pub schema_version: u8,
255 pub record_id: Blake3Hex,
256 pub claim_record_id: Blake3Hex,
257 pub raw_igc_hash: Blake3Hex,
258 pub challenger_resolver_id: String,
259 pub signature: String,
260 pub reason: String,
261 pub created_at: String,
262}
263
264#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
265#[serde(rename_all = "snake_case")]
266pub enum ClaimResolutionOutcome {
267 Approved,
268 Rejected,
269 Superseded,
270 Revoked,
271}
272
273#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
274pub struct ClaimResolutionRecord {
275 pub schema: String,
276 pub schema_version: u8,
277 pub record_id: Blake3Hex,
278 pub raw_igc_hash: Blake3Hex,
279 pub claim_record_id: Blake3Hex,
280 pub resolver_id: String,
281 pub signature: String,
282 pub resolution: ClaimResolutionOutcome,
283 pub basis: Vec<String>,
284 pub created_at: String,
285 pub supersedes: Vec<Blake3Hex>,
286}
287
288#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
289#[serde(rename_all = "snake_case")]
290pub enum IdentityRecoveryBasis {
291 KeyLossRecovery,
292 KeyCompromiseRecovery,
293}
294
295#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
296pub struct IdentityRecoveryRecord {
297 pub schema: String,
298 pub schema_version: u8,
299 pub record_id: Blake3Hex,
300 pub old_pilot_id: PilotId,
301 pub new_pilot_id: PilotId,
302 pub resolver_id: String,
303 pub basis: IdentityRecoveryBasis,
304 pub created_at: String,
305 pub signature: String,
306}
307
308#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
309#[serde(rename_all = "snake_case")]
310pub enum RosterUpdateAction {
311 Add,
312 Remove,
313}
314
315#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
316pub struct ResolverProfile {
317 pub display_name: String,
318 pub service_url: String,
319 pub privacy_policy_url: String,
320 pub public_key_url: String,
321}
322
323#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
324pub struct RosterUpdateRecord {
325 pub schema: String,
326 pub schema_version: u8,
327 pub record_id: Blake3Hex,
328 pub action: RosterUpdateAction,
329 pub resolver_id: String,
330 pub signer_id: String,
331 pub resolver_profile: Option<ResolverProfile>,
332 pub created_at: String,
333 pub signature: String,
334}
335
336#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
337pub struct PublicationModeRecord {
338 pub schema: String,
339 pub schema_version: u8,
340 pub record_id: Blake3Hex,
341 pub raw_igc_hash: Blake3Hex,
342 pub publication_mode: PublicationMode,
343 pub protected_hash: Option<Blake3Hex>,
344 pub supersedes: Option<Blake3Hex>,
345 pub pilot_id: PilotId,
346 pub signature: String,
347 pub created_at: String,
348}
349
350#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
351pub struct DeletionRequestRecord {
352 pub schema: String,
353 pub schema_version: u8,
354 pub record_id: Blake3Hex,
355 pub raw_igc_hash: Blake3Hex,
356 pub pilot_id: PilotId,
357 pub signature: String,
358 pub created_at: String,
359}
360
361impl PrivateAccessRotationRecord {
362 pub fn issue(
363 pilot_id_secret_key: &iroh::SecretKey,
364 private_access_public_key: iroh::PublicKey,
365 supersedes: Option<Blake3Hex>,
366 created_at: impl Into<String>,
367 ) -> Result<Self, PrivateAccessRotationRecordError> {
368 let created_at = created_at.into();
369 if !is_canonical_utc_timestamp(&created_at) {
370 return Err(PrivateAccessRotationRecordError::CreatedAt(created_at));
371 }
372
373 let pilot_id = PilotId::from_public_key(pilot_id_secret_key.public());
374 let private_access_public_key = private_access_public_key.to_string();
375 let record_id = derive_private_access_rotation_record_id(
376 &pilot_id,
377 &private_access_public_key,
378 &supersedes,
379 &created_at,
380 )?;
381 let signature_bytes = private_access_rotation_signing_payload(
382 &pilot_id,
383 &record_id,
384 &private_access_public_key,
385 &supersedes,
386 &created_at,
387 )?;
388 let signature = hex::encode(pilot_id_secret_key.sign(&signature_bytes).to_bytes());
389
390 let record = Self {
391 schema: PRIVATE_ACCESS_ROTATION_RECORD_SCHEMA.to_string(),
392 schema_version: PRIVATE_ACCESS_ROTATION_RECORD_VERSION,
393 record_id,
394 pilot_id,
395 private_access_public_key,
396 supersedes,
397 created_at,
398 signature,
399 };
400 record.validate()?;
401 Ok(record)
402 }
403
404 pub fn validate(&self) -> Result<(), PrivateAccessRotationRecordError> {
405 if self.schema != PRIVATE_ACCESS_ROTATION_RECORD_SCHEMA {
406 return Err(PrivateAccessRotationRecordError::Schema(
407 self.schema.clone(),
408 ));
409 }
410 if self.schema_version != PRIVATE_ACCESS_ROTATION_RECORD_VERSION {
411 return Err(PrivateAccessRotationRecordError::SchemaVersion(
412 self.schema_version,
413 ));
414 }
415 if !is_canonical_utc_timestamp(&self.created_at) {
416 return Err(PrivateAccessRotationRecordError::CreatedAt(
417 self.created_at.clone(),
418 ));
419 }
420 validate_private_access_public_key(&self.private_access_public_key)?;
421
422 let expected_record_id = derive_private_access_rotation_record_id(
423 &self.pilot_id,
424 &self.private_access_public_key,
425 &self.supersedes,
426 &self.created_at,
427 )?;
428 if self.record_id != expected_record_id {
429 return Err(PrivateAccessRotationRecordError::RecordIdMismatch {
430 expected: expected_record_id,
431 found: self.record_id.clone(),
432 });
433 }
434
435 let signature = decode_private_access_rotation_signature_hex(&self.signature)?;
436 let signing_bytes = private_access_rotation_signing_payload(
437 &self.pilot_id,
438 &self.record_id,
439 &self.private_access_public_key,
440 &self.supersedes,
441 &self.created_at,
442 )?;
443 pilot_id_public_key(&self.pilot_id)
444 .map_err(|_| {
445 PrivateAccessRotationRecordError::PilotIdPublicKey(self.pilot_id.to_string())
446 })?
447 .verify(&signing_bytes, &signature)
448 .map_err(|_| PrivateAccessRotationRecordError::SignatureVerification)?;
449
450 Ok(())
451 }
452
453 pub fn private_access_public_key(
454 &self,
455 ) -> Result<iroh::PublicKey, PrivateAccessRotationRecordError> {
456 let bytes = decode_fixed_hex_32(&self.private_access_public_key)
457 .map_err(|_| PrivateAccessRotationRecordError::PrivateAccessPublicKeyEncoding)?;
458 iroh::PublicKey::from_bytes(&bytes)
459 .map_err(|_| PrivateAccessRotationRecordError::PrivateAccessPublicKeyEncoding)
460 }
461}
462
463impl OwnerClaimRecord {
464 pub fn issue(
465 pilot_id_secret_key: &iroh::SecretKey,
466 raw_igc_hash: Blake3Hex,
467 created_at: impl Into<String>,
468 evidence: Vec<serde_json::Value>,
469 ) -> Result<Self, FlightGovernanceRecordError> {
470 let created_at = created_at.into();
471 if !is_canonical_utc_timestamp(&created_at) {
472 return Err(FlightGovernanceRecordError::CreatedAt(created_at));
473 }
474
475 let pilot_id = PilotId::from_public_key(pilot_id_secret_key.public());
476 let record_id =
477 derive_owner_claim_record_id(&raw_igc_hash, &pilot_id, &created_at, &evidence)?;
478 let signature_bytes = owner_claim_signing_payload(
479 &record_id,
480 &raw_igc_hash,
481 &pilot_id,
482 &created_at,
483 &evidence,
484 )?;
485 let signature = hex::encode(pilot_id_secret_key.sign(&signature_bytes).to_bytes());
486
487 let record = Self {
488 schema: OWNER_CLAIM_RECORD_SCHEMA.to_string(),
489 schema_version: OWNER_CLAIM_RECORD_VERSION,
490 record_id,
491 raw_igc_hash,
492 claim_type: OWNER_CLAIM_TYPE.to_string(),
493 pilot_id,
494 signature,
495 created_at,
496 evidence,
497 };
498 record.validate()?;
499 Ok(record)
500 }
501
502 pub fn validate(&self) -> Result<(), FlightGovernanceRecordError> {
503 if self.schema != OWNER_CLAIM_RECORD_SCHEMA {
504 return Err(FlightGovernanceRecordError::Schema(self.schema.clone()));
505 }
506 if self.schema_version != OWNER_CLAIM_RECORD_VERSION {
507 return Err(FlightGovernanceRecordError::SchemaVersion(
508 self.schema_version,
509 ));
510 }
511 if self.claim_type != OWNER_CLAIM_TYPE {
512 return Err(FlightGovernanceRecordError::ClaimType(
513 self.claim_type.clone(),
514 ));
515 }
516 if !is_canonical_utc_timestamp(&self.created_at) {
517 return Err(FlightGovernanceRecordError::CreatedAt(
518 self.created_at.clone(),
519 ));
520 }
521
522 let expected_record_id = derive_owner_claim_record_id(
523 &self.raw_igc_hash,
524 &self.pilot_id,
525 &self.created_at,
526 &self.evidence,
527 )?;
528 if self.record_id != expected_record_id {
529 return Err(FlightGovernanceRecordError::RecordIdMismatch {
530 expected: expected_record_id,
531 found: self.record_id.clone(),
532 });
533 }
534
535 let signature = decode_flight_governance_signature_hex(&self.signature)?;
536 let signing_bytes = owner_claim_signing_payload(
537 &self.record_id,
538 &self.raw_igc_hash,
539 &self.pilot_id,
540 &self.created_at,
541 &self.evidence,
542 )?;
543 flight_pilot_id_public_key(&self.pilot_id)?
544 .verify(&signing_bytes, &signature)
545 .map_err(|_| FlightGovernanceRecordError::SignatureVerification)?;
546
547 Ok(())
548 }
549}
550
551impl ClaimApprovalRecord {
552 pub fn issue(
553 resolver_secret_key: &iroh::SecretKey,
554 claim_record_id: Blake3Hex,
555 raw_igc_hash: Blake3Hex,
556 created_at: impl Into<String>,
557 ) -> Result<Self, FlightGovernanceRecordError> {
558 let created_at = created_at.into();
559 if !is_canonical_utc_timestamp(&created_at) {
560 return Err(FlightGovernanceRecordError::CreatedAt(created_at));
561 }
562
563 let resolver_id = resolver_secret_key.public().to_string();
564 let record_id = derive_claim_approval_record_id(
565 &claim_record_id,
566 &raw_igc_hash,
567 &resolver_id,
568 &created_at,
569 )?;
570 let signature_bytes = claim_approval_signing_payload(
571 &record_id,
572 &claim_record_id,
573 &raw_igc_hash,
574 &resolver_id,
575 &created_at,
576 )?;
577 let signature = hex::encode(resolver_secret_key.sign(&signature_bytes).to_bytes());
578
579 let record = Self {
580 schema: CLAIM_APPROVAL_RECORD_SCHEMA.to_string(),
581 schema_version: CLAIM_APPROVAL_RECORD_VERSION,
582 record_id,
583 claim_record_id,
584 raw_igc_hash,
585 resolver_id,
586 signature,
587 created_at,
588 };
589 record.validate()?;
590 Ok(record)
591 }
592
593 pub fn validate(&self) -> Result<(), FlightGovernanceRecordError> {
594 if self.schema != CLAIM_APPROVAL_RECORD_SCHEMA {
595 return Err(FlightGovernanceRecordError::Schema(self.schema.clone()));
596 }
597 if self.schema_version != CLAIM_APPROVAL_RECORD_VERSION {
598 return Err(FlightGovernanceRecordError::SchemaVersion(
599 self.schema_version,
600 ));
601 }
602 if !is_canonical_utc_timestamp(&self.created_at) {
603 return Err(FlightGovernanceRecordError::CreatedAt(
604 self.created_at.clone(),
605 ));
606 }
607 validate_resolver_id(&self.resolver_id)?;
608
609 let expected_record_id = derive_claim_approval_record_id(
610 &self.claim_record_id,
611 &self.raw_igc_hash,
612 &self.resolver_id,
613 &self.created_at,
614 )?;
615 if self.record_id != expected_record_id {
616 return Err(FlightGovernanceRecordError::RecordIdMismatch {
617 expected: expected_record_id,
618 found: self.record_id.clone(),
619 });
620 }
621
622 let signature = decode_flight_governance_signature_hex(&self.signature)?;
623 let signing_bytes = claim_approval_signing_payload(
624 &self.record_id,
625 &self.claim_record_id,
626 &self.raw_igc_hash,
627 &self.resolver_id,
628 &self.created_at,
629 )?;
630 resolver_public_key(&self.resolver_id)?
631 .verify(&signing_bytes, &signature)
632 .map_err(|_| FlightGovernanceRecordError::SignatureVerification)?;
633
634 Ok(())
635 }
636}
637
638impl ClaimChallengeRecord {
639 pub fn issue(
640 resolver_secret_key: &iroh::SecretKey,
641 claim_record_id: Blake3Hex,
642 raw_igc_hash: Blake3Hex,
643 reason: impl Into<String>,
644 created_at: impl Into<String>,
645 ) -> Result<Self, FlightGovernanceRecordError> {
646 let created_at = created_at.into();
647 if !is_canonical_utc_timestamp(&created_at) {
648 return Err(FlightGovernanceRecordError::CreatedAt(created_at));
649 }
650
651 let challenger_resolver_id = resolver_secret_key.public().to_string();
652 let reason = reason.into();
653 let record_id = derive_claim_challenge_record_id(
654 &claim_record_id,
655 &raw_igc_hash,
656 &challenger_resolver_id,
657 &reason,
658 &created_at,
659 )?;
660 let signature_bytes = claim_challenge_signing_payload(
661 &record_id,
662 &claim_record_id,
663 &raw_igc_hash,
664 &challenger_resolver_id,
665 &reason,
666 &created_at,
667 )?;
668 let signature = hex::encode(resolver_secret_key.sign(&signature_bytes).to_bytes());
669
670 let record = Self {
671 schema: CLAIM_CHALLENGE_RECORD_SCHEMA.to_string(),
672 schema_version: CLAIM_CHALLENGE_RECORD_VERSION,
673 record_id,
674 claim_record_id,
675 raw_igc_hash,
676 challenger_resolver_id,
677 signature,
678 reason,
679 created_at,
680 };
681 record.validate()?;
682 Ok(record)
683 }
684
685 pub fn validate(&self) -> Result<(), FlightGovernanceRecordError> {
686 if self.schema != CLAIM_CHALLENGE_RECORD_SCHEMA {
687 return Err(FlightGovernanceRecordError::Schema(self.schema.clone()));
688 }
689 if self.schema_version != CLAIM_CHALLENGE_RECORD_VERSION {
690 return Err(FlightGovernanceRecordError::SchemaVersion(
691 self.schema_version,
692 ));
693 }
694 if !is_canonical_utc_timestamp(&self.created_at) {
695 return Err(FlightGovernanceRecordError::CreatedAt(
696 self.created_at.clone(),
697 ));
698 }
699 validate_challenger_resolver_id(&self.challenger_resolver_id)?;
700
701 let expected_record_id = derive_claim_challenge_record_id(
702 &self.claim_record_id,
703 &self.raw_igc_hash,
704 &self.challenger_resolver_id,
705 &self.reason,
706 &self.created_at,
707 )?;
708 if self.record_id != expected_record_id {
709 return Err(FlightGovernanceRecordError::RecordIdMismatch {
710 expected: expected_record_id,
711 found: self.record_id.clone(),
712 });
713 }
714
715 let signature = decode_flight_governance_signature_hex(&self.signature)?;
716 let signing_bytes = claim_challenge_signing_payload(
717 &self.record_id,
718 &self.claim_record_id,
719 &self.raw_igc_hash,
720 &self.challenger_resolver_id,
721 &self.reason,
722 &self.created_at,
723 )?;
724 resolver_public_key(&self.challenger_resolver_id)?
725 .verify(&signing_bytes, &signature)
726 .map_err(|_| FlightGovernanceRecordError::SignatureVerification)?;
727
728 Ok(())
729 }
730}
731
732impl ClaimResolutionRecord {
733 pub fn issue(
734 resolver_secret_key: &iroh::SecretKey,
735 raw_igc_hash: Blake3Hex,
736 claim_record_id: Blake3Hex,
737 resolution: ClaimResolutionOutcome,
738 basis: Vec<String>,
739 supersedes: Vec<Blake3Hex>,
740 created_at: impl Into<String>,
741 ) -> Result<Self, FlightGovernanceRecordError> {
742 let created_at = created_at.into();
743 if !is_canonical_utc_timestamp(&created_at) {
744 return Err(FlightGovernanceRecordError::CreatedAt(created_at));
745 }
746
747 let resolver_id = resolver_secret_key.public().to_string();
748 let record_id = derive_claim_resolution_record_id(
749 &raw_igc_hash,
750 &claim_record_id,
751 &resolver_id,
752 resolution,
753 &basis,
754 &created_at,
755 &supersedes,
756 )?;
757 let signature_bytes = claim_resolution_signing_payload(
758 &record_id,
759 &raw_igc_hash,
760 &claim_record_id,
761 &resolver_id,
762 resolution,
763 &basis,
764 &created_at,
765 &supersedes,
766 )?;
767 let signature = hex::encode(resolver_secret_key.sign(&signature_bytes).to_bytes());
768
769 let record = Self {
770 schema: CLAIM_RESOLUTION_RECORD_SCHEMA.to_string(),
771 schema_version: CLAIM_RESOLUTION_RECORD_VERSION,
772 record_id,
773 raw_igc_hash,
774 claim_record_id,
775 resolver_id,
776 signature,
777 resolution,
778 basis,
779 created_at,
780 supersedes,
781 };
782 record.validate()?;
783 Ok(record)
784 }
785
786 pub fn validate(&self) -> Result<(), FlightGovernanceRecordError> {
787 if self.schema != CLAIM_RESOLUTION_RECORD_SCHEMA {
788 return Err(FlightGovernanceRecordError::Schema(self.schema.clone()));
789 }
790 if self.schema_version != CLAIM_RESOLUTION_RECORD_VERSION {
791 return Err(FlightGovernanceRecordError::SchemaVersion(
792 self.schema_version,
793 ));
794 }
795 if !is_canonical_utc_timestamp(&self.created_at) {
796 return Err(FlightGovernanceRecordError::CreatedAt(
797 self.created_at.clone(),
798 ));
799 }
800 validate_resolver_id(&self.resolver_id)?;
801
802 let expected_record_id = derive_claim_resolution_record_id(
803 &self.raw_igc_hash,
804 &self.claim_record_id,
805 &self.resolver_id,
806 self.resolution,
807 &self.basis,
808 &self.created_at,
809 &self.supersedes,
810 )?;
811 if self.record_id != expected_record_id {
812 return Err(FlightGovernanceRecordError::RecordIdMismatch {
813 expected: expected_record_id,
814 found: self.record_id.clone(),
815 });
816 }
817
818 let signature = decode_flight_governance_signature_hex(&self.signature)?;
819 let signing_bytes = claim_resolution_signing_payload(
820 &self.record_id,
821 &self.raw_igc_hash,
822 &self.claim_record_id,
823 &self.resolver_id,
824 self.resolution,
825 &self.basis,
826 &self.created_at,
827 &self.supersedes,
828 )?;
829 resolver_public_key(&self.resolver_id)?
830 .verify(&signing_bytes, &signature)
831 .map_err(|_| FlightGovernanceRecordError::SignatureVerification)?;
832
833 Ok(())
834 }
835}
836
837impl IdentityRecoveryRecord {
838 pub fn issue(
839 resolver_secret_key: &iroh::SecretKey,
840 old_pilot_id: PilotId,
841 new_pilot_id: PilotId,
842 basis: IdentityRecoveryBasis,
843 created_at: impl Into<String>,
844 ) -> Result<Self, FlightGovernanceRecordError> {
845 let created_at = created_at.into();
846 if !is_canonical_utc_timestamp(&created_at) {
847 return Err(FlightGovernanceRecordError::CreatedAt(created_at));
848 }
849 if old_pilot_id == new_pilot_id {
850 return Err(FlightGovernanceRecordError::IdentityRecoverySamePilot);
851 }
852
853 let resolver_id = resolver_secret_key.public().to_string();
854 let record_id = derive_identity_recovery_record_id(
855 &old_pilot_id,
856 &new_pilot_id,
857 &resolver_id,
858 basis,
859 &created_at,
860 )?;
861 let signature_bytes = identity_recovery_signing_payload(
862 &record_id,
863 &old_pilot_id,
864 &new_pilot_id,
865 &resolver_id,
866 basis,
867 &created_at,
868 )?;
869 let signature = hex::encode(resolver_secret_key.sign(&signature_bytes).to_bytes());
870
871 let record = Self {
872 schema: IDENTITY_RECOVERY_RECORD_SCHEMA.to_string(),
873 schema_version: IDENTITY_RECOVERY_RECORD_VERSION,
874 record_id,
875 old_pilot_id,
876 new_pilot_id,
877 resolver_id,
878 basis,
879 created_at,
880 signature,
881 };
882 record.validate()?;
883 Ok(record)
884 }
885
886 pub fn validate(&self) -> Result<(), FlightGovernanceRecordError> {
887 if self.schema != IDENTITY_RECOVERY_RECORD_SCHEMA {
888 return Err(FlightGovernanceRecordError::Schema(self.schema.clone()));
889 }
890 if self.schema_version != IDENTITY_RECOVERY_RECORD_VERSION {
891 return Err(FlightGovernanceRecordError::SchemaVersion(
892 self.schema_version,
893 ));
894 }
895 if self.old_pilot_id == self.new_pilot_id {
896 return Err(FlightGovernanceRecordError::IdentityRecoverySamePilot);
897 }
898 if !is_canonical_utc_timestamp(&self.created_at) {
899 return Err(FlightGovernanceRecordError::CreatedAt(
900 self.created_at.clone(),
901 ));
902 }
903 validate_resolver_id(&self.resolver_id)?;
904
905 let expected_record_id = derive_identity_recovery_record_id(
906 &self.old_pilot_id,
907 &self.new_pilot_id,
908 &self.resolver_id,
909 self.basis,
910 &self.created_at,
911 )?;
912 if self.record_id != expected_record_id {
913 return Err(FlightGovernanceRecordError::RecordIdMismatch {
914 expected: expected_record_id,
915 found: self.record_id.clone(),
916 });
917 }
918
919 let signature = decode_flight_governance_signature_hex(&self.signature)?;
920 let signing_bytes = identity_recovery_signing_payload(
921 &self.record_id,
922 &self.old_pilot_id,
923 &self.new_pilot_id,
924 &self.resolver_id,
925 self.basis,
926 &self.created_at,
927 )?;
928 resolver_public_key(&self.resolver_id)?
929 .verify(&signing_bytes, &signature)
930 .map_err(|_| FlightGovernanceRecordError::SignatureVerification)?;
931
932 Ok(())
933 }
934}
935
936impl RosterUpdateRecord {
937 pub fn issue(
938 signer_secret_key: &iroh::SecretKey,
939 action: RosterUpdateAction,
940 resolver_id: String,
941 resolver_profile: Option<ResolverProfile>,
942 created_at: impl Into<String>,
943 ) -> Result<Self, FlightGovernanceRecordError> {
944 let created_at = created_at.into();
945 if !is_canonical_utc_timestamp(&created_at) {
946 return Err(FlightGovernanceRecordError::CreatedAt(created_at));
947 }
948 validate_resolver_id(&resolver_id)?;
949 validate_roster_profile_presence(action, resolver_profile.as_ref())?;
950
951 let signer_id = signer_secret_key.public().to_string();
952 let record_id = derive_roster_update_record_id(
953 action,
954 &resolver_id,
955 &signer_id,
956 resolver_profile.as_ref(),
957 &created_at,
958 )?;
959 let signature_bytes = roster_update_signing_payload(
960 &record_id,
961 action,
962 &resolver_id,
963 &signer_id,
964 resolver_profile.as_ref(),
965 &created_at,
966 )?;
967 let signature = hex::encode(signer_secret_key.sign(&signature_bytes).to_bytes());
968
969 let record = Self {
970 schema: ROSTER_UPDATE_RECORD_SCHEMA.to_string(),
971 schema_version: ROSTER_UPDATE_RECORD_VERSION,
972 record_id,
973 action,
974 resolver_id,
975 signer_id,
976 resolver_profile,
977 created_at,
978 signature,
979 };
980 record.validate()?;
981 Ok(record)
982 }
983
984 pub fn validate(&self) -> Result<(), FlightGovernanceRecordError> {
985 if self.schema != ROSTER_UPDATE_RECORD_SCHEMA {
986 return Err(FlightGovernanceRecordError::Schema(self.schema.clone()));
987 }
988 if self.schema_version != ROSTER_UPDATE_RECORD_VERSION {
989 return Err(FlightGovernanceRecordError::SchemaVersion(
990 self.schema_version,
991 ));
992 }
993 if !is_canonical_utc_timestamp(&self.created_at) {
994 return Err(FlightGovernanceRecordError::CreatedAt(
995 self.created_at.clone(),
996 ));
997 }
998 validate_resolver_id(&self.resolver_id)?;
999 validate_signer_id(&self.signer_id)?;
1000 validate_roster_profile_presence(self.action, self.resolver_profile.as_ref())?;
1001
1002 let expected_record_id = derive_roster_update_record_id(
1003 self.action,
1004 &self.resolver_id,
1005 &self.signer_id,
1006 self.resolver_profile.as_ref(),
1007 &self.created_at,
1008 )?;
1009 if self.record_id != expected_record_id {
1010 return Err(FlightGovernanceRecordError::RecordIdMismatch {
1011 expected: expected_record_id,
1012 found: self.record_id.clone(),
1013 });
1014 }
1015
1016 let signature = decode_flight_governance_signature_hex(&self.signature)?;
1017 let signing_bytes = roster_update_signing_payload(
1018 &self.record_id,
1019 self.action,
1020 &self.resolver_id,
1021 &self.signer_id,
1022 self.resolver_profile.as_ref(),
1023 &self.created_at,
1024 )?;
1025 signer_public_key(&self.signer_id)?
1026 .verify(&signing_bytes, &signature)
1027 .map_err(|_| FlightGovernanceRecordError::SignatureVerification)?;
1028
1029 Ok(())
1030 }
1031}
1032
1033impl PublicationModeRecord {
1034 pub fn issue(
1035 pilot_id_secret_key: &iroh::SecretKey,
1036 raw_igc_hash: Blake3Hex,
1037 publication_mode: PublicationMode,
1038 protected_hash: Option<Blake3Hex>,
1039 supersedes: Option<Blake3Hex>,
1040 created_at: impl Into<String>,
1041 ) -> Result<Self, FlightGovernanceRecordError> {
1042 let created_at = created_at.into();
1043 if !is_canonical_utc_timestamp(&created_at) {
1044 return Err(FlightGovernanceRecordError::CreatedAt(created_at));
1045 }
1046 validate_publication_mode_hash(&publication_mode, protected_hash.as_ref())?;
1047
1048 let pilot_id = PilotId::from_public_key(pilot_id_secret_key.public());
1049 let record_id = derive_publication_mode_record_id(
1050 &raw_igc_hash,
1051 &publication_mode,
1052 protected_hash.as_ref(),
1053 &supersedes,
1054 &pilot_id,
1055 &created_at,
1056 )?;
1057 let signature_bytes = publication_mode_signing_payload(
1058 &record_id,
1059 &raw_igc_hash,
1060 &publication_mode,
1061 protected_hash.as_ref(),
1062 &supersedes,
1063 &pilot_id,
1064 &created_at,
1065 )?;
1066 let signature = hex::encode(pilot_id_secret_key.sign(&signature_bytes).to_bytes());
1067
1068 let record = Self {
1069 schema: PUBLICATION_MODE_RECORD_SCHEMA.to_string(),
1070 schema_version: PUBLICATION_MODE_RECORD_VERSION,
1071 record_id,
1072 raw_igc_hash,
1073 publication_mode,
1074 protected_hash,
1075 supersedes,
1076 pilot_id,
1077 signature,
1078 created_at,
1079 };
1080 record.validate()?;
1081 Ok(record)
1082 }
1083
1084 pub fn validate(&self) -> Result<(), FlightGovernanceRecordError> {
1085 if self.schema != PUBLICATION_MODE_RECORD_SCHEMA {
1086 return Err(FlightGovernanceRecordError::Schema(self.schema.clone()));
1087 }
1088 if self.schema_version != PUBLICATION_MODE_RECORD_VERSION {
1089 return Err(FlightGovernanceRecordError::SchemaVersion(
1090 self.schema_version,
1091 ));
1092 }
1093 if !is_canonical_utc_timestamp(&self.created_at) {
1094 return Err(FlightGovernanceRecordError::CreatedAt(
1095 self.created_at.clone(),
1096 ));
1097 }
1098 validate_publication_mode_hash(&self.publication_mode, self.protected_hash.as_ref())?;
1099
1100 let expected_record_id = derive_publication_mode_record_id(
1101 &self.raw_igc_hash,
1102 &self.publication_mode,
1103 self.protected_hash.as_ref(),
1104 &self.supersedes,
1105 &self.pilot_id,
1106 &self.created_at,
1107 )?;
1108 if self.record_id != expected_record_id {
1109 return Err(FlightGovernanceRecordError::RecordIdMismatch {
1110 expected: expected_record_id,
1111 found: self.record_id.clone(),
1112 });
1113 }
1114
1115 let signature = decode_flight_governance_signature_hex(&self.signature)?;
1116 let signing_bytes = publication_mode_signing_payload(
1117 &self.record_id,
1118 &self.raw_igc_hash,
1119 &self.publication_mode,
1120 self.protected_hash.as_ref(),
1121 &self.supersedes,
1122 &self.pilot_id,
1123 &self.created_at,
1124 )?;
1125 flight_pilot_id_public_key(&self.pilot_id)?
1126 .verify(&signing_bytes, &signature)
1127 .map_err(|_| FlightGovernanceRecordError::SignatureVerification)?;
1128
1129 Ok(())
1130 }
1131}
1132
1133impl DeletionRequestRecord {
1134 pub fn issue(
1135 pilot_id_secret_key: &iroh::SecretKey,
1136 raw_igc_hash: Blake3Hex,
1137 created_at: impl Into<String>,
1138 ) -> Result<Self, FlightGovernanceRecordError> {
1139 let created_at = created_at.into();
1140 if !is_canonical_utc_timestamp(&created_at) {
1141 return Err(FlightGovernanceRecordError::CreatedAt(created_at));
1142 }
1143
1144 let pilot_id = PilotId::from_public_key(pilot_id_secret_key.public());
1145 let record_id = derive_deletion_request_record_id(&raw_igc_hash, &pilot_id, &created_at)?;
1146 let signature_bytes =
1147 deletion_request_signing_payload(&record_id, &raw_igc_hash, &pilot_id, &created_at)?;
1148 let signature = hex::encode(pilot_id_secret_key.sign(&signature_bytes).to_bytes());
1149
1150 let record = Self {
1151 schema: DELETION_REQUEST_RECORD_SCHEMA.to_string(),
1152 schema_version: DELETION_REQUEST_RECORD_VERSION,
1153 record_id,
1154 raw_igc_hash,
1155 pilot_id,
1156 signature,
1157 created_at,
1158 };
1159 record.validate()?;
1160 Ok(record)
1161 }
1162
1163 pub fn validate(&self) -> Result<(), FlightGovernanceRecordError> {
1164 if self.schema != DELETION_REQUEST_RECORD_SCHEMA {
1165 return Err(FlightGovernanceRecordError::Schema(self.schema.clone()));
1166 }
1167 if self.schema_version != DELETION_REQUEST_RECORD_VERSION {
1168 return Err(FlightGovernanceRecordError::SchemaVersion(
1169 self.schema_version,
1170 ));
1171 }
1172 if !is_canonical_utc_timestamp(&self.created_at) {
1173 return Err(FlightGovernanceRecordError::CreatedAt(
1174 self.created_at.clone(),
1175 ));
1176 }
1177
1178 let expected_record_id = derive_deletion_request_record_id(
1179 &self.raw_igc_hash,
1180 &self.pilot_id,
1181 &self.created_at,
1182 )?;
1183 if self.record_id != expected_record_id {
1184 return Err(FlightGovernanceRecordError::RecordIdMismatch {
1185 expected: expected_record_id,
1186 found: self.record_id.clone(),
1187 });
1188 }
1189
1190 let signature = decode_flight_governance_signature_hex(&self.signature)?;
1191 let signing_bytes = deletion_request_signing_payload(
1192 &self.record_id,
1193 &self.raw_igc_hash,
1194 &self.pilot_id,
1195 &self.created_at,
1196 )?;
1197 flight_pilot_id_public_key(&self.pilot_id)?
1198 .verify(&signing_bytes, &signature)
1199 .map_err(|_| FlightGovernanceRecordError::SignatureVerification)?;
1200
1201 Ok(())
1202 }
1203}
1204
1205#[derive(Serialize)]
1206struct RecordIdPayload<'a> {
1207 schema: &'static str,
1208 schema_version: u8,
1209 pilot_id: &'a PilotId,
1210 pilot_auth_did: &'a DidKey,
1211 supersedes: &'a Option<Blake3Hex>,
1212 created_at: &'a str,
1213}
1214
1215#[derive(Serialize)]
1216struct SigningPayload<'a> {
1217 schema: &'static str,
1218 schema_version: u8,
1219 record_id: &'a Blake3Hex,
1220 pilot_id: &'a PilotId,
1221 pilot_auth_did: &'a DidKey,
1222 supersedes: &'a Option<Blake3Hex>,
1223 created_at: &'a str,
1224}
1225
1226#[derive(Serialize)]
1227struct PrivateAccessRotationRecordIdPayload<'a> {
1228 schema: &'static str,
1229 schema_version: u8,
1230 pilot_id: &'a PilotId,
1231 private_access_public_key: &'a str,
1232 supersedes: &'a Option<Blake3Hex>,
1233 created_at: &'a str,
1234}
1235
1236#[derive(Serialize)]
1237struct PrivateAccessRotationSigningPayload<'a> {
1238 schema: &'static str,
1239 schema_version: u8,
1240 record_id: &'a Blake3Hex,
1241 pilot_id: &'a PilotId,
1242 private_access_public_key: &'a str,
1243 supersedes: &'a Option<Blake3Hex>,
1244 created_at: &'a str,
1245}
1246
1247#[derive(Serialize)]
1248struct OwnerClaimRecordIdPayload<'a> {
1249 schema: &'static str,
1250 schema_version: u8,
1251 raw_igc_hash: &'a Blake3Hex,
1252 claim_type: &'static str,
1253 pilot_id: &'a PilotId,
1254 created_at: &'a str,
1255 evidence: &'a [serde_json::Value],
1256}
1257
1258#[derive(Serialize)]
1259struct OwnerClaimSigningPayload<'a> {
1260 schema: &'static str,
1261 schema_version: u8,
1262 record_id: &'a Blake3Hex,
1263 raw_igc_hash: &'a Blake3Hex,
1264 claim_type: &'static str,
1265 pilot_id: &'a PilotId,
1266 created_at: &'a str,
1267 evidence: &'a [serde_json::Value],
1268}
1269
1270#[derive(Serialize)]
1271struct ClaimApprovalRecordIdPayload<'a> {
1272 schema: &'static str,
1273 schema_version: u8,
1274 claim_record_id: &'a Blake3Hex,
1275 raw_igc_hash: &'a Blake3Hex,
1276 resolver_id: &'a str,
1277 created_at: &'a str,
1278}
1279
1280#[derive(Serialize)]
1281struct ClaimApprovalSigningPayload<'a> {
1282 schema: &'static str,
1283 schema_version: u8,
1284 record_id: &'a Blake3Hex,
1285 claim_record_id: &'a Blake3Hex,
1286 raw_igc_hash: &'a Blake3Hex,
1287 resolver_id: &'a str,
1288 created_at: &'a str,
1289}
1290
1291#[derive(Serialize)]
1292struct ClaimChallengeRecordIdPayload<'a> {
1293 schema: &'static str,
1294 schema_version: u8,
1295 claim_record_id: &'a Blake3Hex,
1296 raw_igc_hash: &'a Blake3Hex,
1297 challenger_resolver_id: &'a str,
1298 reason: &'a str,
1299 created_at: &'a str,
1300}
1301
1302#[derive(Serialize)]
1303struct ClaimChallengeSigningPayload<'a> {
1304 schema: &'static str,
1305 schema_version: u8,
1306 record_id: &'a Blake3Hex,
1307 claim_record_id: &'a Blake3Hex,
1308 raw_igc_hash: &'a Blake3Hex,
1309 challenger_resolver_id: &'a str,
1310 reason: &'a str,
1311 created_at: &'a str,
1312}
1313
1314#[derive(Serialize)]
1315struct ClaimResolutionRecordIdPayload<'a> {
1316 schema: &'static str,
1317 schema_version: u8,
1318 raw_igc_hash: &'a Blake3Hex,
1319 claim_record_id: &'a Blake3Hex,
1320 resolver_id: &'a str,
1321 resolution: ClaimResolutionOutcome,
1322 basis: &'a [String],
1323 created_at: &'a str,
1324 supersedes: &'a [Blake3Hex],
1325}
1326
1327#[derive(Serialize)]
1328struct ClaimResolutionSigningPayload<'a> {
1329 schema: &'static str,
1330 schema_version: u8,
1331 record_id: &'a Blake3Hex,
1332 raw_igc_hash: &'a Blake3Hex,
1333 claim_record_id: &'a Blake3Hex,
1334 resolver_id: &'a str,
1335 resolution: ClaimResolutionOutcome,
1336 basis: &'a [String],
1337 created_at: &'a str,
1338 supersedes: &'a [Blake3Hex],
1339}
1340
1341#[derive(Serialize)]
1342struct IdentityRecoveryRecordIdPayload<'a> {
1343 schema: &'static str,
1344 schema_version: u8,
1345 old_pilot_id: &'a PilotId,
1346 new_pilot_id: &'a PilotId,
1347 resolver_id: &'a str,
1348 basis: IdentityRecoveryBasis,
1349 created_at: &'a str,
1350}
1351
1352#[derive(Serialize)]
1353struct IdentityRecoverySigningPayload<'a> {
1354 schema: &'static str,
1355 schema_version: u8,
1356 record_id: &'a Blake3Hex,
1357 old_pilot_id: &'a PilotId,
1358 new_pilot_id: &'a PilotId,
1359 resolver_id: &'a str,
1360 basis: IdentityRecoveryBasis,
1361 created_at: &'a str,
1362}
1363
1364#[derive(Serialize)]
1365struct RosterUpdateRecordIdPayload<'a> {
1366 schema: &'static str,
1367 schema_version: u8,
1368 action: RosterUpdateAction,
1369 resolver_id: &'a str,
1370 signer_id: &'a str,
1371 resolver_profile: Option<&'a ResolverProfile>,
1372 created_at: &'a str,
1373}
1374
1375#[derive(Serialize)]
1376struct RosterUpdateSigningPayload<'a> {
1377 schema: &'static str,
1378 schema_version: u8,
1379 record_id: &'a Blake3Hex,
1380 action: RosterUpdateAction,
1381 resolver_id: &'a str,
1382 signer_id: &'a str,
1383 resolver_profile: Option<&'a ResolverProfile>,
1384 created_at: &'a str,
1385}
1386
1387#[derive(Serialize)]
1388struct PublicationModeRecordIdPayload<'a> {
1389 schema: &'static str,
1390 schema_version: u8,
1391 raw_igc_hash: &'a Blake3Hex,
1392 publication_mode: &'a PublicationMode,
1393 protected_hash: Option<&'a Blake3Hex>,
1394 supersedes: &'a Option<Blake3Hex>,
1395 pilot_id: &'a PilotId,
1396 created_at: &'a str,
1397}
1398
1399#[derive(Serialize)]
1400struct PublicationModeSigningPayload<'a> {
1401 schema: &'static str,
1402 schema_version: u8,
1403 record_id: &'a Blake3Hex,
1404 raw_igc_hash: &'a Blake3Hex,
1405 publication_mode: &'a PublicationMode,
1406 protected_hash: Option<&'a Blake3Hex>,
1407 supersedes: &'a Option<Blake3Hex>,
1408 pilot_id: &'a PilotId,
1409 created_at: &'a str,
1410}
1411
1412#[derive(Serialize)]
1413struct DeletionRequestRecordIdPayload<'a> {
1414 schema: &'static str,
1415 schema_version: u8,
1416 raw_igc_hash: &'a Blake3Hex,
1417 pilot_id: &'a PilotId,
1418 created_at: &'a str,
1419}
1420
1421#[derive(Serialize)]
1422struct DeletionRequestSigningPayload<'a> {
1423 schema: &'static str,
1424 schema_version: u8,
1425 record_id: &'a Blake3Hex,
1426 raw_igc_hash: &'a Blake3Hex,
1427 pilot_id: &'a PilotId,
1428 created_at: &'a str,
1429}
1430
1431fn derive_record_id(
1432 pilot_id: &PilotId,
1433 pilot_auth_did: &DidKey,
1434 supersedes: &Option<Blake3Hex>,
1435 created_at: &str,
1436) -> Result<Blake3Hex, PilotAuthDidRecordError> {
1437 let payload = RecordIdPayload {
1442 schema: PILOT_AUTH_DID_RECORD_SCHEMA,
1443 schema_version: PILOT_AUTH_DID_RECORD_VERSION,
1444 pilot_id,
1445 pilot_auth_did,
1446 supersedes,
1447 created_at,
1448 };
1449 let canonical_bytes = canonical_json_bytes(&payload)?;
1450 Ok(Blake3Hex::from_hash(blake3::hash(&canonical_bytes)))
1451}
1452
1453fn signing_payload(
1454 pilot_id: &PilotId,
1455 record_id: &Blake3Hex,
1456 pilot_auth_did: &DidKey,
1457 supersedes: &Option<Blake3Hex>,
1458 created_at: &str,
1459) -> Result<Vec<u8>, PilotAuthDidRecordError> {
1460 let payload = SigningPayload {
1461 schema: PILOT_AUTH_DID_RECORD_SCHEMA,
1462 schema_version: PILOT_AUTH_DID_RECORD_VERSION,
1463 record_id,
1464 pilot_id,
1465 pilot_auth_did,
1466 supersedes,
1467 created_at,
1468 };
1469 canonical_json_bytes(&payload)
1470}
1471
1472fn derive_private_access_rotation_record_id(
1473 pilot_id: &PilotId,
1474 private_access_public_key: &str,
1475 supersedes: &Option<Blake3Hex>,
1476 created_at: &str,
1477) -> Result<Blake3Hex, PrivateAccessRotationRecordError> {
1478 validate_private_access_public_key(private_access_public_key)?;
1479 let payload = PrivateAccessRotationRecordIdPayload {
1480 schema: PRIVATE_ACCESS_ROTATION_RECORD_SCHEMA,
1481 schema_version: PRIVATE_ACCESS_ROTATION_RECORD_VERSION,
1482 pilot_id,
1483 private_access_public_key,
1484 supersedes,
1485 created_at,
1486 };
1487 let canonical_bytes = json_canon::to_vec(&payload)?;
1488 Ok(Blake3Hex::from_hash(blake3::hash(&canonical_bytes)))
1489}
1490
1491fn private_access_rotation_signing_payload(
1492 pilot_id: &PilotId,
1493 record_id: &Blake3Hex,
1494 private_access_public_key: &str,
1495 supersedes: &Option<Blake3Hex>,
1496 created_at: &str,
1497) -> Result<Vec<u8>, PrivateAccessRotationRecordError> {
1498 validate_private_access_public_key(private_access_public_key)?;
1499 let payload = PrivateAccessRotationSigningPayload {
1500 schema: PRIVATE_ACCESS_ROTATION_RECORD_SCHEMA,
1501 schema_version: PRIVATE_ACCESS_ROTATION_RECORD_VERSION,
1502 record_id,
1503 pilot_id,
1504 private_access_public_key,
1505 supersedes,
1506 created_at,
1507 };
1508 Ok(json_canon::to_vec(&payload)?)
1509}
1510
1511fn derive_owner_claim_record_id(
1512 raw_igc_hash: &Blake3Hex,
1513 pilot_id: &PilotId,
1514 created_at: &str,
1515 evidence: &[serde_json::Value],
1516) -> Result<Blake3Hex, FlightGovernanceRecordError> {
1517 let payload = OwnerClaimRecordIdPayload {
1518 schema: OWNER_CLAIM_RECORD_SCHEMA,
1519 schema_version: OWNER_CLAIM_RECORD_VERSION,
1520 raw_igc_hash,
1521 claim_type: OWNER_CLAIM_TYPE,
1522 pilot_id,
1523 created_at,
1524 evidence,
1525 };
1526 let canonical_bytes = json_canon::to_vec(&payload)?;
1527 Ok(Blake3Hex::from_hash(blake3::hash(&canonical_bytes)))
1528}
1529
1530fn owner_claim_signing_payload(
1531 record_id: &Blake3Hex,
1532 raw_igc_hash: &Blake3Hex,
1533 pilot_id: &PilotId,
1534 created_at: &str,
1535 evidence: &[serde_json::Value],
1536) -> Result<Vec<u8>, FlightGovernanceRecordError> {
1537 let payload = OwnerClaimSigningPayload {
1538 schema: OWNER_CLAIM_RECORD_SCHEMA,
1539 schema_version: OWNER_CLAIM_RECORD_VERSION,
1540 record_id,
1541 raw_igc_hash,
1542 claim_type: OWNER_CLAIM_TYPE,
1543 pilot_id,
1544 created_at,
1545 evidence,
1546 };
1547 Ok(json_canon::to_vec(&payload)?)
1548}
1549
1550fn derive_claim_approval_record_id(
1551 claim_record_id: &Blake3Hex,
1552 raw_igc_hash: &Blake3Hex,
1553 resolver_id: &str,
1554 created_at: &str,
1555) -> Result<Blake3Hex, FlightGovernanceRecordError> {
1556 validate_resolver_id(resolver_id)?;
1557 let payload = ClaimApprovalRecordIdPayload {
1558 schema: CLAIM_APPROVAL_RECORD_SCHEMA,
1559 schema_version: CLAIM_APPROVAL_RECORD_VERSION,
1560 claim_record_id,
1561 raw_igc_hash,
1562 resolver_id,
1563 created_at,
1564 };
1565 let canonical_bytes = json_canon::to_vec(&payload)?;
1566 Ok(Blake3Hex::from_hash(blake3::hash(&canonical_bytes)))
1567}
1568
1569fn claim_approval_signing_payload(
1570 record_id: &Blake3Hex,
1571 claim_record_id: &Blake3Hex,
1572 raw_igc_hash: &Blake3Hex,
1573 resolver_id: &str,
1574 created_at: &str,
1575) -> Result<Vec<u8>, FlightGovernanceRecordError> {
1576 validate_resolver_id(resolver_id)?;
1577 let payload = ClaimApprovalSigningPayload {
1578 schema: CLAIM_APPROVAL_RECORD_SCHEMA,
1579 schema_version: CLAIM_APPROVAL_RECORD_VERSION,
1580 record_id,
1581 claim_record_id,
1582 raw_igc_hash,
1583 resolver_id,
1584 created_at,
1585 };
1586 Ok(json_canon::to_vec(&payload)?)
1587}
1588
1589fn derive_claim_challenge_record_id(
1590 claim_record_id: &Blake3Hex,
1591 raw_igc_hash: &Blake3Hex,
1592 challenger_resolver_id: &str,
1593 reason: &str,
1594 created_at: &str,
1595) -> Result<Blake3Hex, FlightGovernanceRecordError> {
1596 validate_challenger_resolver_id(challenger_resolver_id)?;
1597 let payload = ClaimChallengeRecordIdPayload {
1598 schema: CLAIM_CHALLENGE_RECORD_SCHEMA,
1599 schema_version: CLAIM_CHALLENGE_RECORD_VERSION,
1600 claim_record_id,
1601 raw_igc_hash,
1602 challenger_resolver_id,
1603 reason,
1604 created_at,
1605 };
1606 let canonical_bytes = json_canon::to_vec(&payload)?;
1607 Ok(Blake3Hex::from_hash(blake3::hash(&canonical_bytes)))
1608}
1609
1610fn claim_challenge_signing_payload(
1611 record_id: &Blake3Hex,
1612 claim_record_id: &Blake3Hex,
1613 raw_igc_hash: &Blake3Hex,
1614 challenger_resolver_id: &str,
1615 reason: &str,
1616 created_at: &str,
1617) -> Result<Vec<u8>, FlightGovernanceRecordError> {
1618 validate_challenger_resolver_id(challenger_resolver_id)?;
1619 let payload = ClaimChallengeSigningPayload {
1620 schema: CLAIM_CHALLENGE_RECORD_SCHEMA,
1621 schema_version: CLAIM_CHALLENGE_RECORD_VERSION,
1622 record_id,
1623 claim_record_id,
1624 raw_igc_hash,
1625 challenger_resolver_id,
1626 reason,
1627 created_at,
1628 };
1629 Ok(json_canon::to_vec(&payload)?)
1630}
1631
1632fn derive_claim_resolution_record_id(
1633 raw_igc_hash: &Blake3Hex,
1634 claim_record_id: &Blake3Hex,
1635 resolver_id: &str,
1636 resolution: ClaimResolutionOutcome,
1637 basis: &[String],
1638 created_at: &str,
1639 supersedes: &[Blake3Hex],
1640) -> Result<Blake3Hex, FlightGovernanceRecordError> {
1641 validate_resolver_id(resolver_id)?;
1642 let payload = ClaimResolutionRecordIdPayload {
1643 schema: CLAIM_RESOLUTION_RECORD_SCHEMA,
1644 schema_version: CLAIM_RESOLUTION_RECORD_VERSION,
1645 raw_igc_hash,
1646 claim_record_id,
1647 resolver_id,
1648 resolution,
1649 basis,
1650 created_at,
1651 supersedes,
1652 };
1653 let canonical_bytes = json_canon::to_vec(&payload)?;
1654 Ok(Blake3Hex::from_hash(blake3::hash(&canonical_bytes)))
1655}
1656
1657fn claim_resolution_signing_payload(
1658 record_id: &Blake3Hex,
1659 raw_igc_hash: &Blake3Hex,
1660 claim_record_id: &Blake3Hex,
1661 resolver_id: &str,
1662 resolution: ClaimResolutionOutcome,
1663 basis: &[String],
1664 created_at: &str,
1665 supersedes: &[Blake3Hex],
1666) -> Result<Vec<u8>, FlightGovernanceRecordError> {
1667 validate_resolver_id(resolver_id)?;
1668 let payload = ClaimResolutionSigningPayload {
1669 schema: CLAIM_RESOLUTION_RECORD_SCHEMA,
1670 schema_version: CLAIM_RESOLUTION_RECORD_VERSION,
1671 record_id,
1672 raw_igc_hash,
1673 claim_record_id,
1674 resolver_id,
1675 resolution,
1676 basis,
1677 created_at,
1678 supersedes,
1679 };
1680 Ok(json_canon::to_vec(&payload)?)
1681}
1682
1683fn derive_identity_recovery_record_id(
1684 old_pilot_id: &PilotId,
1685 new_pilot_id: &PilotId,
1686 resolver_id: &str,
1687 basis: IdentityRecoveryBasis,
1688 created_at: &str,
1689) -> Result<Blake3Hex, FlightGovernanceRecordError> {
1690 if old_pilot_id == new_pilot_id {
1691 return Err(FlightGovernanceRecordError::IdentityRecoverySamePilot);
1692 }
1693 validate_resolver_id(resolver_id)?;
1694 let payload = IdentityRecoveryRecordIdPayload {
1695 schema: IDENTITY_RECOVERY_RECORD_SCHEMA,
1696 schema_version: IDENTITY_RECOVERY_RECORD_VERSION,
1697 old_pilot_id,
1698 new_pilot_id,
1699 resolver_id,
1700 basis,
1701 created_at,
1702 };
1703 let canonical_bytes = json_canon::to_vec(&payload)?;
1704 Ok(Blake3Hex::from_hash(blake3::hash(&canonical_bytes)))
1705}
1706
1707fn identity_recovery_signing_payload(
1708 record_id: &Blake3Hex,
1709 old_pilot_id: &PilotId,
1710 new_pilot_id: &PilotId,
1711 resolver_id: &str,
1712 basis: IdentityRecoveryBasis,
1713 created_at: &str,
1714) -> Result<Vec<u8>, FlightGovernanceRecordError> {
1715 if old_pilot_id == new_pilot_id {
1716 return Err(FlightGovernanceRecordError::IdentityRecoverySamePilot);
1717 }
1718 validate_resolver_id(resolver_id)?;
1719 let payload = IdentityRecoverySigningPayload {
1720 schema: IDENTITY_RECOVERY_RECORD_SCHEMA,
1721 schema_version: IDENTITY_RECOVERY_RECORD_VERSION,
1722 record_id,
1723 old_pilot_id,
1724 new_pilot_id,
1725 resolver_id,
1726 basis,
1727 created_at,
1728 };
1729 Ok(json_canon::to_vec(&payload)?)
1730}
1731
1732fn derive_roster_update_record_id(
1733 action: RosterUpdateAction,
1734 resolver_id: &str,
1735 signer_id: &str,
1736 resolver_profile: Option<&ResolverProfile>,
1737 created_at: &str,
1738) -> Result<Blake3Hex, FlightGovernanceRecordError> {
1739 validate_resolver_id(resolver_id)?;
1740 validate_signer_id(signer_id)?;
1741 validate_roster_profile_presence(action, resolver_profile)?;
1742 let payload = RosterUpdateRecordIdPayload {
1743 schema: ROSTER_UPDATE_RECORD_SCHEMA,
1744 schema_version: ROSTER_UPDATE_RECORD_VERSION,
1745 action,
1746 resolver_id,
1747 signer_id,
1748 resolver_profile,
1749 created_at,
1750 };
1751 let canonical_bytes = json_canon::to_vec(&payload)?;
1752 Ok(Blake3Hex::from_hash(blake3::hash(&canonical_bytes)))
1753}
1754
1755fn roster_update_signing_payload(
1756 record_id: &Blake3Hex,
1757 action: RosterUpdateAction,
1758 resolver_id: &str,
1759 signer_id: &str,
1760 resolver_profile: Option<&ResolverProfile>,
1761 created_at: &str,
1762) -> Result<Vec<u8>, FlightGovernanceRecordError> {
1763 validate_resolver_id(resolver_id)?;
1764 validate_signer_id(signer_id)?;
1765 validate_roster_profile_presence(action, resolver_profile)?;
1766 let payload = RosterUpdateSigningPayload {
1767 schema: ROSTER_UPDATE_RECORD_SCHEMA,
1768 schema_version: ROSTER_UPDATE_RECORD_VERSION,
1769 record_id,
1770 action,
1771 resolver_id,
1772 signer_id,
1773 resolver_profile,
1774 created_at,
1775 };
1776 Ok(json_canon::to_vec(&payload)?)
1777}
1778
1779fn derive_publication_mode_record_id(
1780 raw_igc_hash: &Blake3Hex,
1781 publication_mode: &PublicationMode,
1782 protected_hash: Option<&Blake3Hex>,
1783 supersedes: &Option<Blake3Hex>,
1784 pilot_id: &PilotId,
1785 created_at: &str,
1786) -> Result<Blake3Hex, FlightGovernanceRecordError> {
1787 validate_publication_mode_hash(publication_mode, protected_hash)?;
1788 let payload = PublicationModeRecordIdPayload {
1789 schema: PUBLICATION_MODE_RECORD_SCHEMA,
1790 schema_version: PUBLICATION_MODE_RECORD_VERSION,
1791 raw_igc_hash,
1792 publication_mode,
1793 protected_hash,
1794 supersedes,
1795 pilot_id,
1796 created_at,
1797 };
1798 let canonical_bytes = json_canon::to_vec(&payload)?;
1799 Ok(Blake3Hex::from_hash(blake3::hash(&canonical_bytes)))
1800}
1801
1802fn publication_mode_signing_payload(
1803 record_id: &Blake3Hex,
1804 raw_igc_hash: &Blake3Hex,
1805 publication_mode: &PublicationMode,
1806 protected_hash: Option<&Blake3Hex>,
1807 supersedes: &Option<Blake3Hex>,
1808 pilot_id: &PilotId,
1809 created_at: &str,
1810) -> Result<Vec<u8>, FlightGovernanceRecordError> {
1811 validate_publication_mode_hash(publication_mode, protected_hash)?;
1812 let payload = PublicationModeSigningPayload {
1813 schema: PUBLICATION_MODE_RECORD_SCHEMA,
1814 schema_version: PUBLICATION_MODE_RECORD_VERSION,
1815 record_id,
1816 raw_igc_hash,
1817 publication_mode,
1818 protected_hash,
1819 supersedes,
1820 pilot_id,
1821 created_at,
1822 };
1823 Ok(json_canon::to_vec(&payload)?)
1824}
1825
1826fn derive_deletion_request_record_id(
1827 raw_igc_hash: &Blake3Hex,
1828 pilot_id: &PilotId,
1829 created_at: &str,
1830) -> Result<Blake3Hex, FlightGovernanceRecordError> {
1831 let payload = DeletionRequestRecordIdPayload {
1832 schema: DELETION_REQUEST_RECORD_SCHEMA,
1833 schema_version: DELETION_REQUEST_RECORD_VERSION,
1834 raw_igc_hash,
1835 pilot_id,
1836 created_at,
1837 };
1838 let canonical_bytes = json_canon::to_vec(&payload)?;
1839 Ok(Blake3Hex::from_hash(blake3::hash(&canonical_bytes)))
1840}
1841
1842fn deletion_request_signing_payload(
1843 record_id: &Blake3Hex,
1844 raw_igc_hash: &Blake3Hex,
1845 pilot_id: &PilotId,
1846 created_at: &str,
1847) -> Result<Vec<u8>, FlightGovernanceRecordError> {
1848 let payload = DeletionRequestSigningPayload {
1849 schema: DELETION_REQUEST_RECORD_SCHEMA,
1850 schema_version: DELETION_REQUEST_RECORD_VERSION,
1851 record_id,
1852 raw_igc_hash,
1853 pilot_id,
1854 created_at,
1855 };
1856 Ok(json_canon::to_vec(&payload)?)
1857}
1858
1859fn canonical_json_bytes<T: Serialize>(value: &T) -> Result<Vec<u8>, PilotAuthDidRecordError> {
1860 Ok(json_canon::to_vec(value)?)
1861}
1862
1863fn pilot_id_public_key(pilot_id: &PilotId) -> Result<iroh::PublicKey, PilotAuthDidRecordError> {
1864 let public_key_bytes = decode_fixed_hex_32(pilot_id.public_key_hex())
1865 .map_err(|_| PilotAuthDidRecordError::PilotIdPublicKey(pilot_id.to_string()))?;
1866 iroh::PublicKey::from_bytes(&public_key_bytes)
1867 .map_err(|_| PilotAuthDidRecordError::PilotIdPublicKey(pilot_id.to_string()))
1868}
1869
1870fn decode_fixed_hex_32(value: &str) -> Result<[u8; 32], hex::FromHexError> {
1871 let bytes = hex::decode(value)?;
1872 bytes
1873 .try_into()
1874 .map_err(|_| hex::FromHexError::InvalidStringLength)
1875}
1876
1877fn decode_signature_hex(value: &str) -> Result<iroh::Signature, PilotAuthDidRecordError> {
1878 if value.len() != 128
1879 || !value
1880 .bytes()
1881 .all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f'))
1882 {
1883 return Err(PilotAuthDidRecordError::SignatureEncoding);
1884 }
1885 let bytes = hex::decode(value).map_err(|_| PilotAuthDidRecordError::SignatureEncoding)?;
1886 let signature_bytes: [u8; 64] = bytes
1887 .try_into()
1888 .map_err(|_| PilotAuthDidRecordError::SignatureEncoding)?;
1889 Ok(iroh::Signature::from_bytes(&signature_bytes))
1890}
1891
1892fn validate_private_access_public_key(value: &str) -> Result<(), PrivateAccessRotationRecordError> {
1893 if value.len() != 64
1894 || !value
1895 .bytes()
1896 .all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f'))
1897 {
1898 return Err(PrivateAccessRotationRecordError::PrivateAccessPublicKeyEncoding);
1899 }
1900 decode_fixed_hex_32(value)
1901 .and_then(|bytes| {
1902 iroh::PublicKey::from_bytes(&bytes)
1903 .map(|_| bytes)
1904 .map_err(|_| hex::FromHexError::InvalidStringLength)
1905 })
1906 .map_err(|_| PrivateAccessRotationRecordError::PrivateAccessPublicKeyEncoding)?;
1907 Ok(())
1908}
1909
1910fn validate_resolver_id(value: &str) -> Result<(), FlightGovernanceRecordError> {
1911 if value.len() != 64
1912 || !value
1913 .bytes()
1914 .all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f'))
1915 {
1916 return Err(FlightGovernanceRecordError::ResolverIdEncoding(
1917 value.to_string(),
1918 ));
1919 }
1920 Ok(())
1921}
1922
1923fn validate_challenger_resolver_id(value: &str) -> Result<(), FlightGovernanceRecordError> {
1924 if value.len() != 64
1925 || !value
1926 .bytes()
1927 .all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f'))
1928 {
1929 return Err(FlightGovernanceRecordError::ChallengerResolverIdEncoding(
1930 value.to_string(),
1931 ));
1932 }
1933 Ok(())
1934}
1935
1936fn validate_signer_id(value: &str) -> Result<(), FlightGovernanceRecordError> {
1937 if value.len() != 64
1938 || !value
1939 .bytes()
1940 .all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f'))
1941 {
1942 return Err(FlightGovernanceRecordError::SignerIdEncoding(
1943 value.to_string(),
1944 ));
1945 }
1946 Ok(())
1947}
1948
1949fn validate_roster_profile_presence(
1950 action: RosterUpdateAction,
1951 resolver_profile: Option<&ResolverProfile>,
1952) -> Result<(), FlightGovernanceRecordError> {
1953 match (action, resolver_profile) {
1954 (RosterUpdateAction::Add, Some(_)) | (RosterUpdateAction::Remove, None) => Ok(()),
1955 _ => Err(FlightGovernanceRecordError::RosterProfilePresence),
1956 }
1957}
1958
1959fn validate_publication_mode_hash(
1960 mode: &PublicationMode,
1961 protected_hash: Option<&Blake3Hex>,
1962) -> Result<(), FlightGovernanceRecordError> {
1963 match (mode, protected_hash) {
1964 (PublicationMode::Protected, Some(_)) => Ok(()),
1965 (PublicationMode::Protected, None) => {
1966 Err(FlightGovernanceRecordError::ProtectedHashPresence)
1967 }
1968 (PublicationMode::Public | PublicationMode::Private, None) => Ok(()),
1969 (PublicationMode::Public | PublicationMode::Private, Some(_)) => {
1970 Err(FlightGovernanceRecordError::ProtectedHashPresence)
1971 }
1972 }
1973}
1974
1975fn resolver_public_key(value: &str) -> Result<iroh::PublicKey, FlightGovernanceRecordError> {
1976 let public_key_bytes = decode_fixed_hex_32(value)
1977 .map_err(|_| FlightGovernanceRecordError::ResolverIdEncoding(value.to_string()))?;
1978 iroh::PublicKey::from_bytes(&public_key_bytes)
1979 .map_err(|_| FlightGovernanceRecordError::ResolverIdEncoding(value.to_string()))
1980}
1981
1982fn signer_public_key(value: &str) -> Result<iroh::PublicKey, FlightGovernanceRecordError> {
1983 let public_key_bytes = decode_fixed_hex_32(value)
1984 .map_err(|_| FlightGovernanceRecordError::SignerIdEncoding(value.to_string()))?;
1985 iroh::PublicKey::from_bytes(&public_key_bytes)
1986 .map_err(|_| FlightGovernanceRecordError::SignerIdEncoding(value.to_string()))
1987}
1988
1989fn decode_private_access_rotation_signature_hex(
1990 value: &str,
1991) -> Result<iroh::Signature, PrivateAccessRotationRecordError> {
1992 if value.len() != 128
1993 || !value
1994 .bytes()
1995 .all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f'))
1996 {
1997 return Err(PrivateAccessRotationRecordError::SignatureEncoding);
1998 }
1999 let bytes =
2000 hex::decode(value).map_err(|_| PrivateAccessRotationRecordError::SignatureEncoding)?;
2001 let signature_bytes: [u8; 64] = bytes
2002 .try_into()
2003 .map_err(|_| PrivateAccessRotationRecordError::SignatureEncoding)?;
2004 Ok(iroh::Signature::from_bytes(&signature_bytes))
2005}
2006
2007fn flight_pilot_id_public_key(
2008 pilot_id: &PilotId,
2009) -> Result<iroh::PublicKey, FlightGovernanceRecordError> {
2010 let public_key_bytes = decode_fixed_hex_32(pilot_id.public_key_hex())
2011 .map_err(|_| FlightGovernanceRecordError::PilotIdPublicKey(pilot_id.to_string()))?;
2012 iroh::PublicKey::from_bytes(&public_key_bytes)
2013 .map_err(|_| FlightGovernanceRecordError::PilotIdPublicKey(pilot_id.to_string()))
2014}
2015
2016fn decode_flight_governance_signature_hex(
2017 value: &str,
2018) -> Result<iroh::Signature, FlightGovernanceRecordError> {
2019 if value.len() != 128
2020 || !value
2021 .bytes()
2022 .all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f'))
2023 {
2024 return Err(FlightGovernanceRecordError::SignatureEncoding);
2025 }
2026 let bytes = hex::decode(value).map_err(|_| FlightGovernanceRecordError::SignatureEncoding)?;
2027 let signature_bytes: [u8; 64] = bytes
2028 .try_into()
2029 .map_err(|_| FlightGovernanceRecordError::SignatureEncoding)?;
2030 Ok(iroh::Signature::from_bytes(&signature_bytes))
2031}
2032
2033#[cfg(test)]
2034mod tests {
2035 use super::*;
2036
2037 fn deterministic_secret_key(byte: u8) -> iroh::SecretKey {
2038 iroh::SecretKey::from_bytes(&[byte; 32])
2039 }
2040
2041 #[test]
2042 fn rauth_05_signed_record_round_trips_and_validates() {
2043 let pilot_root = deterministic_secret_key(41);
2044 let pilot_auth_did = DidKey::from_public_key(deterministic_secret_key(42).public());
2045
2046 let record = PilotAuthDidRecord::issue(
2047 &pilot_root,
2048 pilot_auth_did.clone(),
2049 None,
2050 "2026-05-01T09:14:00Z",
2051 )
2052 .unwrap();
2053
2054 record.validate().unwrap();
2055 assert_eq!(record.pilot_auth_did, pilot_auth_did);
2056 }
2057
2058 #[test]
2059 fn rauth_03_rejects_invalid_did_key() {
2060 let err = DidKey::parse("did:key:z0bad").unwrap_err();
2061 assert!(matches!(err, DidKeyError::InvalidFormat(_)));
2062 }
2063
2064 #[test]
2065 fn rthreat_06_record_id_mismatch_is_rejected() {
2066 let pilot_root = deterministic_secret_key(61);
2067 let mut record = PilotAuthDidRecord::issue(
2068 &pilot_root,
2069 DidKey::from_public_key(deterministic_secret_key(62).public()),
2070 None,
2071 "2026-05-01T09:14:00Z",
2072 )
2073 .unwrap();
2074 record.record_id = Blake3Hex::parse("c".repeat(64)).unwrap();
2075
2076 let err = record.validate().unwrap_err();
2077 assert!(matches!(
2078 err,
2079 PilotAuthDidRecordError::RecordIdMismatch { .. }
2080 ));
2081 }
2082
2083 #[test]
2084 fn rauth_05_signature_failure_is_rejected() {
2085 let pilot_root = deterministic_secret_key(71);
2086 let mut record = PilotAuthDidRecord::issue(
2087 &pilot_root,
2088 DidKey::from_public_key(deterministic_secret_key(72).public()),
2089 None,
2090 "2026-05-01T09:14:00Z",
2091 )
2092 .unwrap();
2093 record.signature = "0".repeat(128);
2094
2095 let err = record.validate().unwrap_err();
2096 assert!(matches!(
2097 err,
2098 PilotAuthDidRecordError::SignatureVerification
2099 ));
2100 }
2101
2102 #[test]
2103 fn created_at_must_be_canonical() {
2104 let err = PilotAuthDidRecord::issue(
2105 &deterministic_secret_key(81),
2106 DidKey::from_public_key(deterministic_secret_key(82).public()),
2107 None,
2108 "2026-05-01T09:14:00+00:00",
2109 )
2110 .unwrap_err();
2111 assert!(matches!(err, PilotAuthDidRecordError::CreatedAt(_)));
2112 }
2113
2114 #[test]
2115 fn canonical_json_matches_rfc_8785_number_and_key_order_rules() {
2116 let value = serde_json::json!({
2117 "z": 1.0,
2118 "a": "\u{000f}",
2119 "m": -0.0,
2120 });
2121
2122 let canonical = canonical_json_bytes(&value).unwrap();
2123 assert_eq!(canonical, br#"{"a":"\u000f","m":0,"z":1}"#);
2124 }
2125
2126 #[test]
2127 fn private_access_rotation_record_round_trips_and_validates() {
2128 let pilot_root = deterministic_secret_key(91);
2129 let private_access_key = deterministic_secret_key(92);
2130
2131 let record = PrivateAccessRotationRecord::issue(
2132 &pilot_root,
2133 private_access_key.public(),
2134 None,
2135 "2026-05-01T09:14:00Z",
2136 )
2137 .unwrap();
2138
2139 record.validate().unwrap();
2140 assert_eq!(
2141 record.pilot_id,
2142 PilotId::from_public_key(pilot_root.public())
2143 );
2144 assert_eq!(
2145 record.private_access_public_key().unwrap(),
2146 private_access_key.public()
2147 );
2148 }
2149
2150 #[test]
2151 fn private_access_rotation_rejects_signature_failure() {
2152 let pilot_root = deterministic_secret_key(93);
2153 let mut record = PrivateAccessRotationRecord::issue(
2154 &pilot_root,
2155 deterministic_secret_key(94).public(),
2156 None,
2157 "2026-05-01T09:14:00Z",
2158 )
2159 .unwrap();
2160 record.private_access_public_key = deterministic_secret_key(95).public().to_string();
2161
2162 let err = record.validate().unwrap_err();
2163 assert!(matches!(
2164 err,
2165 PrivateAccessRotationRecordError::RecordIdMismatch { .. }
2166 | PrivateAccessRotationRecordError::SignatureVerification
2167 ));
2168 }
2169
2170 #[test]
2171 fn owner_claim_record_round_trips_and_validates() {
2172 let pilot_root = deterministic_secret_key(101);
2173 let raw_igc_hash = Blake3Hex::parse("d".repeat(64)).unwrap();
2174
2175 let record = OwnerClaimRecord::issue(
2176 &pilot_root,
2177 raw_igc_hash.clone(),
2178 "2026-05-01T09:14:00Z",
2179 Vec::new(),
2180 )
2181 .unwrap();
2182
2183 record.validate().unwrap();
2184 assert_eq!(record.raw_igc_hash, raw_igc_hash);
2185 assert_eq!(record.claim_type, "owner");
2186 assert_eq!(
2187 record.pilot_id,
2188 PilotId::from_public_key(pilot_root.public())
2189 );
2190 }
2191
2192 #[test]
2193 fn deletion_request_record_round_trips_and_validates() {
2194 let pilot_root = deterministic_secret_key(102);
2195 let raw_igc_hash = Blake3Hex::parse("e".repeat(64)).unwrap();
2196
2197 let record =
2198 DeletionRequestRecord::issue(&pilot_root, raw_igc_hash.clone(), "2026-05-01T09:14:00Z")
2199 .unwrap();
2200
2201 record.validate().unwrap();
2202 assert_eq!(record.raw_igc_hash, raw_igc_hash);
2203 assert_eq!(
2204 record.pilot_id,
2205 PilotId::from_public_key(pilot_root.public())
2206 );
2207 }
2208
2209 #[test]
2210 fn publication_mode_record_round_trips_and_validates_hash_presence() {
2211 let pilot_root = deterministic_secret_key(103);
2212 let raw_igc_hash = Blake3Hex::parse("f".repeat(64)).unwrap();
2213 let protected_hash = Blake3Hex::parse("1".repeat(64)).unwrap();
2214
2215 let record = PublicationModeRecord::issue(
2216 &pilot_root,
2217 raw_igc_hash.clone(),
2218 PublicationMode::Protected,
2219 Some(protected_hash.clone()),
2220 None,
2221 "2026-05-01T09:14:00Z",
2222 )
2223 .unwrap();
2224
2225 record.validate().unwrap();
2226 assert_eq!(record.raw_igc_hash, raw_igc_hash);
2227 assert_eq!(record.publication_mode, PublicationMode::Protected);
2228 assert_eq!(record.protected_hash, Some(protected_hash));
2229 assert_eq!(
2230 record.pilot_id,
2231 PilotId::from_public_key(pilot_root.public())
2232 );
2233
2234 let protected_without_hash = PublicationModeRecord::issue(
2235 &pilot_root,
2236 Blake3Hex::parse("2".repeat(64)).unwrap(),
2237 PublicationMode::Protected,
2238 None,
2239 None,
2240 "2026-05-01T09:14:00Z",
2241 )
2242 .unwrap_err();
2243 assert!(matches!(
2244 protected_without_hash,
2245 FlightGovernanceRecordError::ProtectedHashPresence
2246 ));
2247
2248 let public_with_hash = PublicationModeRecord::issue(
2249 &pilot_root,
2250 Blake3Hex::parse("3".repeat(64)).unwrap(),
2251 PublicationMode::Public,
2252 Some(Blake3Hex::parse("4".repeat(64)).unwrap()),
2253 None,
2254 "2026-05-01T09:14:00Z",
2255 )
2256 .unwrap_err();
2257 assert!(matches!(
2258 public_with_hash,
2259 FlightGovernanceRecordError::ProtectedHashPresence
2260 ));
2261 }
2262}