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}