Use AccountAddr as the API type instead of Ed25519VerifyingKey

- verify_log verifies against an address; the wrapped key is crate-private
- AccountRegistry answers in keys, keyed by address; the log stays internal
- crate doc presents the address as an opaque routable id
- AccountError::Log converts log errors at the API boundary

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jazz Turner-Baggs
2026-07-06 13:02:03 -07:00
co-authored by Claude Opus 4.8
parent cb19632207
commit 3e11bfdd50
4 changed files with 60 additions and 20 deletions
+8 -5
View File
@@ -61,11 +61,14 @@ impl AccountRegistry for TestAccountService {
let log = verify_log(addr, signed)?;
log.live_entries()
.iter()
.map(|data| match data {
// A signed log endorsing a non-key is the account's error,
// not a lookup miss — surface it rather than skip it.
EntryData::Ed25519Key(bytes) => Ed25519VerifyingKey::from_bytes(bytes)
.map_err(|_| AccountError::Generic("endorsed key is invalid".into())),
.filter_map(|data| match data {
EntryData::Ed25519Key(bytes) => Some(
// A signed log endorsing a non-key is the account's error,
// not a lookup miss — surface it rather than skip it.
Ed25519VerifyingKey::from_bytes(bytes)
.map_err(|_| AccountError::Generic("endorsed key is invalid".into())),
),
EntryData::Text(_) => None,
})
.collect::<Result<Vec<_>, _>>()
.map(Some)
+11 -9
View File
@@ -3,10 +3,11 @@
//! apart. The bytes are opaque to the server except for the fixed-offset
//! header the extension check reads.
use crypto::Ed25519VerifyingKey;
use crate::AccountAddr;
use crate::account_log::{
AccountEntry, AccountLog, EncodedAccountLog, EntryData, SignedAccountLog,
};
use crate::error::AccountLogError;
use crate::account_log::{AccountEntry, AccountLog, EncodedAccountLog, EntryData, SignedAccountLog};
/// Domain-separation tag, prepended to every signed payload:
///
@@ -193,10 +194,11 @@ fn split_at_checked(body: &[u8], mid: usize) -> Result<(&[u8], &[u8]), AccountLo
/// account: another account's validly-signed log won't verify under this key,
/// so an untrusted server cannot substitute one.
pub fn verify_log(
expected_account: &Ed25519VerifyingKey,
expected_account: &AccountAddr,
log: &SignedAccountLog,
) -> Result<AccountLog, AccountLogError> {
expected_account
.verifying_key()
.verify(log.payload.as_bytes(), &log.signature)
.map_err(|_| AccountLogError::SignatureInvalid)?;
Ok(log.payload.decode())
@@ -332,7 +334,7 @@ mod tests {
#[test]
fn verify_accepts_well_formed_log() {
let account_key = Ed25519SigningKey::generate();
let account_pub = account_key.verifying_key();
let addr = AccountAddr::from(&account_key.verifying_key());
let log = make_log(vec![key(1), key(2)]);
let payload = log.encode();
@@ -341,7 +343,7 @@ mod tests {
payload,
};
assert_eq!(verify_log(&account_pub, &signed).unwrap(), log);
assert_eq!(verify_log(&addr, &signed).unwrap(), log);
}
/// A log validly signed by account A, served as the answer to a query for
@@ -356,7 +358,7 @@ mod tests {
payload,
};
let other = Ed25519SigningKey::generate().verifying_key();
let other = AccountAddr::from(&Ed25519SigningKey::generate().verifying_key());
assert!(matches!(
verify_log(&other, &signed),
Err(AccountLogError::SignatureInvalid)
@@ -367,7 +369,7 @@ mod tests {
#[test]
fn verify_rejects_swapped_payload() {
let account_key = Ed25519SigningKey::generate();
let account_pub = account_key.verifying_key();
let addr = AccountAddr::from(&account_key.verifying_key());
let signature = account_key.sign(make_log(vec![key(1)]).encode().as_bytes());
let signed = SignedAccountLog {
@@ -375,7 +377,7 @@ mod tests {
signature,
};
assert!(matches!(
verify_log(&account_pub, &signed),
verify_log(&addr, &signed),
Err(AccountLogError::SignatureInvalid)
));
}
+2
View File
@@ -8,6 +8,8 @@ pub enum AccountError {
MissingEntry(String),
#[error("invalid account address")]
InvalidAddress,
#[error(transparent)]
Log(#[from] AccountLogError),
}
/// Failures decoding, verifying, or replaying an account log.
+39 -6
View File
@@ -1,22 +1,55 @@
//! Account identity and the signed account log.
//!
//! An account is known by its [`AccountAddr`], an opaque routable id. The
//! account endorses device keys and data by appending to an [`AccountLog`],
//! signed whole on every update and verifiable against the account's address
//! — see that module for the design and its invariants.
//!
//! Applications read account state through [`AccountRegistry`].
#[cfg(feature = "dev")]
mod account;
mod account_log;
mod addr;
mod codec;
mod directory;
mod error;
mod account_log;
use crypto::Ed25519VerifyingKey;
pub use account_log::{AccountEntry, AccountLog, EncodedAccountLog, EntryData, SignedAccountLog};
pub use addr::AccountAddr;
pub use codec::{ACCOUNT_LOG_DOMAIN, verify_extension, verify_log};
pub use error::{AccountError, AccountLogError};
pub use directory::{
AccountDirectory, BUNDLE_VERSION, BundleError, DecodedBundle, DeviceId, DeviceSet, Lamport,
ResolveError, SignedDeviceBundle, decode_bundle_payload, encode_bundle_payload,
resolve_device_ids, verify_bundle,
};
pub use codec::{ACCOUNT_LOG_DOMAIN, verify_extension, verify_log};
pub use error::{AccountError, AccountLogError};
pub use account_log::{AccountEntry, AccountLog, EncodedAccountLog, EntryData, SignedAccountLog};
pub use addr::AccountAddr;
#[cfg(feature = "dev")]
pub use account::TestLogosAccount;
pub use account::{TestAccountService, TestLogosAccount};
/// What applications may ask about any account.
pub trait AccountRegistry {
type Error: std::fmt::Display + std::fmt::Debug;
/// Keys currently endorsed by `addr`. `Ok(None)`: account never published.
fn associated_ed25519_keys(
&self,
addr: &AccountAddr,
) -> Result<Option<Vec<Ed25519VerifyingKey>>, Self::Error>;
/// Is `signer` currently endorsed by `addr`? Provided — one derivation,
/// so implementations cannot diverge on what "associated" means.
fn is_ed25519_associated(
&self,
signer: &Ed25519VerifyingKey,
addr: &AccountAddr,
) -> Result<bool, Self::Error> {
Ok(self
.associated_ed25519_keys(addr)?
.is_some_and(|keys| keys.contains(signer)))
}
}