Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,16 @@ jobs:
run: uv run pytest -v --junitxml=../../conformance-results.xml
env:
AUTHS_BIN: ${{ github.workspace }}/target/debug/auths
# Differential the signed-KEL verdict against keripy's own ingestion path: the
# Rust test shells out to python3, so run it under `uv run` (the conformance
# venv puts keri 1.3.4 on PATH) with the live cross-check enabled. Without this
# the cross-check is gated off and the reject cases are a self-oracle.
- name: KEL-verdict differential vs keripy 1.3.4
working-directory: tests/conformance
env:
KERIPY_INTEROP: "1"
KERIPY_REQUIRED: "1"
run: uv run cargo test --manifest-path ../../Cargo.toml -p auths-keri --test integration keripy_subprocess_agrees_on_accept_and_reject -- --nocapture
- name: Upload conformance results
if: always()
uses: actions/upload-artifact@v4
Expand Down
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions crates/auths-cli/src/commands/init/gather.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub(crate) fn gather_developer_config(
interactive: bool,
out: &Output,
cmd: &InitCommand,
registry_path: &Path,
) -> Result<(
Box<dyn KeyStorage + Send + Sync>,
CreateDeveloperIdentityConfig,
Expand All @@ -34,9 +35,8 @@ pub(crate) fn gather_developer_config(
out.print_success("Prerequisites OK");
out.newline();

let registry_path = get_auths_repo_path()?;
let alias = prompt_for_alias(interactive, cmd)?;
let conflict_policy = prompt_for_conflict_policy(interactive, cmd, &registry_path, out)?;
let conflict_policy = prompt_for_conflict_policy(interactive, cmd, registry_path, out)?;
let git_scope = prompt_for_git_scope(interactive)?;

let mut builder = CreateDeveloperIdentityConfig::builder(KeyAlias::new_unchecked(&alias))
Expand Down Expand Up @@ -80,8 +80,8 @@ pub(crate) fn gather_ci_config(
let (passphrase, passphrase_source) = match std::env::var("AUTHS_PASSPHRASE") {
Ok(p) => (p, "the AUTHS_PASSPHRASE environment variable"),
Err(_) => (
auths_sdk::types::DEFAULT_CI_PASSPHRASE.to_string(),
"the built-in CI default passphrase",
auths_sdk::types::generate_ci_passphrase(),
"a generated per-identity ephemeral passphrase",
),
};
preflight_passphrase(&passphrase, passphrase_source)?;
Expand Down
17 changes: 13 additions & 4 deletions crates/auths-cli/src/commands/init/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ use gather::{
submit_registration,
};
use guided::GuidedSetup;
use helpers::{get_auths_repo_path, offer_shell_completions};
use helpers::offer_shell_completions;
use prompts::{prompt_platform_verification, prompt_profile};

const DEFAULT_KEY_ALIAS: &str = "main";
Expand Down Expand Up @@ -115,6 +115,11 @@ pub struct InitCommand {
#[clap(long)]
pub force: bool,

/// Confirm replacing an existing root identity when running non-interactively. Required with
/// `--force` over an existing identity when there is no TTY to confirm at.
#[clap(long)]
pub confirm_replace: bool,

/// Preview agent configuration without creating files or identities
#[clap(long)]
pub dry_run: bool,
Expand Down Expand Up @@ -306,8 +311,8 @@ fn run_developer_setup(

// GATHER
guide.section("Prerequisites & Configuration");
let (keychain, mut config) = gather_developer_config(interactive, out, cmd)?;
let registry_path = get_auths_repo_path()?;
let registry_path = auths_sdk::paths::resolve_registry_path(ctx.repo_path.clone())?;
let (keychain, mut config) = gather_developer_config(interactive, out, cmd, &registry_path)?;
ensure_registry_dir(&registry_path)?;

let sign_binary_path = which::which("auths-sign").ok();
Expand Down Expand Up @@ -433,7 +438,7 @@ fn run_developer_setup(
// REGISTRATION & DISPLAY
guide.section("Registration & Summary");
let registered = submit_registration(
&get_auths_repo_path()?,
&registry_path,
&cmd.registry,
proof_url,
!cmd.register, // skip unless --register is explicitly passed
Expand Down Expand Up @@ -640,6 +645,7 @@ mod tests {
profile: None,
key_alias: DEFAULT_KEY_ALIAS.to_string(),
force: false,
confirm_replace: false,
dry_run: false,
registry: DEFAULT_REGISTRY_URL.to_string(),
register: false,
Expand Down Expand Up @@ -667,6 +673,7 @@ mod tests {
profile: Some(InitProfile::Ci),
key_alias: "ci-key".to_string(),
force: true,
confirm_replace: false,
dry_run: false,
registry: DEFAULT_REGISTRY_URL.to_string(),
register: false,
Expand Down Expand Up @@ -702,6 +709,7 @@ mod tests {
profile: None,
key_alias: DEFAULT_KEY_ALIAS.to_string(),
force: false,
confirm_replace: false,
dry_run: false,
registry: DEFAULT_REGISTRY_URL.to_string(),
register: false,
Expand All @@ -721,6 +729,7 @@ mod tests {
profile: None,
key_alias: DEFAULT_KEY_ALIAS.to_string(),
force: false,
confirm_replace: false,
dry_run: false,
registry: DEFAULT_REGISTRY_URL.to_string(),
register: false,
Expand Down
21 changes: 21 additions & 0 deletions crates/auths-cli/src/commands/init/prompts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use dialoguer::{Confirm, Input, Select};
use std::path::Path;
use std::sync::Arc;

use auths_sdk::domains::identity::replace_policy::authorize_identity_replacement;
use auths_sdk::keychain::IdentityDID;
use auths_sdk::ports::IdentityStorage;
use auths_sdk::signing::PassphraseProvider;
Expand Down Expand Up @@ -58,6 +59,26 @@ pub(crate) fn prompt_for_conflict_policy(
out: &Output,
) -> Result<IdentityConflictPolicy> {
if cmd.force {
// --force only replaces something when an identity already exists; on a fresh setup it is a
// no-op. Replacing an existing root repoints signing, so require an explicit confirmation —
// an interactive prompt, or --confirm-replace non-interactively — never a silent swap.
let identity_storage = RegistryIdentityStorage::new(registry_path.to_path_buf());
if identity_storage.load_identity().is_ok() {
let confirmed = if interactive {
Confirm::new()
.with_prompt(
"--force will REPLACE your existing root identity with a new one. The old keys \
are not deleted, but signing repoints to the new root. Continue?",
)
.default(false)
.interact()?
} else {
cmd.confirm_replace
};
authorize_identity_replacement(confirmed).map_err(|e| {
anyhow!("{e}\n\nRe-run interactively to confirm, or pass --confirm-replace to replace non-interactively.")
})?;
}
return Ok(IdentityConflictPolicy::ForceNew);
}

Expand Down
26 changes: 26 additions & 0 deletions crates/auths-cli/src/commands/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ use clap::{Parser, Subcommand};
use serde::Serialize;
use std::ffi::CString;
use std::fs;
use std::io::IsTerminal;
use std::path::PathBuf;

use auths_sdk::domains::signing::export_policy::{ExportSensitivity, authorize_key_export};
use auths_sdk::error::AgentError;
use auths_sdk::ffi::ffi;
use auths_sdk::keychain::EncryptedFileStorage;
Expand Down Expand Up @@ -149,6 +151,30 @@ pub fn handle_key(cmd: KeyCommand) -> Result<()> {
passphrase,
format,
} => {
// A plaintext private-key export hands out a usable secret in the clear, so it requires an
// interactive confirmation; refused non-interactively so a script holding AUTHS_PASSPHRASE
// cannot silently exfiltrate the signing key. Public/encrypted exports are not gated.
let sensitivity = match format {
ExportFormat::Pem => ExportSensitivity::PlaintextPrivate,
ExportFormat::Pub => ExportSensitivity::Public,
ExportFormat::Enc => ExportSensitivity::Encrypted,
};
let interactive = std::io::stdin().is_terminal();
let confirmed = if sensitivity == ExportSensitivity::PlaintextPrivate && interactive {
dialoguer::Confirm::new()
.with_prompt(
"Export your UNENCRYPTED private key to stdout? Anyone who reads it controls \
this identity. Continue? [y/N]",
)
.default(false)
.interact()
.context("Failed to read export confirmation")?
} else {
false
};
authorize_key_export(sensitivity, interactive, confirmed)
.map_err(|e| anyhow!("{e}"))?;

let passphrase = match passphrase {
Some(p) => p,
None => prompt_export_passphrase()?,
Expand Down
52 changes: 52 additions & 0 deletions crates/auths-keri/src/cesr_encode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,4 +177,56 @@ mod tests {
let commitment = encode_blake3_digest(blake3::hash(qb64.as_bytes()).as_bytes()).unwrap();
assert_eq!(commitment, "EF_M_u7ASVHXfI8QzdWLq3V9ocSKqxkbujXGbi9QMtP9");
}

#[test]
fn matter_full_len_matches_cesride_encoding() {
use cesride::{Cigar, Matter};
// The full-size table mirrors cesride's matter sizage for the codes auths
// parses. Pin each entry to what cesride actually emits, so a cesride bump
// that shifts a primitive's size fails here loudly instead of drifting the
// length-guard against the decoder.
let cases: [(&str, String); 7] = [
(
"D",
encode_verkey(&[0u8; 32], matter::Codex::Ed25519).unwrap(),
),
(
"B",
encode_verkey(&[0u8; 32], matter::Codex::Ed25519N).unwrap(),
),
(
"1AAJ",
encode_verkey(&[0u8; 33], matter::Codex::ECDSA_256r1).unwrap(),
),
(
"1AAI",
encode_verkey(&[0u8; 33], matter::Codex::ECDSA_256r1N).unwrap(),
),
("E", encode_blake3_digest(&[0u8; 32]).unwrap()),
(
"0B",
Cigar::new_with_raw(&[0u8; 64], None, Some(matter::Codex::Ed25519_Sig))
.and_then(|c| c.qb64())
.unwrap(),
),
(
"0I",
Cigar::new_with_raw(&[0u8; 64], None, Some(matter::Codex::ECDSA_256r1_Sig))
.and_then(|c| c.qb64())
.unwrap(),
),
];
for (code, encoded) in cases {
assert!(
encoded.starts_with(code),
"{code} primitive must encode under its own code, got {encoded:?}"
);
assert_eq!(
matter_full_len(code),
Some(encoded.len()),
"matter_full_len({code:?}) drifted from cesride's {} chars",
encoded.len()
);
}
}
}
Loading
Loading