Extract reusable keychain sigchain model

This commit is contained in:
Eric Wendland 2026-05-26 18:53:20 +02:00
commit 4013c868aa
11 changed files with 938 additions and 20 deletions

View file

@ -403,7 +403,13 @@ fn initialize_owner_keychain(
ops.push(KeychainOp {
id: generated_keychain_op_id("admin-key-add", admin_key.as_str(), now),
created_at: now,
kind: KeychainOpKind::AdminKeyAdd { key: admin_key },
kind: KeychainOpKind::AdminKeyAdd {
key: admin_key,
public_key: Some(public_key.trim().to_owned()),
principal: Some("admin".to_owned()),
valid_after_ms: None,
valid_before_ms: None,
},
});
}
@ -3389,7 +3395,7 @@ async fn keychain_sync_from_peer_since(
.iter()
.filter(|signature| {
if !trusted_admins.contains(&signature.signer)
|| !keychain_signature_uses_claimed_key(signature)
|| !geth_keychain::signature_uses_claimed_key(signature)
{
return false;
}
@ -7578,7 +7584,13 @@ pub fn handle_request(
let op = KeychainOp {
id: generated_keychain_op_id("admin-key-add", admin_key.as_str(), created_at),
created_at,
kind: KeychainOpKind::AdminKeyAdd { key: admin_key },
kind: KeychainOpKind::AdminKeyAdd {
key: admin_key,
public_key: Some(public_key.trim().to_owned()),
principal: Some("admin".to_owned()),
valid_after_ms: None,
valid_before_ms: None,
},
};
store_keychain_op(&store, &op)?;
ops.push(op);
@ -7608,6 +7620,81 @@ pub fn handle_request(
nodes: view.nodes.len(),
}))
}
ControlRequest::KeychainAdminAdd {
admin_key_path,
signing_key_path,
principal,
valid_after_ms,
valid_before_ms,
} => {
let public_key = std::fs::read_to_string(&admin_key_path)?;
let admin_key = KeyId::new(ssh_public_key_fingerprint(&public_key));
let created_at = UnixMillis(geth_store::now_ms());
let op = KeychainOp {
id: generated_keychain_op_id("admin-key-add", admin_key.as_str(), created_at),
created_at,
kind: KeychainOpKind::AdminKeyAdd {
key: admin_key,
public_key: Some(public_key.trim().to_owned()),
principal: principal.or_else(|| Some("admin".to_owned())),
valid_after_ms,
valid_before_ms,
},
};
let signatures = store_and_sign_keychain_ops(
&store,
node,
std::slice::from_ref(&op),
Some(&signing_key_path),
None,
)?;
Ok(ControlResponse::KeychainAdminUpdated {
op,
signatures,
note: "recorded signed admin key addition in the keychain sigchain".to_owned(),
})
}
ControlRequest::KeychainAdminRevoke {
key,
signing_key_path,
admin_key_path,
} => {
let created_at = UnixMillis(geth_store::now_ms());
let op = KeychainOp {
id: generated_keychain_op_id("admin-key-revoke", &key, created_at),
created_at,
kind: KeychainOpKind::AdminKeyRevoke {
key: KeyId::new(key),
},
};
let signatures = store_and_sign_keychain_ops(
&store,
node,
std::slice::from_ref(&op),
Some(&signing_key_path),
admin_key_path.as_deref(),
)?;
Ok(ControlResponse::KeychainAdminUpdated {
op,
signatures,
note: "recorded signed admin key revocation in the keychain sigchain".to_owned(),
})
}
ControlRequest::KeychainAllowedSigners => {
let entries = geth_keychain::allowed_signers(
&load_keychain_ops(&store)?,
&load_keychain_signatures(&store)?,
);
let allowed_signers = geth_keychain::render_allowed_signers(&entries);
Ok(ControlResponse::KeychainAllowedSigners {
entries,
allowed_signers,
note: "derived from active AdminKeyAdd/AdminKeyRevoke operations; compatible with ssh-keygen -Y allowed_signers format".to_owned(),
})
}
ControlRequest::KeychainVerify => Ok(ControlResponse::KeychainVerified {
report: verify_keychain_sigchain_with_ssh(&store, node)?,
}),
ControlRequest::NodeList => {
let view = geth_keychain::reduce_keychain_ops(&load_keychain_ops(&store)?);
Ok(ControlResponse::NodeList {
@ -10414,6 +10501,26 @@ fn store_keychain_op(store: &Store, op: &KeychainOp) -> Result<(), NodeError> {
Ok(())
}
fn verify_keychain_sigchain_with_ssh(
store: &Store,
node: &LocalNode,
) -> Result<geth_keychain::KeychainSigchainReport, NodeError> {
let ops = load_keychain_ops(store)?;
let signatures = load_keychain_signatures(store)?;
Ok(geth_keychain::verify_sigchain(
&ops,
&signatures,
&|op, signature| {
verify_keychain_signature_with_ssh(
node,
op,
&stored_keychain_signature_from_signature(signature),
)
.unwrap_or(false)
},
))
}
fn store_and_sign_keychain_ops(
store: &Store,
node: &LocalNode,
@ -10519,10 +10626,6 @@ fn stored_keychain_signature_from_signature(
}
}
fn keychain_signature_uses_claimed_key(signature: &KeychainOpSignature) -> bool {
KeyId::new(ssh_public_key_fingerprint(&signature.signer_public_key)) == signature.signer
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
struct KeychainSignatureStatus {
total: usize,
@ -10614,11 +10717,12 @@ fn verify_keychain_signature_with_ssh(
}
fn load_keychain_ops(store: &Store) -> Result<Vec<KeychainOp>, NodeError> {
store
let ops = store
.list_keychain_ops()?
.into_iter()
.map(|stored| serde_json::from_str(&stored.op_json).map_err(NodeError::from))
.collect()
.map(|stored| serde_json::from_str::<KeychainOp>(&stored.op_json).map_err(NodeError::from))
.collect::<Result<Vec<_>, NodeError>>()?;
Ok(geth_keychain::sorted_keychain_ops(ops))
}
fn load_keychain_signatures(store: &Store) -> Result<Vec<KeychainOpSignature>, NodeError> {