feat: replace private cas envelope
This commit is contained in:
parent
2dce1f41cf
commit
63ea36bb51
10 changed files with 193 additions and 89 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
|
@ -751,6 +751,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"generic-array",
|
"generic-array",
|
||||||
|
"rand_core 0.6.4",
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1524,10 +1525,12 @@ dependencies = [
|
||||||
name = "geth-cas"
|
name = "geth-cas"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"aes-gcm",
|
||||||
"blake3",
|
"blake3",
|
||||||
"geth-codec",
|
"geth-codec",
|
||||||
"geth-types",
|
"geth-types",
|
||||||
"hex",
|
"hex",
|
||||||
|
"rand 0.9.4",
|
||||||
"serde",
|
"serde",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ repository = "https://example.invalid/local/geth"
|
||||||
rust-version = "1.91"
|
rust-version = "1.91"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
aes-gcm = "0.10"
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
automerge = "0.7"
|
automerge = "0.7"
|
||||||
|
|
|
||||||
|
|
@ -176,8 +176,9 @@ The bootstrap implementation provides:
|
||||||
`--bearer-secret <secret>`
|
`--bearer-secret <secret>`
|
||||||
- private CAS envelope commands:
|
- private CAS envelope commands:
|
||||||
`geth cas add-private <resource> <path>` and
|
`geth cas add-private <resource> <path>` and
|
||||||
`geth cas get-private <resource> <hash> --out <path>`. These use local
|
`geth cas get-private <resource> <hash> --out <path>`. New writes use an
|
||||||
resource secret epochs and are a prototype envelope, not audited AEAD.
|
AES-256-GCM envelope bound to the resource and local secret epoch. This does
|
||||||
|
not claim forward secrecy or post-compromise security.
|
||||||
- local CAS tree objects describe file trees and are stored as CAS blobs
|
- local CAS tree objects describe file trees and are stored as CAS blobs
|
||||||
- local file-root commands: `geth cas root add/list/scan/sync/apply`; root sync
|
- local file-root commands: `geth cas root add/list/scan/sync/apply`; root sync
|
||||||
pulls authorized remote tree metadata and CAS tree bytes into a peer-qualified
|
pulls authorized remote tree metadata and CAS tree bytes into a peer-qualified
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,10 @@ rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
aes-gcm.workspace = true
|
||||||
blake3.workspace = true
|
blake3.workspace = true
|
||||||
hex.workspace = true
|
hex.workspace = true
|
||||||
|
rand.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
geth-codec = { path = "../geth-codec" }
|
geth-codec = { path = "../geth-codec" }
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
pub const CAS_TREE_OBJECT_VERSION: u16 = 1;
|
pub const CAS_TREE_OBJECT_VERSION: u16 = 1;
|
||||||
pub const ENCRYPTED_BLOB_ENVELOPE_VERSION: u16 = 1;
|
pub const ENCRYPTED_BLOB_ENVELOPE_VERSION: u16 = 2;
|
||||||
|
pub const PRIVATE_BLOB_AEAD_ALGORITHM: &str = "geth.aes-256-gcm.v1";
|
||||||
|
pub const PRIVATE_BLOB_PROTOTYPE_ALGORITHM: &str = "geth.blake3-xor.v0.prototype";
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum CasError {
|
pub enum CasError {
|
||||||
|
|
@ -191,23 +193,50 @@ pub fn encrypt_private_blob(
|
||||||
resource: &str,
|
resource: &str,
|
||||||
secret_id: &str,
|
secret_id: &str,
|
||||||
epoch: u64,
|
epoch: u64,
|
||||||
nonce_hex: &str,
|
|
||||||
plaintext: &[u8],
|
plaintext: &[u8],
|
||||||
) -> Result<Vec<u8>, CasError> {
|
) -> Result<Vec<u8>, CasError> {
|
||||||
|
let nonce: [u8; 12] = rand::random();
|
||||||
|
encrypt_private_blob_with_nonce(resource, secret_id, epoch, nonce, plaintext)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encrypt_private_blob_with_nonce(
|
||||||
|
resource: &str,
|
||||||
|
secret_id: &str,
|
||||||
|
epoch: u64,
|
||||||
|
nonce: [u8; 12],
|
||||||
|
plaintext: &[u8],
|
||||||
|
) -> Result<Vec<u8>, CasError> {
|
||||||
|
use aes_gcm::{
|
||||||
|
Aes256Gcm, Nonce,
|
||||||
|
aead::{Aead, KeyInit, Payload},
|
||||||
|
};
|
||||||
|
|
||||||
let key = private_blob_key(resource, secret_id, epoch);
|
let key = private_blob_key(resource, secret_id, epoch);
|
||||||
let nonce = decode_32_byte_hex(nonce_hex)?;
|
let cipher = Aes256Gcm::new_from_slice(&key)
|
||||||
let ciphertext = xor_keystream(&key, &nonce, plaintext);
|
.map_err(|error| CasError::EncryptedEnvelope(error.to_string()))?;
|
||||||
let tag_hex = private_blob_tag(&key, &nonce, &ciphertext);
|
let plaintext_hash = hash_bytes(plaintext);
|
||||||
|
let aad = private_blob_aad(resource, epoch, &plaintext_hash);
|
||||||
|
let ciphertext = cipher
|
||||||
|
.encrypt(
|
||||||
|
Nonce::from_slice(&nonce),
|
||||||
|
Payload {
|
||||||
|
msg: plaintext,
|
||||||
|
aad: aad.as_bytes(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|_| {
|
||||||
|
CasError::EncryptedEnvelope("private blob AEAD encryption failed".to_owned())
|
||||||
|
})?;
|
||||||
let envelope = EncryptedBlobEnvelope {
|
let envelope = EncryptedBlobEnvelope {
|
||||||
version: ENCRYPTED_BLOB_ENVELOPE_VERSION,
|
version: ENCRYPTED_BLOB_ENVELOPE_VERSION,
|
||||||
algorithm: "geth.blake3-xor.v0.prototype".to_owned(),
|
algorithm: PRIVATE_BLOB_AEAD_ALGORITHM.to_owned(),
|
||||||
resource: resource.to_owned(),
|
resource: resource.to_owned(),
|
||||||
epoch,
|
epoch,
|
||||||
plaintext_hash: hash_bytes(plaintext),
|
plaintext_hash,
|
||||||
nonce_hex: nonce_hex.to_owned(),
|
nonce_hex: hex::encode(nonce),
|
||||||
ciphertext,
|
ciphertext,
|
||||||
tag_hex,
|
tag_hex: "included-in-aes-gcm-ciphertext".to_owned(),
|
||||||
note: "prototype private blob envelope; not an audited AEAD and does not claim forward secrecy or post-compromise security".to_owned(),
|
note: "private blob envelope uses AES-256-GCM with a random 96-bit nonce and resource-bound associated data; it does not claim forward secrecy or post-compromise security".to_owned(),
|
||||||
};
|
};
|
||||||
geth_codec::encode_canonical(&envelope).map_err(CasError::from)
|
geth_codec::encode_canonical(&envelope).map_err(CasError::from)
|
||||||
}
|
}
|
||||||
|
|
@ -219,26 +248,53 @@ pub fn decrypt_private_blob(
|
||||||
) -> Result<Vec<u8>, CasError> {
|
) -> Result<Vec<u8>, CasError> {
|
||||||
let envelope: EncryptedBlobEnvelope = geth_codec::decode_canonical(envelope_bytes)?;
|
let envelope: EncryptedBlobEnvelope = geth_codec::decode_canonical(envelope_bytes)?;
|
||||||
if envelope.version != ENCRYPTED_BLOB_ENVELOPE_VERSION {
|
if envelope.version != ENCRYPTED_BLOB_ENVELOPE_VERSION {
|
||||||
|
if envelope.version == 1 || envelope.algorithm == PRIVATE_BLOB_PROTOTYPE_ALGORITHM {
|
||||||
|
return Err(CasError::EncryptedEnvelope(
|
||||||
|
"prototype private blob envelopes are no longer accepted; recreate the private blob with this pre-release".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
return Err(CasError::EncryptedEnvelope(format!(
|
return Err(CasError::EncryptedEnvelope(format!(
|
||||||
"unsupported envelope version {}",
|
"unsupported envelope version {}",
|
||||||
envelope.version
|
envelope.version
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
if envelope.algorithm != PRIVATE_BLOB_AEAD_ALGORITHM {
|
||||||
|
return Err(CasError::EncryptedEnvelope(format!(
|
||||||
|
"unsupported encrypted blob algorithm {}",
|
||||||
|
envelope.algorithm
|
||||||
|
)));
|
||||||
|
}
|
||||||
if envelope.resource != resource {
|
if envelope.resource != resource {
|
||||||
return Err(CasError::EncryptedEnvelope(format!(
|
return Err(CasError::EncryptedEnvelope(format!(
|
||||||
"envelope resource {} does not match requested resource {resource}",
|
"envelope resource {} does not match requested resource {resource}",
|
||||||
envelope.resource
|
envelope.resource
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
let key = private_blob_key(resource, secret_id, envelope.epoch);
|
if envelope.tag_hex != "included-in-aes-gcm-ciphertext" {
|
||||||
let nonce = decode_32_byte_hex(&envelope.nonce_hex)?;
|
|
||||||
let expected_tag = private_blob_tag(&key, &nonce, &envelope.ciphertext);
|
|
||||||
if expected_tag != envelope.tag_hex {
|
|
||||||
return Err(CasError::EncryptedEnvelope(
|
return Err(CasError::EncryptedEnvelope(
|
||||||
"encrypted blob tag verification failed".to_owned(),
|
"invalid AES-GCM envelope tag marker".to_owned(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let plaintext = xor_keystream(&key, &nonce, &envelope.ciphertext);
|
use aes_gcm::{
|
||||||
|
Aes256Gcm, Nonce,
|
||||||
|
aead::{Aead, KeyInit, Payload},
|
||||||
|
};
|
||||||
|
let key = private_blob_key(resource, secret_id, envelope.epoch);
|
||||||
|
let nonce = decode_hex_exact::<12>(&envelope.nonce_hex)?;
|
||||||
|
let cipher = Aes256Gcm::new_from_slice(&key)
|
||||||
|
.map_err(|error| CasError::EncryptedEnvelope(error.to_string()))?;
|
||||||
|
let aad = private_blob_aad(resource, envelope.epoch, &envelope.plaintext_hash);
|
||||||
|
let plaintext = cipher
|
||||||
|
.decrypt(
|
||||||
|
Nonce::from_slice(&nonce),
|
||||||
|
Payload {
|
||||||
|
msg: envelope.ciphertext.as_ref(),
|
||||||
|
aad: aad.as_bytes(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|_| {
|
||||||
|
CasError::EncryptedEnvelope("private blob AEAD verification failed".to_owned())
|
||||||
|
})?;
|
||||||
let plaintext_hash = hash_bytes(&plaintext);
|
let plaintext_hash = hash_bytes(&plaintext);
|
||||||
if plaintext_hash != envelope.plaintext_hash {
|
if plaintext_hash != envelope.plaintext_hash {
|
||||||
return Err(CasError::EncryptedEnvelope(
|
return Err(CasError::EncryptedEnvelope(
|
||||||
|
|
@ -249,42 +305,25 @@ pub fn decrypt_private_blob(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn private_blob_key(resource: &str, secret_id: &str, epoch: u64) -> [u8; 32] {
|
fn private_blob_key(resource: &str, secret_id: &str, epoch: u64) -> [u8; 32] {
|
||||||
let material = format!("geth.private-blob.v0\0{resource}\0{secret_id}\0{epoch}");
|
let material = format!("geth.private-blob.aes-256-gcm.v1\0{resource}\0{secret_id}\0{epoch}");
|
||||||
*blake3::hash(material.as_bytes()).as_bytes()
|
*blake3::hash(material.as_bytes()).as_bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn xor_keystream(key: &[u8; 32], nonce: &[u8; 32], input: &[u8]) -> Vec<u8> {
|
fn private_blob_aad(resource: &str, epoch: u64, plaintext_hash: &BlobHash) -> String {
|
||||||
let mut out = Vec::with_capacity(input.len());
|
format!(
|
||||||
for (chunk_index, chunk) in input.chunks(32).enumerate() {
|
"{}\0{}\0{}\0{}",
|
||||||
let mut block_input = Vec::with_capacity(40);
|
PRIVATE_BLOB_AEAD_ALGORITHM, resource, epoch, plaintext_hash
|
||||||
block_input.extend_from_slice(nonce);
|
)
|
||||||
block_input.extend_from_slice(&(chunk_index as u64).to_le_bytes());
|
|
||||||
let block = blake3::keyed_hash(key, &block_input);
|
|
||||||
out.extend(
|
|
||||||
chunk
|
|
||||||
.iter()
|
|
||||||
.zip(block.as_bytes().iter())
|
|
||||||
.map(|(byte, mask)| byte ^ mask),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn private_blob_tag(key: &[u8; 32], nonce: &[u8; 32], ciphertext: &[u8]) -> String {
|
fn decode_hex_exact<const N: usize>(hex: &str) -> Result<[u8; N], CasError> {
|
||||||
let mut input = Vec::with_capacity(nonce.len() + ciphertext.len());
|
if hex.len() != N * 2 {
|
||||||
input.extend_from_slice(nonce);
|
return Err(CasError::EncryptedEnvelope(format!(
|
||||||
input.extend_from_slice(ciphertext);
|
"nonce must be {N} bytes encoded as lowercase hex"
|
||||||
blake3::keyed_hash(key, &input).to_hex().to_string()
|
)));
|
||||||
}
|
}
|
||||||
|
let mut bytes = [0_u8; N];
|
||||||
fn decode_32_byte_hex(hex: &str) -> Result<[u8; 32], CasError> {
|
for index in 0..N {
|
||||||
if hex.len() != 64 {
|
|
||||||
return Err(CasError::EncryptedEnvelope(
|
|
||||||
"nonce must be 32 bytes encoded as lowercase hex".to_owned(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let mut bytes = [0_u8; 32];
|
|
||||||
for index in 0..32 {
|
|
||||||
bytes[index] = u8::from_str_radix(&hex[index * 2..index * 2 + 2], 16)
|
bytes[index] = u8::from_str_radix(&hex[index * 2..index * 2 + 2], 16)
|
||||||
.map_err(|error| CasError::EncryptedEnvelope(format!("invalid nonce hex: {error}")))?;
|
.map_err(|error| CasError::EncryptedEnvelope(format!("invalid nonce hex: {error}")))?;
|
||||||
}
|
}
|
||||||
|
|
@ -769,11 +808,15 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn private_blob_envelope_roundtrips_and_checks_resource() {
|
fn private_blob_envelope_uses_aead_and_checks_bound_context() {
|
||||||
let plaintext = b"private geth bytes";
|
let plaintext = b"private geth bytes";
|
||||||
let nonce = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
|
let envelope = encrypt_private_blob_with_nonce(
|
||||||
let envelope =
|
"resource:cas:private",
|
||||||
encrypt_private_blob("resource:cas:private", "secret:test", 1, nonce, plaintext)
|
"secret:test",
|
||||||
|
1,
|
||||||
|
[7; 12],
|
||||||
|
plaintext,
|
||||||
|
)
|
||||||
.expect("encrypt");
|
.expect("encrypt");
|
||||||
|
|
||||||
let decrypted = decrypt_private_blob("resource:cas:private", "secret:test", &envelope)
|
let decrypted = decrypt_private_blob("resource:cas:private", "secret:test", &envelope)
|
||||||
|
|
@ -781,6 +824,66 @@ mod tests {
|
||||||
assert_eq!(decrypted, plaintext);
|
assert_eq!(decrypted, plaintext);
|
||||||
assert!(decrypt_private_blob("resource:cas:other", "secret:test", &envelope).is_err());
|
assert!(decrypt_private_blob("resource:cas:other", "secret:test", &envelope).is_err());
|
||||||
assert!(decrypt_private_blob("resource:cas:private", "secret:other", &envelope).is_err());
|
assert!(decrypt_private_blob("resource:cas:private", "secret:other", &envelope).is_err());
|
||||||
|
|
||||||
|
let decoded: EncryptedBlobEnvelope =
|
||||||
|
geth_codec::decode_canonical(&envelope).expect("decode envelope");
|
||||||
|
assert_eq!(decoded.version, ENCRYPTED_BLOB_ENVELOPE_VERSION);
|
||||||
|
assert_eq!(decoded.algorithm, PRIVATE_BLOB_AEAD_ALGORITHM);
|
||||||
|
assert_eq!(decoded.nonce_hex, "070707070707070707070707");
|
||||||
|
assert_eq!(decoded.tag_hex, "included-in-aes-gcm-ciphertext");
|
||||||
|
|
||||||
|
let mut tampered_ciphertext = decoded.clone();
|
||||||
|
tampered_ciphertext.ciphertext[0] ^= 0x01;
|
||||||
|
let tampered_ciphertext =
|
||||||
|
geth_codec::encode_canonical(&tampered_ciphertext).expect("encode tampered");
|
||||||
|
assert!(
|
||||||
|
decrypt_private_blob("resource:cas:private", "secret:test", &tampered_ciphertext)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut tampered_hash = decoded.clone();
|
||||||
|
tampered_hash.plaintext_hash = hash_bytes(b"other plaintext");
|
||||||
|
let tampered_hash = geth_codec::encode_canonical(&tampered_hash).expect("encode tampered");
|
||||||
|
assert!(
|
||||||
|
decrypt_private_blob("resource:cas:private", "secret:test", &tampered_hash).is_err()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn private_blob_envelope_generates_unique_nonces() {
|
||||||
|
let first = encrypt_private_blob("resource:cas:private", "secret:test", 1, b"same")
|
||||||
|
.expect("first encrypt");
|
||||||
|
let second = encrypt_private_blob("resource:cas:private", "secret:test", 1, b"same")
|
||||||
|
.expect("second encrypt");
|
||||||
|
let first: EncryptedBlobEnvelope =
|
||||||
|
geth_codec::decode_canonical(&first).expect("decode first");
|
||||||
|
let second: EncryptedBlobEnvelope =
|
||||||
|
geth_codec::decode_canonical(&second).expect("decode second");
|
||||||
|
assert_ne!(first.nonce_hex, second.nonce_hex);
|
||||||
|
assert_ne!(first.ciphertext, second.ciphertext);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn prototype_private_blob_envelopes_are_rejected() {
|
||||||
|
let envelope = EncryptedBlobEnvelope {
|
||||||
|
version: 1,
|
||||||
|
algorithm: PRIVATE_BLOB_PROTOTYPE_ALGORITHM.to_owned(),
|
||||||
|
resource: "resource:cas:private".to_owned(),
|
||||||
|
epoch: 1,
|
||||||
|
plaintext_hash: hash_bytes(b"private geth bytes"),
|
||||||
|
nonce_hex: "00".repeat(32),
|
||||||
|
ciphertext: vec![1, 2, 3],
|
||||||
|
tag_hex: "prototype-tag".to_owned(),
|
||||||
|
note: "old prototype".to_owned(),
|
||||||
|
};
|
||||||
|
let encoded = geth_codec::encode_canonical(&envelope).expect("encode prototype");
|
||||||
|
let error = decrypt_private_blob("resource:cas:private", "secret:test", &encoded)
|
||||||
|
.expect_err("reject prototype");
|
||||||
|
assert!(
|
||||||
|
error
|
||||||
|
.to_string()
|
||||||
|
.contains("prototype private blob envelopes")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -6174,21 +6174,10 @@ pub fn handle_request(
|
||||||
})?;
|
})?;
|
||||||
let plaintext = std::fs::read(&path)?;
|
let plaintext = std::fs::read(&path)?;
|
||||||
let plaintext_hash = geth_cas::hash_bytes(&plaintext);
|
let plaintext_hash = geth_cas::hash_bytes(&plaintext);
|
||||||
let nonce = geth_crypto::blake3_hex(
|
|
||||||
format!(
|
|
||||||
"{}\0{}\0{}\0{}",
|
|
||||||
resource,
|
|
||||||
secret.secret_id,
|
|
||||||
secret.epoch,
|
|
||||||
geth_store::now_ms()
|
|
||||||
)
|
|
||||||
.as_bytes(),
|
|
||||||
);
|
|
||||||
let encrypted = geth_cas::encrypt_private_blob(
|
let encrypted = geth_cas::encrypt_private_blob(
|
||||||
&resource,
|
&resource,
|
||||||
&secret.secret_id,
|
&secret.secret_id,
|
||||||
secret.epoch,
|
secret.epoch,
|
||||||
&nonce,
|
|
||||||
&plaintext,
|
&plaintext,
|
||||||
)?;
|
)?;
|
||||||
let cas = LocalCas::new(node.paths.cas_dir());
|
let cas = LocalCas::new(node.paths.cas_dir());
|
||||||
|
|
@ -6204,7 +6193,7 @@ pub fn handle_request(
|
||||||
plaintext_hash,
|
plaintext_hash,
|
||||||
encrypted_hash: info.hash,
|
encrypted_hash: info.hash,
|
||||||
size_bytes: info.size_bytes,
|
size_bytes: info.size_bytes,
|
||||||
note: "stored prototype encrypted private blob envelope; no forward secrecy or post-compromise security is claimed".to_owned(),
|
note: "stored AES-256-GCM private blob envelope bound to the resource and secret epoch; no forward secrecy or post-compromise security is claimed".to_owned(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
ControlRequest::CasGet { hash, out } => {
|
ControlRequest::CasGet { hash, out } => {
|
||||||
|
|
@ -6258,7 +6247,9 @@ pub fn handle_request(
|
||||||
plaintext_hash: geth_cas::hash_bytes(&plaintext),
|
plaintext_hash: geth_cas::hash_bytes(&plaintext),
|
||||||
out,
|
out,
|
||||||
size_bytes: plaintext.len() as u64,
|
size_bytes: plaintext.len() as u64,
|
||||||
note: "decrypted prototype private blob envelope with the latest local resource secret epoch".to_owned(),
|
note:
|
||||||
|
"decrypted AES-256-GCM private blob envelope with a local resource secret epoch"
|
||||||
|
.to_owned(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
ControlRequest::CasHash { path } => Ok(ControlResponse::CasHash {
|
ControlRequest::CasHash { path } => Ok(ControlResponse::CasHash {
|
||||||
|
|
|
||||||
|
|
@ -478,6 +478,10 @@ The project is structured for future multi-user local-first authorization:
|
||||||
## Keyhive/BeeKEM Roadmap
|
## Keyhive/BeeKEM Roadmap
|
||||||
|
|
||||||
Resource secret epochs are the v0/v1 approximation for private payload access.
|
Resource secret epochs are the v0/v1 approximation for private payload access.
|
||||||
|
Private CAS writes use an AES-256-GCM envelope keyed from the resource, local
|
||||||
|
secret id, and epoch with resource-bound associated data and random nonces.
|
||||||
|
Prototype BLAKE3-XOR private blob envelopes from earlier pre-deployment builds
|
||||||
|
are rejected and should be recreated from plaintext.
|
||||||
Later designs can add Keyhive-like convergent capabilities and BeeKEM/CGKA-style
|
Later designs can add Keyhive-like convergent capabilities and BeeKEM/CGKA-style
|
||||||
group key evolution. The bootstrap does not implement BeeKEM and does not claim
|
group key evolution. The bootstrap does not implement BeeKEM and does not claim
|
||||||
strong forward secrecy or post-compromise security.
|
strong forward secrecy or post-compromise security.
|
||||||
|
|
|
||||||
|
|
@ -57,22 +57,17 @@ The following commands are tested but may still change while the subsystem
|
||||||
settles:
|
settles:
|
||||||
|
|
||||||
- `geth overlay status|plan|join|leave|interface-plan|up|down|peers|send|recv`
|
- `geth overlay status|plan|join|leave|interface-plan|up|down|peers|send|recv`
|
||||||
|
- `geth cas add-private`
|
||||||
|
- `geth cas get-private`
|
||||||
|
|
||||||
Migration expectation: overlay membership records and authorization resources
|
Migration expectation: overlay membership records and authorization resources
|
||||||
should remain readable, but packet runtime flags, platform activation details,
|
should remain readable, but packet runtime flags, platform activation details,
|
||||||
and route metadata may change before the first deployment tag.
|
and route metadata may change before the first deployment tag.
|
||||||
|
Private CAS writes use an AES-256-GCM envelope, but the command family remains
|
||||||
## Prototype
|
experimental while key envelopes, remote sharing, and forward-secrecy/PCS
|
||||||
|
properties are explicitly out of scope. Prototype BLAKE3-XOR envelopes from
|
||||||
The following commands intentionally do not claim a stable security or storage
|
earlier pre-deployment builds are rejected with a clear error and should be
|
||||||
contract yet:
|
recreated from plaintext.
|
||||||
|
|
||||||
- `geth cas add-private`
|
|
||||||
- `geth cas get-private`
|
|
||||||
|
|
||||||
Migration expectation: prototype private CAS envelopes may be replaced by an
|
|
||||||
audited AEAD/key-envelope design. Existing prototype blobs may need an explicit
|
|
||||||
re-encryption or export/import workflow.
|
|
||||||
|
|
||||||
## Adding Commands
|
## Adding Commands
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -191,22 +191,22 @@ Goal: finish the authorization and remote-input audit before deployment.
|
||||||
Goal: remove prototype cryptography from paths users may treat as real
|
Goal: remove prototype cryptography from paths users may treat as real
|
||||||
confidential storage.
|
confidential storage.
|
||||||
|
|
||||||
- `[ ]` Replace prototype private CAS envelope.
|
- `[x]` Replace prototype private CAS envelope.
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
- `[ ]` The BLAKE3-XOR prototype envelope is not used for new private CAS
|
- `[x]` The BLAKE3-XOR prototype envelope is not used for new private CAS
|
||||||
writes.
|
writes.
|
||||||
- `[ ]` New private CAS writes use a reviewed AEAD construction or an
|
- `[x]` New private CAS writes use a reviewed AEAD construction or an
|
||||||
established envelope format such as age.
|
established envelope format such as age.
|
||||||
- `[ ]` Key derivation, nonce generation, and envelope versioning are
|
- `[x]` Key derivation, nonce generation, and envelope versioning are
|
||||||
documented.
|
documented.
|
||||||
- `[ ]` Tests cover tamper detection, wrong resource, wrong key, and nonce
|
- `[x]` Tests cover tamper detection, wrong resource, wrong key, and nonce
|
||||||
uniqueness behavior.
|
uniqueness behavior.
|
||||||
|
|
||||||
- `[ ]` Define pre-release encrypted blob migration behavior.
|
- `[x]` Define pre-release encrypted blob migration behavior.
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
- `[ ]` Existing prototype envelopes are either rejected with a clear error
|
- `[x]` Existing prototype envelopes are either rejected with a clear error
|
||||||
or migrated through an explicit command.
|
or migrated through an explicit command.
|
||||||
- `[ ]` Docs state that prototype envelopes made before deployment are not a
|
- `[x]` Docs state that prototype envelopes made before deployment are not a
|
||||||
durable security format.
|
durable security format.
|
||||||
|
|
||||||
## Phase 6: Sync Correctness And Fault Testing
|
## Phase 6: Sync Correctness And Fault Testing
|
||||||
|
|
@ -343,7 +343,7 @@ Goal: prove the system works as an actual base layer before broader use.
|
||||||
3. `[x]` Add stable contract and golden JSON tests.
|
3. `[x]` Add stable contract and golden JSON tests.
|
||||||
4. `[x]` Harden store migrations and backup.
|
4. `[x]` Harden store migrations and backup.
|
||||||
5. `[ ]` Complete security-boundary test coverage.
|
5. `[ ]` Complete security-boundary test coverage.
|
||||||
6. `[ ]` Replace prototype private CAS cryptography.
|
6. `[x]` Replace prototype private CAS cryptography.
|
||||||
7. `[ ]` Add fault-injection sync tests.
|
7. `[ ]` Add fault-injection sync tests.
|
||||||
8. `[ ]` Improve automation commands and JSON errors.
|
8. `[ ]` Improve automation commands and JSON errors.
|
||||||
9. `[ ]` Add operational health, doctor, and release gates.
|
9. `[ ]` Add operational health, doctor, and release gates.
|
||||||
|
|
|
||||||
|
|
@ -648,8 +648,12 @@ authorization and durable-state boundaries clear.
|
||||||
- `[x]` `geth cas get-private <resource> <hash> --out <path>` decrypts with
|
- `[x]` `geth cas get-private <resource> <hash> --out <path>` decrypts with
|
||||||
a matching local resource secret epoch.
|
a matching local resource secret epoch.
|
||||||
- `[x]` Access is gated by local resource secret epoch material.
|
- `[x]` Access is gated by local resource secret epoch material.
|
||||||
- `[x]` Tests verify encrypted blob roundtrip and wrong resource/secret
|
- `[x]` New writes use an AES-256-GCM envelope with resource-bound
|
||||||
rejection.
|
associated data and random nonces.
|
||||||
|
- `[x]` Prototype BLAKE3-XOR envelopes from earlier pre-deployment builds are
|
||||||
|
rejected with a clear error.
|
||||||
|
- `[x]` Tests verify encrypted blob roundtrip, tamper detection, wrong
|
||||||
|
resource/secret rejection, old-envelope rejection, and nonce uniqueness.
|
||||||
- `[x]` Docs explicitly avoid claiming forward secrecy or PCS.
|
- `[x]` Docs explicitly avoid claiming forward secrecy or PCS.
|
||||||
|
|
||||||
- `[x]` Iroh-docs KV integration.
|
- `[x]` Iroh-docs KV integration.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue