redesign sshsigchain authority protocol

This commit is contained in:
Eric Wendland 2026-07-18 21:48:27 +02:00
commit 492a37c020
5 changed files with 1542 additions and 867 deletions

View file

@ -4109,20 +4109,22 @@ fn print_response(response: ControlResponse, output: OutputMode) -> Result<()> {
chain_id, chain_id,
records, records,
head, head,
active_admin_keys, active_keys,
users,
devices, devices,
nodes, disclosed_profiles,
incomplete_profiles,
anchor_threshold,
note, note,
} => { } => {
println!("sigchain: {}", input.display()); println!("sigchain: {}", input.display());
println!("chain_id: {chain_id}"); println!("chain_id: {chain_id}");
println!("records: {records}"); println!("records: {records}");
println!("head: {head}"); println!("head: {head}");
println!("active_admin_keys: {active_admin_keys}"); println!("active_keys: {active_keys}");
println!("users: {users}");
println!("devices: {devices}"); println!("devices: {devices}");
println!("nodes: {nodes}"); println!("disclosed_profiles: {disclosed_profiles}");
println!("incomplete_profiles: {incomplete_profiles}");
println!("anchor_threshold: {anchor_threshold}");
eprintln!("note: {note}"); eprintln!("note: {note}");
} }
ControlResponse::KeychainExplained { subject, lines } => { ControlResponse::KeychainExplained { subject, lines } => {

View file

@ -746,10 +746,11 @@ pub enum ControlResponse {
chain_id: String, chain_id: String,
records: usize, records: usize,
head: String, head: String,
active_admin_keys: usize, active_keys: usize,
users: usize,
devices: usize, devices: usize,
nodes: usize, disclosed_profiles: usize,
incomplete_profiles: usize,
anchor_threshold: u16,
note: String, note: String,
}, },
KeychainExplained { KeychainExplained {

View file

@ -13,15 +13,16 @@
mod sshsigchain; mod sshsigchain;
pub use sshsigchain::{ pub use sshsigchain::{
ChainId as SshSigchainChainId, Digest as SshSigchainDigest, AnchorBackendPolicy, AnchorPolicy, AnchorReceipt, AnchorReceiptVerifier, AnchoredHistory,
KEYCHAIN_SSH_SIGCHAIN_PAYLOAD_VERSION, KEYCHAIN_SSH_SIGCHAIN_PROFILE, AuthorityDeviceState, AuthorityKey, AuthorityKeyId, AuthorityKeyState, AuthorityState,
KeychainSshSigchainPolicy, KeychainSshSigchainState, KeychainSshSigchainVerification, AuthorityTransition, ChainId as SshSigchainChainId, Digest as SshSigchainDigest, HeadClaim,
MAX_JSONL_BYTES, MAX_JSONL_LINE_BYTES, MAX_NAMESPACE_BYTES, SSH_SIGCHAIN_NAMESPACE, MAX_JSONL_BYTES, MAX_JSONL_LINE_BYTES, MAX_NAMESPACE_BYTES, Permission, ProfileDisclosure,
SSH_SIGCHAIN_VERIFIER_PRINCIPAL, SSH_SIGCHAIN_VERSION, SshSigchainError, SshSigchainPolicy, ProfileExtension, SSH_SIGCHAIN_ANCHOR_NAMESPACE, SSH_SIGCHAIN_KEY_PROOF_NAMESPACE,
SshSigchainRecord, SshSigchainTrust, SshSigchainVerification, SshSigchainVerifier, SSH_SIGCHAIN_NAMESPACE, SSH_SIGCHAIN_VERIFIER_PRINCIPAL, SSH_SIGCHAIN_VERSION,
decode_keychain_sshsigchain_payload, decode_sshsigchain_jsonl, encode_sshsigchain_jsonl, SshSigchainError, SshSigchainRecord, SshSigchainTrust, SshSigchainVerification,
keychain_sshsigchain_payload, keychain_sshsigchain_unsigned_record, SshSigchainVerifier, authority_key_id, decode_sshsigchain_jsonl, encode_sshsigchain_jsonl,
verify_keychain_sshsigchain, verify_sshsigchain, key_proof_signing_bytes, profile_payload_commitment, select_anchored_head,
verify_anchor_receipts, verify_head_claim, verify_sshsigchain,
}; };
use geth_types::{ use geth_types::{

File diff suppressed because it is too large Load diff

View file

@ -6505,7 +6505,6 @@ pub fn handle_request(
let records = read_sshsigchain_jsonl_file(&input)?; let records = read_sshsigchain_jsonl_file(&input)?;
let trust = geth_keychain::SshSigchainTrust::new( let trust = geth_keychain::SshSigchainTrust::new(
geth_keychain::SshSigchainChainId::from_hex(&chain_id)?, geth_keychain::SshSigchainChainId::from_hex(&chain_id)?,
geth_keychain::KEYCHAIN_SSH_SIGCHAIN_PROFILE,
namespace.unwrap_or_else(|| geth_keychain::SSH_SIGCHAIN_NAMESPACE.to_owned()), namespace.unwrap_or_else(|| geth_keychain::SSH_SIGCHAIN_NAMESPACE.to_owned()),
std::fs::read_to_string(root_key_path)?, std::fs::read_to_string(root_key_path)?,
)?; )?;
@ -6515,12 +6514,13 @@ pub fn handle_request(
chain_id: trust.chain_id.to_hex(), chain_id: trust.chain_id.to_hex(),
records: verified.records, records: verified.records,
head: verified.head.to_hex(), head: verified.head.to_hex(),
active_admin_keys: verified.view.admin_keys.len(), active_keys: verified.state.active_key_count(),
users: verified.view.users.len(), devices: verified.state.devices.values().filter(|device| device.active).count(),
devices: verified.view.devices.len(), disclosed_profiles: verified.state.disclosed_profiles.len(),
nodes: verified.view.nodes.len(), incomplete_profiles: verified.state.incomplete_profiles.len(),
anchor_threshold: verified.state.anchor_policy.threshold,
note: note:
"verified linked SSHSIGCHAIN v1 records against the explicitly pinned root key" "verified sequence-free SSHSIGCHAIN v1 authority records and disclosed profile commitments against the explicitly pinned root key"
.to_owned(), .to_owned(),
}) })
} }
@ -9545,7 +9545,7 @@ fn verify_keychain_sigchain_entries_with_ssh(
fn verify_keychain_sshsigchain_with_ssh( fn verify_keychain_sshsigchain_with_ssh(
records: &[geth_keychain::SshSigchainRecord], records: &[geth_keychain::SshSigchainRecord],
trust: &geth_keychain::SshSigchainTrust, trust: &geth_keychain::SshSigchainTrust,
) -> Result<geth_keychain::KeychainSshSigchainVerification, NodeError> { ) -> Result<geth_keychain::SshSigchainVerification, NodeError> {
geth_ssh_identity::ensure_ssh_keygen_available()?; geth_ssh_identity::ensure_ssh_keygen_available()?;
let verify_dir = tempfile::tempdir()?; let verify_dir = tempfile::tempdir()?;
@ -9601,7 +9601,7 @@ fn verify_keychain_sshsigchain_with_ssh(
let verifier = SshSigchainOpenSshVerifier { let verifier = SshSigchainOpenSshVerifier {
verify_dir: verify_dir.path().to_path_buf(), verify_dir: verify_dir.path().to_path_buf(),
}; };
Ok(geth_keychain::verify_keychain_sshsigchain( Ok(geth_keychain::verify_sshsigchain(
records, trust, &verifier, records, trust, &verifier,
)?) )?)
} }
@ -11045,14 +11045,13 @@ mod tests {
); );
let trust = geth_keychain::SshSigchainTrust::new( let trust = geth_keychain::SshSigchainTrust::new(
geth_keychain::SshSigchainChainId([0x42; 32]), geth_keychain::SshSigchainChainId([0x42; 32]),
geth_keychain::KEYCHAIN_SSH_SIGCHAIN_PROFILE,
geth_keychain::SSH_SIGCHAIN_NAMESPACE, geth_keychain::SSH_SIGCHAIN_NAMESPACE,
&root_public_key, &root_public_key,
) )
.expect("trust tuple"); .expect("trust tuple");
let sign = |record: geth_keychain::SshSigchainRecord| { let sign = |record: geth_keychain::SshSigchainRecord| {
let payload = dir.path().join(format!("record-{}", record.sequence)); let payload = dir.path().join("record");
std::fs::write(&payload, record.signing_bytes().expect("signing bytes")) std::fs::write(&payload, record.signing_bytes().expect("signing bytes"))
.expect("write signing payload"); .expect("write signing payload");
let output = geth_ssh_identity::sign_command( let output = geth_ssh_identity::sign_command(
@ -11072,50 +11071,32 @@ mod tests {
record.with_signature(signature).expect("signed record") record.with_signature(signature).expect("signed record")
}; };
let init_op = KeychainOp {
id: AuthOpId::new("auth-op:sshsigchain-init"),
created_at: UnixMillis(1),
kind: KeychainOpKind::KeychainInit,
};
let init = sign( let init = sign(
geth_keychain::keychain_sshsigchain_unsigned_record( geth_keychain::SshSigchainRecord::unsigned(
&trust, trust.chain_id,
0,
None, None,
&init_op,
&root_public_key, &root_public_key,
geth_keychain::AuthorityTransition::Genesis {
device_id: "device:root".to_owned(),
root_key: geth_keychain::AuthorityKey {
public_key: root_public_key.clone(),
permissions: vec![geth_keychain::Permission::All],
delegable_permissions: vec![geth_keychain::Permission::All],
},
anchor_policy: geth_keychain::AnchorPolicy::default(),
},
vec![],
) )
.expect("unsigned init"), .expect("unsigned init"),
); );
let root_add_op = KeychainOp {
id: AuthOpId::new("auth-op:sshsigchain-root"),
created_at: UnixMillis(2),
kind: KeychainOpKind::AdminKeyAdd {
key: KeyId::new(geth_keychain::admin_key_fingerprint(&root_public_key)),
public_key: Some(root_public_key.clone()),
principal: Some("root".to_owned()),
valid_after_ms: None,
valid_before_ms: None,
},
};
let root_add = sign(
geth_keychain::keychain_sshsigchain_unsigned_record(
&trust,
1,
Some(init.record_hash().expect("init hash")),
&root_add_op,
&root_public_key,
)
.expect("unsigned root add"),
);
let verified = verify_keychain_sshsigchain_with_ssh(&[init.clone(), root_add], &trust) let verified = verify_keychain_sshsigchain_with_ssh(&[init.clone()], &trust)
.expect("verify real OpenSSH SSHSIG chain"); .expect("verify real OpenSSH SSHSIG chain");
assert_eq!(verified.records, 2); assert_eq!(verified.records, 1);
assert_eq!(verified.view.admin_keys.len(), 1); assert_eq!(verified.state.active_key_count(), 1);
let mut tampered = init; let mut tampered = init;
tampered.payload.push(0); tampered.chain_id = geth_keychain::SshSigchainChainId([0x43; 32]);
assert!(verify_keychain_sshsigchain_with_ssh(&[tampered], &trust).is_err()); assert!(verify_keychain_sshsigchain_with_ssh(&[tampered], &trust).is_err());
} }