Add typed schema records and strict version checks
This commit is contained in:
parent
b438dd5201
commit
16defc768a
10 changed files with 3164 additions and 1 deletions
122
Cargo.lock
generated
122
Cargo.lock
generated
|
|
@ -2,6 +2,27 @@
|
|||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "cobs"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "embedded-io"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
|
||||
|
||||
[[package]]
|
||||
name = "embedded-io"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
|
||||
|
||||
[[package]]
|
||||
name = "lazaret-client"
|
||||
version = "0.0.0"
|
||||
|
|
@ -12,6 +33,10 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "lazaret-core"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"postcard",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazaret-escalation-client"
|
||||
|
|
@ -40,3 +65,100 @@ version = "0.0.0"
|
|||
dependencies = [
|
||||
"lazaret-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "postcard"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"
|
||||
dependencies = [
|
||||
"cobs",
|
||||
"embedded-io 0.4.0",
|
||||
"embedded-io 0.6.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ edition = "2024"
|
|||
rust-version = "1.90"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
postcard = { version = "=1.1.3", default-features = false, features = ["use-std"] }
|
||||
serde = { version = "=1.0.228", features = ["derive"] }
|
||||
|
||||
[workspace.lints.rust]
|
||||
unsafe_code = "forbid"
|
||||
unused = { level = "deny", priority = -1 }
|
||||
|
|
|
|||
|
|
@ -5,5 +5,9 @@ edition.workspace = true
|
|||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
postcard.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
85
crates/lazaret-core/src/codec.rs
Normal file
85
crates/lazaret-core/src/codec.rs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
//! Strict bounded Postcard structure checks shared by schema-record persistence and tests.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
pub const MAX_SCHEMA_RECORD_BYTES: usize = 1024 * 1024;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CodecError {
|
||||
RecordTooLarge { length: usize, maximum: usize },
|
||||
TrailingPayload,
|
||||
NonMinimalPayload,
|
||||
MalformedPayload(postcard::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for CodecError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::RecordTooLarge { length, maximum } => {
|
||||
write!(formatter, "schema record length {length} exceeds {maximum}")
|
||||
}
|
||||
Self::TrailingPayload => {
|
||||
formatter.write_str("schema record has trailing payload bytes")
|
||||
}
|
||||
Self::NonMinimalPayload => formatter
|
||||
.write_str("schema record does not use the minimal Postcard representation"),
|
||||
Self::MalformedPayload(error) => write!(formatter, "malformed schema payload: {error}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for CodecError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::MalformedPayload(error) => Some(error),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Encodes one schema value using Postcard's minimal representation.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error when serialization fails or the encoded value exceeds the generic limit.
|
||||
pub fn encode<T: Serialize>(record: &T) -> Result<Vec<u8>, CodecError> {
|
||||
let encoded = postcard::to_stdvec(record).map_err(CodecError::MalformedPayload)?;
|
||||
if encoded.len() > MAX_SCHEMA_RECORD_BYTES {
|
||||
return Err(CodecError::RecordTooLarge {
|
||||
length: encoded.len(),
|
||||
maximum: MAX_SCHEMA_RECORD_BYTES,
|
||||
});
|
||||
}
|
||||
Ok(encoded)
|
||||
}
|
||||
|
||||
/// Decodes one complete, minimally encoded schema value after enforcing the generic size limit.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error for oversized, malformed, trailing, or non-minimal Postcard input.
|
||||
pub fn decode<T>(encoded: &[u8]) -> Result<T, CodecError>
|
||||
where
|
||||
T: DeserializeOwned + Serialize,
|
||||
{
|
||||
if encoded.len() > MAX_SCHEMA_RECORD_BYTES {
|
||||
return Err(CodecError::RecordTooLarge {
|
||||
length: encoded.len(),
|
||||
maximum: MAX_SCHEMA_RECORD_BYTES,
|
||||
});
|
||||
}
|
||||
|
||||
let (record, remainder) =
|
||||
postcard::take_from_bytes(encoded).map_err(CodecError::MalformedPayload)?;
|
||||
if !remainder.is_empty() {
|
||||
return Err(CodecError::TrailingPayload);
|
||||
}
|
||||
let minimal = postcard::to_stdvec(&record).map_err(CodecError::MalformedPayload)?;
|
||||
if minimal != encoded {
|
||||
return Err(CodecError::NonMinimalPayload);
|
||||
}
|
||||
Ok(record)
|
||||
}
|
||||
191
crates/lazaret-core/src/identity.rs
Normal file
191
crates/lazaret-core/src/identity.rs
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const OPAQUE_ID_BYTES: usize = 32;
|
||||
pub const SHA256_BYTES: usize = 32;
|
||||
|
||||
macro_rules! opaque_identity {
|
||||
($($name:ident),+ $(,)?) => {
|
||||
$(
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
pub struct $name([u8; OPAQUE_ID_BYTES]);
|
||||
|
||||
impl $name {
|
||||
#[must_use]
|
||||
pub const fn new(bytes: [u8; OPAQUE_ID_BYTES]) -> Self {
|
||||
Self(bytes)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn as_bytes(&self) -> &[u8; OPAQUE_ID_BYTES] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
)+
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! digest_identity {
|
||||
($($name:ident),+ $(,)?) => {
|
||||
$(
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
pub struct $name([u8; SHA256_BYTES]);
|
||||
|
||||
impl $name {
|
||||
#[must_use]
|
||||
pub const fn new(bytes: [u8; SHA256_BYTES]) -> Self {
|
||||
Self(bytes)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn as_bytes(&self) -> &[u8; SHA256_BYTES] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
)+
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! numeric_identity {
|
||||
($($name:ident($inner:ty)),+ $(,)?) => {
|
||||
$(
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
pub struct $name($inner);
|
||||
|
||||
impl $name {
|
||||
#[must_use]
|
||||
pub const fn new(value: $inner) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn get(self) -> $inner {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
)+
|
||||
};
|
||||
}
|
||||
|
||||
opaque_identity!(
|
||||
DomainId,
|
||||
CoordinatorId,
|
||||
NetworkSidecarId,
|
||||
SandboxId,
|
||||
ClientId,
|
||||
SessionId,
|
||||
SessionAnchorId,
|
||||
RequestId,
|
||||
PreparedArtifactId,
|
||||
PrepareOperationId,
|
||||
ProtocolCredentialId,
|
||||
CapabilityId,
|
||||
ContainerId,
|
||||
PolicyId,
|
||||
);
|
||||
|
||||
digest_identity!(
|
||||
ConfigurationFingerprint,
|
||||
DerivedPolicyDigest,
|
||||
ReadinessFingerprint,
|
||||
BundleDigest,
|
||||
SelectedManifestDigest,
|
||||
IdMapDigest,
|
||||
CertificateDigest,
|
||||
TrustManifestDigest,
|
||||
RequestDigest,
|
||||
StreamDigest,
|
||||
);
|
||||
|
||||
numeric_identity!(
|
||||
GenerationId(u64),
|
||||
ProcessId(u32),
|
||||
ProcessStartTime(u64),
|
||||
GuestProcessGroupId(u32),
|
||||
FileDescriptor(i32),
|
||||
DeviceId(u64),
|
||||
InodeId(u64),
|
||||
MountId(u64),
|
||||
);
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
pub struct HostPath(Vec<u8>);
|
||||
|
||||
impl HostPath {
|
||||
#[must_use]
|
||||
pub const fn new(bytes: Vec<u8>) -> Self {
|
||||
Self(bytes)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
pub struct SourceIdentity(Vec<u8>);
|
||||
|
||||
impl SourceIdentity {
|
||||
#[must_use]
|
||||
pub const fn new(bytes: Vec<u8>) -> Self {
|
||||
Self(bytes)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||
pub struct ProcessIdentity {
|
||||
pub pid: ProcessId,
|
||||
pub start_time: ProcessStartTime,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||
pub struct NamespaceIdentity {
|
||||
pub device: DeviceId,
|
||||
pub inode: InodeId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||
pub struct MountIdentity {
|
||||
pub mount_id: MountId,
|
||||
pub device: DeviceId,
|
||||
pub inode: InodeId,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::any::TypeId;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use super::{
|
||||
ContainerId, CoordinatorId, DomainId, GenerationId, MountIdentity, NamespaceIdentity,
|
||||
NetworkSidecarId, PreparedArtifactId, ProcessIdentity, RequestId, SandboxId, SessionId,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn identity_categories_are_nominally_distinct() {
|
||||
let identities = [
|
||||
TypeId::of::<DomainId>(),
|
||||
TypeId::of::<GenerationId>(),
|
||||
TypeId::of::<CoordinatorId>(),
|
||||
TypeId::of::<NetworkSidecarId>(),
|
||||
TypeId::of::<SandboxId>(),
|
||||
TypeId::of::<SessionId>(),
|
||||
TypeId::of::<RequestId>(),
|
||||
TypeId::of::<PreparedArtifactId>(),
|
||||
TypeId::of::<ContainerId>(),
|
||||
TypeId::of::<ProcessIdentity>(),
|
||||
TypeId::of::<NamespaceIdentity>(),
|
||||
TypeId::of::<MountIdentity>(),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
identities.into_iter().collect::<HashSet<_>>().len(),
|
||||
identities.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
#![doc = "Shared production interfaces for Lazaret processes."]
|
||||
|
||||
pub mod codec;
|
||||
pub mod identity;
|
||||
pub mod records;
|
||||
|
||||
pub const PROTOCOL_NAME: &str = "lazaret";
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
1493
crates/lazaret-core/src/records.rs
Normal file
1493
crates/lazaret-core/src/records.rs
Normal file
File diff suppressed because it is too large
Load diff
1247
crates/lazaret-core/tests/schema_records.rs
Normal file
1247
crates/lazaret-core/tests/schema_records.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -768,3 +768,16 @@ identity with a schema-specific domain separator. Process identities and network
|
|||
observations are attestation evidence but are not part of that fingerprint. The
|
||||
Host Coordinator binds the accepted attestation and generation-bound reachability
|
||||
probe into Network Readiness Evidence before publication.
|
||||
|
||||
`NetworkReadinessAttestation.schema_id` is exactly
|
||||
`lazaret-network-attestation-v1`.
|
||||
`NetworkReadinessIdentity.schema_id` is exactly
|
||||
`lazaret-network-readiness-identity-v1`. They are independent version-dispatch
|
||||
domains: the v1 attestation projects only to the v1 readiness identity, and any
|
||||
other identifier or version is refused without fallback or cross-domain
|
||||
interpretation.
|
||||
|
||||
The readiness-fingerprint domain separator is the UTF-8 bytes of
|
||||
`lazaret-network-readiness-identity-v1` followed by exactly one zero byte. This
|
||||
fixes the domain-separation context only; it does not select the byte encoding of
|
||||
the canonical readiness-identity fields.
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ kind = "invariant"
|
|||
symbols = ["TypeOK", "DomainGenerationBounds", "RuntimeRoleIdentitySeparation", "GenerationScopedIdentitiesDistinct"]
|
||||
owners = ["IP-01", "IP-10", "IP-14", "IP-15", "IP-30"]
|
||||
levels = ["V1"]
|
||||
verifier = "Type/property tests and lifecycle state assertions"
|
||||
verifier = "IP-01 strict schema-codec, malformed-version, record-invariant, and nominal identity tests plus later lifecycle state assertions"
|
||||
|
||||
[[coverage]]
|
||||
model = "Lazaret"
|
||||
|
|
|
|||
Loading…
Reference in a new issue