Skip to main content

redevplugin_contracts/
release_signing.rs

1use base64::Engine;
2use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use sha2::{Digest, Sha256};
6use std::collections::BTreeMap;
7use std::error::Error;
8use std::fmt;
9
10pub const ROOT_DELEGATION_SCHEMA_VERSION: &str = "redevplugin.release_root_delegation.v1";
11pub const PACKAGE_SIGNATURE_SCHEMA_VERSION: &str = "redevplugin.package_signature.v1";
12pub const RELEASE_METADATA_SCHEMA_VERSION_V5: &str = "redevplugin.release_metadata.v5";
13pub const RELEASE_METADATA_SCHEMA_VERSION_V6: &str = "redevplugin.release_metadata.v6";
14pub const RELEASE_METADATA_SCHEMA_VERSION: &str = RELEASE_METADATA_SCHEMA_VERSION_V6;
15pub const SOURCE_POLICY_SCHEMA_VERSION: &str = "redevplugin.release_source_policy.v2";
16pub const SOURCE_POLICY_POINTER_SCHEMA_VERSION: &str =
17    "redevplugin.release_source_policy_pointer.v1";
18pub const REVOCATION_SCHEMA_VERSION: &str = "redevplugin.release_revocation.v2";
19pub const REVOCATION_POINTER_SCHEMA_VERSION: &str = "redevplugin.release_revocation_pointer.v1";
20pub const SIGNING_LEDGER_EVIDENCE_SCHEMA_VERSION: &str =
21    "redevplugin.release_signing_ledger_evidence.v1";
22pub const SIGNING_SUBJECT_SCHEMA_VERSION: &str = "redevplugin.release_signing_subject.v1";
23pub const SIGNATURE_ENVELOPE_SCHEMA_VERSION: &str = "redevplugin.release_signature_envelope.v1";
24pub const SIGNING_LEDGER_SCHEMA_VERSION: &str = "redevplugin.release_signing_ledger.v1";
25pub const SIGNING_LEDGER_ENTRY_SCHEMA_VERSION: &str = "redevplugin.release_signing_ledger_entry.v1";
26pub const SIGNING_LEDGER_LOG_LEAF_SCHEMA_VERSION: &str =
27    "redevplugin.release_signing_ledger_log_leaf.v1";
28pub const SIGNING_LEDGER_RECEIPT_SCHEMA_VERSION: &str =
29    "redevplugin.release_signing_ledger_receipt.v1";
30pub const SIGNATURE_ALGORITHM_ED25519: &str = "ed25519";
31pub const GENESIS_PREVIOUS_EPOCH: &str = "0";
32pub const GENESIS_PREVIOUS_DOCUMENT_SHA256: &str =
33    "0000000000000000000000000000000000000000000000000000000000000000";
34
35const MAX_DOCUMENT_BYTES: usize = 1024 * 1024;
36const SIGNING_PREFIX: &[u8] = b"REDEVPLUGIN-SIGNING-V1\0";
37
38#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
39pub enum SigningUsage {
40    RootDelegation,
41    Package,
42    ReleaseMetadata,
43    SourcePolicy,
44    SourcePolicyPointer,
45    Revocation,
46    RevocationPointer,
47}
48
49impl SigningUsage {
50    pub const fn as_str(self) -> &'static str {
51        match self {
52            Self::RootDelegation => "redevplugin.release-signing.root-delegation.v1",
53            Self::Package => "redevplugin.release-signing.package.v1",
54            Self::ReleaseMetadata => "redevplugin.release-signing.release-metadata.v1",
55            Self::SourcePolicy => "redevplugin.release-signing.source-policy-document.v1",
56            Self::SourcePolicyPointer => "redevplugin.release-signing.source-policy-pointer.v1",
57            Self::Revocation => "redevplugin.release-signing.revocation-document.v1",
58            Self::RevocationPointer => "redevplugin.release-signing.revocation-pointer.v1",
59        }
60    }
61}
62
63#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
64pub enum DelegatedKeyUsage {
65    #[serde(rename = "package")]
66    Package,
67    #[serde(rename = "release_metadata")]
68    ReleaseMetadata,
69    #[serde(rename = "host_capability_contract")]
70    HostCapabilityContract,
71    #[serde(rename = "source_policy_document")]
72    SourcePolicy,
73    #[serde(rename = "source_policy_pointer")]
74    SourcePolicyPointer,
75    #[serde(rename = "revocation_document")]
76    Revocation,
77    #[serde(rename = "revocation_pointer")]
78    RevocationPointer,
79    #[serde(rename = "signing_ledger")]
80    SigningLedger,
81    #[serde(rename = "trusted_time")]
82    TrustedTime,
83}
84
85impl DelegatedKeyUsage {
86    const fn rank(self) -> u8 {
87        match self {
88            Self::Package => 0,
89            Self::ReleaseMetadata => 1,
90            Self::HostCapabilityContract => 2,
91            Self::Revocation => 3,
92            Self::RevocationPointer => 4,
93            Self::SourcePolicy => 5,
94            Self::SourcePolicyPointer => 6,
95            Self::SigningLedger => 7,
96            Self::TrustedTime => 8,
97        }
98    }
99}
100
101#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
102#[serde(deny_unknown_fields)]
103pub struct RootDelegatedKey {
104    pub algorithm: String,
105    pub key_id: String,
106    pub public_key: String,
107    pub usages: Vec<DelegatedKeyUsage>,
108    pub channels: Vec<String>,
109    pub valid_from: String,
110    pub valid_until: String,
111}
112
113#[derive(Clone, Debug, Eq, PartialEq)]
114pub struct RootDelegationInput {
115    pub source_id: String,
116    pub root_epoch: String,
117    pub previous_root_epoch: String,
118    pub previous_delegation_sha256: String,
119    pub generated_at: String,
120    pub expires_at: String,
121    pub delegated_keys: Vec<RootDelegatedKey>,
122    pub key_id: String,
123}
124
125#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
126#[serde(deny_unknown_fields)]
127pub struct RootDelegationV1 {
128    pub schema_version: String,
129    pub source_id: String,
130    pub root_epoch: String,
131    pub previous_root_epoch: String,
132    pub previous_delegation_sha256: String,
133    pub generated_at: String,
134    pub expires_at: String,
135    pub delegated_keys: Vec<RootDelegatedKey>,
136    pub key_id: String,
137    pub signature: String,
138}
139
140#[derive(Clone, Debug, Eq, PartialEq)]
141pub struct PackageSigningInput {
142    pub source_id: String,
143    pub channel: String,
144    pub version: String,
145    pub algorithm: String,
146    pub key_id: String,
147    pub publisher_id: String,
148    pub plugin_id: String,
149    pub package_hash: String,
150    pub manifest_hash: String,
151    pub entries_hash: String,
152    pub signed_at: String,
153}
154
155#[derive(Clone, Debug, Eq, PartialEq)]
156pub struct PackageVerificationContext {
157    pub source_id: String,
158    pub channel: String,
159    pub version: String,
160}
161
162#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
163#[serde(deny_unknown_fields)]
164pub struct PackageSignatureV1 {
165    pub schema_version: String,
166    pub algorithm: String,
167    pub key_id: String,
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub publisher_id: Option<String>,
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub plugin_id: Option<String>,
172    pub package_hash: String,
173    pub manifest_hash: String,
174    pub entries_hash: String,
175    pub signature: String,
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub signed_at: Option<String>,
178}
179
180#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
181#[serde(deny_unknown_fields)]
182pub struct ReleaseDistributionRef {
183    pub distribution: String,
184    pub artifact_ref: String,
185}
186
187#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
188#[serde(deny_unknown_fields)]
189pub struct ReleasePackageHashSet {
190    pub package_sha256: String,
191    pub manifest_sha256: String,
192    pub entries_sha256: String,
193}
194
195#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
196#[serde(deny_unknown_fields)]
197pub struct ReleaseMetadataSignatureRef {
198    pub algorithm: String,
199    pub key_id: String,
200    pub signature_ref: String,
201    pub source_policy_epoch: String,
202    pub revocation_epoch: String,
203}
204
205#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
206#[serde(deny_unknown_fields)]
207pub struct PackageReleaseSignatureRef {
208    pub algorithm: String,
209    pub key_id: String,
210    pub signature_bundle_ref: String,
211    pub source_policy_epoch: String,
212    pub revocation_epoch: String,
213}
214
215#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
216#[serde(deny_unknown_fields)]
217pub struct ReleaseCompatibility {
218    pub min_redevplugin_version: String,
219    pub min_runtime_version: String,
220    pub ui_protocol_version: String,
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub supported_targets: Option<Vec<String>>,
223}
224
225#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
226#[serde(deny_unknown_fields)]
227pub struct HostCapabilityContractRef {
228    pub publisher_id: String,
229    pub contract_id: String,
230    pub contract_version: String,
231    pub artifact_ref: String,
232    pub artifact_sha256: String,
233    pub manifest_ref: String,
234    pub manifest_sha256: String,
235    pub signature_ref: String,
236    pub signature_sha256: String,
237    pub signature_key_id: String,
238    pub signature_policy_epoch: String,
239    pub signature_revocation_epoch: String,
240    pub compatibility_ref: String,
241    pub compatibility_sha256: String,
242    pub generated_client_ref: String,
243    pub generated_client_sha256: String,
244    pub notices_ref: String,
245    pub notices_sha256: String,
246}
247
248#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
249#[serde(deny_unknown_fields)]
250pub struct HostCapabilityRequirementRef {
251    pub capability_id: String,
252    pub capability_version: String,
253    pub contract: HostCapabilityContractRef,
254}
255
256#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
257#[serde(deny_unknown_fields)]
258pub struct ReleaseHostRequirement {
259    pub host_id: String,
260    #[serde(skip_serializing_if = "Option::is_none")]
261    pub min_host_version: Option<String>,
262    #[serde(skip_serializing_if = "Option::is_none")]
263    pub required_capability_contracts: Option<Vec<HostCapabilityRequirementRef>>,
264}
265
266#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
267#[serde(deny_unknown_fields)]
268pub struct ReleaseEvidence {
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub notices_sha256: Option<String>,
271    #[serde(skip_serializing_if = "Option::is_none")]
272    pub provenance_sha256: Option<String>,
273    #[serde(skip_serializing_if = "Option::is_none")]
274    pub generated_at: Option<String>,
275}
276
277#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
278#[serde(deny_unknown_fields)]
279pub struct ReleaseMetadataV5 {
280    pub schema_version: String,
281    pub source_id: String,
282    pub release_metadata_ref: String,
283    pub publisher_id: String,
284    pub plugin_id: String,
285    pub version: String,
286    pub distribution_ref: ReleaseDistributionRef,
287    pub hashes: ReleasePackageHashSet,
288    pub release_metadata_signature: ReleaseMetadataSignatureRef,
289    pub package_signature: PackageReleaseSignatureRef,
290    pub compatibility: ReleaseCompatibility,
291    #[serde(skip_serializing_if = "Option::is_none")]
292    pub host_requirements: Option<Vec<ReleaseHostRequirement>>,
293    #[serde(skip_serializing_if = "Option::is_none")]
294    pub release_evidence: Option<ReleaseEvidence>,
295    #[serde(skip_serializing_if = "Option::is_none")]
296    pub metadata: Option<BTreeMap<String, String>>,
297}
298
299#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
300#[serde(deny_unknown_fields)]
301pub struct SourcePolicyLimits {
302    pub document_max_lifetime_seconds: u32,
303    pub future_skew_seconds: u32,
304    pub activation_lease_max_seconds: u32,
305    pub refresh_interval_max_seconds: u32,
306    pub failure_teardown_deadline_seconds: u32,
307}
308
309impl Default for SourcePolicyLimits {
310    fn default() -> Self {
311        Self {
312            document_max_lifetime_seconds: 86_400,
313            future_skew_seconds: 300,
314            activation_lease_max_seconds: 300,
315            refresh_interval_max_seconds: 60,
316            failure_teardown_deadline_seconds: 30,
317        }
318    }
319}
320
321#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
322#[serde(deny_unknown_fields)]
323pub struct SourcePolicyActiveKeys {
324    pub package: Vec<String>,
325    pub release_metadata: Vec<String>,
326    pub host_capability_contract: Vec<String>,
327    pub source_policy_pointer: Vec<String>,
328    pub revocation_document: Vec<String>,
329    pub revocation_pointer: Vec<String>,
330}
331
332#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
333#[serde(deny_unknown_fields)]
334pub struct SourcePolicyCapabilityPublisherScope {
335    pub key_id: String,
336    pub allowed_publishers: Vec<String>,
337}
338
339#[derive(Clone, Debug, Eq, PartialEq)]
340pub struct SourcePolicyInput {
341    pub source_id: String,
342    pub channel: String,
343    pub epoch: String,
344    pub previous_epoch: String,
345    pub previous_document_sha256: String,
346    pub root_epoch: String,
347    pub source_type: String,
348    pub source_class: String,
349    pub allowed_publishers: Vec<String>,
350    pub allowed_artifact_hosts: Vec<String>,
351    pub active_keys: SourcePolicyActiveKeys,
352    pub capability_publisher_scopes: Vec<SourcePolicyCapabilityPublisherScope>,
353    pub require_signature: bool,
354    pub install_policy: String,
355    pub unsigned_policy: String,
356    pub downgrade_policy: String,
357    pub minimum_revocation_epoch: String,
358    pub limits: SourcePolicyLimits,
359    pub generated_at: String,
360    pub expires_at: String,
361    pub key_id: String,
362}
363
364#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
365#[serde(deny_unknown_fields)]
366pub struct SourcePolicyV2 {
367    pub schema_version: String,
368    pub source_id: String,
369    pub channel: String,
370    pub epoch: String,
371    pub previous_epoch: String,
372    pub previous_document_sha256: String,
373    pub root_epoch: String,
374    pub source_type: String,
375    pub source_class: String,
376    pub allowed_publishers: Vec<String>,
377    pub allowed_artifact_hosts: Vec<String>,
378    pub active_keys: SourcePolicyActiveKeys,
379    pub capability_publisher_scopes: Vec<SourcePolicyCapabilityPublisherScope>,
380    pub require_signature: bool,
381    pub install_policy: String,
382    pub unsigned_policy: String,
383    pub downgrade_policy: String,
384    pub minimum_revocation_epoch: String,
385    pub limits: SourcePolicyLimits,
386    pub generated_at: String,
387    pub expires_at: String,
388    pub key_id: String,
389    pub signature: String,
390}
391
392#[derive(Clone, Debug, Eq, PartialEq)]
393pub struct ReleasePointerInput {
394    pub source_id: String,
395    pub channel: String,
396    pub epoch: String,
397    pub previous_epoch: String,
398    pub previous_document_sha256: String,
399    pub r#ref: String,
400    pub document_sha256: String,
401    pub generated_at: String,
402    pub expires_at: String,
403    pub key_id: String,
404}
405
406#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
407#[serde(deny_unknown_fields)]
408pub struct SourcePolicyPointerV1 {
409    pub schema_version: String,
410    pub source_id: String,
411    pub channel: String,
412    pub epoch: String,
413    pub previous_epoch: String,
414    pub previous_document_sha256: String,
415    #[serde(rename = "ref")]
416    pub r#ref: String,
417    pub document_sha256: String,
418    pub generated_at: String,
419    pub expires_at: String,
420    pub key_id: String,
421    pub signature: String,
422}
423
424#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
425#[serde(deny_unknown_fields)]
426pub struct RevokedRelease {
427    pub publisher_id: String,
428    pub plugin_id: String,
429    pub version: String,
430    pub release_metadata_sha256: String,
431    pub revoked_at: String,
432}
433
434#[derive(Clone, Debug, Eq, PartialEq)]
435pub struct RevocationInput {
436    pub source_id: String,
437    pub channel: String,
438    pub epoch: String,
439    pub previous_epoch: String,
440    pub previous_document_sha256: String,
441    pub root_epoch: String,
442    pub generated_at: String,
443    pub expires_at: String,
444    pub revoked_key_ids: Vec<String>,
445    pub revoked_releases: Vec<RevokedRelease>,
446    pub key_id: String,
447}
448
449#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
450#[serde(deny_unknown_fields)]
451pub struct RevocationV2 {
452    pub schema_version: String,
453    pub source_id: String,
454    pub channel: String,
455    pub epoch: String,
456    pub previous_epoch: String,
457    pub previous_document_sha256: String,
458    pub root_epoch: String,
459    pub generated_at: String,
460    pub expires_at: String,
461    pub revoked_key_ids: Vec<String>,
462    pub revoked_releases: Vec<RevokedRelease>,
463    pub key_id: String,
464    pub signature: String,
465}
466
467#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
468#[serde(deny_unknown_fields)]
469pub struct RevocationPointerV1 {
470    pub schema_version: String,
471    pub source_id: String,
472    pub channel: String,
473    pub epoch: String,
474    pub previous_epoch: String,
475    pub previous_document_sha256: String,
476    #[serde(rename = "ref")]
477    pub r#ref: String,
478    pub document_sha256: String,
479    pub generated_at: String,
480    pub expires_at: String,
481    pub key_id: String,
482    pub signature: String,
483}
484
485#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
486#[serde(deny_unknown_fields)]
487pub struct SigningLedgerEvidenceV1 {
488    pub schema_version: String,
489    pub source_id: String,
490    #[serde(default, skip_serializing_if = "Option::is_none")]
491    pub channel: Option<String>,
492    pub subject_identity_sha256: String,
493    pub signing_preimage_sha256: String,
494    pub signature_envelope_sha256: String,
495    pub receipt_ref: String,
496    pub receipt_sha256: String,
497    pub checkpoint_ref: String,
498    pub checkpoint_sha256: String,
499    pub inclusion_proof_ref: String,
500    pub inclusion_proof_sha256: String,
501    pub latest_proof_ref: String,
502    pub latest_proof_sha256: String,
503    #[serde(default, skip_serializing_if = "Option::is_none")]
504    pub consistency_proof_ref: Option<String>,
505    #[serde(default, skip_serializing_if = "Option::is_none")]
506    pub consistency_proof_sha256: Option<String>,
507}
508
509#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
510#[serde(rename_all = "snake_case")]
511pub enum SigningSubjectUsage {
512    RootDelegation,
513    Package,
514    ReleaseMetadata,
515    SourcePolicyDocument,
516    SourcePolicyPointer,
517    RevocationDocument,
518    RevocationPointer,
519}
520
521#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
522#[serde(deny_unknown_fields)]
523pub struct SigningSubjectV1 {
524    pub schema_version: String,
525    pub usage: SigningSubjectUsage,
526    pub source_id: String,
527    #[serde(default, skip_serializing_if = "Option::is_none")]
528    pub channel: Option<String>,
529    #[serde(default, skip_serializing_if = "Option::is_none")]
530    pub root_epoch: Option<String>,
531    #[serde(default, skip_serializing_if = "Option::is_none")]
532    pub publisher_id: Option<String>,
533    #[serde(default, skip_serializing_if = "Option::is_none")]
534    pub plugin_id: Option<String>,
535    #[serde(default, skip_serializing_if = "Option::is_none")]
536    pub version: Option<String>,
537    #[serde(default, skip_serializing_if = "Option::is_none")]
538    pub artifact_or_metadata_identity_sha256: Option<String>,
539    #[serde(default, skip_serializing_if = "Option::is_none")]
540    pub epoch: Option<String>,
541}
542
543#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
544#[serde(deny_unknown_fields)]
545pub struct SignatureEnvelopeV1 {
546    pub schema_version: String,
547    pub subject_identity_sha256: String,
548    pub signing_preimage_sha256: String,
549    pub algorithm: String,
550    pub key_id: String,
551    pub signature: String,
552}
553
554#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
555#[serde(rename_all = "snake_case")]
556pub enum SigningLedgerEntryState {
557    Reserved,
558    Finalized,
559    TerminalFailed,
560}
561
562#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
563#[serde(rename_all = "snake_case")]
564pub enum SigningLedgerFailureCode {
565    SignerRejected,
566    SubjectConflict,
567    LedgerRejected,
568}
569
570#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
571#[serde(deny_unknown_fields)]
572pub struct SigningLedgerEntryV1 {
573    pub schema_version: String,
574    pub state: SigningLedgerEntryState,
575    pub subject: SigningSubjectV1,
576    pub subject_identity_sha256: String,
577    pub signing_preimage_sha256: String,
578    pub algorithm: String,
579    pub key_id: String,
580    pub revision: u64,
581    pub reserved_at: String,
582    #[serde(default, skip_serializing_if = "Option::is_none")]
583    pub signature_envelope: Option<SignatureEnvelopeV1>,
584    #[serde(default, skip_serializing_if = "Option::is_none")]
585    pub signature_envelope_sha256: Option<String>,
586    #[serde(default, skip_serializing_if = "Option::is_none")]
587    pub finalized_at: Option<String>,
588    #[serde(default, skip_serializing_if = "Option::is_none")]
589    pub failure_code: Option<SigningLedgerFailureCode>,
590    #[serde(default, skip_serializing_if = "Option::is_none")]
591    pub failed_at: Option<String>,
592}
593
594#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
595#[serde(deny_unknown_fields)]
596pub struct SigningLedgerLogLeafV1 {
597    pub schema_version: String,
598    pub source_id: String,
599    #[serde(default, skip_serializing_if = "Option::is_none")]
600    pub channel: Option<String>,
601    pub subject_identity_sha256: String,
602    pub signing_preimage_sha256: String,
603    pub signature_envelope_sha256: String,
604    pub sequence: u64,
605}
606
607#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
608#[serde(deny_unknown_fields)]
609pub struct SigningLedgerCheckpointV1 {
610    pub schema_version: String,
611    pub kind: String,
612    pub log_id: String,
613    pub tree_size: u64,
614    pub log_root_hash: String,
615    pub latest_map_root_hash: String,
616    pub checkpoint_time: String,
617    pub key_id: String,
618    pub signature: String,
619}
620
621#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
622#[serde(deny_unknown_fields)]
623pub struct SigningLedgerReceiptV1 {
624    pub schema_version: String,
625    pub log_id: String,
626    pub source_id: String,
627    #[serde(default, skip_serializing_if = "Option::is_none")]
628    pub channel: Option<String>,
629    pub subject_identity_sha256: String,
630    pub signing_preimage_sha256: String,
631    pub signature_envelope_sha256: String,
632    pub sequence: u64,
633    pub leaf_index: u64,
634    pub tree_size: u64,
635    pub log_root_hash: String,
636    pub latest_map_root_hash: String,
637    pub checkpoint_sha256: String,
638    pub checkpoint_time: String,
639    pub key_id: String,
640    pub signature: String,
641}
642
643#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
644#[serde(deny_unknown_fields)]
645pub struct SigningLedgerInclusionProofV1 {
646    pub schema_version: String,
647    pub kind: String,
648    pub log_id: String,
649    pub leaf_index: u64,
650    pub tree_size: u64,
651    pub nodes: Vec<String>,
652}
653
654#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
655#[serde(deny_unknown_fields)]
656pub struct SigningLedgerLatestProofV1 {
657    pub schema_version: String,
658    pub kind: String,
659    pub log_id: String,
660    pub subject_identity_sha256: String,
661    pub present: bool,
662    #[serde(default, skip_serializing_if = "Option::is_none")]
663    pub sequence: Option<u64>,
664    #[serde(default, skip_serializing_if = "Option::is_none")]
665    pub signing_preimage_sha256: Option<String>,
666    #[serde(default, skip_serializing_if = "Option::is_none")]
667    pub signature_envelope_sha256: Option<String>,
668    pub siblings: Vec<String>,
669}
670
671#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
672#[serde(deny_unknown_fields)]
673pub struct SigningLedgerConsistencyProofV1 {
674    pub schema_version: String,
675    pub kind: String,
676    pub log_id: String,
677    pub old_tree_size: u64,
678    pub new_tree_size: u64,
679    pub nodes: Vec<String>,
680}
681
682#[derive(Clone, Copy, Debug, Eq, PartialEq)]
683pub enum ReleaseContractError {
684    InvalidDocument,
685    InvalidSignature,
686}
687
688impl fmt::Display for ReleaseContractError {
689    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
690        match self {
691            Self::InvalidDocument => formatter.write_str("release contract document is invalid"),
692            Self::InvalidSignature => formatter.write_str("release contract signature is invalid"),
693        }
694    }
695}
696
697impl Error for ReleaseContractError {}
698
699#[derive(Clone, Copy, Debug)]
700pub struct SignatureVerificationRequest<'a> {
701    pub usage: SigningUsage,
702    pub key_id: &'a str,
703    pub signing_preimage_sha256: [u8; 32],
704    pub signature: &'a [u8],
705}
706
707pub trait SignatureVerifier {
708    fn verify_signature(&self, request: SignatureVerificationRequest<'_>) -> bool;
709}
710
711impl<F> SignatureVerifier for F
712where
713    F: for<'a> Fn(SignatureVerificationRequest<'a>) -> bool,
714{
715    fn verify_signature(&self, request: SignatureVerificationRequest<'_>) -> bool {
716        self(request)
717    }
718}
719
720pub fn build_root_delegation(
721    input: &RootDelegationInput,
722    signature: &[u8],
723) -> Result<RootDelegationV1, ReleaseContractError> {
724    require_signature_bytes(signature)?;
725    let document = root_delegation_from_input(input, encode_signature(signature));
726    validate_root_delegation(&document, true)?;
727    Ok(document)
728}
729
730pub fn root_delegation_signing_preimage(
731    input: &RootDelegationInput,
732) -> Result<Vec<u8>, ReleaseContractError> {
733    let document = root_delegation_from_input(input, String::new());
734    validate_root_delegation(&document, false)?;
735    preimage_without_top_level_signature(SigningUsage::RootDelegation, &document)
736}
737
738pub fn canonical_root_delegation(
739    document: &RootDelegationV1,
740) -> Result<Vec<u8>, ReleaseContractError> {
741    validate_root_delegation(document, true)?;
742    canonical_json(document)
743}
744
745pub fn verify_root_delegation(
746    document: &RootDelegationV1,
747    verifier: &impl SignatureVerifier,
748) -> Result<(), ReleaseContractError> {
749    let input = RootDelegationInput {
750        source_id: document.source_id.clone(),
751        root_epoch: document.root_epoch.clone(),
752        previous_root_epoch: document.previous_root_epoch.clone(),
753        previous_delegation_sha256: document.previous_delegation_sha256.clone(),
754        generated_at: document.generated_at.clone(),
755        expires_at: document.expires_at.clone(),
756        delegated_keys: document.delegated_keys.clone(),
757        key_id: document.key_id.clone(),
758    };
759    verify_encoded_signature(
760        SigningUsage::RootDelegation,
761        &document.key_id,
762        &root_delegation_signing_preimage(&input)?,
763        &document.signature,
764        verifier,
765    )
766}
767
768pub fn build_package_signature(
769    input: &PackageSigningInput,
770    signature: &[u8],
771) -> Result<PackageSignatureV1, ReleaseContractError> {
772    require_signature_bytes(signature)?;
773    let document = PackageSignatureV1 {
774        schema_version: PACKAGE_SIGNATURE_SCHEMA_VERSION.to_owned(),
775        algorithm: input.algorithm.clone(),
776        key_id: input.key_id.clone(),
777        publisher_id: Some(input.publisher_id.clone()),
778        plugin_id: Some(input.plugin_id.clone()),
779        package_hash: input.package_hash.clone(),
780        manifest_hash: input.manifest_hash.clone(),
781        entries_hash: input.entries_hash.clone(),
782        signature: encode_signature(signature),
783        signed_at: Some(input.signed_at.clone()),
784    };
785    validate_package_signature(
786        &PackageVerificationContext {
787            source_id: input.source_id.clone(),
788            channel: input.channel.clone(),
789            version: input.version.clone(),
790        },
791        &document,
792        true,
793    )?;
794    Ok(document)
795}
796
797pub fn package_signing_preimage(
798    input: &PackageSigningInput,
799) -> Result<Vec<u8>, ReleaseContractError> {
800    validate_package_input(input)?;
801    let payload = serde_json::json!({
802        "channel": input.channel,
803        "package_signature": {
804            "algorithm": input.algorithm,
805            "entries_hash": input.entries_hash,
806            "key_id": input.key_id,
807            "manifest_hash": input.manifest_hash,
808            "package_hash": input.package_hash,
809            "plugin_id": input.plugin_id,
810            "publisher_id": input.publisher_id,
811            "schema_version": PACKAGE_SIGNATURE_SCHEMA_VERSION,
812            "signed_at": input.signed_at,
813        },
814        "source_id": input.source_id,
815        "version": input.version,
816    });
817    signing_preimage(SigningUsage::Package, &payload)
818}
819
820pub fn canonical_package_signature(
821    context: &PackageVerificationContext,
822    document: &PackageSignatureV1,
823) -> Result<Vec<u8>, ReleaseContractError> {
824    validate_package_signature(context, document, true)?;
825    canonical_json(document)
826}
827
828pub fn verify_package_signature(
829    context: &PackageVerificationContext,
830    document: &PackageSignatureV1,
831    verifier: &impl SignatureVerifier,
832) -> Result<(), ReleaseContractError> {
833    validate_package_signature(context, document, true)?;
834    let input = package_input_from_document(context, document)?;
835    verify_encoded_signature(
836        SigningUsage::Package,
837        &document.key_id,
838        &package_signing_preimage(&input)?,
839        &document.signature,
840        verifier,
841    )
842}
843
844pub fn build_release_metadata(
845    document: &ReleaseMetadataV5,
846) -> Result<ReleaseMetadataV5, ReleaseContractError> {
847    validate_release_metadata(document)?;
848    Ok(document.clone())
849}
850
851pub fn release_metadata_signing_preimage(
852    channel: &str,
853    document: &ReleaseMetadataV5,
854) -> Result<Vec<u8>, ReleaseContractError> {
855    if !valid_new_id(channel) {
856        return Err(ReleaseContractError::InvalidDocument);
857    }
858    let built = build_release_metadata(document)?;
859    let payload = serde_json::json!({"channel": channel, "release_metadata": built});
860    signing_preimage(SigningUsage::ReleaseMetadata, &payload)
861}
862
863pub fn canonical_release_metadata(
864    document: &ReleaseMetadataV5,
865) -> Result<Vec<u8>, ReleaseContractError> {
866    validate_release_metadata(document)?;
867    canonical_json(document)
868}
869
870pub fn verify_release_metadata(
871    channel: &str,
872    document: &ReleaseMetadataV5,
873    signature: &[u8],
874    verifier: &impl SignatureVerifier,
875) -> Result<(), ReleaseContractError> {
876    require_signature_bytes(signature).map_err(|_| ReleaseContractError::InvalidSignature)?;
877    verify_raw_signature(
878        SigningUsage::ReleaseMetadata,
879        &document.release_metadata_signature.key_id,
880        &release_metadata_signing_preimage(channel, document)?,
881        signature,
882        verifier,
883    )
884}
885
886pub fn build_source_policy(
887    input: &SourcePolicyInput,
888    signature: &[u8],
889) -> Result<SourcePolicyV2, ReleaseContractError> {
890    require_signature_bytes(signature)?;
891    let document = source_policy_from_input(input, encode_signature(signature));
892    validate_source_policy(&document, true)?;
893    Ok(document)
894}
895
896pub fn source_policy_signing_preimage(
897    input: &SourcePolicyInput,
898) -> Result<Vec<u8>, ReleaseContractError> {
899    let document = source_policy_from_input(input, String::new());
900    validate_source_policy(&document, false)?;
901    preimage_without_top_level_signature(SigningUsage::SourcePolicy, &document)
902}
903
904pub fn canonical_source_policy(document: &SourcePolicyV2) -> Result<Vec<u8>, ReleaseContractError> {
905    validate_source_policy(document, true)?;
906    canonical_json(document)
907}
908
909pub fn verify_source_policy(
910    document: &SourcePolicyV2,
911    verifier: &impl SignatureVerifier,
912) -> Result<(), ReleaseContractError> {
913    let input = source_policy_input_from_document(document);
914    verify_encoded_signature(
915        SigningUsage::SourcePolicy,
916        &document.key_id,
917        &source_policy_signing_preimage(&input)?,
918        &document.signature,
919        verifier,
920    )
921}
922
923pub fn build_source_policy_pointer(
924    input: &ReleasePointerInput,
925    signature: &[u8],
926) -> Result<SourcePolicyPointerV1, ReleaseContractError> {
927    require_signature_bytes(signature)?;
928    let document = SourcePolicyPointerV1 {
929        schema_version: SOURCE_POLICY_POINTER_SCHEMA_VERSION.to_owned(),
930        source_id: input.source_id.clone(),
931        channel: input.channel.clone(),
932        epoch: input.epoch.clone(),
933        previous_epoch: input.previous_epoch.clone(),
934        previous_document_sha256: input.previous_document_sha256.clone(),
935        r#ref: input.r#ref.clone(),
936        document_sha256: input.document_sha256.clone(),
937        generated_at: input.generated_at.clone(),
938        expires_at: input.expires_at.clone(),
939        key_id: input.key_id.clone(),
940        signature: encode_signature(signature),
941    };
942    validate_source_policy_pointer(&document, true)?;
943    Ok(document)
944}
945
946pub fn source_policy_pointer_signing_preimage(
947    input: &ReleasePointerInput,
948) -> Result<Vec<u8>, ReleaseContractError> {
949    let document = SourcePolicyPointerV1 {
950        schema_version: SOURCE_POLICY_POINTER_SCHEMA_VERSION.to_owned(),
951        source_id: input.source_id.clone(),
952        channel: input.channel.clone(),
953        epoch: input.epoch.clone(),
954        previous_epoch: input.previous_epoch.clone(),
955        previous_document_sha256: input.previous_document_sha256.clone(),
956        r#ref: input.r#ref.clone(),
957        document_sha256: input.document_sha256.clone(),
958        generated_at: input.generated_at.clone(),
959        expires_at: input.expires_at.clone(),
960        key_id: input.key_id.clone(),
961        signature: String::new(),
962    };
963    validate_source_policy_pointer(&document, false)?;
964    preimage_without_top_level_signature(SigningUsage::SourcePolicyPointer, &document)
965}
966
967pub fn canonical_source_policy_pointer(
968    document: &SourcePolicyPointerV1,
969) -> Result<Vec<u8>, ReleaseContractError> {
970    validate_source_policy_pointer(document, true)?;
971    canonical_json(document)
972}
973
974pub fn verify_source_policy_pointer(
975    document: &SourcePolicyPointerV1,
976    verifier: &impl SignatureVerifier,
977) -> Result<(), ReleaseContractError> {
978    let input = pointer_input_from_source_policy(document);
979    verify_encoded_signature(
980        SigningUsage::SourcePolicyPointer,
981        &document.key_id,
982        &source_policy_pointer_signing_preimage(&input)?,
983        &document.signature,
984        verifier,
985    )
986}
987
988pub fn build_revocation(
989    input: &RevocationInput,
990    signature: &[u8],
991) -> Result<RevocationV2, ReleaseContractError> {
992    require_signature_bytes(signature)?;
993    let document = revocation_from_input(input, encode_signature(signature));
994    validate_revocation(&document, true)?;
995    Ok(document)
996}
997
998pub fn revocation_signing_preimage(
999    input: &RevocationInput,
1000) -> Result<Vec<u8>, ReleaseContractError> {
1001    let document = revocation_from_input(input, String::new());
1002    validate_revocation(&document, false)?;
1003    preimage_without_top_level_signature(SigningUsage::Revocation, &document)
1004}
1005
1006pub fn canonical_revocation(document: &RevocationV2) -> Result<Vec<u8>, ReleaseContractError> {
1007    validate_revocation(document, true)?;
1008    canonical_json(document)
1009}
1010
1011pub fn verify_revocation(
1012    document: &RevocationV2,
1013    verifier: &impl SignatureVerifier,
1014) -> Result<(), ReleaseContractError> {
1015    let input = revocation_input_from_document(document);
1016    verify_encoded_signature(
1017        SigningUsage::Revocation,
1018        &document.key_id,
1019        &revocation_signing_preimage(&input)?,
1020        &document.signature,
1021        verifier,
1022    )
1023}
1024
1025pub fn build_revocation_pointer(
1026    input: &ReleasePointerInput,
1027    signature: &[u8],
1028) -> Result<RevocationPointerV1, ReleaseContractError> {
1029    require_signature_bytes(signature)?;
1030    let document = RevocationPointerV1 {
1031        schema_version: REVOCATION_POINTER_SCHEMA_VERSION.to_owned(),
1032        source_id: input.source_id.clone(),
1033        channel: input.channel.clone(),
1034        epoch: input.epoch.clone(),
1035        previous_epoch: input.previous_epoch.clone(),
1036        previous_document_sha256: input.previous_document_sha256.clone(),
1037        r#ref: input.r#ref.clone(),
1038        document_sha256: input.document_sha256.clone(),
1039        generated_at: input.generated_at.clone(),
1040        expires_at: input.expires_at.clone(),
1041        key_id: input.key_id.clone(),
1042        signature: encode_signature(signature),
1043    };
1044    validate_revocation_pointer(&document, true)?;
1045    Ok(document)
1046}
1047
1048pub fn revocation_pointer_signing_preimage(
1049    input: &ReleasePointerInput,
1050) -> Result<Vec<u8>, ReleaseContractError> {
1051    let document = RevocationPointerV1 {
1052        schema_version: REVOCATION_POINTER_SCHEMA_VERSION.to_owned(),
1053        source_id: input.source_id.clone(),
1054        channel: input.channel.clone(),
1055        epoch: input.epoch.clone(),
1056        previous_epoch: input.previous_epoch.clone(),
1057        previous_document_sha256: input.previous_document_sha256.clone(),
1058        r#ref: input.r#ref.clone(),
1059        document_sha256: input.document_sha256.clone(),
1060        generated_at: input.generated_at.clone(),
1061        expires_at: input.expires_at.clone(),
1062        key_id: input.key_id.clone(),
1063        signature: String::new(),
1064    };
1065    validate_revocation_pointer(&document, false)?;
1066    preimage_without_top_level_signature(SigningUsage::RevocationPointer, &document)
1067}
1068
1069pub fn canonical_revocation_pointer(
1070    document: &RevocationPointerV1,
1071) -> Result<Vec<u8>, ReleaseContractError> {
1072    validate_revocation_pointer(document, true)?;
1073    canonical_json(document)
1074}
1075
1076pub fn verify_revocation_pointer(
1077    document: &RevocationPointerV1,
1078    verifier: &impl SignatureVerifier,
1079) -> Result<(), ReleaseContractError> {
1080    let input = pointer_input_from_revocation(document);
1081    verify_encoded_signature(
1082        SigningUsage::RevocationPointer,
1083        &document.key_id,
1084        &revocation_pointer_signing_preimage(&input)?,
1085        &document.signature,
1086        verifier,
1087    )
1088}
1089
1090pub fn decode_root_delegation(raw: &[u8]) -> Result<RootDelegationV1, ReleaseContractError> {
1091    decode_canonical_document(raw, |value| validate_root_delegation(value, true))
1092}
1093
1094pub fn decode_package_signature(
1095    raw: &[u8],
1096    context: &PackageVerificationContext,
1097) -> Result<PackageSignatureV1, ReleaseContractError> {
1098    decode_canonical_document(raw, |value| {
1099        validate_package_signature(context, value, true)
1100    })
1101}
1102
1103pub fn decode_release_metadata(raw: &[u8]) -> Result<ReleaseMetadataV5, ReleaseContractError> {
1104    decode_canonical_document(raw, validate_release_metadata)
1105}
1106
1107pub fn decode_source_policy(raw: &[u8]) -> Result<SourcePolicyV2, ReleaseContractError> {
1108    decode_canonical_document(raw, |value| validate_source_policy(value, true))
1109}
1110
1111pub fn decode_source_policy_pointer(
1112    raw: &[u8],
1113) -> Result<SourcePolicyPointerV1, ReleaseContractError> {
1114    decode_canonical_document(raw, |value| validate_source_policy_pointer(value, true))
1115}
1116
1117pub fn decode_revocation(raw: &[u8]) -> Result<RevocationV2, ReleaseContractError> {
1118    decode_canonical_document(raw, |value| validate_revocation(value, true))
1119}
1120
1121pub fn decode_revocation_pointer(raw: &[u8]) -> Result<RevocationPointerV1, ReleaseContractError> {
1122    decode_canonical_document(raw, |value| validate_revocation_pointer(value, true))
1123}
1124
1125pub fn decode_signing_ledger_evidence(
1126    raw: &[u8],
1127) -> Result<SigningLedgerEvidenceV1, ReleaseContractError> {
1128    if raw.len() > 64 * 1024 {
1129        return Err(ReleaseContractError::InvalidDocument);
1130    }
1131    decode_canonical_document(raw, validate_signing_ledger_evidence)
1132}
1133
1134pub fn canonical_signing_subject(
1135    value: &SigningSubjectV1,
1136) -> Result<Vec<u8>, ReleaseContractError> {
1137    validate_signing_subject(value)?;
1138    canonical_json(value)
1139}
1140
1141pub fn decode_signing_subject(raw: &[u8]) -> Result<SigningSubjectV1, ReleaseContractError> {
1142    decode_canonical_document(raw, validate_signing_subject)
1143}
1144
1145pub fn canonical_signature_envelope(
1146    value: &SignatureEnvelopeV1,
1147) -> Result<Vec<u8>, ReleaseContractError> {
1148    validate_signature_envelope(value)?;
1149    canonical_json(value)
1150}
1151
1152pub fn decode_signature_envelope(raw: &[u8]) -> Result<SignatureEnvelopeV1, ReleaseContractError> {
1153    decode_canonical_document(raw, validate_signature_envelope)
1154}
1155
1156pub fn canonical_signing_ledger_entry(
1157    value: &SigningLedgerEntryV1,
1158) -> Result<Vec<u8>, ReleaseContractError> {
1159    validate_signing_ledger_entry(value)?;
1160    canonical_json(value)
1161}
1162
1163pub fn decode_signing_ledger_entry(
1164    raw: &[u8],
1165) -> Result<SigningLedgerEntryV1, ReleaseContractError> {
1166    decode_canonical_document(raw, validate_signing_ledger_entry)
1167}
1168
1169pub fn decode_signing_ledger_log_leaf(
1170    raw: &[u8],
1171) -> Result<SigningLedgerLogLeafV1, ReleaseContractError> {
1172    decode_canonical_document(raw, validate_signing_ledger_log_leaf)
1173}
1174
1175pub fn decode_signing_ledger_checkpoint(
1176    raw: &[u8],
1177) -> Result<SigningLedgerCheckpointV1, ReleaseContractError> {
1178    decode_canonical_document(raw, validate_signing_ledger_checkpoint)
1179}
1180
1181pub fn decode_signing_ledger_receipt(
1182    raw: &[u8],
1183) -> Result<SigningLedgerReceiptV1, ReleaseContractError> {
1184    decode_canonical_document(raw, validate_signing_ledger_receipt)
1185}
1186
1187pub fn decode_signing_ledger_inclusion_proof(
1188    raw: &[u8],
1189) -> Result<SigningLedgerInclusionProofV1, ReleaseContractError> {
1190    decode_canonical_document(raw, validate_signing_ledger_inclusion_proof)
1191}
1192
1193pub fn decode_signing_ledger_latest_proof(
1194    raw: &[u8],
1195) -> Result<SigningLedgerLatestProofV1, ReleaseContractError> {
1196    decode_canonical_document(raw, validate_signing_ledger_latest_proof)
1197}
1198
1199pub fn decode_signing_ledger_consistency_proof(
1200    raw: &[u8],
1201) -> Result<SigningLedgerConsistencyProofV1, ReleaseContractError> {
1202    decode_canonical_document(raw, validate_signing_ledger_consistency_proof)
1203}
1204
1205fn root_delegation_from_input(input: &RootDelegationInput, signature: String) -> RootDelegationV1 {
1206    RootDelegationV1 {
1207        schema_version: ROOT_DELEGATION_SCHEMA_VERSION.to_owned(),
1208        source_id: input.source_id.clone(),
1209        root_epoch: input.root_epoch.clone(),
1210        previous_root_epoch: input.previous_root_epoch.clone(),
1211        previous_delegation_sha256: input.previous_delegation_sha256.clone(),
1212        generated_at: input.generated_at.clone(),
1213        expires_at: input.expires_at.clone(),
1214        delegated_keys: input.delegated_keys.clone(),
1215        key_id: input.key_id.clone(),
1216        signature,
1217    }
1218}
1219
1220fn source_policy_from_input(input: &SourcePolicyInput, signature: String) -> SourcePolicyV2 {
1221    SourcePolicyV2 {
1222        schema_version: SOURCE_POLICY_SCHEMA_VERSION.to_owned(),
1223        source_id: input.source_id.clone(),
1224        channel: input.channel.clone(),
1225        epoch: input.epoch.clone(),
1226        previous_epoch: input.previous_epoch.clone(),
1227        previous_document_sha256: input.previous_document_sha256.clone(),
1228        root_epoch: input.root_epoch.clone(),
1229        source_type: input.source_type.clone(),
1230        source_class: input.source_class.clone(),
1231        allowed_publishers: input.allowed_publishers.clone(),
1232        allowed_artifact_hosts: input.allowed_artifact_hosts.clone(),
1233        active_keys: input.active_keys.clone(),
1234        capability_publisher_scopes: input.capability_publisher_scopes.clone(),
1235        require_signature: input.require_signature,
1236        install_policy: input.install_policy.clone(),
1237        unsigned_policy: input.unsigned_policy.clone(),
1238        downgrade_policy: input.downgrade_policy.clone(),
1239        minimum_revocation_epoch: input.minimum_revocation_epoch.clone(),
1240        limits: input.limits,
1241        generated_at: input.generated_at.clone(),
1242        expires_at: input.expires_at.clone(),
1243        key_id: input.key_id.clone(),
1244        signature,
1245    }
1246}
1247
1248fn revocation_from_input(input: &RevocationInput, signature: String) -> RevocationV2 {
1249    RevocationV2 {
1250        schema_version: REVOCATION_SCHEMA_VERSION.to_owned(),
1251        source_id: input.source_id.clone(),
1252        channel: input.channel.clone(),
1253        epoch: input.epoch.clone(),
1254        previous_epoch: input.previous_epoch.clone(),
1255        previous_document_sha256: input.previous_document_sha256.clone(),
1256        root_epoch: input.root_epoch.clone(),
1257        generated_at: input.generated_at.clone(),
1258        expires_at: input.expires_at.clone(),
1259        revoked_key_ids: input.revoked_key_ids.clone(),
1260        revoked_releases: input.revoked_releases.clone(),
1261        key_id: input.key_id.clone(),
1262        signature,
1263    }
1264}
1265
1266fn package_input_from_document(
1267    context: &PackageVerificationContext,
1268    document: &PackageSignatureV1,
1269) -> Result<PackageSigningInput, ReleaseContractError> {
1270    Ok(PackageSigningInput {
1271        source_id: context.source_id.clone(),
1272        channel: context.channel.clone(),
1273        version: context.version.clone(),
1274        algorithm: document.algorithm.clone(),
1275        key_id: document.key_id.clone(),
1276        publisher_id: document
1277            .publisher_id
1278            .clone()
1279            .ok_or(ReleaseContractError::InvalidDocument)?,
1280        plugin_id: document
1281            .plugin_id
1282            .clone()
1283            .ok_or(ReleaseContractError::InvalidDocument)?,
1284        package_hash: document.package_hash.clone(),
1285        manifest_hash: document.manifest_hash.clone(),
1286        entries_hash: document.entries_hash.clone(),
1287        signed_at: document
1288            .signed_at
1289            .clone()
1290            .ok_or(ReleaseContractError::InvalidDocument)?,
1291    })
1292}
1293
1294fn source_policy_input_from_document(document: &SourcePolicyV2) -> SourcePolicyInput {
1295    SourcePolicyInput {
1296        source_id: document.source_id.clone(),
1297        channel: document.channel.clone(),
1298        epoch: document.epoch.clone(),
1299        previous_epoch: document.previous_epoch.clone(),
1300        previous_document_sha256: document.previous_document_sha256.clone(),
1301        root_epoch: document.root_epoch.clone(),
1302        source_type: document.source_type.clone(),
1303        source_class: document.source_class.clone(),
1304        allowed_publishers: document.allowed_publishers.clone(),
1305        allowed_artifact_hosts: document.allowed_artifact_hosts.clone(),
1306        active_keys: document.active_keys.clone(),
1307        capability_publisher_scopes: document.capability_publisher_scopes.clone(),
1308        require_signature: document.require_signature,
1309        install_policy: document.install_policy.clone(),
1310        unsigned_policy: document.unsigned_policy.clone(),
1311        downgrade_policy: document.downgrade_policy.clone(),
1312        minimum_revocation_epoch: document.minimum_revocation_epoch.clone(),
1313        limits: document.limits,
1314        generated_at: document.generated_at.clone(),
1315        expires_at: document.expires_at.clone(),
1316        key_id: document.key_id.clone(),
1317    }
1318}
1319
1320fn revocation_input_from_document(document: &RevocationV2) -> RevocationInput {
1321    RevocationInput {
1322        source_id: document.source_id.clone(),
1323        channel: document.channel.clone(),
1324        epoch: document.epoch.clone(),
1325        previous_epoch: document.previous_epoch.clone(),
1326        previous_document_sha256: document.previous_document_sha256.clone(),
1327        root_epoch: document.root_epoch.clone(),
1328        generated_at: document.generated_at.clone(),
1329        expires_at: document.expires_at.clone(),
1330        revoked_key_ids: document.revoked_key_ids.clone(),
1331        revoked_releases: document.revoked_releases.clone(),
1332        key_id: document.key_id.clone(),
1333    }
1334}
1335
1336fn pointer_input_from_source_policy(document: &SourcePolicyPointerV1) -> ReleasePointerInput {
1337    ReleasePointerInput {
1338        source_id: document.source_id.clone(),
1339        channel: document.channel.clone(),
1340        epoch: document.epoch.clone(),
1341        previous_epoch: document.previous_epoch.clone(),
1342        previous_document_sha256: document.previous_document_sha256.clone(),
1343        r#ref: document.r#ref.clone(),
1344        document_sha256: document.document_sha256.clone(),
1345        generated_at: document.generated_at.clone(),
1346        expires_at: document.expires_at.clone(),
1347        key_id: document.key_id.clone(),
1348    }
1349}
1350
1351fn pointer_input_from_revocation(document: &RevocationPointerV1) -> ReleasePointerInput {
1352    ReleasePointerInput {
1353        source_id: document.source_id.clone(),
1354        channel: document.channel.clone(),
1355        epoch: document.epoch.clone(),
1356        previous_epoch: document.previous_epoch.clone(),
1357        previous_document_sha256: document.previous_document_sha256.clone(),
1358        r#ref: document.r#ref.clone(),
1359        document_sha256: document.document_sha256.clone(),
1360        generated_at: document.generated_at.clone(),
1361        expires_at: document.expires_at.clone(),
1362        key_id: document.key_id.clone(),
1363    }
1364}
1365
1366fn canonical_json(value: &impl Serialize) -> Result<Vec<u8>, ReleaseContractError> {
1367    let value = serde_json::to_value(value).map_err(|_| ReleaseContractError::InvalidDocument)?;
1368    validate_canonical_value(&value)?;
1369    serde_json::to_vec(&value).map_err(|_| ReleaseContractError::InvalidDocument)
1370}
1371
1372fn validate_canonical_value(value: &Value) -> Result<(), ReleaseContractError> {
1373    match value {
1374        Value::Null | Value::Bool(_) | Value::String(_) => Ok(()),
1375        Value::Number(number) if number.as_u64().is_some() => Ok(()),
1376        Value::Array(values) => values.iter().try_for_each(validate_canonical_value),
1377        Value::Object(values) => values.values().try_for_each(validate_canonical_value),
1378        _ => Err(ReleaseContractError::InvalidDocument),
1379    }
1380}
1381
1382fn signing_preimage(
1383    usage: SigningUsage,
1384    value: &impl Serialize,
1385) -> Result<Vec<u8>, ReleaseContractError> {
1386    let payload = canonical_json(value)?;
1387    let mut preimage =
1388        Vec::with_capacity(SIGNING_PREFIX.len() + usage.as_str().len() + 1 + payload.len());
1389    preimage.extend_from_slice(SIGNING_PREFIX);
1390    preimage.extend_from_slice(usage.as_str().as_bytes());
1391    preimage.push(0);
1392    preimage.extend_from_slice(&payload);
1393    Ok(preimage)
1394}
1395
1396fn preimage_without_top_level_signature(
1397    usage: SigningUsage,
1398    document: &impl Serialize,
1399) -> Result<Vec<u8>, ReleaseContractError> {
1400    let mut value =
1401        serde_json::to_value(document).map_err(|_| ReleaseContractError::InvalidDocument)?;
1402    let object = value
1403        .as_object_mut()
1404        .ok_or(ReleaseContractError::InvalidDocument)?;
1405    object.remove("signature");
1406    signing_preimage(usage, &value)
1407}
1408
1409fn decode_canonical_document<T>(
1410    raw: &[u8],
1411    validate: impl FnOnce(&T) -> Result<(), ReleaseContractError>,
1412) -> Result<T, ReleaseContractError>
1413where
1414    T: for<'de> Deserialize<'de> + Serialize,
1415{
1416    if raw.is_empty() || raw.len() > MAX_DOCUMENT_BYTES || std::str::from_utf8(raw).is_err() {
1417        return Err(ReleaseContractError::InvalidDocument);
1418    }
1419    let document: T =
1420        serde_json::from_slice(raw).map_err(|_| ReleaseContractError::InvalidDocument)?;
1421    validate(&document)?;
1422    if canonical_json(&document)? != raw {
1423        return Err(ReleaseContractError::InvalidDocument);
1424    }
1425    Ok(document)
1426}
1427
1428fn verify_encoded_signature(
1429    usage: SigningUsage,
1430    key_id: &str,
1431    preimage: &[u8],
1432    encoded_signature: &str,
1433    verifier: &impl SignatureVerifier,
1434) -> Result<(), ReleaseContractError> {
1435    let signature =
1436        decode_signature(encoded_signature).map_err(|_| ReleaseContractError::InvalidSignature)?;
1437    verify_raw_signature(usage, key_id, preimage, &signature, verifier)
1438}
1439
1440fn verify_raw_signature(
1441    usage: SigningUsage,
1442    key_id: &str,
1443    preimage: &[u8],
1444    signature: &[u8],
1445    verifier: &impl SignatureVerifier,
1446) -> Result<(), ReleaseContractError> {
1447    let signing_preimage_sha256: [u8; 32] = Sha256::digest(preimage).into();
1448    if signature.len() != 64
1449        || !verifier.verify_signature(SignatureVerificationRequest {
1450            usage,
1451            key_id,
1452            signing_preimage_sha256,
1453            signature,
1454        })
1455    {
1456        return Err(ReleaseContractError::InvalidSignature);
1457    }
1458    Ok(())
1459}
1460
1461fn require_signature_bytes(signature: &[u8]) -> Result<(), ReleaseContractError> {
1462    if signature.len() != 64 {
1463        return Err(ReleaseContractError::InvalidDocument);
1464    }
1465    Ok(())
1466}
1467
1468fn encode_signature(signature: &[u8]) -> String {
1469    BASE64_STANDARD.encode(signature)
1470}
1471
1472fn decode_signature(value: &str) -> Result<Vec<u8>, ReleaseContractError> {
1473    if value.len() != 88 || !value.ends_with("==") {
1474        return Err(ReleaseContractError::InvalidDocument);
1475    }
1476    let decoded = BASE64_STANDARD
1477        .decode(value)
1478        .map_err(|_| ReleaseContractError::InvalidDocument)?;
1479    if decoded.len() != 64 || BASE64_STANDARD.encode(&decoded) != value {
1480        return Err(ReleaseContractError::InvalidDocument);
1481    }
1482    Ok(decoded)
1483}
1484
1485fn validate_root_delegation(
1486    value: &RootDelegationV1,
1487    require_signature: bool,
1488) -> Result<(), ReleaseContractError> {
1489    if value.schema_version != ROOT_DELEGATION_SCHEMA_VERSION
1490        || !valid_new_id(&value.source_id)
1491        || !valid_new_id(&value.key_id)
1492    {
1493        return invalid_document();
1494    }
1495    validate_epoch_chain(
1496        &value.root_epoch,
1497        &value.previous_root_epoch,
1498        &value.previous_delegation_sha256,
1499    )?;
1500    let (_, expires_at) = validate_time_range(&value.generated_at, &value.expires_at, None)?;
1501    if value.delegated_keys.is_empty() || value.delegated_keys.len() > 32 {
1502        return invalid_document();
1503    }
1504    let mut previous_key_id = "";
1505    for key in &value.delegated_keys {
1506        if key.algorithm != SIGNATURE_ALGORITHM_ED25519
1507            || !valid_new_id(&key.key_id)
1508            || key.key_id.as_str() <= previous_key_id
1509        {
1510            return invalid_document();
1511        }
1512        let public_key = BASE64_STANDARD
1513            .decode(&key.public_key)
1514            .map_err(|_| ReleaseContractError::InvalidDocument)?;
1515        if public_key.len() != 32 || BASE64_STANDARD.encode(&public_key) != key.public_key {
1516            return invalid_document();
1517        }
1518        if key.usages.is_empty() || key.usages.len() > 9 {
1519            return invalid_document();
1520        }
1521        let mut previous_usage = None;
1522        for usage in &key.usages {
1523            let rank = usage.rank();
1524            if previous_usage.is_some_and(|previous| rank <= previous) {
1525                return invalid_document();
1526            }
1527            previous_usage = Some(rank);
1528        }
1529        let source_wide = key.usages.iter().all(|usage| {
1530            matches!(
1531                usage,
1532                DelegatedKeyUsage::SigningLedger | DelegatedKeyUsage::TrustedTime
1533            )
1534        });
1535        let has_source_wide = key.usages.iter().any(|usage| {
1536            matches!(
1537                usage,
1538                DelegatedKeyUsage::SigningLedger | DelegatedKeyUsage::TrustedTime
1539            )
1540        });
1541        if has_source_wide != source_wide {
1542            return invalid_document();
1543        }
1544        validate_sorted_ids(
1545            &key.channels,
1546            if source_wide { 0 } else { 1 },
1547            if source_wide { 0 } else { 16 },
1548            true,
1549        )?;
1550        let (_, valid_until) = validate_time_range(&key.valid_from, &key.valid_until, None)?;
1551        if valid_until > expires_at {
1552            return invalid_document();
1553        }
1554        previous_key_id = &key.key_id;
1555    }
1556    validate_signature_field(&value.signature, require_signature)
1557}
1558
1559fn validate_package_input(value: &PackageSigningInput) -> Result<(), ReleaseContractError> {
1560    if !valid_new_id(&value.source_id)
1561        || !valid_new_id(&value.channel)
1562        || value.algorithm != SIGNATURE_ALGORITHM_ED25519
1563        || !valid_new_id(&value.key_id)
1564        || !valid_legacy_id(&value.publisher_id)
1565        || !valid_legacy_id(&value.plugin_id)
1566        || !valid_semver(&value.version)
1567        || !valid_prefixed_sha256(&value.package_hash)
1568        || !valid_prefixed_sha256(&value.manifest_hash)
1569        || !valid_prefixed_sha256(&value.entries_hash)
1570        || canonical_timestamp_seconds(&value.signed_at).is_none()
1571    {
1572        return invalid_document();
1573    }
1574    Ok(())
1575}
1576
1577fn validate_package_signature(
1578    context: &PackageVerificationContext,
1579    value: &PackageSignatureV1,
1580    require_signature: bool,
1581) -> Result<(), ReleaseContractError> {
1582    if value.schema_version != PACKAGE_SIGNATURE_SCHEMA_VERSION {
1583        return invalid_document();
1584    }
1585    let input = package_input_from_document(context, value)?;
1586    validate_package_input(&input)?;
1587    validate_signature_field(&value.signature, require_signature)
1588}
1589
1590fn validate_release_metadata(value: &ReleaseMetadataV5) -> Result<(), ReleaseContractError> {
1591    if !valid_release_metadata_ui_protocol_pair(
1592        &value.schema_version,
1593        &value.compatibility.ui_protocol_version,
1594    ) || !valid_new_id(&value.source_id)
1595        || !valid_legacy_id(&value.publisher_id)
1596        || !valid_legacy_id(&value.plugin_id)
1597        || !valid_semver(&value.version)
1598        || !valid_artifact_ref(&value.release_metadata_ref)
1599    {
1600        return invalid_document();
1601    }
1602    if !matches!(
1603        value.distribution_ref.distribution.as_str(),
1604        "registry_ref" | "host_artifact_ref"
1605    ) || !valid_artifact_ref(&value.distribution_ref.artifact_ref)
1606    {
1607        return invalid_document();
1608    }
1609    if !valid_legacy_sha256(&value.hashes.package_sha256)
1610        || !valid_legacy_sha256(&value.hashes.manifest_sha256)
1611        || !valid_legacy_sha256(&value.hashes.entries_sha256)
1612    {
1613        return invalid_document();
1614    }
1615    let metadata_signature = &value.release_metadata_signature;
1616    if metadata_signature.algorithm != SIGNATURE_ALGORITHM_ED25519
1617        || !valid_new_id(&metadata_signature.key_id)
1618        || !valid_artifact_ref(&metadata_signature.signature_ref)
1619        || !valid_epoch(&metadata_signature.source_policy_epoch)
1620        || !valid_epoch(&metadata_signature.revocation_epoch)
1621    {
1622        return invalid_document();
1623    }
1624    let package_signature = &value.package_signature;
1625    if package_signature.algorithm != SIGNATURE_ALGORITHM_ED25519
1626        || !valid_new_id(&package_signature.key_id)
1627        || !valid_artifact_ref(&package_signature.signature_bundle_ref)
1628        || !valid_epoch(&package_signature.source_policy_epoch)
1629        || !valid_epoch(&package_signature.revocation_epoch)
1630    {
1631        return invalid_document();
1632    }
1633    if !valid_semver(&value.compatibility.min_redevplugin_version)
1634        || !valid_semver(&value.compatibility.min_runtime_version)
1635    {
1636        return invalid_document();
1637    }
1638    if let Some(targets) = &value.compatibility.supported_targets {
1639        let mut previous = "";
1640        for target in targets {
1641            if !matches!(
1642                target.as_str(),
1643                "darwin/amd64" | "darwin/arm64" | "linux/amd64" | "linux/arm64"
1644            ) || target.as_str() <= previous
1645            {
1646                return invalid_document();
1647            }
1648            previous = target;
1649        }
1650    }
1651    validate_host_requirements(value.host_requirements.as_deref().unwrap_or(&[]))?;
1652    if let Some(evidence) = &value.release_evidence {
1653        if evidence
1654            .notices_sha256
1655            .as_deref()
1656            .is_some_and(|digest| !valid_legacy_sha256(digest))
1657            || evidence
1658                .provenance_sha256
1659                .as_deref()
1660                .is_some_and(|digest| !valid_legacy_sha256(digest))
1661            || evidence
1662                .generated_at
1663                .as_deref()
1664                .is_some_and(|generated| canonical_timestamp_seconds(generated).is_none())
1665        {
1666            return invalid_document();
1667        }
1668    }
1669    if let Some(metadata) = &value.metadata {
1670        if metadata.len() > 128
1671            || metadata
1672                .iter()
1673                .any(|(key, item)| key.is_empty() || key.len() > 128 || item.len() > 4096)
1674        {
1675            return invalid_document();
1676        }
1677    }
1678    Ok(())
1679}
1680
1681fn valid_release_metadata_ui_protocol_pair(
1682    schema_version: &str,
1683    ui_protocol_version: &str,
1684) -> bool {
1685    matches!(
1686        (schema_version, ui_protocol_version),
1687        (RELEASE_METADATA_SCHEMA_VERSION_V5, "plugin-ui-v5")
1688            | (RELEASE_METADATA_SCHEMA_VERSION_V6, "plugin-ui-v6")
1689    )
1690}
1691
1692fn validate_host_requirements(
1693    values: &[ReleaseHostRequirement],
1694) -> Result<(), ReleaseContractError> {
1695    let mut previous_host = "";
1696    for value in values {
1697        if !valid_legacy_id(&value.host_id) || value.host_id.as_str() <= previous_host {
1698            return invalid_document();
1699        }
1700        if value
1701            .min_host_version
1702            .as_deref()
1703            .is_some_and(|version| !valid_semver(version))
1704        {
1705            return invalid_document();
1706        }
1707        let mut previous_capability = String::new();
1708        for capability in value
1709            .required_capability_contracts
1710            .as_deref()
1711            .unwrap_or(&[])
1712        {
1713            let identity = format!(
1714                "{}\0{}",
1715                capability.capability_id, capability.capability_version
1716            );
1717            if !valid_legacy_id(&capability.capability_id)
1718                || !valid_semver(&capability.capability_version)
1719                || identity <= previous_capability
1720            {
1721                return invalid_document();
1722            }
1723            validate_capability_contract_ref(&capability.contract)?;
1724            previous_capability = identity;
1725        }
1726        previous_host = &value.host_id;
1727    }
1728    Ok(())
1729}
1730
1731fn validate_capability_contract_ref(
1732    value: &HostCapabilityContractRef,
1733) -> Result<(), ReleaseContractError> {
1734    if !valid_legacy_id(&value.publisher_id)
1735        || !valid_legacy_id(&value.contract_id)
1736        || !valid_legacy_id(&value.signature_key_id)
1737        || !valid_semver(&value.contract_version)
1738        || !valid_epoch(&value.signature_policy_epoch)
1739        || !valid_epoch(&value.signature_revocation_epoch)
1740    {
1741        return invalid_document();
1742    }
1743    for reference in [
1744        &value.artifact_ref,
1745        &value.manifest_ref,
1746        &value.signature_ref,
1747        &value.compatibility_ref,
1748        &value.generated_client_ref,
1749        &value.notices_ref,
1750    ] {
1751        if !valid_artifact_ref(reference) {
1752            return invalid_document();
1753        }
1754    }
1755    for digest in [
1756        &value.artifact_sha256,
1757        &value.manifest_sha256,
1758        &value.signature_sha256,
1759        &value.compatibility_sha256,
1760        &value.generated_client_sha256,
1761        &value.notices_sha256,
1762    ] {
1763        if !valid_sha256(digest) {
1764            return invalid_document();
1765        }
1766    }
1767    Ok(())
1768}
1769
1770fn validate_source_policy(
1771    value: &SourcePolicyV2,
1772    require_signature: bool,
1773) -> Result<(), ReleaseContractError> {
1774    if value.schema_version != SOURCE_POLICY_SCHEMA_VERSION
1775        || !valid_new_id(&value.source_id)
1776        || !valid_new_id(&value.channel)
1777        || !valid_new_id(&value.key_id)
1778    {
1779        return invalid_document();
1780    }
1781    validate_epoch_chain(
1782        &value.epoch,
1783        &value.previous_epoch,
1784        &value.previous_document_sha256,
1785    )?;
1786    if !valid_positive_epoch(&value.root_epoch)
1787        || !valid_epoch(&value.minimum_revocation_epoch)
1788        || !matches!(value.source_type.as_str(), "registry" | "host_artifact")
1789        || !matches!(
1790            value.source_class.as_str(),
1791            "official" | "curated" | "community" | "private"
1792        )
1793    {
1794        return invalid_document();
1795    }
1796    validate_sorted_ids(&value.allowed_publishers, 1, 1024, true)?;
1797    if value.allowed_artifact_hosts.len() > 1024 {
1798        return invalid_document();
1799    }
1800    let mut previous_host = "";
1801    for host in &value.allowed_artifact_hosts {
1802        if host.len() > 253
1803            || !valid_hostname(host)
1804            || host.to_ascii_lowercase() != *host
1805            || host.as_str() <= previous_host
1806        {
1807            return invalid_document();
1808        }
1809        previous_host = host;
1810    }
1811    for keys in [
1812        &value.active_keys.package,
1813        &value.active_keys.release_metadata,
1814        &value.active_keys.source_policy_pointer,
1815        &value.active_keys.revocation_document,
1816        &value.active_keys.revocation_pointer,
1817    ] {
1818        validate_sorted_ids(keys, 1, 16, true)?;
1819    }
1820    validate_sorted_ids(&value.active_keys.host_capability_contract, 0, 16, true)?;
1821    if value.capability_publisher_scopes.len() != value.active_keys.host_capability_contract.len() {
1822        return invalid_document();
1823    }
1824    for (index, scope) in value.capability_publisher_scopes.iter().enumerate() {
1825        if scope.key_id != value.active_keys.host_capability_contract[index] {
1826            return invalid_document();
1827        }
1828        validate_sorted_ids(&scope.allowed_publishers, 1, 1024, false)?;
1829    }
1830    if !matches!(
1831        value.install_policy.as_str(),
1832        "allow" | "review_required" | "block"
1833    ) || !matches!(
1834        value.unsigned_policy.as_str(),
1835        "dev_only" | "review_required" | "block"
1836    ) || !matches!(value.downgrade_policy.as_str(), "review_required" | "block")
1837        || value.limits != SourcePolicyLimits::default()
1838    {
1839        return invalid_document();
1840    }
1841    validate_time_range(&value.generated_at, &value.expires_at, Some(24 * 60 * 60))?;
1842    validate_signature_field(&value.signature, require_signature)
1843}
1844
1845fn validate_source_policy_pointer(
1846    value: &SourcePolicyPointerV1,
1847    require_signature: bool,
1848) -> Result<(), ReleaseContractError> {
1849    validate_pointer(
1850        &value.schema_version,
1851        SOURCE_POLICY_POINTER_SCHEMA_VERSION,
1852        &value.source_id,
1853        &value.channel,
1854        &value.epoch,
1855        &value.previous_epoch,
1856        &value.previous_document_sha256,
1857        &value.r#ref,
1858        &value.document_sha256,
1859        &value.generated_at,
1860        &value.expires_at,
1861        &value.key_id,
1862        &value.signature,
1863        require_signature,
1864    )
1865}
1866
1867fn validate_revocation_pointer(
1868    value: &RevocationPointerV1,
1869    require_signature: bool,
1870) -> Result<(), ReleaseContractError> {
1871    validate_pointer(
1872        &value.schema_version,
1873        REVOCATION_POINTER_SCHEMA_VERSION,
1874        &value.source_id,
1875        &value.channel,
1876        &value.epoch,
1877        &value.previous_epoch,
1878        &value.previous_document_sha256,
1879        &value.r#ref,
1880        &value.document_sha256,
1881        &value.generated_at,
1882        &value.expires_at,
1883        &value.key_id,
1884        &value.signature,
1885        require_signature,
1886    )
1887}
1888
1889#[allow(clippy::too_many_arguments)]
1890fn validate_pointer(
1891    schema_version: &str,
1892    expected_schema_version: &str,
1893    source_id: &str,
1894    channel: &str,
1895    epoch: &str,
1896    previous_epoch: &str,
1897    previous_digest: &str,
1898    reference: &str,
1899    document_digest: &str,
1900    generated_at: &str,
1901    expires_at: &str,
1902    key_id: &str,
1903    signature: &str,
1904    require_signature: bool,
1905) -> Result<(), ReleaseContractError> {
1906    if schema_version != expected_schema_version
1907        || !valid_new_id(source_id)
1908        || !valid_new_id(channel)
1909        || !valid_new_id(key_id)
1910    {
1911        return invalid_document();
1912    }
1913    validate_epoch_chain(epoch, previous_epoch, previous_digest)?;
1914    if !valid_artifact_ref(reference)
1915        || !valid_sha256(document_digest)
1916        || document_digest == GENESIS_PREVIOUS_DOCUMENT_SHA256
1917    {
1918        return invalid_document();
1919    }
1920    validate_time_range(generated_at, expires_at, Some(24 * 60 * 60))?;
1921    validate_signature_field(signature, require_signature)
1922}
1923
1924fn validate_revocation(
1925    value: &RevocationV2,
1926    require_signature: bool,
1927) -> Result<(), ReleaseContractError> {
1928    if value.schema_version != REVOCATION_SCHEMA_VERSION
1929        || !valid_new_id(&value.source_id)
1930        || !valid_new_id(&value.channel)
1931        || !valid_new_id(&value.key_id)
1932    {
1933        return invalid_document();
1934    }
1935    validate_epoch_chain(
1936        &value.epoch,
1937        &value.previous_epoch,
1938        &value.previous_document_sha256,
1939    )?;
1940    if !valid_positive_epoch(&value.root_epoch) {
1941        return invalid_document();
1942    }
1943    let (_, expires_at) =
1944        validate_time_range(&value.generated_at, &value.expires_at, Some(24 * 60 * 60))?;
1945    validate_sorted_ids(&value.revoked_key_ids, 0, 4096, true)?;
1946    if value.revoked_releases.len() > 16_384 {
1947        return invalid_document();
1948    }
1949    let mut previous = String::new();
1950    for revoked in &value.revoked_releases {
1951        let identity = format!(
1952            "{}\0{}\0{}\0{}",
1953            revoked.publisher_id,
1954            revoked.plugin_id,
1955            revoked.version,
1956            revoked.release_metadata_sha256
1957        );
1958        let revoked_at = canonical_timestamp_seconds(&revoked.revoked_at)
1959            .ok_or(ReleaseContractError::InvalidDocument)?;
1960        if !valid_legacy_id(&revoked.publisher_id)
1961            || !valid_legacy_id(&revoked.plugin_id)
1962            || !valid_semver(&revoked.version)
1963            || !valid_sha256(&revoked.release_metadata_sha256)
1964            || identity <= previous
1965            || revoked_at > expires_at
1966        {
1967            return invalid_document();
1968        }
1969        previous = identity;
1970    }
1971    validate_signature_field(&value.signature, require_signature)
1972}
1973
1974fn validate_signing_ledger_evidence(
1975    value: &SigningLedgerEvidenceV1,
1976) -> Result<(), ReleaseContractError> {
1977    if value.schema_version != SIGNING_LEDGER_EVIDENCE_SCHEMA_VERSION
1978        || !valid_new_id(&value.source_id)
1979        || value
1980            .channel
1981            .as_deref()
1982            .is_some_and(|item| !valid_new_id(item))
1983    {
1984        return Err(ReleaseContractError::InvalidDocument);
1985    }
1986    for digest in [
1987        &value.subject_identity_sha256,
1988        &value.signing_preimage_sha256,
1989        &value.signature_envelope_sha256,
1990        &value.receipt_sha256,
1991        &value.checkpoint_sha256,
1992        &value.inclusion_proof_sha256,
1993        &value.latest_proof_sha256,
1994    ] {
1995        if !valid_sha256(digest) {
1996            return Err(ReleaseContractError::InvalidDocument);
1997        }
1998    }
1999    for reference in [
2000        &value.receipt_ref,
2001        &value.checkpoint_ref,
2002        &value.inclusion_proof_ref,
2003        &value.latest_proof_ref,
2004    ] {
2005        if !valid_artifact_ref(reference) {
2006            return Err(ReleaseContractError::InvalidDocument);
2007        }
2008    }
2009    if value.consistency_proof_ref.is_some() != value.consistency_proof_sha256.is_some() {
2010        return Err(ReleaseContractError::InvalidDocument);
2011    }
2012    if let (Some(reference), Some(digest)) = (
2013        value.consistency_proof_ref.as_deref(),
2014        value.consistency_proof_sha256.as_deref(),
2015    ) && (!valid_artifact_ref(reference) || !valid_sha256(digest))
2016    {
2017        return Err(ReleaseContractError::InvalidDocument);
2018    }
2019    Ok(())
2020}
2021
2022fn validate_signing_subject(value: &SigningSubjectV1) -> Result<(), ReleaseContractError> {
2023    if value.schema_version != SIGNING_SUBJECT_SCHEMA_VERSION || !valid_new_id(&value.source_id) {
2024        return invalid_document();
2025    }
2026    let valid = match value.usage {
2027        SigningSubjectUsage::RootDelegation => {
2028            value
2029                .root_epoch
2030                .as_deref()
2031                .is_some_and(valid_positive_epoch)
2032                && value.channel.is_none()
2033                && value.publisher_id.is_none()
2034                && value.plugin_id.is_none()
2035                && value.version.is_none()
2036                && value.artifact_or_metadata_identity_sha256.is_none()
2037                && value.epoch.is_none()
2038        }
2039        SigningSubjectUsage::Package | SigningSubjectUsage::ReleaseMetadata => {
2040            value.channel.as_deref().is_some_and(valid_new_id)
2041                && value.publisher_id.as_deref().is_some_and(valid_legacy_id)
2042                && value.plugin_id.as_deref().is_some_and(valid_legacy_id)
2043                && value.version.as_deref().is_some_and(valid_semver)
2044                && value
2045                    .artifact_or_metadata_identity_sha256
2046                    .as_deref()
2047                    .is_some_and(valid_sha256)
2048                && value.root_epoch.is_none()
2049                && value.epoch.is_none()
2050        }
2051        SigningSubjectUsage::SourcePolicyDocument
2052        | SigningSubjectUsage::SourcePolicyPointer
2053        | SigningSubjectUsage::RevocationDocument
2054        | SigningSubjectUsage::RevocationPointer => {
2055            value.channel.as_deref().is_some_and(valid_new_id)
2056                && value.epoch.as_deref().is_some_and(valid_positive_epoch)
2057                && value.root_epoch.is_none()
2058                && value.publisher_id.is_none()
2059                && value.plugin_id.is_none()
2060                && value.version.is_none()
2061                && value.artifact_or_metadata_identity_sha256.is_none()
2062        }
2063    };
2064    if !valid {
2065        return invalid_document();
2066    }
2067    Ok(())
2068}
2069
2070fn validate_signature_envelope(value: &SignatureEnvelopeV1) -> Result<(), ReleaseContractError> {
2071    if value.schema_version != SIGNATURE_ENVELOPE_SCHEMA_VERSION
2072        || !valid_sha256(&value.subject_identity_sha256)
2073        || !valid_sha256(&value.signing_preimage_sha256)
2074        || value.algorithm != SIGNATURE_ALGORITHM_ED25519
2075        || !valid_new_id(&value.key_id)
2076        || decode_signature(&value.signature).is_err()
2077    {
2078        return invalid_document();
2079    }
2080    Ok(())
2081}
2082
2083fn validate_signing_ledger_entry(value: &SigningLedgerEntryV1) -> Result<(), ReleaseContractError> {
2084    if value.schema_version != SIGNING_LEDGER_ENTRY_SCHEMA_VERSION
2085        || !valid_sha256(&value.subject_identity_sha256)
2086        || !valid_sha256(&value.signing_preimage_sha256)
2087        || value.algorithm != SIGNATURE_ALGORITHM_ED25519
2088        || !valid_new_id(&value.key_id)
2089        || !valid_json_safe_positive(value.revision)
2090    {
2091        return invalid_document();
2092    }
2093    validate_signing_subject(&value.subject)?;
2094    if sha256_hex(&canonical_json(&value.subject)?) != value.subject_identity_sha256 {
2095        return invalid_document();
2096    }
2097    let reserved_at = canonical_timestamp_seconds(&value.reserved_at)
2098        .ok_or(ReleaseContractError::InvalidDocument)?;
2099    match value.state {
2100        SigningLedgerEntryState::Reserved => {
2101            if value.signature_envelope.is_some()
2102                || value.signature_envelope_sha256.is_some()
2103                || value.finalized_at.is_some()
2104                || value.failure_code.is_some()
2105                || value.failed_at.is_some()
2106            {
2107                return invalid_document();
2108            }
2109        }
2110        SigningLedgerEntryState::Finalized => {
2111            let envelope = value
2112                .signature_envelope
2113                .as_ref()
2114                .ok_or(ReleaseContractError::InvalidDocument)?;
2115            validate_signature_envelope(envelope)?;
2116            let envelope_digest = value
2117                .signature_envelope_sha256
2118                .as_deref()
2119                .ok_or(ReleaseContractError::InvalidDocument)?;
2120            let finalized_at = value
2121                .finalized_at
2122                .as_deref()
2123                .and_then(canonical_timestamp_seconds)
2124                .ok_or(ReleaseContractError::InvalidDocument)?;
2125            if value.failure_code.is_some()
2126                || value.failed_at.is_some()
2127                || finalized_at < reserved_at
2128                || envelope.subject_identity_sha256 != value.subject_identity_sha256
2129                || envelope.signing_preimage_sha256 != value.signing_preimage_sha256
2130                || envelope.algorithm != value.algorithm
2131                || envelope.key_id != value.key_id
2132                || !valid_sha256(envelope_digest)
2133                || sha256_hex(&canonical_json(envelope)?) != envelope_digest
2134            {
2135                return invalid_document();
2136            }
2137        }
2138        SigningLedgerEntryState::TerminalFailed => {
2139            let failed_at = value
2140                .failed_at
2141                .as_deref()
2142                .and_then(canonical_timestamp_seconds)
2143                .ok_or(ReleaseContractError::InvalidDocument)?;
2144            if value.signature_envelope.is_some()
2145                || value.signature_envelope_sha256.is_some()
2146                || value.finalized_at.is_some()
2147                || value.failure_code.is_none()
2148                || failed_at < reserved_at
2149            {
2150                return invalid_document();
2151            }
2152        }
2153    }
2154    Ok(())
2155}
2156
2157fn validate_signing_ledger_log_leaf(
2158    value: &SigningLedgerLogLeafV1,
2159) -> Result<(), ReleaseContractError> {
2160    if value.schema_version != SIGNING_LEDGER_LOG_LEAF_SCHEMA_VERSION
2161        || !valid_new_id(&value.source_id)
2162        || value
2163            .channel
2164            .as_deref()
2165            .is_some_and(|channel| !valid_new_id(channel))
2166        || !valid_sha256(&value.subject_identity_sha256)
2167        || !valid_sha256(&value.signing_preimage_sha256)
2168        || !valid_sha256(&value.signature_envelope_sha256)
2169        || !valid_json_safe_positive(value.sequence)
2170    {
2171        return invalid_document();
2172    }
2173    Ok(())
2174}
2175
2176fn validate_signing_ledger_checkpoint(
2177    value: &SigningLedgerCheckpointV1,
2178) -> Result<(), ReleaseContractError> {
2179    if value.schema_version != SIGNING_LEDGER_SCHEMA_VERSION
2180        || value.kind != "checkpoint"
2181        || !valid_new_id(&value.log_id)
2182        || !valid_json_safe_positive(value.tree_size)
2183        || !valid_sha256(&value.log_root_hash)
2184        || !valid_sha256(&value.latest_map_root_hash)
2185        || canonical_timestamp_seconds(&value.checkpoint_time).is_none()
2186        || !valid_new_id(&value.key_id)
2187        || decode_signature(&value.signature).is_err()
2188    {
2189        return invalid_document();
2190    }
2191    Ok(())
2192}
2193
2194fn validate_signing_ledger_receipt(
2195    value: &SigningLedgerReceiptV1,
2196) -> Result<(), ReleaseContractError> {
2197    if value.schema_version != SIGNING_LEDGER_RECEIPT_SCHEMA_VERSION
2198        || !valid_new_id(&value.log_id)
2199        || !valid_new_id(&value.source_id)
2200        || value
2201            .channel
2202            .as_deref()
2203            .is_some_and(|channel| !valid_new_id(channel))
2204        || !valid_sha256(&value.subject_identity_sha256)
2205        || !valid_sha256(&value.signing_preimage_sha256)
2206        || !valid_sha256(&value.signature_envelope_sha256)
2207        || !valid_json_safe_positive(value.sequence)
2208        || value.leaf_index != value.sequence - 1
2209        || !valid_json_safe_positive(value.tree_size)
2210        || value.tree_size < value.sequence
2211        || !valid_sha256(&value.log_root_hash)
2212        || !valid_sha256(&value.latest_map_root_hash)
2213        || !valid_sha256(&value.checkpoint_sha256)
2214        || canonical_timestamp_seconds(&value.checkpoint_time).is_none()
2215        || !valid_new_id(&value.key_id)
2216        || decode_signature(&value.signature).is_err()
2217    {
2218        return invalid_document();
2219    }
2220    Ok(())
2221}
2222
2223fn valid_ledger_nodes(values: &[String], expected: Option<usize>) -> bool {
2224    expected.map_or(values.len() <= 64, |count| values.len() == count)
2225        && values.iter().all(|node| valid_sha256(node))
2226}
2227
2228fn validate_signing_ledger_inclusion_proof(
2229    value: &SigningLedgerInclusionProofV1,
2230) -> Result<(), ReleaseContractError> {
2231    if value.schema_version != SIGNING_LEDGER_SCHEMA_VERSION
2232        || value.kind != "inclusion_proof"
2233        || !valid_new_id(&value.log_id)
2234        || !valid_json_safe_positive(value.tree_size)
2235        || value.leaf_index >= value.tree_size
2236        || !valid_ledger_nodes(&value.nodes, None)
2237    {
2238        return invalid_document();
2239    }
2240    Ok(())
2241}
2242
2243fn validate_signing_ledger_latest_proof(
2244    value: &SigningLedgerLatestProofV1,
2245) -> Result<(), ReleaseContractError> {
2246    if value.schema_version != SIGNING_LEDGER_SCHEMA_VERSION
2247        || value.kind != "latest_proof"
2248        || !valid_new_id(&value.log_id)
2249        || !valid_sha256(&value.subject_identity_sha256)
2250        || !valid_ledger_nodes(&value.siblings, Some(256))
2251    {
2252        return invalid_document();
2253    }
2254    if value.present {
2255        if !value.sequence.is_some_and(valid_json_safe_positive)
2256            || !value
2257                .signing_preimage_sha256
2258                .as_deref()
2259                .is_some_and(valid_sha256)
2260            || !value
2261                .signature_envelope_sha256
2262                .as_deref()
2263                .is_some_and(valid_sha256)
2264        {
2265            return invalid_document();
2266        }
2267    } else if value.sequence.is_some()
2268        || value.signing_preimage_sha256.is_some()
2269        || value.signature_envelope_sha256.is_some()
2270    {
2271        return invalid_document();
2272    }
2273    Ok(())
2274}
2275
2276fn validate_signing_ledger_consistency_proof(
2277    value: &SigningLedgerConsistencyProofV1,
2278) -> Result<(), ReleaseContractError> {
2279    if value.schema_version != SIGNING_LEDGER_SCHEMA_VERSION
2280        || value.kind != "consistency_proof"
2281        || !valid_new_id(&value.log_id)
2282        || !valid_json_safe_positive(value.old_tree_size)
2283        || value.new_tree_size < value.old_tree_size
2284        || value.new_tree_size > 9_007_199_254_740_991
2285        || !valid_ledger_nodes(&value.nodes, None)
2286    {
2287        return invalid_document();
2288    }
2289    Ok(())
2290}
2291
2292fn valid_json_safe_positive(value: u64) -> bool {
2293    value > 0 && value <= 9_007_199_254_740_991
2294}
2295
2296fn sha256_hex(value: &[u8]) -> String {
2297    Sha256::digest(value)
2298        .iter()
2299        .map(|byte| format!("{byte:02x}"))
2300        .collect()
2301}
2302
2303fn invalid_document<T>() -> Result<T, ReleaseContractError> {
2304    Err(ReleaseContractError::InvalidDocument)
2305}
2306
2307fn validate_signature_field(value: &str, required: bool) -> Result<(), ReleaseContractError> {
2308    if !required && value.is_empty() {
2309        return Ok(());
2310    }
2311    decode_signature(value).map(|_| ())
2312}
2313
2314fn validate_epoch_chain(
2315    epoch: &str,
2316    previous_epoch: &str,
2317    previous_digest: &str,
2318) -> Result<(), ReleaseContractError> {
2319    if !valid_positive_epoch(epoch)
2320        || !valid_epoch(previous_epoch)
2321        || !valid_sha256(previous_digest)
2322        || increment_decimal(previous_epoch).as_deref() != Some(epoch)
2323    {
2324        return invalid_document();
2325    }
2326    if previous_epoch == GENESIS_PREVIOUS_EPOCH {
2327        if previous_digest != GENESIS_PREVIOUS_DOCUMENT_SHA256 {
2328            return invalid_document();
2329        }
2330    } else if previous_digest == GENESIS_PREVIOUS_DOCUMENT_SHA256 {
2331        return invalid_document();
2332    }
2333    Ok(())
2334}
2335
2336fn increment_decimal(value: &str) -> Option<String> {
2337    if !valid_epoch(value) {
2338        return None;
2339    }
2340    let mut bytes = value.as_bytes().to_vec();
2341    let mut index = bytes.len();
2342    while index > 0 {
2343        index -= 1;
2344        if bytes[index] < b'9' {
2345            bytes[index] += 1;
2346            return String::from_utf8(bytes).ok();
2347        }
2348        bytes[index] = b'0';
2349    }
2350    bytes.insert(0, b'1');
2351    String::from_utf8(bytes).ok()
2352}
2353
2354fn validate_time_range(
2355    generated_at: &str,
2356    expires_at: &str,
2357    maximum_seconds: Option<i64>,
2358) -> Result<(i64, i64), ReleaseContractError> {
2359    let generated =
2360        canonical_timestamp_seconds(generated_at).ok_or(ReleaseContractError::InvalidDocument)?;
2361    let expires =
2362        canonical_timestamp_seconds(expires_at).ok_or(ReleaseContractError::InvalidDocument)?;
2363    if expires <= generated || maximum_seconds.is_some_and(|maximum| expires - generated > maximum)
2364    {
2365        return invalid_document();
2366    }
2367    Ok((generated, expires))
2368}
2369
2370fn canonical_timestamp_seconds(value: &str) -> Option<i64> {
2371    let bytes = value.as_bytes();
2372    if bytes.len() != 20
2373        || bytes[4] != b'-'
2374        || bytes[7] != b'-'
2375        || bytes[10] != b'T'
2376        || bytes[13] != b':'
2377        || bytes[16] != b':'
2378        || bytes[19] != b'Z'
2379    {
2380        return None;
2381    }
2382    let year = parse_digits(&bytes[0..4])? as i32;
2383    let month = parse_digits(&bytes[5..7])? as u32;
2384    let day = parse_digits(&bytes[8..10])? as u32;
2385    let hour = parse_digits(&bytes[11..13])? as i64;
2386    let minute = parse_digits(&bytes[14..16])? as i64;
2387    let second = parse_digits(&bytes[17..19])? as i64;
2388    if !(1..=12).contains(&month)
2389        || day < 1
2390        || day > days_in_month(year, month)
2391        || hour > 23
2392        || minute > 59
2393        || second > 59
2394    {
2395        return None;
2396    }
2397    Some(days_from_civil(year, month, day) * 86_400 + hour * 3_600 + minute * 60 + second)
2398}
2399
2400fn parse_digits(bytes: &[u8]) -> Option<u32> {
2401    if bytes.is_empty() || bytes.iter().any(|byte| !byte.is_ascii_digit()) {
2402        return None;
2403    }
2404    bytes.iter().try_fold(0_u32, |value, byte| {
2405        value.checked_mul(10)?.checked_add(u32::from(byte - b'0'))
2406    })
2407}
2408
2409fn days_in_month(year: i32, month: u32) -> u32 {
2410    match month {
2411        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
2412        4 | 6 | 9 | 11 => 30,
2413        2 if year % 400 == 0 || (year % 4 == 0 && year % 100 != 0) => 29,
2414        2 => 28,
2415        _ => 0,
2416    }
2417}
2418
2419fn days_from_civil(year: i32, month: u32, day: u32) -> i64 {
2420    let adjusted_year = i64::from(year) - i64::from(month <= 2);
2421    let era = if adjusted_year >= 0 {
2422        adjusted_year
2423    } else {
2424        adjusted_year - 399
2425    } / 400;
2426    let year_of_era = adjusted_year - era * 400;
2427    let adjusted_month = i64::from(month) + if month > 2 { -3 } else { 9 };
2428    let day_of_year = (153 * adjusted_month + 2) / 5 + i64::from(day) - 1;
2429    let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
2430    era * 146_097 + day_of_era - 719_468
2431}
2432
2433fn validate_sorted_ids(
2434    values: &[String],
2435    minimum: usize,
2436    maximum: usize,
2437    lower: bool,
2438) -> Result<(), ReleaseContractError> {
2439    if values.len() < minimum || values.len() > maximum {
2440        return invalid_document();
2441    }
2442    let mut previous = "";
2443    for value in values {
2444        let valid = if lower {
2445            valid_new_id(value)
2446        } else {
2447            valid_legacy_id(value)
2448        };
2449        if !valid || value.as_str() <= previous {
2450            return invalid_document();
2451        }
2452        previous = value;
2453    }
2454    Ok(())
2455}
2456
2457fn valid_new_id(value: &str) -> bool {
2458    value.len() <= 128
2459        && value.as_bytes().first().is_some_and(u8::is_ascii_lowercase)
2460        && value.bytes().all(|byte| {
2461            byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'.' | b'_' | b'-')
2462        })
2463}
2464
2465fn valid_legacy_id(value: &str) -> bool {
2466    value.len() <= 128
2467        && value
2468            .as_bytes()
2469            .first()
2470            .is_some_and(u8::is_ascii_alphanumeric)
2471        && value
2472            .bytes()
2473            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
2474}
2475
2476fn valid_epoch(value: &str) -> bool {
2477    value == "0" || valid_positive_epoch(value)
2478}
2479
2480fn valid_positive_epoch(value: &str) -> bool {
2481    !value.is_empty()
2482        && value.as_bytes()[0].is_ascii_digit()
2483        && value.as_bytes()[0] != b'0'
2484        && value.bytes().all(|byte| byte.is_ascii_digit())
2485}
2486
2487fn valid_sha256(value: &str) -> bool {
2488    value.len() == 64
2489        && value
2490            .bytes()
2491            .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
2492}
2493
2494fn valid_prefixed_sha256(value: &str) -> bool {
2495    value.strip_prefix("sha256:").is_some_and(valid_sha256)
2496}
2497
2498fn valid_legacy_sha256(value: &str) -> bool {
2499    valid_sha256(value) || valid_prefixed_sha256(value)
2500}
2501
2502fn valid_artifact_ref(value: &str) -> bool {
2503    !value.is_empty()
2504        && value.len() <= 1024
2505        && !value.starts_with('/')
2506        && !value.contains('\\')
2507        && !value.contains(['?', '#'])
2508        && value.bytes().all(|byte| {
2509            byte.is_ascii_alphanumeric()
2510                || matches!(byte, b'.' | b'_' | b'/' | b'@' | b'+' | b'~' | b'-')
2511        })
2512        && value
2513            .split('/')
2514            .all(|segment| !segment.is_empty() && segment != "." && segment != "..")
2515}
2516
2517fn valid_hostname(value: &str) -> bool {
2518    !value.is_empty()
2519        && value
2520            .as_bytes()
2521            .first()
2522            .is_some_and(u8::is_ascii_alphanumeric)
2523        && value
2524            .as_bytes()
2525            .last()
2526            .is_some_and(u8::is_ascii_alphanumeric)
2527        && value
2528            .bytes()
2529            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-'))
2530}
2531
2532fn valid_semver(value: &str) -> bool {
2533    semver::Version::parse(value)
2534        .map(|version| version.to_string() == value)
2535        .unwrap_or(false)
2536}