Skip to main content

crypto_dispatch/
provider.rs

1// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Inspectable provider decisions for algorithm-selected dispatch.
6
7use crypto_core::Algorithm;
8
9use crate::AlgorithmError;
10
11/// Operation requested from the dispatch provider.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13#[non_exhaustive]
14pub enum ProviderOperation {
15    /// Generate a fresh raw keypair.
16    GenerateKeyPair,
17    /// Reconstruct a keypair from caller-owned secret material.
18    DeriveKeyPair,
19    /// Produce a detached signature.
20    Sign,
21    /// Verify a detached signature.
22    Verify,
23    /// Derive a raw key-agreement shared secret.
24    DeriveSharedSecret,
25    /// Encapsulate to a KEM public key.
26    KemEncapsulate,
27    /// Decapsulate a KEM ciphertext.
28    KemDecapsulate,
29}
30
31/// Concrete implementation class selected by dispatch.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33#[non_exhaustive]
34pub enum ProviderKind {
35    /// Package-owned Rust implementation selected by Cargo feature policy.
36    PackageOwnedRust,
37}
38
39/// Runtime lane in which the provider executes.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41#[non_exhaustive]
42pub enum ProviderLane {
43    /// Native Rust target.
44    Native,
45    /// `wasm32` Rust target using package-owned implementations.
46    Wasm,
47}
48
49/// Residency of secret key material used by this dispatch package.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51#[non_exhaustive]
52pub enum KeyResidency {
53    /// Secret material resides in caller or provider process memory.
54    ProcessMemory,
55}
56
57/// Secret-input copy behavior at the provider boundary.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59#[non_exhaustive]
60pub enum KeyCopyBoundary {
61    /// The operation has no secret-key input.
62    NoSecretInput,
63    /// The provider borrows caller-owned secret bytes for the duration of one call.
64    BorrowedCallerSecret,
65    /// The provider creates a new secret owner for the result.
66    ProviderCreatesSecret,
67}
68
69/// Sensitivity and cleanup contract for the provider result.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71#[non_exhaustive]
72pub enum ProviderOutputPolicy {
73    /// The result contains no secret-bearing output.
74    PublicOnly,
75    /// Secret output is returned in a zeroizing Rust owner.
76    ZeroizingSecret,
77}
78
79/// Fixed provider-policy reason recorded for a decision.
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81#[non_exhaustive]
82pub enum ProviderPolicyReason {
83    /// The requested operation and algorithm are supported in the compiled lane.
84    SelectedCompiledImplementation,
85    /// The algorithm does not implement the requested operation family.
86    RejectedOperationMismatch,
87    /// The matching implementation was not compiled into this package.
88    RejectedFeatureDisabled,
89}
90
91/// Whether dispatch selected or rejected the requested provider.
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93#[non_exhaustive]
94pub enum ProviderDisposition {
95    /// The provider was selected.
96    Selected,
97    /// The provider was rejected without trying another implementation.
98    Rejected,
99}
100
101/// Explicit fallback policy attached to every provider decision.
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103#[non_exhaustive]
104pub enum FallbackPolicy {
105    /// Cross-provider and cross-lane fallback is prohibited.
106    Prohibited,
107}
108
109/// Complete, non-secret provider decision produced before dispatch executes.
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111#[non_exhaustive]
112pub struct ProviderDecision {
113    /// Requested operation.
114    pub operation: ProviderOperation,
115    /// Requested algorithm.
116    pub algorithm: Algorithm,
117    /// Provider implementation class considered by policy.
118    pub provider_kind: ProviderKind,
119    /// Runtime lane considered by policy.
120    pub lane: ProviderLane,
121    /// Whether the provider was selected or rejected.
122    pub disposition: ProviderDisposition,
123    /// Fixed reason for the selection or rejection.
124    pub reason: ProviderPolicyReason,
125    /// Secret-key residency for the route.
126    pub key_residency: KeyResidency,
127    /// Secret-input copy behavior for the route.
128    pub key_copy_boundary: KeyCopyBoundary,
129    /// Output sensitivity and cleanup contract.
130    pub output_policy: ProviderOutputPolicy,
131    /// Explicit fallback disposition.
132    pub fallback: FallbackPolicy,
133}
134
135impl ProviderDecision {
136    /// Returns true only when the reviewed provider was selected.
137    #[must_use]
138    pub fn is_selected(self) -> bool {
139        self.disposition == ProviderDisposition::Selected
140    }
141}
142
143/// Resolve one provider decision without executing cryptographic work.
144#[must_use]
145pub fn provider_decision(operation: ProviderOperation, algorithm: Algorithm) -> ProviderDecision {
146    let operation_supported = operation_supports_algorithm(operation, algorithm);
147    let implementation_compiled = implementation_is_compiled(algorithm);
148    let (disposition, reason) = if !operation_supported {
149        (
150            ProviderDisposition::Rejected,
151            ProviderPolicyReason::RejectedOperationMismatch,
152        )
153    } else if !implementation_compiled {
154        (
155            ProviderDisposition::Rejected,
156            ProviderPolicyReason::RejectedFeatureDisabled,
157        )
158    } else {
159        (
160            ProviderDisposition::Selected,
161            ProviderPolicyReason::SelectedCompiledImplementation,
162        )
163    };
164
165    ProviderDecision {
166        operation,
167        algorithm,
168        provider_kind: ProviderKind::PackageOwnedRust,
169        lane: compiled_lane(),
170        disposition,
171        reason,
172        key_residency: KeyResidency::ProcessMemory,
173        key_copy_boundary: key_copy_boundary(operation),
174        output_policy: output_policy(operation),
175        fallback: FallbackPolicy::Prohibited,
176    }
177}
178
179pub(crate) fn require_provider(
180    operation: ProviderOperation,
181    algorithm: Algorithm,
182) -> Result<ProviderDecision, AlgorithmError> {
183    let decision = provider_decision(operation, algorithm);
184    if decision.is_selected() {
185        Ok(decision)
186    } else {
187        Err(AlgorithmError::UnsupportedAlgorithm(algorithm))
188    }
189}
190
191const fn compiled_lane() -> ProviderLane {
192    #[cfg(target_arch = "wasm32")]
193    {
194        ProviderLane::Wasm
195    }
196    #[cfg(not(target_arch = "wasm32"))]
197    {
198        ProviderLane::Native
199    }
200}
201
202const fn key_copy_boundary(operation: ProviderOperation) -> KeyCopyBoundary {
203    match operation {
204        ProviderOperation::GenerateKeyPair => KeyCopyBoundary::ProviderCreatesSecret,
205        ProviderOperation::DeriveKeyPair
206        | ProviderOperation::Sign
207        | ProviderOperation::DeriveSharedSecret
208        | ProviderOperation::KemDecapsulate => KeyCopyBoundary::BorrowedCallerSecret,
209        ProviderOperation::Verify | ProviderOperation::KemEncapsulate => {
210            KeyCopyBoundary::NoSecretInput
211        }
212    }
213}
214
215const fn output_policy(operation: ProviderOperation) -> ProviderOutputPolicy {
216    match operation {
217        ProviderOperation::GenerateKeyPair
218        | ProviderOperation::DeriveKeyPair
219        | ProviderOperation::DeriveSharedSecret
220        | ProviderOperation::KemEncapsulate
221        | ProviderOperation::KemDecapsulate => ProviderOutputPolicy::ZeroizingSecret,
222        ProviderOperation::Sign | ProviderOperation::Verify => ProviderOutputPolicy::PublicOnly,
223    }
224}
225
226const fn operation_supports_algorithm(operation: ProviderOperation, algorithm: Algorithm) -> bool {
227    match operation {
228        ProviderOperation::GenerateKeyPair | ProviderOperation::DeriveKeyPair => {
229            !matches!(algorithm, Algorithm::SlhDsaSha2_128s)
230        }
231        ProviderOperation::Sign | ProviderOperation::Verify => matches!(
232            algorithm,
233            Algorithm::Ed25519
234                | Algorithm::P256
235                | Algorithm::P384
236                | Algorithm::P521
237                | Algorithm::Secp256k1
238                | Algorithm::MlDsa44
239                | Algorithm::MlDsa65
240                | Algorithm::MlDsa87
241        ),
242        ProviderOperation::DeriveSharedSecret => matches!(
243            algorithm,
244            Algorithm::P256 | Algorithm::P384 | Algorithm::P521 | Algorithm::X25519
245        ),
246        ProviderOperation::KemEncapsulate | ProviderOperation::KemDecapsulate => matches!(
247            algorithm,
248            Algorithm::MlKem512 | Algorithm::MlKem768 | Algorithm::MlKem1024 | Algorithm::XWing768
249        ),
250    }
251}
252
253const fn implementation_is_compiled(algorithm: Algorithm) -> bool {
254    match algorithm {
255        Algorithm::Ed25519 => cfg!(feature = "ed25519"),
256        Algorithm::X25519 => cfg!(feature = "x25519"),
257        Algorithm::P256 => cfg!(feature = "p256"),
258        Algorithm::P384 => cfg!(feature = "p384"),
259        Algorithm::P521 => cfg!(feature = "p521"),
260        Algorithm::Secp256k1 => cfg!(feature = "secp256k1"),
261        Algorithm::MlDsa44 => cfg!(feature = "ml-dsa-44"),
262        Algorithm::MlDsa65 => cfg!(feature = "ml-dsa-65"),
263        Algorithm::MlDsa87 => cfg!(feature = "ml-dsa-87"),
264        Algorithm::SlhDsaSha2_128s => false,
265        Algorithm::MlKem512 => cfg!(feature = "ml-kem-512"),
266        Algorithm::MlKem768 => cfg!(feature = "ml-kem-768"),
267        Algorithm::MlKem1024 => cfg!(feature = "ml-kem-1024"),
268        Algorithm::XWing768 => cfg!(feature = "x-wing"),
269    }
270}