Verify stored keychain SSH signatures
This commit is contained in:
parent
788522404d
commit
f7f14f6b27
10 changed files with 160 additions and 15 deletions
|
|
@ -126,7 +126,9 @@ Roadmap items should be actionable and checkable:
|
||||||
`keychain status`. `keychain init --signing-key <path>` signs recorded
|
`keychain status`. `keychain init --signing-key <path>` signs recorded
|
||||||
keychain ops with `ssh-keygen -Y sign` under the
|
keychain ops with `ssh-keygen -Y sign` under the
|
||||||
`geth.keychain.v1@geth.local` namespace and stores signatures locally.
|
`geth.keychain.v1@geth.local` namespace and stores signatures locally.
|
||||||
Verification before accepting replicated keychain ops is still roadmap work.
|
`keychain status` verifies stored signatures with OpenSSH when public key
|
||||||
|
material is available. Verification before accepting replicated keychain ops
|
||||||
|
is still roadmap work.
|
||||||
- Local CAS supports pin/unpin metadata, surfaced through `cas list`, and
|
- Local CAS supports pin/unpin metadata, surfaced through `cas list`, and
|
||||||
`cas cleanup` evicts unpinned blobs while retaining pinned blobs. The daemon
|
`cas cleanup` evicts unpinned blobs while retaining pinned blobs. The daemon
|
||||||
can fetch CAS blobs from an imported signed peer card over Iroh when the peer
|
can fetch CAS blobs from an imported signed peer card over Iroh when the peer
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,8 @@ the keychain initialization/admin-key operations and signs their canonical
|
||||||
payloads through `ssh-keygen -Y sign` using the
|
payloads through `ssh-keygen -Y sign` using the
|
||||||
`geth.keychain.v1@geth.local` namespace. This is the bootstrap path for
|
`geth.keychain.v1@geth.local` namespace. This is the bootstrap path for
|
||||||
admin/YubiKey-rooted trust. `geth keychain status` reports the number of stored
|
admin/YubiKey-rooted trust. `geth keychain status` reports the number of stored
|
||||||
keychain signatures; signature verification for replicated keychain ops is still
|
keychain signatures plus how many currently verify with OpenSSH; rejecting
|
||||||
future work.
|
unsigned or invalid replicated keychain ops is still future work.
|
||||||
|
|
||||||
The daemon can also install itself as a user service:
|
The daemon can also install itself as a user service:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1103,6 +1103,8 @@ fn print_response(response: ControlResponse, json: bool) -> Result<()> {
|
||||||
println!("initialized: {}", status.initialized);
|
println!("initialized: {}", status.initialized);
|
||||||
println!("admin_keys: {}", status.admin_keys);
|
println!("admin_keys: {}", status.admin_keys);
|
||||||
println!("signatures: {}", status.signatures);
|
println!("signatures: {}", status.signatures);
|
||||||
|
println!("verified_signatures: {}", status.verified_signatures);
|
||||||
|
println!("failed_signatures: {}", status.failed_signatures);
|
||||||
println!("users: {}", status.users);
|
println!("users: {}", status.users);
|
||||||
println!("devices: {}", status.devices);
|
println!("devices: {}", status.devices);
|
||||||
println!("nodes: {}", status.nodes);
|
println!("nodes: {}", status.nodes);
|
||||||
|
|
|
||||||
|
|
@ -593,6 +593,8 @@ pub struct KeychainStatusResponse {
|
||||||
pub initialized: bool,
|
pub initialized: bool,
|
||||||
pub admin_keys: usize,
|
pub admin_keys: usize,
|
||||||
pub signatures: usize,
|
pub signatures: usize,
|
||||||
|
pub verified_signatures: usize,
|
||||||
|
pub failed_signatures: usize,
|
||||||
pub users: usize,
|
pub users: usize,
|
||||||
pub devices: usize,
|
pub devices: usize,
|
||||||
pub nodes: usize,
|
pub nodes: usize,
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ pub struct KeychainOp {
|
||||||
pub struct KeychainOpSignature {
|
pub struct KeychainOpSignature {
|
||||||
pub op_id: AuthOpId,
|
pub op_id: AuthOpId,
|
||||||
pub signer: KeyId,
|
pub signer: KeyId,
|
||||||
|
pub signer_public_key: String,
|
||||||
pub namespace: String,
|
pub namespace: String,
|
||||||
pub signature: Vec<u8>,
|
pub signature: Vec<u8>,
|
||||||
pub created_at: UnixMillis,
|
pub created_at: UnixMillis,
|
||||||
|
|
|
||||||
|
|
@ -2890,7 +2890,7 @@ pub fn handle_request(
|
||||||
}
|
}
|
||||||
|
|
||||||
let signatures = if let Some(signing_key_path) = signing_key_path {
|
let signatures = if let Some(signing_key_path) = signing_key_path {
|
||||||
let signer =
|
let (signer, signer_public_key) =
|
||||||
keychain_signer_from_paths(&signing_key_path, admin_key_path.as_deref())?;
|
keychain_signer_from_paths(&signing_key_path, admin_key_path.as_deref())?;
|
||||||
let mut signatures = Vec::new();
|
let mut signatures = Vec::new();
|
||||||
for op in &ops {
|
for op in &ops {
|
||||||
|
|
@ -2900,6 +2900,7 @@ pub fn handle_request(
|
||||||
op,
|
op,
|
||||||
&signing_key_path,
|
&signing_key_path,
|
||||||
&signer,
|
&signer,
|
||||||
|
&signer_public_key,
|
||||||
)?);
|
)?);
|
||||||
}
|
}
|
||||||
signatures
|
signatures
|
||||||
|
|
@ -2911,10 +2912,13 @@ pub fn handle_request(
|
||||||
}
|
}
|
||||||
ControlRequest::KeychainStatus => {
|
ControlRequest::KeychainStatus => {
|
||||||
let view = geth_keychain::reduce_keychain_ops(&load_keychain_ops(&store)?);
|
let view = geth_keychain::reduce_keychain_ops(&load_keychain_ops(&store)?);
|
||||||
|
let signature_status = verify_keychain_signatures(&store, node)?;
|
||||||
Ok(ControlResponse::KeychainStatus(KeychainStatusResponse {
|
Ok(ControlResponse::KeychainStatus(KeychainStatusResponse {
|
||||||
initialized: view.initialized,
|
initialized: view.initialized,
|
||||||
admin_keys: view.admin_keys.len(),
|
admin_keys: view.admin_keys.len(),
|
||||||
signatures: store.list_keychain_signatures()?.len(),
|
signatures: signature_status.total,
|
||||||
|
verified_signatures: signature_status.verified,
|
||||||
|
failed_signatures: signature_status.failed,
|
||||||
users: view.users.len(),
|
users: view.users.len(),
|
||||||
devices: view.devices.len(),
|
devices: view.devices.len(),
|
||||||
nodes: view.nodes.len(),
|
nodes: view.nodes.len(),
|
||||||
|
|
@ -4021,12 +4025,15 @@ fn store_keychain_op(store: &Store, op: &KeychainOp) -> Result<(), NodeError> {
|
||||||
fn keychain_signer_from_paths(
|
fn keychain_signer_from_paths(
|
||||||
signing_key_path: &Path,
|
signing_key_path: &Path,
|
||||||
admin_key_path: Option<&Path>,
|
admin_key_path: Option<&Path>,
|
||||||
) -> Result<KeyId, NodeError> {
|
) -> Result<(KeyId, String), NodeError> {
|
||||||
let public_key_path = admin_key_path
|
let public_key_path = admin_key_path
|
||||||
.map(Path::to_path_buf)
|
.map(Path::to_path_buf)
|
||||||
.unwrap_or_else(|| Path::new(&format!("{}.pub", signing_key_path.display())).to_path_buf());
|
.unwrap_or_else(|| Path::new(&format!("{}.pub", signing_key_path.display())).to_path_buf());
|
||||||
let public_key = std::fs::read_to_string(public_key_path)?;
|
let public_key = std::fs::read_to_string(public_key_path)?;
|
||||||
Ok(KeyId::new(ssh_public_key_fingerprint(&public_key)))
|
Ok((
|
||||||
|
KeyId::new(ssh_public_key_fingerprint(&public_key)),
|
||||||
|
public_key,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sign_keychain_op_with_ssh(
|
fn sign_keychain_op_with_ssh(
|
||||||
|
|
@ -4035,6 +4042,7 @@ fn sign_keychain_op_with_ssh(
|
||||||
op: &KeychainOp,
|
op: &KeychainOp,
|
||||||
signing_key_path: &Path,
|
signing_key_path: &Path,
|
||||||
signer: &KeyId,
|
signer: &KeyId,
|
||||||
|
signer_public_key: &str,
|
||||||
) -> Result<KeychainOpSignature, NodeError> {
|
) -> Result<KeychainOpSignature, NodeError> {
|
||||||
geth_ssh_identity::ensure_ssh_keygen_available()?;
|
geth_ssh_identity::ensure_ssh_keygen_available()?;
|
||||||
let signature_dir = node.paths.home().join("keychain-signatures");
|
let signature_dir = node.paths.home().join("keychain-signatures");
|
||||||
|
|
@ -4062,6 +4070,7 @@ fn sign_keychain_op_with_ssh(
|
||||||
let signature = KeychainOpSignature {
|
let signature = KeychainOpSignature {
|
||||||
op_id: op.id.clone(),
|
op_id: op.id.clone(),
|
||||||
signer: signer.clone(),
|
signer: signer.clone(),
|
||||||
|
signer_public_key: signer_public_key.to_owned(),
|
||||||
namespace: geth_keychain::KEYCHAIN_SIGNATURE_NAMESPACE.to_owned(),
|
namespace: geth_keychain::KEYCHAIN_SIGNATURE_NAMESPACE.to_owned(),
|
||||||
signature: signature_bytes,
|
signature: signature_bytes,
|
||||||
created_at,
|
created_at,
|
||||||
|
|
@ -4069,6 +4078,7 @@ fn sign_keychain_op_with_ssh(
|
||||||
store.insert_keychain_signature(&StoredKeychainSignature {
|
store.insert_keychain_signature(&StoredKeychainSignature {
|
||||||
op_id: signature.op_id.to_string(),
|
op_id: signature.op_id.to_string(),
|
||||||
signer: signature.signer.to_string(),
|
signer: signature.signer.to_string(),
|
||||||
|
signer_public_key: signature.signer_public_key.clone(),
|
||||||
namespace: signature.namespace.clone(),
|
namespace: signature.namespace.clone(),
|
||||||
signature: signature.signature.clone(),
|
signature: signature.signature.clone(),
|
||||||
created_at_ms: signature.created_at.0,
|
created_at_ms: signature.created_at.0,
|
||||||
|
|
@ -4076,6 +4086,96 @@ fn sign_keychain_op_with_ssh(
|
||||||
Ok(signature)
|
Ok(signature)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
|
struct KeychainSignatureStatus {
|
||||||
|
total: usize,
|
||||||
|
verified: usize,
|
||||||
|
failed: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_keychain_signatures(
|
||||||
|
store: &Store,
|
||||||
|
node: &LocalNode,
|
||||||
|
) -> Result<KeychainSignatureStatus, NodeError> {
|
||||||
|
let ops = load_keychain_ops(store)?;
|
||||||
|
let ops_by_id = ops
|
||||||
|
.into_iter()
|
||||||
|
.map(|op| (op.id.to_string(), op))
|
||||||
|
.collect::<BTreeMap<_, _>>();
|
||||||
|
let signatures = store.list_keychain_signatures()?;
|
||||||
|
let total = signatures.len();
|
||||||
|
let mut verified = 0;
|
||||||
|
let mut failed = 0;
|
||||||
|
for signature in signatures {
|
||||||
|
let Some(op) = ops_by_id.get(&signature.op_id) else {
|
||||||
|
failed += 1;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if verify_keychain_signature_with_ssh(node, op, &signature)? {
|
||||||
|
verified += 1;
|
||||||
|
} else {
|
||||||
|
failed += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(KeychainSignatureStatus {
|
||||||
|
total,
|
||||||
|
verified,
|
||||||
|
failed,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_keychain_signature_with_ssh(
|
||||||
|
node: &LocalNode,
|
||||||
|
op: &KeychainOp,
|
||||||
|
signature: &StoredKeychainSignature,
|
||||||
|
) -> Result<bool, NodeError> {
|
||||||
|
if signature.signer_public_key.trim().is_empty() {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
if geth_ssh_identity::ensure_ssh_keygen_available().is_err() {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let verify_dir = node.paths.home().join("keychain-signatures").join("verify");
|
||||||
|
std::fs::create_dir_all(&verify_dir)?;
|
||||||
|
let stable_id = geth_crypto::blake3_hex(
|
||||||
|
format!(
|
||||||
|
"{}\0{}\0{}\0{}",
|
||||||
|
op.id, signature.signer, signature.namespace, signature.created_at_ms
|
||||||
|
)
|
||||||
|
.as_bytes(),
|
||||||
|
);
|
||||||
|
let payload_path = verify_dir.join(format!("{stable_id}.payload"));
|
||||||
|
let signature_path = verify_dir.join(format!("{stable_id}.sig"));
|
||||||
|
let allowed_signers_path = verify_dir.join(format!("{stable_id}.allowed-signers"));
|
||||||
|
std::fs::write(&payload_path, geth_keychain::keychain_signing_payload(op)?)?;
|
||||||
|
std::fs::write(&signature_path, &signature.signature)?;
|
||||||
|
std::fs::write(
|
||||||
|
&allowed_signers_path,
|
||||||
|
format!(
|
||||||
|
"{} {}\n",
|
||||||
|
signature.signer,
|
||||||
|
signature.signer_public_key.trim()
|
||||||
|
),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let payload = std::fs::File::open(&payload_path)?;
|
||||||
|
let output = std::process::Command::new("ssh-keygen")
|
||||||
|
.arg("-Y")
|
||||||
|
.arg("verify")
|
||||||
|
.arg("-f")
|
||||||
|
.arg(&allowed_signers_path)
|
||||||
|
.arg("-I")
|
||||||
|
.arg(&signature.signer)
|
||||||
|
.arg("-n")
|
||||||
|
.arg(&signature.namespace)
|
||||||
|
.arg("-s")
|
||||||
|
.arg(&signature_path)
|
||||||
|
.stdin(std::process::Stdio::from(payload))
|
||||||
|
.output()?;
|
||||||
|
Ok(output.status.success())
|
||||||
|
}
|
||||||
|
|
||||||
fn load_keychain_ops(store: &Store) -> Result<Vec<KeychainOp>, NodeError> {
|
fn load_keychain_ops(store: &Store) -> Result<Vec<KeychainOp>, NodeError> {
|
||||||
store
|
store
|
||||||
.list_keychain_ops()?
|
.list_keychain_ops()?
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ impl Store {
|
||||||
CREATE TABLE IF NOT EXISTS keychain_signatures (
|
CREATE TABLE IF NOT EXISTS keychain_signatures (
|
||||||
op_id TEXT NOT NULL,
|
op_id TEXT NOT NULL,
|
||||||
signer TEXT NOT NULL,
|
signer TEXT NOT NULL,
|
||||||
|
signer_public_key TEXT NOT NULL DEFAULT '',
|
||||||
namespace TEXT NOT NULL,
|
namespace TEXT NOT NULL,
|
||||||
signature BLOB NOT NULL,
|
signature BLOB NOT NULL,
|
||||||
created_at_ms INTEGER NOT NULL,
|
created_at_ms INTEGER NOT NULL,
|
||||||
|
|
@ -205,6 +206,30 @@ impl Store {
|
||||||
INSERT OR IGNORE INTO meta(key, value) VALUES ('schema_version', '1');
|
INSERT OR IGNORE INTO meta(key, value) VALUES ('schema_version', '1');
|
||||||
"#,
|
"#,
|
||||||
)?;
|
)?;
|
||||||
|
self.add_column_if_missing(
|
||||||
|
"keychain_signatures",
|
||||||
|
"signer_public_key",
|
||||||
|
"TEXT NOT NULL DEFAULT ''",
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_column_if_missing(
|
||||||
|
&self,
|
||||||
|
table: &str,
|
||||||
|
column: &str,
|
||||||
|
definition: &str,
|
||||||
|
) -> Result<(), StoreError> {
|
||||||
|
let mut stmt = self.conn.prepare(&format!("PRAGMA table_info({table})"))?;
|
||||||
|
let columns = stmt
|
||||||
|
.query_map([], |row| row.get::<_, String>(1))?
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
if !columns.iter().any(|name| name == column) {
|
||||||
|
self.conn.execute(
|
||||||
|
&format!("ALTER TABLE {table} ADD COLUMN {column} {definition}"),
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -935,12 +960,13 @@ impl Store {
|
||||||
) -> Result<(), StoreError> {
|
) -> Result<(), StoreError> {
|
||||||
self.conn.execute(
|
self.conn.execute(
|
||||||
r#"INSERT OR REPLACE INTO keychain_signatures(
|
r#"INSERT OR REPLACE INTO keychain_signatures(
|
||||||
op_id, signer, namespace, signature, created_at_ms
|
op_id, signer, signer_public_key, namespace, signature, created_at_ms
|
||||||
)
|
)
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5)"#,
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6)"#,
|
||||||
params![
|
params![
|
||||||
signature.op_id,
|
signature.op_id,
|
||||||
signature.signer,
|
signature.signer,
|
||||||
|
signature.signer_public_key,
|
||||||
signature.namespace,
|
signature.namespace,
|
||||||
signature.signature,
|
signature.signature,
|
||||||
signature.created_at_ms
|
signature.created_at_ms
|
||||||
|
|
@ -951,16 +977,17 @@ impl Store {
|
||||||
|
|
||||||
pub fn list_keychain_signatures(&self) -> Result<Vec<StoredKeychainSignature>, StoreError> {
|
pub fn list_keychain_signatures(&self) -> Result<Vec<StoredKeychainSignature>, StoreError> {
|
||||||
let mut stmt = self.conn.prepare(
|
let mut stmt = self.conn.prepare(
|
||||||
r#"SELECT op_id, signer, namespace, signature, created_at_ms
|
r#"SELECT op_id, signer, signer_public_key, namespace, signature, created_at_ms
|
||||||
FROM keychain_signatures ORDER BY created_at_ms, op_id, signer, namespace"#,
|
FROM keychain_signatures ORDER BY created_at_ms, op_id, signer, namespace"#,
|
||||||
)?;
|
)?;
|
||||||
let rows = stmt.query_map([], |row| {
|
let rows = stmt.query_map([], |row| {
|
||||||
Ok(StoredKeychainSignature {
|
Ok(StoredKeychainSignature {
|
||||||
op_id: row.get(0)?,
|
op_id: row.get(0)?,
|
||||||
signer: row.get(1)?,
|
signer: row.get(1)?,
|
||||||
namespace: row.get(2)?,
|
signer_public_key: row.get(2)?,
|
||||||
signature: row.get(3)?,
|
namespace: row.get(3)?,
|
||||||
created_at_ms: row.get(4)?,
|
signature: row.get(4)?,
|
||||||
|
created_at_ms: row.get(5)?,
|
||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
rows.collect::<Result<Vec<_>, _>>()
|
rows.collect::<Result<Vec<_>, _>>()
|
||||||
|
|
@ -1342,6 +1369,7 @@ pub struct StoredKeychainOp {
|
||||||
pub struct StoredKeychainSignature {
|
pub struct StoredKeychainSignature {
|
||||||
pub op_id: String,
|
pub op_id: String,
|
||||||
pub signer: String,
|
pub signer: String,
|
||||||
|
pub signer_public_key: String,
|
||||||
pub namespace: String,
|
pub namespace: String,
|
||||||
pub signature: Vec<u8>,
|
pub signature: Vec<u8>,
|
||||||
pub created_at_ms: i64,
|
pub created_at_ms: i64,
|
||||||
|
|
@ -1547,6 +1575,7 @@ mod tests {
|
||||||
let signature = StoredKeychainSignature {
|
let signature = StoredKeychainSignature {
|
||||||
op_id: "op:keychain:1".to_owned(),
|
op_id: "op:keychain:1".to_owned(),
|
||||||
signer: "ssh:blake3:admin".to_owned(),
|
signer: "ssh:blake3:admin".to_owned(),
|
||||||
|
signer_public_key: "ssh-ed25519 AAAAADMIN eric@geth".to_owned(),
|
||||||
namespace: "geth.keychain.v1@geth.local".to_owned(),
|
namespace: "geth.keychain.v1@geth.local".to_owned(),
|
||||||
signature: b"-----BEGIN SSH SIGNATURE-----".to_vec(),
|
signature: b"-----BEGIN SSH SIGNATURE-----".to_vec(),
|
||||||
created_at_ms: 3,
|
created_at_ms: 3,
|
||||||
|
|
|
||||||
|
|
@ -710,6 +710,8 @@ fn keychain_init_and_status_use_local_keychain_log() {
|
||||||
assert!(status.initialized);
|
assert!(status.initialized);
|
||||||
assert_eq!(status.admin_keys, 1);
|
assert_eq!(status.admin_keys, 1);
|
||||||
assert_eq!(status.signatures, 0);
|
assert_eq!(status.signatures, 0);
|
||||||
|
assert_eq!(status.verified_signatures, 0);
|
||||||
|
assert_eq!(status.failed_signatures, 0);
|
||||||
assert_eq!(status.users, 0);
|
assert_eq!(status.users, 0);
|
||||||
}
|
}
|
||||||
other => panic!("unexpected response: {other:?}"),
|
other => panic!("unexpected response: {other:?}"),
|
||||||
|
|
@ -752,6 +754,7 @@ fn keychain_init_can_record_openssh_signatures() {
|
||||||
assert_eq!(signatures.len(), 2);
|
assert_eq!(signatures.len(), 2);
|
||||||
assert!(signatures.iter().all(|signature| {
|
assert!(signatures.iter().all(|signature| {
|
||||||
signature.namespace == "geth.keychain.v1@geth.local"
|
signature.namespace == "geth.keychain.v1@geth.local"
|
||||||
|
&& signature.signer_public_key.starts_with("ssh-ed25519 ")
|
||||||
&& !signature.signature.is_empty()
|
&& !signature.signature.is_empty()
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
@ -769,6 +772,8 @@ fn keychain_init_can_record_openssh_signatures() {
|
||||||
match response {
|
match response {
|
||||||
geth_control::ControlResponse::KeychainStatus(status) => {
|
geth_control::ControlResponse::KeychainStatus(status) => {
|
||||||
assert_eq!(status.signatures, 2);
|
assert_eq!(status.signatures, 2);
|
||||||
|
assert_eq!(status.verified_signatures, 2);
|
||||||
|
assert_eq!(status.failed_signatures, 0);
|
||||||
}
|
}
|
||||||
other => panic!("unexpected response: {other:?}"),
|
other => panic!("unexpected response: {other:?}"),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -239,8 +239,9 @@ reports the reduced local view. `keychain init --signing-key <path>` writes the
|
||||||
canonical keychain signing payloads, runs `ssh-keygen -Y sign` with the explicit
|
canonical keychain signing payloads, runs `ssh-keygen -Y sign` with the explicit
|
||||||
`geth.keychain.v1@geth.local` namespace, and stores the resulting OpenSSH
|
`geth.keychain.v1@geth.local` namespace, and stores the resulting OpenSSH
|
||||||
signatures in local SQLite. `keychain status` reports the stored signature
|
signatures in local SQLite. `keychain status` reports the stored signature
|
||||||
count. Verification and rejection of unsigned replicated keychain operations are
|
count and verifies stored signatures against their canonical payloads with
|
||||||
still future work.
|
OpenSSH when possible. Rejection of unsigned or invalid replicated keychain
|
||||||
|
operations is still future work.
|
||||||
|
|
||||||
The authorization plane is `geth-auth`: resource-local signed operation logs,
|
The authorization plane is `geth-auth`: resource-local signed operation logs,
|
||||||
grants, revocations, groups, and `auth explain`. Auth operations reduce into a
|
grants, revocations, groups, and `auth explain`. Auth operations reduce into a
|
||||||
|
|
|
||||||
|
|
@ -175,10 +175,13 @@ resource-scoped capability decisions.
|
||||||
`geth.keychain.v1@geth.local` namespace.
|
`geth.keychain.v1@geth.local` namespace.
|
||||||
- `[x]` Keychain OpenSSH signatures are stored in local SQLite.
|
- `[x]` Keychain OpenSSH signatures are stored in local SQLite.
|
||||||
- `[x]` `geth keychain status` reports the stored keychain signature count.
|
- `[x]` `geth keychain status` reports the stored keychain signature count.
|
||||||
|
- `[x]` `geth keychain status` verifies stored keychain signatures against
|
||||||
|
canonical payloads with OpenSSH when public key material is available.
|
||||||
- `[x]` Missing `ssh-keygen` or unavailable hardware keys produce clear
|
- `[x]` Missing `ssh-keygen` or unavailable hardware keys produce clear
|
||||||
errors during signing.
|
errors during signing.
|
||||||
- `[x]` Tests cover signed keychain init with a generated local OpenSSH key
|
- `[x]` Tests cover signed keychain init with a generated local OpenSSH key
|
||||||
when `ssh-keygen` is available.
|
when `ssh-keygen` is available.
|
||||||
|
- `[x]` Tests cover local OpenSSH verification of stored keychain signatures.
|
||||||
- `[ ]` Future completion verifies signatures before accepting replicated
|
- `[ ]` Future completion verifies signatures before accepting replicated
|
||||||
keychain ops.
|
keychain ops.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue