Skip to content

Commit 98f54cc

Browse files
authored
Merge pull request #173 from auths-dev/dev-macFingerprint
feat: Apple Enclave fingerprint working
2 parents 6cea4ae + 4cf20c3 commit 98f54cc

83 files changed

Lines changed: 3867 additions & 935 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.auths/allowed_signers

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
# auths:managed — do not edit manually
22
# auths:attestation
3-
zDnaeozdqZm6u6rx8pc8RjSFVXRdoyACavgoRMQQx1qCXvsdm@auths.local namespaces="git" ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBF4bP1XrwmGIzv5AR3L64MzVmhncKSJZvUm/vRaNFQ5k6yREvLIJwOmAI7ifc9oaTWdLOW/JD/fx3AzDRhNEyNU=
3+
<<<<<<< Updated upstream
4+
zDnaeTDAGwQd8YFykWwyeQEQC8hrHHWbeb9AsoJanKqheTQ9g@auths.local namespaces="git" ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBClLNRlBdgjEEPozFdM4rZh556aLyLCJLj77b+Ru5ACTaqMmXLuRlUWkonba8LKP2NKBWNme+4+tRLYngOaDDxo=
5+
=======
6+
zDnaeQaiejhv26gSRpcw2GuXyFwnBsg5d4LnEYmwswkL6xqNq@auths.local namespaces="git" ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBAI983sl/v/wrXA3Eh6z1pbEUrSISl90Ydt6pagriWA6af/KRqhnahp5ZfUFLDxBNRRLHj8y/aWvWO9NqCQWRXI=
7+
>>>>>>> Stashed changes
48
# auths:manual

.pre-commit-config.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,14 @@ repos:
5151

5252
- id: cargo-clippy
5353
name: cargo clippy
54-
entry: cargo clippy --all-targets --all-features -- -D warnings
54+
entry: cargo clippy --all-targets --all-features --keep-going -- -D warnings
5555
language: system
5656
types: [rust]
5757
pass_filenames: false
5858

5959
- id: cargo-clippy-packages
6060
name: cargo clippy (packages/)
61-
entry: bash -c 'for d in packages/auths-node packages/auths-python packages/auths-verifier-swift; do [ -f "$d/Cargo.toml" ] && CARGO_TARGET_DIR=../../target cargo clippy --manifest-path "$d/Cargo.toml" --all-targets -- -D warnings || exit 1; done'
61+
entry: bash -c 'failed=0; for d in packages/auths-node packages/auths-python packages/auths-verifier-swift; do [ -f "$d/Cargo.toml" ] || continue; CARGO_TARGET_DIR=../../target cargo clippy --manifest-path "$d/Cargo.toml" --all-targets --keep-going -- -D warnings || failed=1; done; exit $failed'
6262
language: system
6363
types: [rust]
6464
pass_filenames: false
@@ -97,7 +97,7 @@ repos:
9797

9898
- id: cargo-test
9999
name: cargo test
100-
entry: cargo nextest run --workspace --profile pre-commit
100+
entry: cargo nextest run --workspace --profile pre-commit --no-fail-fast
101101
language: system
102102
types: [rust]
103103
pass_filenames: false

crates/auths-cli/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ glob.workspace = true
3737
auths-policy.workspace = true
3838
auths-index.workspace = true
3939
auths-crypto.workspace = true
40-
auths-sdk = { workspace = true, features = ["backend-git", "witness-server", "witness-client", "indexed-storage"] }
40+
auths-sdk = { workspace = true, features = ["backend-git", "witness-server", "witness-client", "indexed-storage", "keychain-secure-enclave"] }
4141
auths-transparency = { workspace = true, features = ["native"] }
4242
auths-pairing-protocol.workspace = true
4343
auths-telemetry = { workspace = true, features = ["sink-http"] }

crates/auths-cli/src/commands/agent/mod.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,17 @@ fn unlock_agent(key_alias: &str) -> Result<()> {
541541

542542
let keychain = auths_sdk::keychain::get_platform_keychain()
543543
.map_err(|e| anyhow!("Failed to get platform keychain: {}", e))?;
544+
545+
if keychain.is_hardware_backend() {
546+
return Err(anyhow!(
547+
"Agent-mode signing requires a software-backed key. Key '{}' is hardware-backed \
548+
(Secure Enclave) and cannot export raw key material needed by the SSH agent. \
549+
Use direct signing instead (which dispatches through the Secure Enclave), \
550+
or initialize a separate software-backed identity for agent use.",
551+
key_alias
552+
));
553+
}
554+
544555
let (_identity_did, _role, encrypted_data) = keychain
545556
.load_key(&auths_sdk::keychain::KeyAlias::new_unchecked(key_alias))
546557
.map_err(|e| anyhow!("Failed to load key '{}': {}", key_alias, e))?;

crates/auths-cli/src/commands/artifact/sign.rs

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -52,19 +52,13 @@ pub fn handle_sign(
5252
result.attestation_json.as_bytes(),
5353
);
5454
let alias = KeyAlias::new_unchecked(device_key);
55-
let (_, _role, encrypted) = ctx
56-
.key_storage
57-
.load_key(&alias)
58-
.context("Failed to load device key for log signature")?;
59-
let passphrase = passphrase_provider
60-
.get_passphrase("Re-enter passphrase for log signature:")
61-
.map_err(|e| anyhow::anyhow!("Passphrase error: {e}"))?;
62-
let pkcs8 = auths_sdk::crypto::decrypt_keypair(&encrypted, &passphrase)
63-
.context("Failed to decrypt key for log signature")?;
64-
let parsed = auths_crypto::parse_key_material(&pkcs8)
65-
.map_err(|e| anyhow::anyhow!("Failed to parse key: {e}"))?;
66-
let sig = auths_crypto::typed_sign(&parsed.seed, &pae)
67-
.map_err(|e| anyhow::anyhow!("Failed to sign DSSE PAE: {e}"))?;
55+
let (sig, _pubkey, _curve) = auths_sdk::keychain::sign_with_key(
56+
ctx.key_storage.as_ref(),
57+
&alias,
58+
passphrase_provider.as_ref(),
59+
&pae,
60+
)
61+
.context("Failed to sign DSSE PAE for log submission")?;
6862
Some(sig)
6963
} else {
7064
None

crates/auths-cli/src/commands/artifact/verify.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -310,20 +310,26 @@ pub async fn handle_verify(
310310
fn resolve_identity_key(
311311
identity_bundle: &Option<PathBuf>,
312312
attestation: &Attestation,
313-
) -> Result<(Vec<u8>, CanonicalDid)> {
313+
) -> Result<(auths_verifier::DevicePublicKey, CanonicalDid)> {
314314
if let Some(bundle_path) = identity_bundle {
315315
let bundle_content = fs::read_to_string(bundle_path)
316316
.with_context(|| format!("Failed to read identity bundle: {:?}", bundle_path))?;
317317
let bundle: IdentityBundle = serde_json::from_str(&bundle_content)
318318
.with_context(|| format!("Failed to parse identity bundle: {:?}", bundle_path))?;
319-
let pk = hex::decode(bundle.public_key_hex.as_str())
319+
let pk_bytes = hex::decode(bundle.public_key_hex.as_str())
320320
.context("Invalid public key hex in bundle")?;
321+
let curve = auths_crypto::CurveType::from_public_key_len(pk_bytes.len())
322+
.ok_or_else(|| anyhow!("Invalid bundle public key length: {}", pk_bytes.len()))?;
323+
let pk = auths_verifier::DevicePublicKey::try_new(curve, &pk_bytes)
324+
.map_err(|e| anyhow!("Invalid bundle public key: {e}"))?;
321325
Ok((pk, bundle.identity_did.into()))
322326
} else {
323327
// Resolve public key from the issuer DID
324328
let issuer = &attestation.issuer;
325-
let (pk, _curve) = resolve_pk_from_did(issuer)
329+
let (pk_bytes, curve) = resolve_pk_from_did(issuer)
326330
.with_context(|| format!("Failed to resolve public key from issuer DID '{}'. Use --identity-bundle for stateless verification.", issuer))?;
331+
let pk = auths_verifier::DevicePublicKey::try_new(curve, &pk_bytes)
332+
.map_err(|e| anyhow!("Invalid issuer public key resolved from DID: {e}"))?;
327333
Ok((pk, issuer.clone()))
328334
}
329335
}
@@ -357,7 +363,7 @@ fn resolve_pk_from_did(did: &str) -> Result<(Vec<u8>, auths_crypto::CurveType)>
357363
/// Verify witness receipts if provided.
358364
async fn verify_witnesses(
359365
chain: &[Attestation],
360-
root_pk: &[u8],
366+
root_pk: &auths_verifier::DevicePublicKey,
361367
receipts_path: &Option<PathBuf>,
362368
witness_keys_raw: &[String],
363369
threshold: usize,

crates/auths-cli/src/commands/auth.rs

Lines changed: 15 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
11
use anyhow::{Context, Result, anyhow};
22
use clap::{Parser, Subcommand};
33

4-
use auths_crypto::Pkcs8Der;
5-
use auths_sdk::crypto::decrypt_keypair;
6-
use auths_sdk::crypto::extract_seed_from_pkcs8;
7-
use auths_sdk::crypto::provider_bridge;
8-
use auths_sdk::keychain::KeyStorage;
94
use auths_sdk::storage_layout::layout;
105

116
use crate::factories::storage::build_auths_context;
12-
use auths_sdk::workflows::auth::sign_auth_challenge;
137

148
use crate::commands::executable::ExecutableCommand;
159
use crate::config::CliConfig;
@@ -76,33 +70,22 @@ fn handle_auth_challenge(nonce: &str, domain: &str, ctx: &CliConfig) -> Result<(
7670
let key_alias = auths_sdk::keychain::KeyAlias::new(&key_alias_str)
7771
.map_err(|e| anyhow!("Invalid key alias: {e}"))?;
7872

79-
let (_stored_did, _role, encrypted_key) = auths_ctx
80-
.key_storage
81-
.load_key(&key_alias)
82-
.with_context(|| format!("Failed to load key '{}'", key_alias_str))?;
83-
84-
let passphrase =
85-
passphrase_provider.get_passphrase(&format!("Enter passphrase for '{}':", key_alias))?;
86-
let pkcs8_bytes = decrypt_keypair(&encrypted_key, &passphrase)
87-
.context("Failed to decrypt key (invalid passphrase?)")?;
88-
89-
let pkcs8 = Pkcs8Der::new(&pkcs8_bytes[..]);
90-
let seed =
91-
extract_seed_from_pkcs8(&pkcs8).context("Failed to extract seed from key material")?;
92-
93-
// Derive public key from the seed instead of resolving via KEL
94-
let public_key_bytes = provider_bridge::ed25519_public_key_from_seed_sync(&seed)
95-
.context("Failed to derive public key from seed")?;
96-
let public_key_hex = hex::encode(public_key_bytes);
97-
98-
let result = sign_auth_challenge(
99-
nonce,
100-
domain,
101-
&seed,
102-
&public_key_hex,
103-
controller_did.as_str(),
73+
let message = auths_sdk::workflows::auth::build_auth_challenge_message(nonce, domain)
74+
.context("Failed to build auth challenge payload")?;
75+
76+
let (signature_bytes, public_key_bytes, _curve) = auths_sdk::keychain::sign_with_key(
77+
auths_ctx.key_storage.as_ref(),
78+
&key_alias,
79+
passphrase_provider.as_ref(),
80+
message.as_bytes(),
10481
)
105-
.context("Failed to sign auth challenge")?;
82+
.with_context(|| format!("Failed to sign auth challenge with key '{}'", key_alias))?;
83+
84+
let result = auths_sdk::workflows::auth::SignedAuthChallenge {
85+
signature_hex: hex::encode(&signature_bytes),
86+
public_key_hex: hex::encode(&public_key_bytes),
87+
did: controller_did.to_string(),
88+
};
10689

10790
if is_json_mode() {
10891
JsonResponse::success(

crates/auths-cli/src/commands/device/verify_attestation.rs

Lines changed: 30 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -165,36 +165,38 @@ fn effective_trust_policy(cmd: &VerifyCommand) -> TrustPolicy {
165165
}
166166
}
167167

168+
/// Wrap raw pubkey bytes from a trust store (pin or roots.json) into a curve-tagged
169+
/// `DevicePublicKey`. Infers the curve from length via `CurveType::from_public_key_len`.
170+
fn bytes_to_device_public_key(
171+
bytes: &[u8],
172+
source: &str,
173+
) -> Result<auths_verifier::DevicePublicKey> {
174+
let curve = auths_crypto::CurveType::from_public_key_len(bytes.len())
175+
.ok_or_else(|| anyhow!("Invalid {} public key length: {}", source, bytes.len()))?;
176+
auths_verifier::DevicePublicKey::try_new(curve, bytes)
177+
.map_err(|e| anyhow!("Invalid {} public key: {e}", source))
178+
}
179+
168180
/// Resolve the issuer public key from various sources.
169181
///
170182
/// Resolution precedence:
171-
/// 1. --issuer-pk (direct key, bypasses trust)
183+
/// 1. `--issuer-pk` (direct key, bypasses trust)
172184
/// 2. Pinned identity store
173-
/// 3. roots.json file
185+
/// 3. `roots.json` file
174186
/// 4. Trust policy (TOFU prompt or explicit rejection)
175187
fn resolve_issuer_key(
176188
now: chrono::DateTime<Utc>,
177189
cmd: &VerifyCommand,
178190
att: &Attestation,
179-
) -> Result<Vec<u8>> {
191+
) -> Result<auths_verifier::DevicePublicKey> {
180192
// 1. Direct key takes precedence
181193
if let Some(ref pk_hex) = cmd.issuer_pk {
182194
let pk_bytes =
183195
hex::decode(pk_hex).context("Invalid hex string provided for issuer public key")?;
184-
let curve = match pk_bytes.len() {
185-
32 => auths_crypto::CurveType::Ed25519,
186-
33 | 65 => auths_crypto::CurveType::P256,
187-
_ => {
188-
return Err(anyhow!(
189-
"Invalid issuer public key length: {}",
190-
pk_bytes.len()
191-
));
192-
}
193-
};
194-
// Validate via DevicePublicKey type system
195-
auths_verifier::DevicePublicKey::try_new(curve, &pk_bytes)
196-
.map_err(|e| anyhow!("Invalid issuer public key: {e}"))?;
197-
return Ok(pk_bytes);
196+
let curve = auths_crypto::CurveType::from_public_key_len(pk_bytes.len())
197+
.ok_or_else(|| anyhow!("Invalid issuer public key length: {}", pk_bytes.len()))?;
198+
return auths_verifier::DevicePublicKey::try_new(curve, &pk_bytes)
199+
.map_err(|e| anyhow!("Invalid issuer public key: {e}"));
198200
}
199201

200202
// Determine the DID to look up
@@ -209,7 +211,7 @@ fn resolve_issuer_key(
209211
if !is_json_mode() {
210212
println!("Using pinned identity: {}", did);
211213
}
212-
return Ok(pin.public_key_bytes()?);
214+
return bytes_to_device_public_key(&pin.public_key_bytes()?, "pinned identity");
213215
}
214216

215217
// 3. Check roots.json file
@@ -240,7 +242,7 @@ fn resolve_issuer_key(
240242
trust_level: TrustLevel::OrgPolicy,
241243
};
242244
store.pin(pin)?;
243-
return Ok(root.public_key_bytes()?);
245+
return bytes_to_device_public_key(&root.public_key_bytes()?, "roots.json");
244246
}
245247
}
246248

@@ -297,7 +299,7 @@ async fn run_verify(now: chrono::DateTime<Utc>, cmd: &VerifyCommand) -> Result<V
297299
}
298300

299301
// 3. Resolve issuer public key
300-
let issuer_pk_bytes = resolve_issuer_key(now, cmd, &att)?;
302+
let issuer_pk = resolve_issuer_key(now, cmd, &att)?;
301303

302304
let required_capability: Option<Capability> = cmd.require_capability.as_ref().map(|cap| {
303305
cap.parse::<Capability>().unwrap_or_else(|e| {
@@ -308,9 +310,9 @@ async fn run_verify(now: chrono::DateTime<Utc>, cmd: &VerifyCommand) -> Result<V
308310

309311
// 5. Verify the attestation (with or without capability check)
310312
let verify_result = if let Some(ref cap) = required_capability {
311-
verify_with_capability(&att, cap, &issuer_pk_bytes).await
313+
verify_with_capability(&att, cap, &issuer_pk).await
312314
} else {
313-
verify_with_keys(&att, &issuer_pk_bytes).await
315+
verify_with_keys(&att, &issuer_pk).await
314316
};
315317

316318
match verify_result {
@@ -330,13 +332,10 @@ async fn run_verify(now: chrono::DateTime<Utc>, cmd: &VerifyCommand) -> Result<V
330332
threshold: cmd.witness_threshold,
331333
};
332334

333-
let report = verify_chain_with_witnesses(
334-
std::slice::from_ref(&att),
335-
&issuer_pk_bytes,
336-
&config,
337-
)
338-
.await
339-
.context("Witness chain verification failed")?;
335+
let report =
336+
verify_chain_with_witnesses(std::slice::from_ref(&att), &issuer_pk, &config)
337+
.await
338+
.context("Witness chain verification failed")?;
340339

341340
if !report.is_valid() {
342341
if !is_json_mode()
@@ -447,21 +446,9 @@ pub async fn handle_verify_attestation(
447446

448447
let issuer_pk_bytes = hex::decode(issuer_pubkey_hex)
449448
.context("Invalid hex string provided for issuer public key")?;
449+
let issuer_pk = bytes_to_device_public_key(&issuer_pk_bytes, "issuer")?;
450450

451-
let curve = match issuer_pk_bytes.len() {
452-
32 => auths_crypto::CurveType::Ed25519,
453-
33 | 65 => auths_crypto::CurveType::P256,
454-
_ => {
455-
return Err(anyhow!(
456-
"Invalid issuer public key length: {}",
457-
issuer_pk_bytes.len()
458-
));
459-
}
460-
};
461-
auths_verifier::DevicePublicKey::try_new(curve, &issuer_pk_bytes)
462-
.map_err(|e| anyhow!("Invalid issuer public key: {e}"))?;
463-
464-
match verify_with_keys(&att, &issuer_pk_bytes).await {
451+
match verify_with_keys(&att, &issuer_pk).await {
465452
Ok(_) => {
466453
println!("Attestation verified successfully.");
467454
Ok(())

crates/auths-cli/src/commands/id/identity.rs

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use anyhow::{Context, Result, anyhow};
22
use clap::{ArgAction, Parser, Subcommand};
3-
use ring::signature::KeyPair;
43
use serde::Serialize;
54
use serde_json;
65
use std::fs;
@@ -402,6 +401,7 @@ pub fn handle_id(
402401
passphrase_provider.as_ref(),
403402
&get_platform_keychain()?,
404403
None,
404+
auths_crypto::CurveType::default(),
405405
) {
406406
Ok((controller_did_keri, alias)) => {
407407
println!("\n✅ Identity created.");
@@ -592,23 +592,19 @@ pub fn handle_id(
592592
.load_all_attestations()
593593
.unwrap_or_default();
594594

595-
// Load the public key from keychain
595+
// Load the public key from keychain (handles SE and software keys)
596596
let keychain = get_platform_keychain()?;
597-
let (_, _role, encrypted_key) = keychain
598-
.load_key(&KeyAlias::new_unchecked(&alias))
599-
.with_context(|| format!("Key '{}' not found in keychain", alias))?;
600-
601-
// Decrypt to get public key
602-
let pass = passphrase_provider
603-
.get_passphrase(&format!("Enter passphrase for key '{}':", alias))?;
604-
let pkcs8_bytes = auths_sdk::crypto::decrypt_keypair(&encrypted_key, &pass)
605-
.context("Failed to decrypt key")?;
606-
let keypair = auths_sdk::identity::load_keypair_from_der_or_seed(&pkcs8_bytes)?;
597+
let alias_typed = KeyAlias::new_unchecked(&alias);
598+
let (public_key_bytes, _curve) = auths_sdk::keychain::extract_public_key_bytes(
599+
keychain.as_ref(),
600+
&alias_typed,
601+
passphrase_provider.as_ref(),
602+
)
603+
.with_context(|| format!("Failed to extract public key for '{}'", alias))?;
607604
#[allow(clippy::disallowed_methods)]
608-
// INVARIANT: hex::encode of Ed25519 pubkey always produces valid hex
609-
let public_key_hex = auths_verifier::PublicKeyHex::new_unchecked(hex::encode(
610-
keypair.public_key().as_ref(),
611-
));
605+
// INVARIANT: hex::encode of pubkey bytes always produces valid hex
606+
let public_key_hex =
607+
auths_verifier::PublicKeyHex::new_unchecked(hex::encode(&public_key_bytes));
612608

613609
// Create the bundle
614610
let bundle = IdentityBundle {

crates/auths-cli/src/commands/id/migrate.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,7 @@ fn perform_gpg_migration(
436436
&passphrase_provider,
437437
keychain.as_ref(),
438438
None,
439+
auths_crypto::CurveType::default(),
439440
) {
440441
Ok((controller_did, alias)) => {
441442
out.print_success(&format!("Created Auths identity: {}", controller_did));
@@ -832,6 +833,7 @@ fn perform_ssh_migration(
832833
&passphrase_provider,
833834
keychain.as_ref(),
834835
None,
836+
auths_crypto::CurveType::default(),
835837
) {
836838
Ok((controller_did, alias)) => {
837839
out.print_success(&format!("Created Auths identity: {}", controller_did));

0 commit comments

Comments
 (0)