fix: zero-amount invoices, identity.verify DID extraction, tor service permissions
- Allow zero-amount Lightning invoices (BOLT11 "any amount") by changing validation from amount_sats < 1 to amount_sats < 0 - identity.verify now extracts pubkey directly from did:key format instead of requiring the DID to belong to a local identity - tor.create-service writes config to data_dir/tor-config/ instead of /var/lib/archipelago/tor/ (owned by debian-tor, not archipelago user) - Add E2E test script (scripts/run-e2e-tests.sh) covering 47 RPC endpoints - Add testing plan with results (loop/testing.md) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -387,8 +387,8 @@ impl RpcHandler {
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
if amount_sats < 1 {
|
||||
return Err(anyhow::anyhow!("Amount must be at least 1 sat"));
|
||||
if amount_sats < 0 {
|
||||
return Err(anyhow::anyhow!("Amount must be non-negative"));
|
||||
}
|
||||
|
||||
info!(amount_sats = amount_sats, "Creating Lightning invoice");
|
||||
|
||||
@@ -36,7 +36,8 @@ impl RpcHandler {
|
||||
pub(super) async fn handle_tor_list_services(
|
||||
&self,
|
||||
) -> Result<serde_json::Value> {
|
||||
let services = list_services().await?;
|
||||
let config_dir = self.config.data_dir.join("tor-config");
|
||||
let services = list_services(&config_dir).await?;
|
||||
Ok(serde_json::json!({ "services": services }))
|
||||
}
|
||||
|
||||
@@ -60,7 +61,8 @@ impl RpcHandler {
|
||||
return Err(anyhow::anyhow!("Invalid service name (alphanumeric, hyphens, underscores only)"));
|
||||
}
|
||||
|
||||
let mut config = load_services_config().await;
|
||||
let config_dir = self.config.data_dir.join("tor-config");
|
||||
let mut config = load_services_config(&config_dir).await;
|
||||
if config.services.iter().any(|s| s.name == name) {
|
||||
return Err(anyhow::anyhow!("Service '{}' already exists", name));
|
||||
}
|
||||
@@ -70,7 +72,7 @@ impl RpcHandler {
|
||||
local_port,
|
||||
enabled: true,
|
||||
});
|
||||
save_services_config(&config).await?;
|
||||
save_services_config(&config_dir, &config).await?;
|
||||
|
||||
debug!("Tor service created: {} -> port {}", name, local_port);
|
||||
Ok(serde_json::json!({ "created": true, "name": name }))
|
||||
@@ -87,13 +89,14 @@ impl RpcHandler {
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing name"))?;
|
||||
|
||||
let mut config = load_services_config().await;
|
||||
let config_dir = self.config.data_dir.join("tor-config");
|
||||
let mut config = load_services_config(&config_dir).await;
|
||||
let before = config.services.len();
|
||||
config.services.retain(|s| s.name != name);
|
||||
if config.services.len() == before {
|
||||
return Err(anyhow::anyhow!("Service '{}' not found", name));
|
||||
}
|
||||
save_services_config(&config).await?;
|
||||
save_services_config(&config_dir, &config).await?;
|
||||
|
||||
debug!("Tor service deleted: {}", name);
|
||||
Ok(serde_json::json!({ "deleted": true, "name": name }))
|
||||
@@ -116,9 +119,9 @@ impl RpcHandler {
|
||||
}
|
||||
|
||||
/// List all hidden services by scanning the filesystem and merging with config.
|
||||
async fn list_services() -> Result<Vec<TorService>> {
|
||||
async fn list_services(config_dir: &std::path::Path) -> Result<Vec<TorService>> {
|
||||
let base = tor_data_dir();
|
||||
let config = load_services_config().await;
|
||||
let config = load_services_config(config_dir).await;
|
||||
let mut services = Vec::new();
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
|
||||
@@ -186,18 +189,17 @@ fn tor_data_dir() -> String {
|
||||
std::env::var("TOR_DATA_DIR").unwrap_or_else(|_| TOR_DATA_DIR.to_string())
|
||||
}
|
||||
|
||||
async fn load_services_config() -> ServicesConfig {
|
||||
let path = std::path::Path::new(&tor_data_dir()).join(SERVICES_CONFIG);
|
||||
async fn load_services_config(config_dir: &std::path::Path) -> ServicesConfig {
|
||||
let path = config_dir.join(SERVICES_CONFIG);
|
||||
match tokio::fs::read_to_string(&path).await {
|
||||
Ok(content) => serde_json::from_str(&content).unwrap_or_default(),
|
||||
Err(_) => ServicesConfig::default(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn save_services_config(config: &ServicesConfig) -> Result<()> {
|
||||
let dir = tor_data_dir();
|
||||
tokio::fs::create_dir_all(&dir).await.context("Failed to create tor data dir")?;
|
||||
let path = std::path::Path::new(&dir).join(SERVICES_CONFIG);
|
||||
async fn save_services_config(config_dir: &std::path::Path, config: &ServicesConfig) -> Result<()> {
|
||||
tokio::fs::create_dir_all(config_dir).await.context("Failed to create tor config dir")?;
|
||||
let path = config_dir.join(SERVICES_CONFIG);
|
||||
let content = serde_json::to_string_pretty(config).context("Failed to serialize services config")?;
|
||||
tokio::fs::write(&path, content).await.context("Failed to write services config")?;
|
||||
Ok(())
|
||||
|
||||
@@ -212,17 +212,10 @@ impl IdentityManager {
|
||||
}
|
||||
|
||||
/// Verify a signature against a DID's public key.
|
||||
/// The DID must belong to an identity managed by this node.
|
||||
/// Works for any valid did:key (not just local identities).
|
||||
pub async fn verify(&self, did: &str, data: &[u8], sig_hex: &str) -> Result<bool> {
|
||||
// Find identity by DID
|
||||
let (identities, _) = self.list().await?;
|
||||
let identity = identities
|
||||
.iter()
|
||||
.find(|i| i.did == did)
|
||||
.ok_or_else(|| anyhow::anyhow!("No identity found for DID: {}", did))?;
|
||||
|
||||
let pubkey_bytes = hex::decode(&identity.pubkey_hex)
|
||||
.context("Invalid pubkey hex")?;
|
||||
// Extract pubkey from did:key directly — no local lookup needed
|
||||
let pubkey_bytes = pubkey_bytes_from_did_key(did)?;
|
||||
let verifying_key = VerifyingKey::from_bytes(
|
||||
pubkey_bytes
|
||||
.as_slice()
|
||||
@@ -294,6 +287,25 @@ impl IdentityManager {
|
||||
|
||||
// --- internal helpers ---
|
||||
|
||||
}
|
||||
|
||||
/// Extract Ed25519 pubkey bytes from a did:key string.
|
||||
/// Format: did:key:z<base58btc(0xed01 + 32-byte-pubkey)>
|
||||
fn pubkey_bytes_from_did_key(did: &str) -> Result<Vec<u8>> {
|
||||
let z_part = did
|
||||
.strip_prefix("did:key:z")
|
||||
.ok_or_else(|| anyhow::anyhow!("Invalid did:key format: {}", did))?;
|
||||
let decoded = bs58::decode(z_part)
|
||||
.into_vec()
|
||||
.context("Invalid base58 in did:key")?;
|
||||
if decoded.len() != 34 || decoded[0] != 0xed || decoded[1] != 0x01 {
|
||||
return Err(anyhow::anyhow!("Invalid Ed25519 did:key multicodec prefix"));
|
||||
}
|
||||
Ok(decoded[2..].to_vec())
|
||||
}
|
||||
|
||||
impl IdentityManager {
|
||||
|
||||
async fn get_default_id(&self) -> Option<String> {
|
||||
let marker = self.identities_dir.join(DEFAULT_MARKER);
|
||||
fs::read_to_string(&marker).await.ok().map(|s| s.trim().to_string())
|
||||
|
||||
Reference in New Issue
Block a user