Skip to main content

riffdb_types/
hash.rs

1//! Versioned, domain-separated SHA-256 and HMAC-SHA-256 digests.
2
3use std::fmt;
4
5use hmac::{Hmac, KeyInit, Mac};
6use sha2::{Digest, Sha256};
7
8use crate::{
9    AdapterConformanceManifestHash, ApplicationExportManifestHash, ApplicationExportPageHash,
10    ApplicationExportReceiptHash, ApplicationInstallationPlanHash,
11    ApplicationInstallationReceiptHash, ApplicationLockHash, ApplicationManifestHash,
12    ApplicationPortabilityManifestHash, ApplicationReimportReceiptHash,
13    ApplicationRoleDefinitionHash, ApplicationRoleHash, ApplicationSourceHash, CanonicalInputHash,
14    CanonicalValueHash, CapabilityTokenDigest, ColumnarDefinitionSemanticsHashV1,
15    ColumnarProjectionSpecHashV1, ConflictKeyHash, ContractBundleHash, ContractMigrationInputHash,
16    ContractMigrationJournalHash, ContractMigrationValidationDigest, ContractPlanRootHash,
17    DigestKey, DigestKeyId, EntityKeyHash, EntityRecordHash, EntityTransitionHash,
18    EventConsumerIdentityHash, EventHash, GeneratedArtifactHash, MigrationBundleHash,
19    MigrationSourceHash, OfflineMaintenanceInputHash, PartitionKeyHash,
20    PartitionSetObservationHash, PlanHash, ProjectionApplyHash, ProjectionPlanHash,
21    ProjectionProviderDescriptorHash, QueryModuleHash, QueryParameterHash, QueryPlanHash,
22    QuerySourceHash, ReactiveModuleHash, ReactiveOperationHash, ReactiveSourceHash,
23    ScheduledAttemptHash, SchemaHash, SourceHash,
24};
25
26/// Hash framing and algorithm scheme defined by ADR-0011.
27pub const DIGEST_SCHEME_V1: u8 = 0x01;
28
29const HASH_PREFIX: &[u8] = b"RIFFDB-HASH\0";
30const HMAC_PREFIX: &[u8] = b"RIFFDB-HMAC\0";
31
32/// An unkeyed SHA-256 domain from the accepted central registry.
33#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
34pub enum HashDomain {
35    /// Canonical value content.
36    CanonicalValue,
37    /// Canonical contract source.
38    Source,
39    /// Exact migration source document.
40    MigrationSource,
41    /// Canonical contract bundle.
42    ContractBundle,
43    /// Canonical compiled migration bundle.
44    MigrationBundle,
45    /// Executable command plan.
46    Plan,
47    /// Validated projection plan.
48    ProjectionPlan,
49    /// Canonical sealed projection-provider descriptor.
50    ProjectionProviderDescriptor,
51    /// Closed RiffQL query access program.
52    QueryPlan,
53    /// Ordered selected partition/index/provider epoch observations.
54    PartitionSetObservation,
55    /// Canonical immutable query module.
56    QueryModule,
57    /// Exact reactive source document.
58    ReactiveSource,
59    /// Canonical reactive operation plan.
60    ReactiveOperation,
61    /// Canonical immutable reactive module.
62    ReactiveModule,
63    /// Exact durable event consumer identity.
64    EventConsumerIdentity,
65    /// Canonical application manifest.
66    ApplicationManifest,
67    /// Author-owned symbolic application source manifest.
68    ApplicationSource,
69    /// Compiler-owned exact application lock.
70    ApplicationLock,
71    /// Compiler-generated application artifact.
72    GeneratedArtifact,
73    /// Exact tenant-unbound application role definition.
74    ApplicationRoleDefinition,
75    /// Canonical compiled application role.
76    ApplicationRole,
77    /// Exact RiffQL source document.
78    QuerySource,
79    /// Canonical name-addressed RiffQL parameter set.
80    QueryParameters,
81    /// Ordered semantic plan set for one contract bundle.
82    ContractPlanRoot,
83    /// Canonical command input.
84    CommandInput,
85    /// Durable event content.
86    Event,
87    /// Complete canonical entity-record preimage.
88    EntityRecord,
89    /// One canonical committed entity transition.
90    EntityTransition,
91    /// Canonical entity key.
92    EntityKey,
93    /// Canonical conflict key.
94    ConflictKey,
95    /// Canonical logical partition key.
96    PartitionKey,
97    /// Generated schema content.
98    Schema,
99    /// One complete checked projection apply request.
100    ProjectionApply,
101    /// One checked offline-maintenance semantic input.
102    OfflineMaintenanceInput,
103    /// One checked contract-migration semantic input.
104    ContractMigrationInput,
105    /// One canonical contract-migration journal state.
106    ContractMigrationJournal,
107    /// One complete staged contract-migration validation.
108    ContractMigrationValidation,
109    /// One bounded command-batch source, item, or checkpoint document.
110    CommandBatch,
111    /// Generated contextual-reaction idempotency.
112    ContextualReaction,
113    /// Opaque MCP reactive-resource wakeup generation.
114    ReactiveWakeup,
115    /// One bounded scheduled workflow attempt.
116    ScheduledAttempt,
117    /// One canonical application installation plan.
118    ApplicationInstallationPlan,
119    /// One canonical terminal installation receipt.
120    ApplicationInstallationReceipt,
121    /// One canonical adapter conformance manifest.
122    AdapterConformanceManifest,
123    /// One canonical page of symbolic application-export records.
124    ApplicationExportPage,
125    /// One canonical symbolic application-export manifest.
126    ApplicationExportManifest,
127    /// One terminal symbolic application-export receipt.
128    ApplicationExportReceipt,
129    /// One opaque checkpoint cursor for a symbolic application export.
130    ApplicationExportCursor,
131    /// Immutable export genesis and ordered page-ledger prefix (ADR-0232).
132    ApplicationExportLedger,
133    /// One adapter-owned compiled reimport mapping manifest.
134    ApplicationPortabilityManifest,
135    /// One terminal application reimport reconciliation receipt.
136    ApplicationReimportReceipt,
137    /// One complete exact-result provider activation checkpoint.
138    ExactResultCheckpoint,
139    /// One schema-bound columnar definition or complete projection specification.
140    ColumnarProjectionSpec,
141    /// Canonical independently evaluated authoritative projection source rows.
142    ProjectionSourceRows,
143    /// Immutable authoritative projection replay-source descriptor.
144    ProjectionSourceDescriptor,
145    /// Captured authoritative projection source origin commitment.
146    ProjectionSourceOrigin,
147    /// Per-row canonical copy progress of one immutable replay-source candidate.
148    ProjectionSourceCopyProgress,
149}
150
151impl HashDomain {
152    /// Every registered unkeyed domain, for compatibility and collision checks.
153    pub const ALL: [Self; 57] = [
154        Self::CanonicalValue,
155        Self::Source,
156        Self::MigrationSource,
157        Self::ContractBundle,
158        Self::MigrationBundle,
159        Self::Plan,
160        Self::ProjectionPlan,
161        Self::ProjectionProviderDescriptor,
162        Self::QueryPlan,
163        Self::PartitionSetObservation,
164        Self::QueryModule,
165        Self::ReactiveSource,
166        Self::ReactiveOperation,
167        Self::ReactiveModule,
168        Self::EventConsumerIdentity,
169        Self::ApplicationManifest,
170        Self::ApplicationSource,
171        Self::ApplicationLock,
172        Self::GeneratedArtifact,
173        Self::ApplicationRoleDefinition,
174        Self::ApplicationRole,
175        Self::QuerySource,
176        Self::QueryParameters,
177        Self::ContractPlanRoot,
178        Self::CommandInput,
179        Self::Event,
180        Self::EntityRecord,
181        Self::EntityTransition,
182        Self::EntityKey,
183        Self::ConflictKey,
184        Self::PartitionKey,
185        Self::Schema,
186        Self::ProjectionApply,
187        Self::OfflineMaintenanceInput,
188        Self::ContractMigrationInput,
189        Self::ContractMigrationJournal,
190        Self::ContractMigrationValidation,
191        Self::CommandBatch,
192        Self::ContextualReaction,
193        Self::ReactiveWakeup,
194        Self::ScheduledAttempt,
195        Self::ApplicationInstallationPlan,
196        Self::ApplicationInstallationReceipt,
197        Self::AdapterConformanceManifest,
198        Self::ApplicationExportPage,
199        Self::ApplicationExportManifest,
200        Self::ApplicationExportReceipt,
201        Self::ApplicationExportCursor,
202        Self::ApplicationExportLedger,
203        Self::ApplicationPortabilityManifest,
204        Self::ApplicationReimportReceipt,
205        Self::ExactResultCheckpoint,
206        Self::ColumnarProjectionSpec,
207        Self::ProjectionSourceRows,
208        Self::ProjectionSourceDescriptor,
209        Self::ProjectionSourceOrigin,
210        Self::ProjectionSourceCopyProgress,
211    ];
212
213    /// Returns the immutable ASCII v1 domain label.
214    pub const fn label(self) -> &'static str {
215        match self {
216            Self::CanonicalValue => "riffdb.canonical-value/v1",
217            Self::Source => "riffdb.source/v1",
218            Self::MigrationSource => "riffdb.migration-source/v1",
219            Self::ContractBundle => "riffdb.contract-bundle/v1",
220            Self::MigrationBundle => "riffdb.migration-bundle/v1",
221            Self::Plan => "riffdb.plan/v1",
222            Self::ProjectionPlan => "riffdb.projection-plan/v1",
223            Self::ProjectionProviderDescriptor => "riffdb.projection-provider-descriptor/v1",
224            Self::QueryPlan => "riffdb.query-plan/v1",
225            Self::PartitionSetObservation => "riffdb.partition-set-observation/v1",
226            Self::QueryModule => "riffdb.query-module/v1",
227            Self::ReactiveSource => "riffdb.reactive-source/v1",
228            Self::ReactiveOperation => "riffdb.reactive-operation/v1",
229            Self::ReactiveModule => "riffdb.reactive-module/v1",
230            Self::EventConsumerIdentity => "riffdb.event-consumer-identity/v1",
231            Self::ApplicationManifest => "riffdb.application-manifest/v1",
232            Self::ApplicationSource => "riffdb.application-source/v1",
233            Self::ApplicationLock => "riffdb.application-lock/v1",
234            Self::GeneratedArtifact => "riffdb.generated-artifact/v1",
235            Self::ApplicationRoleDefinition => "riffdb.application-role-definition/v1",
236            Self::ApplicationRole => "riffdb.application-role/v1",
237            Self::QuerySource => "riffdb.query-source/v1",
238            Self::QueryParameters => "riffdb.query-parameters/v1",
239            Self::ContractPlanRoot => "riffdb.contract-plan-root/v1",
240            Self::CommandInput => "riffdb.command-input/v1",
241            Self::Event => "riffdb.event/v1",
242            Self::EntityRecord => "riffdb.entity-record/v1",
243            Self::EntityTransition => "riffdb.entity-transition/v1",
244            Self::EntityKey => "riffdb.entity-key/v1",
245            Self::ConflictKey => "riffdb.conflict-key/v1",
246            Self::PartitionKey => "riffdb.partition-key/v1",
247            Self::Schema => "riffdb.schema/v1",
248            Self::ProjectionApply => "riffdb.projection-apply/v1",
249            Self::OfflineMaintenanceInput => "riffdb.offline-maintenance-input/v1",
250            Self::ContractMigrationInput => "riffdb.contract-migration-input/v1",
251            Self::ContractMigrationJournal => "riffdb.contract-migration-journal/v1",
252            Self::ContractMigrationValidation => "riffdb.contract-migration-validation/v1",
253            Self::CommandBatch => "riffdb.command-batch/v1",
254            Self::ContextualReaction => "riffdb.contextual-reaction/v1",
255            Self::ReactiveWakeup => "riffdb.reactive-wakeup/v1",
256            Self::ScheduledAttempt => "riffdb.scheduled-attempt/v1",
257            Self::ApplicationInstallationPlan => "riffdb.application-installation-plan/v1",
258            Self::ApplicationInstallationReceipt => "riffdb.application-installation-receipt/v1",
259            Self::AdapterConformanceManifest => "riffdb.adapter-conformance-manifest/v1",
260            Self::ApplicationExportPage => "riffdb.application-export-page/v1",
261            Self::ApplicationExportManifest => "riffdb.application-export-manifest/v1",
262            Self::ApplicationExportReceipt => "riffdb.application-export-receipt/v1",
263            Self::ApplicationExportCursor => "riffdb.application-export-cursor/v1",
264            Self::ApplicationExportLedger => "riffdb.application-export-ledger/v1",
265            Self::ApplicationPortabilityManifest => "riffdb.application-portability-manifest/v1",
266            Self::ApplicationReimportReceipt => "riffdb.application-reimport-receipt/v1",
267            Self::ExactResultCheckpoint => "riffdb.exact-result-checkpoint/v1",
268            Self::ColumnarProjectionSpec => "riffdb.columnar-projection-spec/v1",
269            Self::ProjectionSourceRows => "riffdb.projection-source-rows/v1",
270            Self::ProjectionSourceDescriptor => "riffdb.projection-source-descriptor/v1",
271            Self::ProjectionSourceOrigin => "riffdb.projection-source-origin/v1",
272            Self::ProjectionSourceCopyProgress => "riffdb.projection-source-copy-progress/v1",
273        }
274    }
275}
276
277/// A keyed HMAC-SHA-256 domain from the accepted central registry.
278#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
279pub enum KeyedHashDomain {
280    /// Caller-provided idempotency key lookup.
281    IdempotencyKey,
282    /// Decoded opaque capability-token lookup.
283    CapabilityToken,
284    /// Server-sealed contextual causation token.
285    ContextualCausation,
286}
287
288impl KeyedHashDomain {
289    /// Every registered keyed domain, for compatibility and collision checks.
290    pub const ALL: [Self; 3] = [
291        Self::IdempotencyKey,
292        Self::CapabilityToken,
293        Self::ContextualCausation,
294    ];
295
296    /// Returns the immutable ASCII v1 domain label.
297    pub const fn label(self) -> &'static str {
298        match self {
299            Self::IdempotencyKey => "riffdb.idempotency-key/v1",
300            Self::CapabilityToken => "riffdb.capability-token/v1",
301            Self::ContextualCausation => "riffdb.contextual-causation/v1",
302        }
303    }
304}
305
306/// A versioned unkeyed content digest.
307#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
308pub struct ContentDigest {
309    scheme: u8,
310    domain: HashDomain,
311    bytes: [u8; 32],
312}
313
314impl ContentDigest {
315    const fn new(domain: HashDomain, bytes: [u8; 32]) -> Self {
316        Self {
317            scheme: DIGEST_SCHEME_V1,
318            domain,
319            bytes,
320        }
321    }
322
323    /// Reconstructs a supported stored digest representation.
324    ///
325    /// This checks the scheme only. It does not verify a preimage, publication,
326    /// source ancestry, or authority. Callers must check the expected domain and
327    /// independently verify the authoritative bytes before using the digest.
328    #[must_use]
329    pub const fn from_stored_parts(
330        scheme: u8,
331        domain: HashDomain,
332        bytes: [u8; 32],
333    ) -> Option<Self> {
334        if scheme != DIGEST_SCHEME_V1 {
335            return None;
336        }
337        Some(Self::new(domain, bytes))
338    }
339
340    /// Returns the hash scheme version.
341    pub const fn scheme(self) -> u8 {
342        self.scheme
343    }
344
345    /// Returns the immutable semantic domain used to calculate the digest.
346    pub const fn domain(self) -> HashDomain {
347        self.domain
348    }
349
350    /// Returns the 32 digest bytes.
351    pub const fn as_bytes(&self) -> &[u8; 32] {
352        &self.bytes
353    }
354}
355
356impl fmt::Debug for ContentDigest {
357    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
358        formatter
359            .debug_struct("ContentDigest")
360            .field("scheme", &self.scheme)
361            .field("domain", &self.domain)
362            .field("bytes", &HexDigest(&self.bytes))
363            .finish()
364    }
365}
366
367/// A versioned keyed digest that records the selected key ID without exposing
368/// key material.
369#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
370pub struct KeyedDigest {
371    scheme: u8,
372    key_id: DigestKeyId,
373    bytes: [u8; 32],
374}
375
376impl KeyedDigest {
377    /// Constructs a v1 keyed digest from its key ID and digest bytes.
378    pub const fn new(key_id: DigestKeyId, bytes: [u8; 32]) -> Self {
379        Self {
380            scheme: DIGEST_SCHEME_V1,
381            key_id,
382            bytes,
383        }
384    }
385
386    /// Returns the HMAC scheme version.
387    pub const fn scheme(self) -> u8 {
388        self.scheme
389    }
390
391    /// Returns the non-secret digest-key identifier.
392    pub const fn key_id(self) -> DigestKeyId {
393        self.key_id
394    }
395
396    /// Returns the 32 digest bytes.
397    pub const fn as_bytes(&self) -> &[u8; 32] {
398        &self.bytes
399    }
400}
401
402impl fmt::Debug for KeyedDigest {
403    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
404        formatter
405            .debug_struct("KeyedDigest")
406            .field("scheme", &self.scheme)
407            .field("key_id", &self.key_id)
408            .field("bytes", &"[REDACTED]")
409            .finish()
410    }
411}
412
413/// Computes a domain-separated v1 SHA-256 content digest.
414pub fn hash(domain: HashDomain, payload: &[u8]) -> ContentDigest {
415    let mut hasher = Sha256::new();
416    write_frame(&mut hasher, HASH_PREFIX, domain.label(), payload);
417    ContentDigest::new(domain, hasher.finalize().into())
418}
419
420/// A streamed payload did not match its declared v1 frame length.
421#[derive(Clone, Copy, Debug, Eq, PartialEq)]
422pub struct HashLengthMismatch;
423
424impl fmt::Display for HashLengthMismatch {
425    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
426        formatter.write_str("content hash payload length mismatch")
427    }
428}
429
430impl std::error::Error for HashLengthMismatch {}
431
432/// Constant-space v1 content hashing with an exact, checked payload length.
433///
434/// The length precedes the payload in the immutable v1 framing. Callers must
435/// measure and stream the same immutable input. Excess input poisons the
436/// hasher; neither incomplete nor poisoned input can produce a digest.
437pub struct ContentHasher {
438    hasher: Sha256,
439    domain: HashDomain,
440    remaining: Option<u64>,
441}
442
443impl fmt::Debug for ContentHasher {
444    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
445        formatter.write_str("ContentHasher([REDACTED])")
446    }
447}
448
449impl ContentHasher {
450    /// Starts an exact-length, domain-separated SHA-256 v1 frame.
451    #[must_use]
452    pub fn new(domain: HashDomain, payload_length: u64) -> Self {
453        let mut hasher = Sha256::new();
454        write_frame_header(&mut hasher, HASH_PREFIX, domain.label(), payload_length);
455        Self {
456            hasher,
457            domain,
458            remaining: Some(payload_length),
459        }
460    }
461
462    /// Adds bytes without buffering them, refusing and poisoning excess input.
463    pub fn update(&mut self, payload: &[u8]) -> Result<(), HashLengthMismatch> {
464        self.remaining = self.remaining.and_then(|remaining| {
465            u64::try_from(payload.len())
466                .ok()
467                .and_then(|length| remaining.checked_sub(length))
468        });
469        if self.remaining.is_none() {
470            return Err(HashLengthMismatch);
471        }
472        Digest::update(&mut self.hasher, payload);
473        Ok(())
474    }
475
476    /// Finishes only after exactly the declared number of bytes was supplied.
477    pub fn finish(self) -> Result<ContentDigest, HashLengthMismatch> {
478        if self.remaining != Some(0) {
479            return Err(HashLengthMismatch);
480        }
481        Ok(ContentDigest::new(
482            self.domain,
483            self.hasher.finalize().into(),
484        ))
485    }
486}
487
488/// Hashes one bounded command-batch source, item, or checkpoint document.
489#[must_use]
490pub fn hash_command_batch_document(payload: &[u8]) -> ContentDigest {
491    hash(HashDomain::CommandBatch, payload)
492}
493
494macro_rules! typed_hash_function {
495    ($(#[$meta:meta])* $name:ident, $domain:ident, $output:ident) => {
496        $(#[$meta])*
497        #[must_use]
498        pub fn $name(payload: &[u8]) -> $output {
499            $output::from_bytes(*hash(HashDomain::$domain, payload).as_bytes())
500        }
501    };
502}
503
504typed_hash_function!(
505    /// Hashes a canonical value document in its immutable v1 domain.
506    hash_canonical_value,
507    CanonicalValue,
508    CanonicalValueHash
509);
510
511typed_hash_function!(
512    /// Hashes one purpose-framed canonical columnar definition-semantics document.
513    hash_columnar_definition_semantics,
514    ColumnarProjectionSpec,
515    ColumnarDefinitionSemanticsHashV1
516);
517typed_hash_function!(
518    /// Hashes one purpose-framed complete schema-bound columnar specification.
519    hash_columnar_projection_spec,
520    ColumnarProjectionSpec,
521    ColumnarProjectionSpecHashV1
522);
523typed_hash_function!(
524    /// Hashes canonical contract source in its immutable v1 domain.
525    hash_source,
526    Source,
527    SourceHash
528);
529typed_hash_function!(
530    /// Hashes one exact migration source document in its immutable v1 domain.
531    hash_migration_source,
532    MigrationSource,
533    MigrationSourceHash
534);
535typed_hash_function!(
536    /// Hashes an immutable contract bundle in its v1 domain.
537    hash_contract_bundle,
538    ContractBundle,
539    ContractBundleHash
540);
541typed_hash_function!(
542    /// Hashes one canonical compiled migration bundle in its immutable v1 domain.
543    hash_migration_bundle,
544    MigrationBundle,
545    MigrationBundleHash
546);
547typed_hash_function!(
548    /// Hashes an executable command plan in its immutable v1 domain.
549    hash_plan,
550    Plan,
551    PlanHash
552);
553typed_hash_function!(
554    /// Hashes a validated projection plan in its immutable v1 domain.
555    hash_projection_plan,
556    ProjectionPlan,
557    ProjectionPlanHash
558);
559typed_hash_function!(
560    /// Hashes a canonical sealed projection-provider descriptor.
561    hash_projection_provider_descriptor,
562    ProjectionProviderDescriptor,
563    ProjectionProviderDescriptorHash
564);
565typed_hash_function!(
566    /// Hashes a closed RiffQL query access program in its immutable v1 domain.
567    hash_query_plan,
568    QueryPlan,
569    QueryPlanHash
570);
571typed_hash_function!(
572    /// Hashes ordered bounded partition/index/provider epoch observations.
573    hash_partition_set_observation,
574    PartitionSetObservation,
575    PartitionSetObservationHash
576);
577typed_hash_function!(
578    /// Hashes a canonical immutable query module in its immutable v1 domain.
579    hash_query_module,
580    QueryModule,
581    QueryModuleHash
582);
583typed_hash_function!(
584    /// Hashes one exact reactive source document.
585    hash_reactive_source,
586    ReactiveSource,
587    ReactiveSourceHash
588);
589typed_hash_function!(
590    /// Hashes one canonical reactive operation plan.
591    hash_reactive_operation,
592    ReactiveOperation,
593    ReactiveOperationHash
594);
595typed_hash_function!(
596    /// Hashes one canonical immutable reactive module.
597    hash_reactive_module,
598    ReactiveModule,
599    ReactiveModuleHash
600);
601typed_hash_function!(
602    /// Hashes one exact durable event consumer identity.
603    hash_event_consumer_identity,
604    EventConsumerIdentity,
605    EventConsumerIdentityHash
606);
607typed_hash_function!(
608    /// Hashes one canonical bounded scheduled workflow attempt.
609    hash_scheduled_attempt,
610    ScheduledAttempt,
611    ScheduledAttemptHash
612);
613typed_hash_function!(
614    /// Hashes one canonical exact application installation plan.
615    hash_application_installation_plan,
616    ApplicationInstallationPlan,
617    ApplicationInstallationPlanHash
618);
619typed_hash_function!(
620    /// Hashes one canonical terminal application installation receipt.
621    hash_application_installation_receipt,
622    ApplicationInstallationReceipt,
623    ApplicationInstallationReceiptHash
624);
625typed_hash_function!(
626    /// Hashes one canonical adapter conformance manifest.
627    hash_adapter_conformance_manifest,
628    AdapterConformanceManifest,
629    AdapterConformanceManifestHash
630);
631typed_hash_function!(
632    /// Hashes one canonical symbolic application-export page.
633    hash_application_export_page,
634    ApplicationExportPage,
635    ApplicationExportPageHash
636);
637typed_hash_function!(
638    /// Hashes one canonical symbolic application-export manifest.
639    hash_application_export_manifest,
640    ApplicationExportManifest,
641    ApplicationExportManifestHash
642);
643typed_hash_function!(
644    /// Hashes one terminal symbolic application-export receipt.
645    hash_application_export_receipt,
646    ApplicationExportReceipt,
647    ApplicationExportReceiptHash
648);
649typed_hash_function!(
650    /// Hashes one adapter-owned compiled reimport mapping manifest.
651    hash_application_portability_manifest,
652    ApplicationPortabilityManifest,
653    ApplicationPortabilityManifestHash
654);
655typed_hash_function!(
656    /// Hashes one terminal application reimport reconciliation receipt.
657    hash_application_reimport_receipt,
658    ApplicationReimportReceipt,
659    ApplicationReimportReceiptHash
660);
661typed_hash_function!(
662    /// Hashes one canonical application manifest in its immutable v1 domain.
663    hash_application_manifest,
664    ApplicationManifest,
665    ApplicationManifestHash
666);
667typed_hash_function!(
668    /// Hashes one canonical symbolic application source manifest.
669    hash_application_source,
670    ApplicationSource,
671    ApplicationSourceHash
672);
673typed_hash_function!(
674    /// Hashes one canonical compiler-owned application lock.
675    hash_application_lock,
676    ApplicationLock,
677    ApplicationLockHash
678);
679typed_hash_function!(
680    /// Hashes one compiler-generated application artifact.
681    hash_generated_artifact,
682    GeneratedArtifact,
683    GeneratedArtifactHash
684);
685typed_hash_function!(
686    /// Hashes one exact tenant-unbound application role definition.
687    hash_application_role_definition,
688    ApplicationRoleDefinition,
689    ApplicationRoleDefinitionHash
690);
691typed_hash_function!(
692    /// Hashes one canonical compiled application role in its immutable v1 domain.
693    hash_application_role,
694    ApplicationRole,
695    ApplicationRoleHash
696);
697typed_hash_function!(
698    /// Hashes one exact RiffQL source document in its immutable v1 domain.
699    hash_query_source,
700    QuerySource,
701    QuerySourceHash
702);
703typed_hash_function!(
704    /// Hashes one canonical name-addressed RiffQL parameter set.
705    hash_query_parameters,
706    QueryParameters,
707    QueryParameterHash
708);
709typed_hash_function!(
710    /// Hashes an ordered contract semantic-plan set in its immutable v1 domain.
711    hash_contract_plan_root,
712    ContractPlanRoot,
713    ContractPlanRootHash
714);
715typed_hash_function!(
716    /// Hashes canonical command input in its immutable v1 domain.
717    hash_command_input,
718    CommandInput,
719    CanonicalInputHash
720);
721typed_hash_function!(
722    /// Hashes canonical event content in its immutable v1 domain.
723    hash_event,
724    Event,
725    EventHash
726);
727typed_hash_function!(
728    /// Hashes a complete canonical entity-record preimage in its immutable v1 domain.
729    hash_entity_record,
730    EntityRecord,
731    EntityRecordHash
732);
733typed_hash_function!(
734    /// Hashes one canonical committed entity transition in its immutable v1 domain.
735    hash_entity_transition,
736    EntityTransition,
737    EntityTransitionHash
738);
739typed_hash_function!(
740    /// Hashes canonical entity-key bytes in their immutable v1 domain.
741    hash_entity_key,
742    EntityKey,
743    EntityKeyHash
744);
745typed_hash_function!(
746    /// Hashes canonical conflict-key bytes in their immutable v1 domain.
747    hash_conflict_key,
748    ConflictKey,
749    ConflictKeyHash
750);
751typed_hash_function!(
752    /// Hashes canonical partition-key bytes in their immutable v1 domain.
753    hash_partition_key,
754    PartitionKey,
755    PartitionKeyHash
756);
757typed_hash_function!(
758    /// Hashes generated schema content in its immutable v1 domain.
759    hash_schema,
760    Schema,
761    SchemaHash
762);
763typed_hash_function!(
764    /// Hashes the canonical bytes of a complete checked projection apply request.
765    ///
766    /// This primitive performs domain separation only. The storage API owns the
767    /// semantic request validation and canonical preimage construction.
768    hash_projection_apply,
769    ProjectionApply,
770    ProjectionApplyHash
771);
772typed_hash_function!(
773    /// Hashes one canonical offline-maintenance semantic input.
774    hash_offline_maintenance_input,
775    OfflineMaintenanceInput,
776    OfflineMaintenanceInputHash
777);
778typed_hash_function!(
779    /// Hashes one canonical contract-migration semantic input.
780    hash_contract_migration_input,
781    ContractMigrationInput,
782    ContractMigrationInputHash
783);
784typed_hash_function!(
785    /// Hashes one canonical contract-migration journal state.
786    hash_contract_migration_journal,
787    ContractMigrationJournal,
788    ContractMigrationJournalHash
789);
790typed_hash_function!(
791    /// Hashes one complete staged contract-migration validation report.
792    hash_contract_migration_validation,
793    ContractMigrationValidation,
794    ContractMigrationValidationDigest
795);
796
797/// Computes a domain-separated v1 HMAC-SHA-256 lookup digest.
798pub fn keyed_hash(
799    domain: KeyedHashDomain,
800    key_id: DigestKeyId,
801    key: &DigestKey,
802    payload: &[u8],
803) -> KeyedDigest {
804    keyed_hash_secret(domain, key_id, key.expose_secret(), payload)
805}
806
807/// Computes a domain-separated v1 HMAC-SHA-256 lookup digest from borrowed
808/// secret key bytes.
809pub fn keyed_hash_secret(
810    domain: KeyedHashDomain,
811    key_id: DigestKeyId,
812    key_bytes: &[u8; 32],
813    payload: &[u8],
814) -> KeyedDigest {
815    let mut mac = Hmac::<Sha256>::new_from_slice(key_bytes)
816        .expect("HMAC-SHA-256 accepts keys of every length");
817    write_frame(&mut mac, HMAC_PREFIX, domain.label(), payload);
818    KeyedDigest::new(key_id, mac.finalize().into_bytes().into())
819}
820
821/// Computes the v1 capability-token lookup digest from exactly 32 decoded raw
822/// token bytes.
823#[must_use]
824pub fn hash_capability_token(
825    key_id: DigestKeyId,
826    key: &DigestKey,
827    raw_token: &[u8; 32],
828) -> CapabilityTokenDigest {
829    hash_capability_token_secret(key_id, key.expose_secret(), raw_token)
830}
831
832/// Computes the v1 capability-token lookup digest from borrowed secret key
833/// bytes and exactly 32 decoded raw token bytes.
834#[must_use]
835pub fn hash_capability_token_secret(
836    key_id: DigestKeyId,
837    key_bytes: &[u8; 32],
838    raw_token: &[u8; 32],
839) -> CapabilityTokenDigest {
840    let digest = keyed_hash_secret(
841        KeyedHashDomain::CapabilityToken,
842        key_id,
843        key_bytes,
844        raw_token,
845    );
846    CapabilityTokenDigest::from_hmac_bytes(key_id, *digest.as_bytes())
847}
848
849fn write_frame<T: sha2::digest::Update>(
850    target: &mut T,
851    prefix: &[u8],
852    domain: &str,
853    payload: &[u8],
854) {
855    write_frame_header(target, prefix, domain, payload.len() as u64);
856    target.update(payload);
857}
858
859fn write_frame_header<T: sha2::digest::Update>(
860    target: &mut T,
861    prefix: &[u8],
862    domain: &str,
863    payload_length: u64,
864) {
865    let domain = domain.as_bytes();
866    debug_assert!(u16::try_from(domain.len()).is_ok());
867    let domain_length = domain.len() as u16;
868    target.update(prefix);
869    target.update(&[DIGEST_SCHEME_V1]);
870    target.update(&domain_length.to_be_bytes());
871    target.update(domain);
872    target.update(&payload_length.to_be_bytes());
873}
874
875struct HexDigest<'a>(&'a [u8; 32]);
876
877impl fmt::Debug for HexDigest<'_> {
878    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
879        for byte in self.0 {
880            write!(formatter, "{byte:02x}")?;
881        }
882        Ok(())
883    }
884}
885
886#[cfg(test)]
887mod tests {
888    use std::collections::BTreeSet;
889
890    use super::*;
891
892    #[test]
893    // req: REP-002
894    fn streaming_hash_preserves_v1_framing_across_chunk_boundaries() {
895        let payload: Vec<u8> = (0..=255).cycle().take(1025).collect();
896        for domain in HashDomain::ALL {
897            for length in [0, 1, 55, 56, 63, 64, 65, 1025] {
898                for chunk_size in [1, 7, 64, 129] {
899                    let mut streamed = ContentHasher::new(domain, length as u64);
900                    for chunk in payload[..length].chunks(chunk_size) {
901                        streamed.update(chunk).expect("within declared length");
902                    }
903                    assert_eq!(
904                        streamed.finish().expect("complete"),
905                        hash(domain, &payload[..length])
906                    );
907                }
908            }
909        }
910    }
911
912    #[test]
913    // req: REP-002
914    fn streaming_hash_refuses_short_extra_and_poisoned_input() {
915        let mut short = ContentHasher::new(HashDomain::Schema, 2);
916        short.update(b"a").expect("prefix");
917        assert_eq!(short.finish(), Err(HashLengthMismatch));
918        let mut extra = ContentHasher::new(HashDomain::Schema, 1);
919        assert_eq!(extra.update(b"secret"), Err(HashLengthMismatch));
920        assert_eq!(extra.update(b"a"), Err(HashLengthMismatch));
921        assert_eq!(extra.finish(), Err(HashLengthMismatch));
922        let mut complete = ContentHasher::new(HashDomain::Schema, 1);
923        complete.update(b"a").expect("complete");
924        assert_eq!(complete.update(b"b"), Err(HashLengthMismatch));
925        assert_eq!(complete.finish(), Err(HashLengthMismatch));
926        assert_eq!(
927            ContentHasher::new(HashDomain::Schema, u64::MAX).finish(),
928            Err(HashLengthMismatch)
929        );
930    }
931
932    #[test]
933    fn domain_registry_has_no_duplicate_labels() {
934        let mut labels = BTreeSet::new();
935        for domain in HashDomain::ALL {
936            assert!(labels.insert(domain.label()));
937        }
938        for domain in KeyedHashDomain::ALL {
939            assert!(labels.insert(domain.label()));
940        }
941    }
942
943    #[test]
944    fn identical_payloads_are_separated_by_domain() {
945        let values = HashDomain::ALL
946            .map(|domain| hash(domain, b"same payload"))
947            .into_iter()
948            .collect::<BTreeSet<_>>();
949        assert_eq!(values.len(), HashDomain::ALL.len());
950    }
951
952    #[test]
953    fn content_digest_retains_its_domain() {
954        for domain in HashDomain::ALL {
955            assert_eq!(hash(domain, b"payload").domain(), domain);
956        }
957    }
958}