specify selective disclosure and anchor policy
Some checks failed
CI / fmt, clippy, docs (push) Failing after 5s
CI / test (ubuntu-latest) (push) Failing after 5s
CI / iroh integration smoke tests (push) Failing after 5s
CodeQL / Analyze Rust (push) Failing after 4s
Security / RustSec cargo-audit (push) Failing after 5s
CI / test (macos-latest) (push) Has been cancelled
CI / test (windows-latest) (push) Has been cancelled
Some checks failed
CI / fmt, clippy, docs (push) Failing after 5s
CI / test (ubuntu-latest) (push) Failing after 5s
CI / iroh integration smoke tests (push) Failing after 5s
CodeQL / Analyze Rust (push) Failing after 4s
Security / RustSec cargo-audit (push) Failing after 5s
CI / test (macos-latest) (push) Has been cancelled
CI / test (windows-latest) (push) Has been cancelled
This commit is contained in:
parent
492a37c020
commit
76eb785ee2
11 changed files with 834 additions and 387 deletions
|
|
@ -13,16 +13,17 @@
|
|||
mod sshsigchain;
|
||||
|
||||
pub use sshsigchain::{
|
||||
AnchorBackendPolicy, AnchorPolicy, AnchorReceipt, AnchorReceiptVerifier, AnchoredHistory,
|
||||
AuthorityDeviceState, AuthorityKey, AuthorityKeyId, AuthorityKeyState, AuthorityState,
|
||||
AuthorityTransition, ChainId as SshSigchainChainId, Digest as SshSigchainDigest, HeadClaim,
|
||||
MAX_JSONL_BYTES, MAX_JSONL_LINE_BYTES, MAX_NAMESPACE_BYTES, Permission, ProfileDisclosure,
|
||||
ProfileExtension, SSH_SIGCHAIN_ANCHOR_NAMESPACE, SSH_SIGCHAIN_KEY_PROOF_NAMESPACE,
|
||||
SSH_SIGCHAIN_NAMESPACE, SSH_SIGCHAIN_VERIFIER_PRINCIPAL, SSH_SIGCHAIN_VERSION,
|
||||
SshSigchainError, SshSigchainRecord, SshSigchainTrust, SshSigchainVerification,
|
||||
SshSigchainVerifier, authority_key_id, decode_sshsigchain_jsonl, encode_sshsigchain_jsonl,
|
||||
key_proof_signing_bytes, profile_payload_commitment, select_anchored_head,
|
||||
verify_anchor_receipts, verify_head_claim, verify_sshsigchain,
|
||||
AnchorAttesterPolicy, AnchorBackendPolicy, AnchorPolicy, AnchorReceipt, AnchorReceiptVerifier,
|
||||
AnchoredHistory, AuthorityDeviceState, AuthorityKey, AuthorityKeyId, AuthorityKeyState,
|
||||
AuthorityState, AuthorityTransition, ChainId as SshSigchainChainId,
|
||||
Digest as SshSigchainDigest, HeadClaim, MAX_JSONL_BYTES, MAX_JSONL_LINE_BYTES,
|
||||
MAX_NAMESPACE_BYTES, Permission, ProfileDisclosure, ProfileExtension,
|
||||
SSH_SIGCHAIN_ANCHOR_NAMESPACE, SSH_SIGCHAIN_KEY_PROOF_NAMESPACE, SSH_SIGCHAIN_NAMESPACE,
|
||||
SSH_SIGCHAIN_VERIFIER_PRINCIPAL, SSH_SIGCHAIN_VERSION, SshSigchainError, SshSigchainRecord,
|
||||
SshSigchainTrust, SshSigchainVerification, SshSigchainVerifier, VerifiedAnchoredHistory,
|
||||
authority_key_id, decode_sshsigchain_jsonl, encode_sshsigchain_jsonl, key_proof_signing_bytes,
|
||||
profile_payload_commitment, select_anchored_head, verify_anchor_receipts,
|
||||
verify_anchored_history, verify_head_claim, verify_sshsigchain,
|
||||
};
|
||||
|
||||
use geth_types::{
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ pub enum Permission {
|
|||
KeyAddAny,
|
||||
KeyRevokeSelf,
|
||||
KeyRevokeAny,
|
||||
PermissionManage,
|
||||
ManagePermissions,
|
||||
AnchorPolicy,
|
||||
AnchorAttest,
|
||||
ProfileWrite(String),
|
||||
|
|
@ -140,23 +140,28 @@ pub struct AnchorBackendPolicy {
|
|||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct AnchorAttesterPolicy {
|
||||
pub key_id: AuthorityKeyId,
|
||||
pub weight: u16,
|
||||
pub required: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct AnchorPolicy {
|
||||
/// Zero disables required external anchoring while retaining rollback checks against cached heads.
|
||||
pub threshold: u16,
|
||||
/// Required weight from distinct authorized head-claim signers.
|
||||
pub attester_threshold: u16,
|
||||
/// Explicit attesters. An empty list with a zero threshold permits any active `AnchorAttest` key.
|
||||
pub attesters: Vec<AnchorAttesterPolicy>,
|
||||
/// At least one valid backend receipt must come from every named class.
|
||||
pub required_classes: Vec<String>,
|
||||
/// Required weight from distinct backends.
|
||||
pub backend_threshold: u16,
|
||||
pub backends: Vec<AnchorBackendPolicy>,
|
||||
}
|
||||
|
||||
impl Default for AnchorPolicy {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
threshold: 0,
|
||||
backends: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub enum AuthorityTransition {
|
||||
Genesis {
|
||||
device_id: String,
|
||||
|
|
@ -553,12 +558,12 @@ fn authorize_record<V: SshSigchainVerifier + ?Sized>(
|
|||
delegable_permissions,
|
||||
..
|
||||
} => {
|
||||
require_permission(signer, &Permission::PermissionManage)?;
|
||||
require_permission(signer, &Permission::ManagePermissions)?;
|
||||
ensure_delegation(signer, permissions)?;
|
||||
ensure_delegation(signer, delegable_permissions)?;
|
||||
}
|
||||
AuthorityTransition::PermissionRevoke { .. } => {
|
||||
require_permission(signer, &Permission::PermissionManage)?;
|
||||
require_permission(signer, &Permission::ManagePermissions)?;
|
||||
}
|
||||
AuthorityTransition::AnchorPolicySet { .. } => {
|
||||
require_permission(signer, &Permission::AnchorPolicy)?;
|
||||
|
|
@ -820,6 +825,7 @@ pub fn verify_head_claim<V: SshSigchainVerifier + ?Sized>(
|
|||
return Err(SshSigchainError::UnauthorizedHeadClaim);
|
||||
}
|
||||
if claim.signature.is_empty()
|
||||
|| claim.signature.len() > MAX_SIGNATURE_BYTES
|
||||
|| !verifier.verify(
|
||||
SSH_SIGCHAIN_ANCHOR_NAMESPACE,
|
||||
&claim.signing_bytes()?,
|
||||
|
|
@ -832,20 +838,83 @@ pub fn verify_head_claim<V: SshSigchainVerifier + ?Sized>(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn verify_head_claims<V: SshSigchainVerifier + ?Sized>(
|
||||
policy: &AnchorPolicy,
|
||||
verification: &SshSigchainVerification,
|
||||
claims: &[HeadClaim],
|
||||
verifier: &V,
|
||||
) -> Result<Vec<usize>, SshSigchainError> {
|
||||
let mut valid_indices = Vec::new();
|
||||
let mut seen = BTreeSet::new();
|
||||
let mut weight = 0u32;
|
||||
for (index, claim) in claims.iter().enumerate() {
|
||||
if seen.contains(&claim.signer_key_id)
|
||||
|| verify_head_claim(verification, claim, verifier).is_err()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
seen.insert(claim.signer_key_id);
|
||||
let attester = policy
|
||||
.attesters
|
||||
.iter()
|
||||
.find(|attester| attester.key_id == claim.signer_key_id);
|
||||
if policy.attesters.is_empty() {
|
||||
valid_indices.push(index);
|
||||
} else if let Some(attester) = attester {
|
||||
weight += u32::from(attester.weight);
|
||||
valid_indices.push(index);
|
||||
}
|
||||
}
|
||||
for attester in &policy.attesters {
|
||||
if attester.required
|
||||
&& !valid_indices
|
||||
.iter()
|
||||
.any(|index| claims[*index].signer_key_id == attester.key_id)
|
||||
{
|
||||
return Err(SshSigchainError::RequiredAttesterMissing(attester.key_id));
|
||||
}
|
||||
}
|
||||
if valid_indices.is_empty() {
|
||||
return Err(SshSigchainError::NoAuthorizedHeadClaim);
|
||||
}
|
||||
if weight < u32::from(policy.attester_threshold) {
|
||||
return Err(SshSigchainError::AttesterThresholdNotMet {
|
||||
required: policy.attester_threshold,
|
||||
actual: weight,
|
||||
});
|
||||
}
|
||||
Ok(valid_indices)
|
||||
}
|
||||
|
||||
pub fn verify_anchor_receipts<R: AnchorReceiptVerifier + ?Sized>(
|
||||
policy: &AnchorPolicy,
|
||||
claim: &HeadClaim,
|
||||
receipts: &[AnchorReceipt],
|
||||
verifier: &R,
|
||||
) -> Result<(), SshSigchainError> {
|
||||
verify_anchor_receipts_for_claims(policy, std::slice::from_ref(claim), receipts, verifier)
|
||||
}
|
||||
|
||||
fn verify_anchor_receipts_for_claims<R: AnchorReceiptVerifier + ?Sized>(
|
||||
policy: &AnchorPolicy,
|
||||
claims: &[HeadClaim],
|
||||
receipts: &[AnchorReceipt],
|
||||
verifier: &R,
|
||||
) -> Result<(), SshSigchainError> {
|
||||
validate_anchor_policy(policy)?;
|
||||
let claim_hash = claim.claim_hash()?;
|
||||
let mut weight = 0u32;
|
||||
let mut witnessed_classes = BTreeSet::new();
|
||||
for backend in &policy.backends {
|
||||
let valid = receipts.iter().any(|receipt| {
|
||||
receipt.backend_id == backend.backend_id
|
||||
&& receipt.claim_hash == claim_hash
|
||||
&& verifier.verify_receipt(backend, claim, receipt)
|
||||
let valid = claims.iter().any(|claim| {
|
||||
let Ok(claim_hash) = claim.claim_hash() else {
|
||||
return false;
|
||||
};
|
||||
receipts.iter().any(|receipt| {
|
||||
receipt.evidence.len() <= MAX_PAYLOAD_BYTES
|
||||
&& receipt.backend_id == backend.backend_id
|
||||
&& receipt.claim_hash == claim_hash
|
||||
&& verifier.verify_receipt(backend, claim, receipt)
|
||||
})
|
||||
});
|
||||
if backend.required && !valid {
|
||||
return Err(SshSigchainError::RequiredAnchorMissing(
|
||||
|
|
@ -854,11 +923,17 @@ pub fn verify_anchor_receipts<R: AnchorReceiptVerifier + ?Sized>(
|
|||
}
|
||||
if valid {
|
||||
weight += u32::from(backend.weight);
|
||||
witnessed_classes.insert(backend.class.clone());
|
||||
}
|
||||
}
|
||||
if weight < u32::from(policy.threshold) {
|
||||
for class in &policy.required_classes {
|
||||
if !witnessed_classes.contains(class) {
|
||||
return Err(SshSigchainError::RequiredAnchorClassMissing(class.clone()));
|
||||
}
|
||||
}
|
||||
if weight < u32::from(policy.backend_threshold) {
|
||||
return Err(SshSigchainError::AnchorThresholdNotMet {
|
||||
required: policy.threshold,
|
||||
required: policy.backend_threshold,
|
||||
actual: weight,
|
||||
});
|
||||
}
|
||||
|
|
@ -868,18 +943,60 @@ pub fn verify_anchor_receipts<R: AnchorReceiptVerifier + ?Sized>(
|
|||
#[derive(Clone, Debug)]
|
||||
pub struct AnchoredHistory<'a> {
|
||||
pub verification: &'a SshSigchainVerification,
|
||||
pub claim: &'a HeadClaim,
|
||||
pub claims: &'a [HeadClaim],
|
||||
pub receipts: &'a [AnchorReceipt],
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct VerifiedAnchoredHistory<'a> {
|
||||
verification: &'a SshSigchainVerification,
|
||||
}
|
||||
|
||||
impl<'a> VerifiedAnchoredHistory<'a> {
|
||||
#[must_use]
|
||||
pub fn verification(&self) -> &'a SshSigchainVerification {
|
||||
self.verification
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_anchored_history<'a, V, R>(
|
||||
history: AnchoredHistory<'a>,
|
||||
signature_verifier: &V,
|
||||
receipt_verifier: &R,
|
||||
) -> Result<VerifiedAnchoredHistory<'a>, SshSigchainError>
|
||||
where
|
||||
V: SshSigchainVerifier + ?Sized,
|
||||
R: AnchorReceiptVerifier + ?Sized,
|
||||
{
|
||||
let valid_claims = verify_head_claims(
|
||||
&history.verification.state.head_anchor_policy,
|
||||
history.verification,
|
||||
history.claims,
|
||||
signature_verifier,
|
||||
)?;
|
||||
let claims: Vec<_> = valid_claims
|
||||
.into_iter()
|
||||
.map(|index| history.claims[index].clone())
|
||||
.collect();
|
||||
verify_anchor_receipts_for_claims(
|
||||
&history.verification.state.head_anchor_policy,
|
||||
&claims,
|
||||
history.receipts,
|
||||
receipt_verifier,
|
||||
)?;
|
||||
Ok(VerifiedAnchoredHistory {
|
||||
verification: history.verification,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn select_anchored_head<'a>(
|
||||
histories: &'a [AnchoredHistory<'a>],
|
||||
histories: &'a [VerifiedAnchoredHistory<'a>],
|
||||
cached_head: Option<Digest>,
|
||||
) -> Result<&'a AnchoredHistory<'a>, SshSigchainError> {
|
||||
) -> Result<&'a VerifiedAnchoredHistory<'a>, SshSigchainError> {
|
||||
let mut candidates: Vec<_> = histories
|
||||
.iter()
|
||||
.filter(|history| {
|
||||
history.claim.head == history.verification.head
|
||||
&& cached_head.is_none_or(|head| history.verification.history.contains(&head))
|
||||
cached_head.is_none_or(|head| history.verification.history.contains(&head))
|
||||
})
|
||||
.collect();
|
||||
if candidates.is_empty() {
|
||||
|
|
@ -1062,7 +1179,9 @@ pub enum SshSigchainError {
|
|||
UnknownKey,
|
||||
#[error("key is inactive")]
|
||||
InactiveKey,
|
||||
#[error("anchor policy contains invalid, duplicate, or unsatisfiable backend rules")]
|
||||
#[error(
|
||||
"anchor policy contains invalid, duplicate, unsorted, or unsatisfiable attester/backend rules"
|
||||
)]
|
||||
InvalidAnchorPolicy,
|
||||
#[error("head claim does not name the verified chain head")]
|
||||
ClaimHeadMismatch,
|
||||
|
|
@ -1070,8 +1189,16 @@ pub enum SshSigchainError {
|
|||
UnauthorizedHeadClaim,
|
||||
#[error("head claim signature is invalid")]
|
||||
InvalidHeadClaimSignature,
|
||||
#[error("no supplied head claim has a distinct authorized signer at this head")]
|
||||
NoAuthorizedHeadClaim,
|
||||
#[error("required head attester {0:?} has no valid claim")]
|
||||
RequiredAttesterMissing(AuthorityKeyId),
|
||||
#[error("head attester threshold not met: required {required}, got {actual}")]
|
||||
AttesterThresholdNotMet { required: u16, actual: u32 },
|
||||
#[error("required anchor backend {0} has no valid receipt")]
|
||||
RequiredAnchorMissing(String),
|
||||
#[error("required anchor backend class {0} has no valid receipt")]
|
||||
RequiredAnchorClassMissing(String),
|
||||
#[error("anchor threshold not met: required {required}, got {actual}")]
|
||||
AnchorThresholdNotMet { required: u16, actual: u32 },
|
||||
#[error("no valid anchored head was supplied")]
|
||||
|
|
@ -1156,6 +1283,34 @@ fn validate_authority_key(key: &AuthorityKey) -> Result<(), SshSigchainError> {
|
|||
}
|
||||
|
||||
fn validate_anchor_policy(policy: &AnchorPolicy) -> Result<(), SshSigchainError> {
|
||||
let mut attester_ids = BTreeSet::new();
|
||||
let mut total_attester_weight = 0u32;
|
||||
let mut previous_attester = None;
|
||||
for attester in &policy.attesters {
|
||||
if attester.weight == 0
|
||||
|| !attester_ids.insert(attester.key_id)
|
||||
|| previous_attester.is_some_and(|previous| previous >= attester.key_id)
|
||||
{
|
||||
return Err(SshSigchainError::InvalidAnchorPolicy);
|
||||
}
|
||||
previous_attester = Some(attester.key_id);
|
||||
total_attester_weight += u32::from(attester.weight);
|
||||
}
|
||||
if u32::from(policy.attester_threshold) > total_attester_weight {
|
||||
return Err(SshSigchainError::InvalidAnchorPolicy);
|
||||
}
|
||||
let required_classes: BTreeSet<_> = policy.required_classes.iter().cloned().collect();
|
||||
if required_classes.len() != policy.required_classes.len()
|
||||
|| !policy
|
||||
.required_classes
|
||||
.windows(2)
|
||||
.all(|pair| pair[0] < pair[1])
|
||||
{
|
||||
return Err(SshSigchainError::InvalidAnchorPolicy);
|
||||
}
|
||||
for class in &policy.required_classes {
|
||||
validate_identifier(class.clone())?;
|
||||
}
|
||||
let mut ids = BTreeSet::new();
|
||||
let mut total = 0u32;
|
||||
let mut previous_id: Option<&str> = None;
|
||||
|
|
@ -1173,7 +1328,15 @@ fn validate_anchor_policy(policy: &AnchorPolicy) -> Result<(), SshSigchainError>
|
|||
previous_id = Some(&backend.backend_id);
|
||||
total += u32::from(backend.weight);
|
||||
}
|
||||
if u32::from(policy.threshold) > total {
|
||||
if u32::from(policy.backend_threshold) > total {
|
||||
return Err(SshSigchainError::InvalidAnchorPolicy);
|
||||
}
|
||||
if !required_classes.iter().all(|class| {
|
||||
policy
|
||||
.backends
|
||||
.iter()
|
||||
.any(|backend| &backend.class == class)
|
||||
}) {
|
||||
return Err(SshSigchainError::InvalidAnchorPolicy);
|
||||
}
|
||||
Ok(())
|
||||
|
|
@ -1343,7 +1506,7 @@ fn encode_permissions(
|
|||
Permission::KeyAddAny => out.push(4),
|
||||
Permission::KeyRevokeSelf => out.push(5),
|
||||
Permission::KeyRevokeAny => out.push(6),
|
||||
Permission::PermissionManage => out.push(7),
|
||||
Permission::ManagePermissions => out.push(7),
|
||||
Permission::AnchorPolicy => out.push(8),
|
||||
Permission::AnchorAttest => out.push(9),
|
||||
Permission::ProfileWrite(profile) => {
|
||||
|
|
@ -1361,7 +1524,18 @@ fn encode_permissions(
|
|||
|
||||
fn encode_anchor_policy(out: &mut Vec<u8>, policy: &AnchorPolicy) -> Result<(), SshSigchainError> {
|
||||
validate_anchor_policy(policy)?;
|
||||
out.extend_from_slice(&policy.threshold.to_be_bytes());
|
||||
out.extend_from_slice(&policy.attester_threshold.to_be_bytes());
|
||||
push_u16(out, policy.attesters.len())?;
|
||||
for attester in &policy.attesters {
|
||||
out.extend_from_slice(&(attester.key_id.0).0);
|
||||
out.extend_from_slice(&attester.weight.to_be_bytes());
|
||||
out.push(u8::from(attester.required));
|
||||
}
|
||||
push_u16(out, policy.required_classes.len())?;
|
||||
for class in &policy.required_classes {
|
||||
push_u16_bytes(out, class.as_bytes())?;
|
||||
}
|
||||
out.extend_from_slice(&policy.backend_threshold.to_be_bytes());
|
||||
push_u16(out, policy.backends.len())?;
|
||||
for backend in &policy.backends {
|
||||
push_u16_bytes(out, backend.backend_id.as_bytes())?;
|
||||
|
|
@ -1467,6 +1641,19 @@ mod tests {
|
|||
|
||||
struct TestVerifier;
|
||||
|
||||
struct TestReceiptVerifier;
|
||||
|
||||
impl AnchorReceiptVerifier for TestReceiptVerifier {
|
||||
fn verify_receipt(
|
||||
&self,
|
||||
_: &AnchorBackendPolicy,
|
||||
_: &HeadClaim,
|
||||
receipt: &AnchorReceipt,
|
||||
) -> bool {
|
||||
receipt.evidence == b"valid"
|
||||
}
|
||||
}
|
||||
|
||||
impl SshSigchainVerifier for TestVerifier {
|
||||
fn verify(
|
||||
&self,
|
||||
|
|
@ -1537,6 +1724,17 @@ mod tests {
|
|||
record.with_signature(signature).expect("signed")
|
||||
}
|
||||
|
||||
fn signed_claim(trust: &SshSigchainTrust, verification: &SshSigchainVerification) -> HeadClaim {
|
||||
let claim =
|
||||
HeadClaim::unsigned(trust.chain_id, verification.head, ROOT_KEY).expect("claim");
|
||||
let signature = test_signature(
|
||||
SSH_SIGCHAIN_ANCHOR_NAMESPACE,
|
||||
&claim.signing_bytes().expect("claim bytes"),
|
||||
ROOT_KEY,
|
||||
);
|
||||
claim.with_signature(signature).expect("signed claim")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chain_order_uses_only_parent_hashes() {
|
||||
let trust = trust();
|
||||
|
|
@ -1654,7 +1852,33 @@ mod tests {
|
|||
)
|
||||
.expect("valid key add");
|
||||
assert_eq!(verified.state.active_key_count(), 2);
|
||||
let invalid_proof = signed_record(
|
||||
&trust,
|
||||
Some(add_device.record_hash().expect("hash")),
|
||||
ROOT_KEY,
|
||||
AuthorityTransition::KeyAdd {
|
||||
device_id: "device:phone".to_owned(),
|
||||
key: key.clone(),
|
||||
proof: vec![1],
|
||||
},
|
||||
vec![],
|
||||
);
|
||||
assert_eq!(
|
||||
verify_sshsigchain(
|
||||
&[first.clone(), add_device.clone(), invalid_proof],
|
||||
&trust,
|
||||
&TestVerifier
|
||||
),
|
||||
Err(SshSigchainError::InvalidKeyProof)
|
||||
);
|
||||
let bad_key = authority_key(SECOND_KEY, vec![Permission::AnchorPolicy]);
|
||||
let bad_proof_message =
|
||||
key_proof_signing_bytes(trust.chain_id, "device:phone", &bad_key).expect("proof bytes");
|
||||
let bad_proof = test_signature(
|
||||
SSH_SIGCHAIN_KEY_PROOF_NAMESPACE,
|
||||
&bad_proof_message,
|
||||
SECOND_KEY,
|
||||
);
|
||||
let bad = signed_record(
|
||||
&trust,
|
||||
Some(add_device.record_hash().expect("hash")),
|
||||
|
|
@ -1662,13 +1886,15 @@ mod tests {
|
|||
AuthorityTransition::KeyAdd {
|
||||
device_id: "device:phone".to_owned(),
|
||||
key: bad_key,
|
||||
proof: vec![1],
|
||||
proof: bad_proof,
|
||||
},
|
||||
vec![],
|
||||
);
|
||||
assert!(matches!(
|
||||
verify_sshsigchain(&[first, add_device, bad], &trust, &TestVerifier),
|
||||
Err(SshSigchainError::InvalidKeyProof | SshSigchainError::DeviceCeilingExceeded(_))
|
||||
Err(SshSigchainError::DeviceCeilingExceeded(
|
||||
Permission::AnchorPolicy
|
||||
))
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -1745,32 +1971,20 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn head_claims_require_current_authority_and_policy_receipts() {
|
||||
struct ReceiptVerifier;
|
||||
impl AnchorReceiptVerifier for ReceiptVerifier {
|
||||
fn verify_receipt(
|
||||
&self,
|
||||
_: &AnchorBackendPolicy,
|
||||
_: &HeadClaim,
|
||||
receipt: &AnchorReceipt,
|
||||
) -> bool {
|
||||
receipt.evidence == b"valid"
|
||||
}
|
||||
}
|
||||
|
||||
let trust = trust();
|
||||
let verification =
|
||||
verify_sshsigchain(&[genesis(&trust)], &trust, &TestVerifier).expect("chain");
|
||||
let claim =
|
||||
HeadClaim::unsigned(trust.chain_id, verification.head, ROOT_KEY).expect("claim");
|
||||
let signature = test_signature(
|
||||
SSH_SIGCHAIN_ANCHOR_NAMESPACE,
|
||||
&claim.signing_bytes().expect("bytes"),
|
||||
ROOT_KEY,
|
||||
);
|
||||
let claim = claim.with_signature(signature).expect("signed claim");
|
||||
let claim = signed_claim(&trust, &verification);
|
||||
verify_head_claim(&verification, &claim, &TestVerifier).expect("authorized claim");
|
||||
let policy = AnchorPolicy {
|
||||
threshold: 1,
|
||||
attester_threshold: 1,
|
||||
attesters: vec![AnchorAttesterPolicy {
|
||||
key_id: trust.root_key_id().expect("root ID"),
|
||||
weight: 1,
|
||||
required: true,
|
||||
}],
|
||||
required_classes: vec!["transparency".to_owned()],
|
||||
backend_threshold: 1,
|
||||
backends: vec![AnchorBackendPolicy {
|
||||
backend_id: "transparency-main".to_owned(),
|
||||
class: "transparency".to_owned(),
|
||||
|
|
@ -1784,9 +1998,20 @@ mod tests {
|
|||
claim_hash: claim.claim_hash().expect("claim hash"),
|
||||
evidence: b"valid".to_vec(),
|
||||
};
|
||||
verify_anchor_receipts(&policy, &claim, &[receipt], &ReceiptVerifier).expect("receipts");
|
||||
assert_eq!(
|
||||
verify_head_claims(
|
||||
&policy,
|
||||
&verification,
|
||||
std::slice::from_ref(&claim),
|
||||
&TestVerifier
|
||||
)
|
||||
.expect("attesters"),
|
||||
vec![0]
|
||||
);
|
||||
verify_anchor_receipts(&policy, &claim, &[receipt], &TestReceiptVerifier)
|
||||
.expect("receipts");
|
||||
assert!(matches!(
|
||||
verify_anchor_receipts(&policy, &claim, &[], &ReceiptVerifier),
|
||||
verify_anchor_receipts(&policy, &claim, &[], &TestReceiptVerifier),
|
||||
Err(SshSigchainError::RequiredAnchorMissing(_))
|
||||
));
|
||||
}
|
||||
|
|
@ -1796,7 +2021,7 @@ mod tests {
|
|||
let trust = trust();
|
||||
let first = genesis(&trust);
|
||||
let new_policy = AnchorPolicy {
|
||||
threshold: 1,
|
||||
backend_threshold: 1,
|
||||
backends: vec![AnchorBackendPolicy {
|
||||
backend_id: "nostr-main".to_owned(),
|
||||
class: "nostr".to_owned(),
|
||||
|
|
@ -1804,6 +2029,7 @@ mod tests {
|
|||
weight: 1,
|
||||
required: true,
|
||||
}],
|
||||
..AnchorPolicy::default()
|
||||
};
|
||||
let change = signed_record(
|
||||
&trust,
|
||||
|
|
@ -1852,32 +2078,34 @@ mod tests {
|
|||
);
|
||||
let va = verify_sshsigchain(&[first.clone(), a], &trust, &TestVerifier).expect("a");
|
||||
let vb = verify_sshsigchain(&[first, b], &trust, &TestVerifier).expect("b");
|
||||
let ca = HeadClaim::unsigned(trust.chain_id, va.head, ROOT_KEY).expect("claim a");
|
||||
let cb = HeadClaim::unsigned(trust.chain_id, vb.head, ROOT_KEY).expect("claim b");
|
||||
let claims_a = vec![signed_claim(&trust, &va)];
|
||||
let claims_b = vec![signed_claim(&trust, &vb)];
|
||||
let anchored_a = verify_anchored_history(
|
||||
AnchoredHistory {
|
||||
verification: &va,
|
||||
claims: &claims_a,
|
||||
receipts: &[],
|
||||
},
|
||||
&TestVerifier,
|
||||
&TestReceiptVerifier,
|
||||
)
|
||||
.expect("anchored a");
|
||||
let anchored_b = verify_anchored_history(
|
||||
AnchoredHistory {
|
||||
verification: &vb,
|
||||
claims: &claims_b,
|
||||
receipts: &[],
|
||||
},
|
||||
&TestVerifier,
|
||||
&TestReceiptVerifier,
|
||||
)
|
||||
.expect("anchored b");
|
||||
assert!(matches!(
|
||||
select_anchored_head(
|
||||
&[
|
||||
AnchoredHistory {
|
||||
verification: &va,
|
||||
claim: &ca
|
||||
},
|
||||
AnchoredHistory {
|
||||
verification: &vb,
|
||||
claim: &cb
|
||||
}
|
||||
],
|
||||
None
|
||||
),
|
||||
select_anchored_head(&[anchored_a.clone(), anchored_b], None),
|
||||
Err(SshSigchainError::ForkDetected)
|
||||
));
|
||||
assert!(matches!(
|
||||
select_anchored_head(
|
||||
&[AnchoredHistory {
|
||||
verification: &va,
|
||||
claim: &ca
|
||||
}],
|
||||
Some(Digest([3; 32]))
|
||||
),
|
||||
select_anchored_head(&[anchored_a], Some(Digest([3; 32]))),
|
||||
Err(SshSigchainError::RollbackDetected)
|
||||
));
|
||||
}
|
||||
|
|
@ -1900,4 +2128,28 @@ mod tests {
|
|||
Err(SshSigchainError::JsonLine { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signing_bytes_match_the_published_base_vector() {
|
||||
let record = SshSigchainRecord::unsigned(
|
||||
ChainId([0; 32]),
|
||||
None,
|
||||
ROOT_KEY,
|
||||
AuthorityTransition::Noop,
|
||||
vec![],
|
||||
)
|
||||
.expect("unsigned vector");
|
||||
assert_eq!(
|
||||
hex::encode(record.signing_bytes().expect("bytes")),
|
||||
concat!(
|
||||
"5353435301",
|
||||
"0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"00",
|
||||
"eea8e117bae085d4a9793d8b7d726a89558b63e515a6e1ca32d526f93b24c8a2",
|
||||
"00107373682d656432353531392041514944",
|
||||
"08",
|
||||
"0000"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue