Skip to content
Closed
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
1 change: 1 addition & 0 deletions tap-agent/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ web-sys = { version = "0.3", features = [
"Response",
"Headers",
], optional = true }
hex = "0.4"
aes-gcm = "0.10.3"
aes = "0.8"
aes-kw = "0.2"
Expand Down
20 changes: 20 additions & 0 deletions tap-agent/src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,26 @@ impl TapAgent {
}
}

/// Creates a new TapAgent from a secret helper
///
/// Invokes the secret helper to retrieve the private key for the given DID,
/// then creates a TapAgent using that key.
///
/// # Arguments
///
/// * `config` - The secret helper configuration
/// * `did` - The DID to fetch the key for
/// * `debug` - Whether to enable debug mode
#[cfg(not(target_arch = "wasm32"))]
pub async fn from_secret_helper(
config: &crate::secret_helper::SecretHelperConfig,
did: &str,
debug: bool,
) -> Result<(Self, String)> {
let (private_key, key_type) = config.get_key(did)?;
Self::from_private_key(&private_key, key_type, debug).await
}

/// Creates a new TapAgent from an existing private key
///
/// This function creates a new TapAgent using a provided private key,
Expand Down
142 changes: 142 additions & 0 deletions tap-agent/src/agent_key_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,35 @@ impl AgentKeyManager {
Ok(())
}

/// Get the raw private key bytes and key type for a DID
///
/// Checks generated_keys first (raw bytes), falls back to extracting
/// from the secrets JWK "d" parameter.
pub fn get_private_key(&self, did: &str) -> Result<(Vec<u8>, KeyType)> {
// Check generated_keys first (has raw bytes)
if let Ok(generated_keys) = self.generated_keys.read() {
if let Some(key) = generated_keys.get(did) {
return Ok((key.private_key.clone(), key.key_type));
}
} else {
return Err(Error::FailedToAcquireResolverReadLock);
}

// Fall back to secrets (JWK format)
if let Ok(secrets) = self.secrets.read() {
if let Some(secret) = secrets.get(did) {
return crate::key_manager::extract_private_key_from_secret(secret);
}
} else {
return Err(Error::FailedToAcquireResolverReadLock);
}

Err(Error::KeyNotFound(format!(
"Private key not found for DID: {}",
did
)))
}

/// Save keys to storage if a storage path is configured
pub fn save_to_storage(&self) -> Result<()> {
// Skip if no storage path is configured
Expand Down Expand Up @@ -399,6 +428,11 @@ impl KeyManager for AgentKeyManager {
Arc::clone(&self.secrets)
}

/// Get the raw private key bytes and key type for a DID
fn get_private_key(&self, did: &str) -> Result<(Vec<u8>, KeyType)> {
AgentKeyManager::get_private_key(self, did)
}

/// Generate a new key with the specified options
fn generate_key(&self, options: DIDGenerationOptions) -> Result<GeneratedKey> {
self.generate_key_internal(options, true)
Expand Down Expand Up @@ -1053,3 +1087,111 @@ impl KeyManagerPacking for AgentKeyManager {
.map_err(|e| Error::from(MessageError::KeyManager(e.to_string())))
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::agent::TapAgent;
use crate::did::{DIDGenerationOptions, KeyType};
use crate::key_manager::KeyManager;

#[test]
fn test_get_private_key_for_generated_key() {
let km = AgentKeyManager::new();
let key = km
.generate_key(DIDGenerationOptions {
key_type: KeyType::Ed25519,
})
.unwrap();

let (private_key, key_type) = km.get_private_key(&key.did).unwrap();
assert_eq!(private_key, key.private_key);
assert_eq!(key_type, KeyType::Ed25519);
}

#[test]
fn test_get_private_key_for_storage_loaded_key() {
// Simulate a key loaded from storage (only in secrets, not in generated_keys)
let km = AgentKeyManager::new();
let key = km
.generate_key(DIDGenerationOptions {
key_type: KeyType::Ed25519,
})
.unwrap();

// Create a new key manager and load only the secret (simulating storage load)
let km2 = AgentKeyManager::new();
let secret = km.secrets().read().unwrap().get(&key.did).cloned().unwrap();
km2.secrets()
.write()
.unwrap()
.insert(key.did.clone(), secret);

// Should still be able to extract the private key from the secret
let (private_key, key_type) = km2.get_private_key(&key.did).unwrap();
assert_eq!(private_key, key.private_key);
assert_eq!(key_type, KeyType::Ed25519);
}

#[cfg(feature = "crypto-p256")]
#[test]
fn test_get_private_key_p256() {
let km = AgentKeyManager::new();
let key = km
.generate_key(DIDGenerationOptions {
key_type: KeyType::P256,
})
.unwrap();

let (private_key, key_type) = km.get_private_key(&key.did).unwrap();
assert_eq!(private_key, key.private_key);
assert_eq!(key_type, KeyType::P256);
}

#[cfg(feature = "crypto-secp256k1")]
#[test]
fn test_get_private_key_secp256k1() {
let km = AgentKeyManager::new();
let key = km
.generate_key(DIDGenerationOptions {
key_type: KeyType::Secp256k1,
})
.unwrap();

let (private_key, key_type) = km.get_private_key(&key.did).unwrap();
assert_eq!(private_key, key.private_key);
assert_eq!(key_type, KeyType::Secp256k1);
}

#[test]
fn test_get_private_key_unknown_did() {
let km = AgentKeyManager::new();
let result = km.get_private_key("did:key:nonexistent");
assert!(result.is_err());
match result.unwrap_err() {
Error::KeyNotFound(_) => {} // expected
other => panic!("Expected KeyNotFound, got: {:?}", other),
}
}

#[tokio::test]
async fn test_get_private_key_roundtrip() {
let km = AgentKeyManager::new();
let key = km
.generate_key(DIDGenerationOptions {
key_type: KeyType::Ed25519,
})
.unwrap();

// Export
let (private_key, key_type) = km.get_private_key(&key.did).unwrap();

// Reimport
let (_agent, new_did) = TapAgent::from_private_key(&private_key, key_type, false)
.await
.unwrap();

// Same DID
assert_eq!(new_did, key.did);
}
}
58 changes: 58 additions & 0 deletions tap-agent/src/key_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ pub trait KeyManager: Send + Sync + std::fmt::Debug + 'static {
/// Get a list of all DIDs in the key manager
fn list_keys(&self) -> Result<Vec<String>>;

/// Get the raw private key bytes and key type for a DID
fn get_private_key(&self, did: &str) -> Result<(Vec<u8>, crate::did::KeyType)>;

/// Add a signing key to the key manager
async fn add_signing_key(&self, key: Arc<dyn SigningKey + Send + Sync>) -> Result<()>;

Expand Down Expand Up @@ -121,6 +124,45 @@ pub trait KeyManager: Send + Sync + std::fmt::Debug + 'static {
async fn decrypt_jwe(&self, jwe: &str, expected_kid: Option<&str>) -> Result<Vec<u8>>;
}

/// Extract private key bytes and key type from a Secret's JWK "d" parameter
pub fn extract_private_key_from_secret(secret: &Secret) -> Result<(Vec<u8>, crate::did::KeyType)> {
match &secret.secret_material {
SecretMaterial::JWK { private_key_jwk } => {
let d = private_key_jwk
.get("d")
.and_then(|v| v.as_str())
.ok_or_else(|| {
Error::KeyNotFound("Secret JWK missing 'd' parameter".to_string())
})?;

let private_key = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, d)
.map_err(|e| {
Error::Cryptography(format!("Failed to decode private key from JWK: {}", e))
})?;

let kty = private_key_jwk.get("kty").and_then(|v| v.as_str());
let crv = private_key_jwk.get("crv").and_then(|v| v.as_str());

let key_type = match (kty, crv) {
#[cfg(feature = "crypto-ed25519")]
(Some("OKP"), Some("Ed25519")) => crate::did::KeyType::Ed25519,
#[cfg(feature = "crypto-p256")]
(Some("EC"), Some("P-256")) => crate::did::KeyType::P256,
#[cfg(feature = "crypto-secp256k1")]
(Some("EC"), Some("secp256k1")) => crate::did::KeyType::Secp256k1,
_ => {
return Err(Error::KeyNotFound(format!(
"Unsupported key type: kty={:?}, crv={:?}",
kty, crv
)))
}
};

Ok((private_key, key_type))
}
}
}

/// A default implementation of the KeyManager trait.
#[derive(Debug, Clone)]
pub struct DefaultKeyManager {
Expand Down Expand Up @@ -174,6 +216,22 @@ impl KeyManager for DefaultKeyManager {
Arc::clone(&self.secrets)
}

/// Get the raw private key bytes and key type for a DID
fn get_private_key(&self, did: &str) -> Result<(Vec<u8>, crate::did::KeyType)> {
if let Ok(secrets) = self.secrets.read() {
if let Some(secret) = secrets.get(did) {
return extract_private_key_from_secret(secret);
}
} else {
return Err(Error::FailedToAcquireResolverReadLock);
}

Err(Error::KeyNotFound(format!(
"Private key not found for DID: {}",
did
)))
}

/// Generate a new key with the specified options
fn generate_key(&self, options: DIDGenerationOptions) -> Result<GeneratedKey> {
// Generate the key
Expand Down
12 changes: 11 additions & 1 deletion tap-agent/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ pub mod oob;
/// Payment link functionality
pub mod payment_link;

/// Secret helper for external key management
#[cfg(not(target_arch = "wasm32"))]
pub mod secret_helper;

/// Key storage utilities
pub mod storage;

Expand All @@ -132,7 +136,9 @@ pub use did::{
VerificationMaterial, VerificationMethod, VerificationMethodType,
};
pub use error::{Error, Result};
pub use key_manager::{KeyManager, Secret, SecretMaterial, SecretType};
pub use key_manager::{
extract_private_key_from_secret, KeyManager, Secret, SecretMaterial, SecretType,
};
pub use storage::{KeyStorage, StoredKey};

// Agent key re-exports
Expand All @@ -154,6 +160,10 @@ pub use payment_link::{
DEFAULT_PAYMENT_SERVICE_URL,
};

// Secret helper re-exports
#[cfg(not(target_arch = "wasm32"))]
pub use secret_helper::{SecretHelperConfig, SecretHelperOutput};

// Native-only DID resolver re-exports
#[cfg(not(target_arch = "wasm32"))]
pub use did::MultiResolver;
Expand Down
Loading
Loading