make keychain records append-only
This commit is contained in:
parent
8780350d41
commit
9e5871d02a
3 changed files with 95 additions and 14 deletions
|
|
@ -9524,19 +9524,48 @@ fn import_verified_sigchain(
|
|||
}
|
||||
let existing_ops = load_keychain_ops(store)?
|
||||
.into_iter()
|
||||
.map(|op| op.id)
|
||||
.collect::<BTreeSet<_>>();
|
||||
.map(|op| (op.id.clone(), op))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
let existing_signatures = load_keychain_signatures(store)?
|
||||
.into_iter()
|
||||
.map(|signature| {
|
||||
(
|
||||
signature.op_id,
|
||||
signature.signer,
|
||||
signature.namespace,
|
||||
signature.signature,
|
||||
(
|
||||
signature.op_id.clone(),
|
||||
signature.signer.clone(),
|
||||
signature.namespace.clone(),
|
||||
),
|
||||
signature,
|
||||
)
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
for op in ops {
|
||||
if existing_ops
|
||||
.get(&op.id)
|
||||
.is_some_and(|existing| existing != op)
|
||||
{
|
||||
return Err(NodeError::Unauthorized(format!(
|
||||
"refusing keychain operation {} because that ID already names different immutable content",
|
||||
op.id
|
||||
)));
|
||||
}
|
||||
}
|
||||
for signature in signatures {
|
||||
let identity = (
|
||||
signature.op_id.clone(),
|
||||
signature.signer.clone(),
|
||||
signature.namespace.clone(),
|
||||
);
|
||||
if existing_signatures
|
||||
.get(&identity)
|
||||
.is_some_and(|existing| existing != signature)
|
||||
{
|
||||
return Err(NodeError::Unauthorized(format!(
|
||||
"refusing keychain signature for {} because its signer and namespace already name different immutable content",
|
||||
signature.op_id
|
||||
)));
|
||||
}
|
||||
}
|
||||
let mut signatures_imported = 0;
|
||||
let mut ops_imported = 0;
|
||||
for op in ops {
|
||||
|
|
@ -9547,7 +9576,7 @@ fn import_verified_sigchain(
|
|||
.collect::<Vec<_>>();
|
||||
store
|
||||
.insert_keychain_op_with_signatures(&stored_keychain_op_from_op(op)?, &op_signatures)?;
|
||||
if !existing_ops.contains(&op.id) {
|
||||
if !existing_ops.contains_key(&op.id) {
|
||||
ops_imported += 1;
|
||||
}
|
||||
for signature in signatures
|
||||
|
|
@ -9558,9 +9587,8 @@ fn import_verified_sigchain(
|
|||
signature.op_id.clone(),
|
||||
signature.signer.clone(),
|
||||
signature.namespace.clone(),
|
||||
signature.signature.clone(),
|
||||
);
|
||||
signatures_imported += usize::from(!existing_signatures.contains(&key));
|
||||
signatures_imported += usize::from(!existing_signatures.contains_key(&key));
|
||||
}
|
||||
}
|
||||
Ok(geth_control::KeychainFetchImportReport {
|
||||
|
|
|
|||
|
|
@ -1145,7 +1145,7 @@ impl Store {
|
|||
|
||||
pub fn insert_keychain_op(&self, op: &StoredKeychainOp) -> Result<(), StoreError> {
|
||||
self.conn.execute(
|
||||
r#"INSERT OR REPLACE INTO keychain_ops(op_id, op_json, created_at_ms)
|
||||
r#"INSERT OR IGNORE INTO keychain_ops(op_id, op_json, created_at_ms)
|
||||
VALUES (?1, ?2, ?3)"#,
|
||||
params![op.op_id, op.op_json, op.created_at_ms],
|
||||
)?;
|
||||
|
|
@ -1196,7 +1196,7 @@ impl Store {
|
|||
signature: &StoredKeychainSignature,
|
||||
) -> Result<(), StoreError> {
|
||||
self.conn.execute(
|
||||
r#"INSERT OR REPLACE INTO keychain_signatures(
|
||||
r#"INSERT OR IGNORE INTO keychain_signatures(
|
||||
op_id, signer, signer_public_key, namespace, signature, created_at_ms
|
||||
)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)"#,
|
||||
|
|
@ -1265,13 +1265,13 @@ impl Store {
|
|||
signatures: &[StoredKeychainSignature],
|
||||
) -> Result<(), StoreError> {
|
||||
tx.execute(
|
||||
r#"INSERT OR REPLACE INTO keychain_ops(op_id, op_json, created_at_ms)
|
||||
r#"INSERT OR IGNORE INTO keychain_ops(op_id, op_json, created_at_ms)
|
||||
VALUES (?1, ?2, ?3)"#,
|
||||
params![op.op_id, op.op_json, op.created_at_ms],
|
||||
)?;
|
||||
for signature in signatures {
|
||||
tx.execute(
|
||||
r#"INSERT OR REPLACE INTO keychain_signatures(
|
||||
r#"INSERT OR IGNORE INTO keychain_signatures(
|
||||
op_id, signer, signer_public_key, namespace, signature, created_at_ms
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6)"#,
|
||||
params![
|
||||
|
|
@ -2331,6 +2331,54 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keychain_records_are_append_only_by_operation_and_signature_identity() {
|
||||
let store = Store::open_memory().expect("open");
|
||||
let original = StoredKeychainOp {
|
||||
op_id: "op:keychain:immutable".to_owned(),
|
||||
op_json: r#"{"id":"op:keychain:immutable","name":"original"}"#.to_owned(),
|
||||
created_at_ms: 1,
|
||||
};
|
||||
let replacement = StoredKeychainOp {
|
||||
op_id: original.op_id.clone(),
|
||||
op_json: r#"{"id":"op:keychain:immutable","name":"replacement"}"#.to_owned(),
|
||||
created_at_ms: 2,
|
||||
};
|
||||
let original_signature = StoredKeychainSignature {
|
||||
op_id: original.op_id.clone(),
|
||||
signer: "key:admin".to_owned(),
|
||||
signer_public_key: "ssh-ed25519 AAAA original".to_owned(),
|
||||
namespace: "geth.keychain.v1@geth.local".to_owned(),
|
||||
signature: b"original-signature".to_vec(),
|
||||
created_at_ms: 1,
|
||||
};
|
||||
let replacement_signature = StoredKeychainSignature {
|
||||
signer_public_key: "ssh-ed25519 AAAA replacement".to_owned(),
|
||||
signature: b"replacement-signature".to_vec(),
|
||||
created_at_ms: 2,
|
||||
..original_signature.clone()
|
||||
};
|
||||
|
||||
store
|
||||
.insert_keychain_op_with_signatures(
|
||||
&original,
|
||||
std::slice::from_ref(&original_signature),
|
||||
)
|
||||
.expect("insert original");
|
||||
store
|
||||
.insert_keychain_op_with_signatures(
|
||||
&replacement,
|
||||
std::slice::from_ref(&replacement_signature),
|
||||
)
|
||||
.expect("attempt replacement");
|
||||
|
||||
assert_eq!(store.list_keychain_ops().expect("list ops"), vec![original]);
|
||||
assert_eq!(
|
||||
store.list_keychain_signatures().expect("list signatures"),
|
||||
vec![original_signature]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keychain_ops_with_signatures_batch_commits_multiple_ops() {
|
||||
let store = Store::open_memory().expect("open");
|
||||
|
|
|
|||
|
|
@ -474,6 +474,11 @@ over the canonical payload. See `docs/sigchain-keychain.md` for the detailed
|
|||
sigchain design. This is currently a pull-based signed operation log, not a
|
||||
CRDT or Keyhive-style convergent authority.
|
||||
|
||||
Keychain operation IDs and `(operation ID, signer, namespace)` signature
|
||||
identities are immutable in local storage. An attempted re-import with an
|
||||
existing identity but different bytes is rejected before it can replace local
|
||||
trust state; idempotent repeats leave the original bytes unchanged.
|
||||
|
||||
The replacement static-publication design is specified in
|
||||
[`sshsigchain-v2.md`](sshsigchain-v2.md). Its reusable core has an explicit
|
||||
out-of-band `(chain ID, profile, SSHSIG namespace, root public key)` trust
|
||||
|
|
|
|||
Loading…
Reference in a new issue