Skip to main content

igc_net/governance/
record.rs

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    // The written spec says `record_id` is derived from `record_without_signature`,
1438    // but that becomes circular if `record_id` itself remains inside the payload.
1439    // The workable interpretation is to hash the unsigned payload without both
1440    // `record_id` and `signature`, then sign the payload that includes `record_id`.
1441    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}