use std::error::Error;
use std::fmt::{self, Display, Formatter};
use serde::{Deserialize, Serialize};
use crate::{CollectionId, RecoveryEra, crc32c};
pub const OPERATION_LOCATOR_FORMAT_VERSION: u16 = 2;
pub const ARTIFACT_LOCATOR_FORMAT_VERSION: u16 = 2;
pub const ARTIFACT_CHUNK_ALIGNMENT: u64 = 4 * 1024;
pub const MAX_ARTIFACT_CHUNK_BYTES: u32 = 1024 * 1024;
const OPERATION_MAGIC: &[u8; 8] = b"LDOPLOC2";
const OPERATION_LOCATOR_BYTES: usize = 208;
const OPERATION_LOCATOR_BODY_BYTES: usize = OPERATION_LOCATOR_BYTES - 4;
const ARTIFACT_MAGIC: &[u8; 8] = b"LDARLOC2";
const ARTIFACT_LOCATOR_BYTES: usize = 224;
const ARTIFACT_LOCATOR_BODY_BYTES: usize = ARTIFACT_LOCATOR_BYTES - 4;
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum OperationAuthorityKindV1 {
Catalog,
Administration,
Security,
Backup,
Maintenance,
Diagnostic,
Portability,
}
impl OperationAuthorityKindV1 {
const fn tag(self) -> u8 {
match self {
Self::Catalog => 1,
Self::Administration => 2,
Self::Security => 3,
Self::Backup => 4,
Self::Maintenance => 5,
Self::Diagnostic => 6,
Self::Portability => 7,
}
}
fn from_tag(tag: u8) -> Result<Self, OperationLocatorError> {
match tag {
1 => Ok(Self::Catalog),
2 => Ok(Self::Administration),
3 => Ok(Self::Security),
4 => Ok(Self::Backup),
5 => Ok(Self::Maintenance),
6 => Ok(Self::Diagnostic),
7 => Ok(Self::Portability),
value => Err(OperationLocatorError::UnsupportedAuthority(value)),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct OperationLocatorV1 {
format_version: u16,
authority: OperationAuthorityKindV1,
collection_id: CollectionId,
authority_identity: [u8; 32],
operation_identity: [u8; 32],
recovery_era: RecoveryEra,
authority_revision: u64,
receipt_ordinal: u64,
semantic_digest: [u8; 32],
submitted_at: u64,
expires_at: u64,
resource_digest: [u8; 32],
}
impl OperationLocatorV1 {
#[allow(clippy::too_many_arguments)]
pub fn new(
authority: OperationAuthorityKindV1,
collection_id: CollectionId,
authority_identity: [u8; 32],
operation_identity: [u8; 32],
recovery_era: RecoveryEra,
authority_revision: u64,
receipt_ordinal: u64,
semantic_digest: [u8; 32],
submitted_at: u64,
expires_at: u64,
resource_digest: [u8; 32],
) -> Result<Self, OperationLocatorError> {
let locator = Self {
format_version: OPERATION_LOCATOR_FORMAT_VERSION,
authority,
collection_id,
authority_identity,
operation_identity,
recovery_era,
authority_revision,
receipt_ordinal,
semantic_digest,
submitted_at,
expires_at,
resource_digest,
};
locator.validate()?;
Ok(locator)
}
#[must_use]
pub const fn authority(&self) -> OperationAuthorityKindV1 {
self.authority
}
#[must_use]
pub const fn collection_id(&self) -> CollectionId {
self.collection_id
}
#[must_use]
pub const fn authority_identity(&self) -> [u8; 32] {
self.authority_identity
}
#[must_use]
pub const fn operation_identity(&self) -> [u8; 32] {
self.operation_identity
}
#[must_use]
pub const fn recovery_era(&self) -> RecoveryEra {
self.recovery_era
}
#[must_use]
pub const fn authority_revision(&self) -> u64 {
self.authority_revision
}
#[must_use]
pub const fn receipt_ordinal(&self) -> u64 {
self.receipt_ordinal
}
#[must_use]
pub const fn semantic_digest(&self) -> [u8; 32] {
self.semantic_digest
}
#[must_use]
pub const fn submitted_at(&self) -> u64 {
self.submitted_at
}
#[must_use]
pub const fn expires_at(&self) -> u64 {
self.expires_at
}
#[must_use]
pub const fn resource_digest(&self) -> [u8; 32] {
self.resource_digest
}
#[must_use]
pub fn binding_digest(&self) -> [u8; 32] {
*blake3::hash(&self.encode()).as_bytes()
}
#[must_use]
pub fn encode(&self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(OPERATION_LOCATOR_BYTES);
bytes.extend_from_slice(OPERATION_MAGIC);
bytes.extend_from_slice(&OPERATION_LOCATOR_FORMAT_VERSION.to_be_bytes());
bytes.push(self.authority.tag());
bytes.push(0);
bytes.extend_from_slice(&self.collection_id.to_be_bytes());
bytes.extend_from_slice(&self.authority_identity);
bytes.extend_from_slice(&self.operation_identity);
bytes.extend_from_slice(&self.recovery_era.to_be_bytes());
bytes.extend_from_slice(&self.authority_revision.to_be_bytes());
bytes.extend_from_slice(&self.receipt_ordinal.to_be_bytes());
bytes.extend_from_slice(&self.semantic_digest);
bytes.extend_from_slice(&self.submitted_at.to_be_bytes());
bytes.extend_from_slice(&self.expires_at.to_be_bytes());
bytes.extend_from_slice(&self.resource_digest);
let checksum = crc32c(&bytes);
bytes.extend_from_slice(&checksum.to_be_bytes());
bytes
}
pub fn decode(bytes: &[u8]) -> Result<Self, OperationLocatorError> {
require_fixed_envelope(
bytes,
OPERATION_MAGIC,
OPERATION_LOCATOR_BYTES,
OPERATION_LOCATOR_BODY_BYTES,
)?;
let format_version = read_u16(bytes, 8)?;
if format_version != OPERATION_LOCATOR_FORMAT_VERSION {
return Err(OperationLocatorError::UnsupportedOperationVersion(format_version));
}
if read_byte(bytes, 11)? != 0 {
return Err(OperationLocatorError::NonCanonicalReservedByte);
}
let locator = Self {
format_version,
authority: OperationAuthorityKindV1::from_tag(read_byte(bytes, 10)?)?,
collection_id: CollectionId::from_be_bytes(read_array(bytes, 12)?)
.map_err(|_| OperationLocatorError::InvalidCollection)?,
authority_identity: read_array(bytes, 28)?,
operation_identity: read_array(bytes, 60)?,
recovery_era: RecoveryEra::from_be_bytes(read_array(bytes, 92)?)
.map_err(|_| OperationLocatorError::InvalidRecoveryEra)?,
authority_revision: read_u64(bytes, 108)?,
receipt_ordinal: read_u64(bytes, 116)?,
semantic_digest: read_array(bytes, 124)?,
submitted_at: read_u64(bytes, 156)?,
expires_at: read_u64(bytes, 164)?,
resource_digest: read_array(bytes, 172)?,
};
locator.validate()?;
Ok(locator)
}
pub fn validate_current(&self) -> Result<(), OperationLocatorError> {
self.validate()
}
fn validate(&self) -> Result<(), OperationLocatorError> {
if self.format_version != OPERATION_LOCATOR_FORMAT_VERSION {
return Err(OperationLocatorError::UnsupportedOperationVersion(self.format_version));
}
if self.authority_identity == [0; 32] || self.operation_identity == [0; 32] {
return Err(OperationLocatorError::InvalidOperationIdentity);
}
if self.authority_revision == 0 || self.receipt_ordinal == 0 {
return Err(OperationLocatorError::InvalidAuthorityPosition);
}
if self.semantic_digest == [0; 32] || self.resource_digest == [0; 32] {
return Err(OperationLocatorError::InvalidDigest);
}
if self.expires_at <= self.submitted_at {
return Err(OperationLocatorError::InvalidExpiry);
}
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ArtifactLocatorV1 {
format_version: u16,
authority: OperationAuthorityKindV1,
collection_id: CollectionId,
authority_identity: [u8; 32],
recovery_era: RecoveryEra,
operation_identity: [u8; 32],
operation_binding_digest: [u8; 32],
artifact_identity: [u8; 32],
bytes: u64,
sha256: [u8; 32],
expires_at: u64,
}
impl ArtifactLocatorV1 {
pub fn new(
operation: &OperationLocatorV1,
artifact_identity: [u8; 32],
bytes: u64,
sha256: [u8; 32],
expires_at: u64,
) -> Result<Self, OperationLocatorError> {
operation.validate()?;
let locator = Self {
format_version: ARTIFACT_LOCATOR_FORMAT_VERSION,
authority: operation.authority,
collection_id: operation.collection_id,
authority_identity: operation.authority_identity,
recovery_era: operation.recovery_era,
operation_identity: operation.operation_identity,
operation_binding_digest: operation.binding_digest(),
artifact_identity,
bytes,
sha256,
expires_at,
};
locator.validate()?;
Ok(locator)
}
#[must_use]
pub const fn authority(&self) -> OperationAuthorityKindV1 {
self.authority
}
#[must_use]
pub const fn collection_id(&self) -> CollectionId {
self.collection_id
}
#[must_use]
pub const fn authority_identity(&self) -> [u8; 32] {
self.authority_identity
}
#[must_use]
pub const fn recovery_era(&self) -> RecoveryEra {
self.recovery_era
}
#[must_use]
pub const fn operation_identity(&self) -> [u8; 32] {
self.operation_identity
}
#[must_use]
pub const fn operation_binding_digest(&self) -> [u8; 32] {
self.operation_binding_digest
}
#[must_use]
pub const fn artifact_identity(&self) -> [u8; 32] {
self.artifact_identity
}
#[must_use]
pub const fn bytes(&self) -> u64 {
self.bytes
}
#[must_use]
pub const fn sha256(&self) -> [u8; 32] {
self.sha256
}
#[must_use]
pub const fn expires_at(&self) -> u64 {
self.expires_at
}
pub fn require_operation(&self, operation: &OperationLocatorV1) -> Result<(), OperationLocatorError> {
operation.validate()?;
if self.authority != operation.authority
|| self.collection_id != operation.collection_id
|| self.authority_identity != operation.authority_identity
|| self.recovery_era != operation.recovery_era
|| self.operation_identity != operation.operation_identity
|| self.operation_binding_digest != operation.binding_digest()
{
return Err(OperationLocatorError::OperationBindingMismatch);
}
Ok(())
}
#[must_use]
pub fn encode(&self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(ARTIFACT_LOCATOR_BYTES);
bytes.extend_from_slice(ARTIFACT_MAGIC);
bytes.extend_from_slice(&ARTIFACT_LOCATOR_FORMAT_VERSION.to_be_bytes());
bytes.push(self.authority.tag());
bytes.push(0);
bytes.extend_from_slice(&self.collection_id.to_be_bytes());
bytes.extend_from_slice(&self.authority_identity);
bytes.extend_from_slice(&self.recovery_era.to_be_bytes());
bytes.extend_from_slice(&self.operation_identity);
bytes.extend_from_slice(&self.operation_binding_digest);
bytes.extend_from_slice(&self.artifact_identity);
bytes.extend_from_slice(&self.bytes.to_be_bytes());
bytes.extend_from_slice(&self.sha256);
bytes.extend_from_slice(&self.expires_at.to_be_bytes());
let checksum = crc32c(&bytes);
bytes.extend_from_slice(&checksum.to_be_bytes());
bytes
}
pub fn decode(bytes: &[u8]) -> Result<Self, OperationLocatorError> {
require_fixed_envelope(
bytes,
ARTIFACT_MAGIC,
ARTIFACT_LOCATOR_BYTES,
ARTIFACT_LOCATOR_BODY_BYTES,
)?;
let format_version = read_u16(bytes, 8)?;
if format_version != ARTIFACT_LOCATOR_FORMAT_VERSION {
return Err(OperationLocatorError::UnsupportedArtifactVersion(format_version));
}
if read_byte(bytes, 11)? != 0 {
return Err(OperationLocatorError::NonCanonicalReservedByte);
}
let locator = Self {
format_version,
authority: OperationAuthorityKindV1::from_tag(read_byte(bytes, 10)?)?,
collection_id: CollectionId::from_be_bytes(read_array(bytes, 12)?)
.map_err(|_| OperationLocatorError::InvalidCollection)?,
authority_identity: read_array(bytes, 28)?,
recovery_era: RecoveryEra::from_be_bytes(read_array(bytes, 60)?)
.map_err(|_| OperationLocatorError::InvalidRecoveryEra)?,
operation_identity: read_array(bytes, 76)?,
operation_binding_digest: read_array(bytes, 108)?,
artifact_identity: read_array(bytes, 140)?,
bytes: read_u64(bytes, 172)?,
sha256: read_array(bytes, 180)?,
expires_at: read_u64(bytes, 212)?,
};
locator.validate()?;
Ok(locator)
}
pub fn validate_current(&self) -> Result<(), OperationLocatorError> {
self.validate()
}
fn validate(&self) -> Result<(), OperationLocatorError> {
if self.format_version != ARTIFACT_LOCATOR_FORMAT_VERSION {
return Err(OperationLocatorError::UnsupportedArtifactVersion(self.format_version));
}
if self.authority_identity == [0; 32]
|| self.operation_identity == [0; 32]
|| self.operation_binding_digest == [0; 32]
|| self.artifact_identity == [0; 32]
|| self.sha256 == [0; 32]
{
return Err(OperationLocatorError::InvalidDigest);
}
if self.bytes == 0 || self.expires_at == 0 {
return Err(OperationLocatorError::InvalidArtifactBounds);
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ArtifactReadRangeV1 {
pub offset: u64,
pub length: u32,
}
impl ArtifactReadRangeV1 {
pub fn new(offset: u64, length: u32, total_bytes: u64) -> Result<Self, OperationLocatorError> {
if !offset.is_multiple_of(ARTIFACT_CHUNK_ALIGNMENT) || !(1..=MAX_ARTIFACT_CHUNK_BYTES).contains(&length) {
return Err(OperationLocatorError::InvalidArtifactRange);
}
let end = offset
.checked_add(u64::from(length))
.ok_or(OperationLocatorError::ArtifactRangeOverflow)?;
if offset >= total_bytes
|| end > total_bytes
|| (end < total_bytes && !u64::from(length).is_multiple_of(ARTIFACT_CHUNK_ALIGNMENT))
{
return Err(OperationLocatorError::InvalidArtifactRange);
}
Ok(Self { offset, length })
}
pub fn end(self) -> Result<u64, OperationLocatorError> {
self.offset
.checked_add(u64::from(self.length))
.ok_or(OperationLocatorError::ArtifactRangeOverflow)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VerifiedArtifactChunkV1 {
pub offset: u64,
pub bytes: Vec<u8>,
pub chunk_sha256: [u8; 32],
pub artifact_sha256: [u8; 32],
pub next_offset: Option<u64>,
pub complete: bool,
}
pub fn verify_artifact_chunk_v1(
artifact: &ArtifactLocatorV1,
range: ArtifactReadRangeV1,
bytes: Vec<u8>,
authority_artifact_sha256: [u8; 32],
) -> Result<VerifiedArtifactChunkV1, OperationLocatorError> {
artifact.validate()?;
let canonical_range = ArtifactReadRangeV1::new(range.offset, range.length, artifact.bytes)?;
if bytes.len() != range.length as usize || authority_artifact_sha256 != artifact.sha256 {
return Err(OperationLocatorError::ArtifactDigestMismatch);
}
let chunk_sha256: [u8; 32] = aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA256, &bytes)
.as_ref()
.try_into()
.map_err(|_| OperationLocatorError::ArtifactDigestMismatch)?;
let end = canonical_range.end()?;
let complete = end == artifact.bytes;
if range.offset == 0 && complete && chunk_sha256 != artifact.sha256 {
return Err(OperationLocatorError::ArtifactDigestMismatch);
}
Ok(VerifiedArtifactChunkV1 {
offset: range.offset,
bytes,
chunk_sha256,
artifact_sha256: artifact.sha256,
next_offset: (!complete).then_some(end),
complete,
})
}
fn require_fixed_envelope(
bytes: &[u8],
magic: &[u8; 8],
expected_length: usize,
body_length: usize,
) -> Result<(), OperationLocatorError> {
if bytes.len() != expected_length {
return Err(OperationLocatorError::InvalidLength {
expected: expected_length,
actual: bytes.len(),
});
}
if bytes.get(..8) != Some(magic) {
return Err(OperationLocatorError::BadMagic);
}
let expected = read_u32(bytes, body_length)?;
let actual = crc32c(bytes.get(..body_length).ok_or(OperationLocatorError::Truncated)?);
if expected != actual {
return Err(OperationLocatorError::Checksum { expected, actual });
}
Ok(())
}
fn read_byte(bytes: &[u8], offset: usize) -> Result<u8, OperationLocatorError> {
bytes.get(offset).copied().ok_or(OperationLocatorError::Truncated)
}
fn read_array<const N: usize>(bytes: &[u8], offset: usize) -> Result<[u8; N], OperationLocatorError> {
bytes
.get(offset..offset.checked_add(N).ok_or(OperationLocatorError::Truncated)?)
.ok_or(OperationLocatorError::Truncated)?
.try_into()
.map_err(|_| OperationLocatorError::Truncated)
}
fn read_u16(bytes: &[u8], offset: usize) -> Result<u16, OperationLocatorError> {
Ok(u16::from_be_bytes(read_array(bytes, offset)?))
}
fn read_u32(bytes: &[u8], offset: usize) -> Result<u32, OperationLocatorError> {
Ok(u32::from_be_bytes(read_array(bytes, offset)?))
}
fn read_u64(bytes: &[u8], offset: usize) -> Result<u64, OperationLocatorError> {
Ok(u64::from_be_bytes(read_array(bytes, offset)?))
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum OperationLocatorError {
InvalidLength { expected: usize, actual: usize },
BadMagic,
Truncated,
Checksum { expected: u32, actual: u32 },
UnsupportedOperationVersion(u16),
UnsupportedArtifactVersion(u16),
UnsupportedAuthority(u8),
NonCanonicalReservedByte,
InvalidCollection,
InvalidRecoveryEra,
InvalidOperationIdentity,
InvalidAuthorityPosition,
InvalidDigest,
InvalidExpiry,
InvalidArtifactBounds,
OperationBindingMismatch,
InvalidArtifactRange,
ArtifactRangeOverflow,
ArtifactDigestMismatch,
}
impl Display for OperationLocatorError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidLength { expected, actual } => {
write!(
formatter,
"locator length {actual} does not match current length {expected}"
)
}
Self::BadMagic => formatter.write_str("locator magic is not current"),
Self::Truncated => formatter.write_str("locator is truncated"),
Self::Checksum { expected, actual } => {
write!(formatter, "locator checksum {actual} does not match {expected}")
}
Self::UnsupportedOperationVersion(version) => {
write!(formatter, "operation locator version {version} is unsupported")
}
Self::UnsupportedArtifactVersion(version) => {
write!(formatter, "artifact locator version {version} is unsupported")
}
Self::UnsupportedAuthority(authority) => {
write!(formatter, "operation authority tag {authority} is unsupported")
}
Self::NonCanonicalReservedByte => formatter.write_str("locator reserved byte is nonzero"),
Self::InvalidCollection => formatter.write_str("locator collection identity is invalid"),
Self::InvalidRecoveryEra => formatter.write_str("locator recovery era is invalid"),
Self::InvalidOperationIdentity => formatter.write_str("operation identity must be nonzero"),
Self::InvalidAuthorityPosition => {
formatter.write_str("authority revision and receipt ordinal must be nonzero")
}
Self::InvalidDigest => formatter.write_str("locator digest must be nonzero"),
Self::InvalidExpiry => formatter.write_str("operation expiry must follow submission"),
Self::InvalidArtifactBounds => formatter.write_str("artifact size or expiry is invalid"),
Self::OperationBindingMismatch => formatter.write_str("artifact is not bound to the supplied operation"),
Self::InvalidArtifactRange => {
formatter.write_str("artifact range is unaligned, empty, oversized, or outside the artifact")
}
Self::ArtifactRangeOverflow => formatter.write_str("artifact range overflows u64"),
Self::ArtifactDigestMismatch => {
formatter.write_str("artifact chunk or full digest does not match authoritative evidence")
}
}
}
}
impl Error for OperationLocatorError {}
#[cfg(test)]
mod tests {
use super::*;
fn operation() -> Result<OperationLocatorV1, Box<dyn Error>> {
Ok(OperationLocatorV1::new(
OperationAuthorityKindV1::Diagnostic,
CollectionId::new(1)?,
[2; 32],
[3; 32],
RecoveryEra::new(4)?,
5,
6,
[7; 32],
8,
70_000,
[9; 32],
)?)
}
#[test]
fn operation_and_artifact_locators_round_trip_exactly() -> Result<(), Box<dyn Error>> {
let operation = operation()?;
assert_eq!(OperationLocatorV1::decode(&operation.encode())?, operation);
let artifact = ArtifactLocatorV1::new(&operation, [10; 32], 8_192, [11; 32], 60_000)?;
assert_eq!(ArtifactLocatorV1::decode(&artifact.encode())?, artifact);
artifact.require_operation(&operation)?;
Ok(())
}
#[test]
fn malformed_locators_and_ranges_fail_closed() -> Result<(), Box<dyn Error>> {
let operation = operation()?;
let mut bytes = operation.encode();
bytes[40] ^= 1;
assert!(OperationLocatorV1::decode(&bytes).is_err());
let artifact = ArtifactLocatorV1::new(&operation, [10; 32], 8_193, [11; 32], 60_000)?;
assert!(ArtifactReadRangeV1::new(1, 4_096, artifact.bytes()).is_err());
assert!(ArtifactReadRangeV1::new(0, 4_095, artifact.bytes()).is_err());
let final_range = ArtifactReadRangeV1::new(8_192, 1, artifact.bytes())?;
assert_eq!(final_range.end()?, 8_193);
Ok(())
}
}