use crate::error::{Error, Result};
#[cfg(feature = "network")]
use crate::internal::{fingerprint_to_hex, keyid_to_hex};
use crate::internal::parse_cert;
const MAX_KEY_RESPONSE_SIZE: u64 = 10 * 1024 * 1024;
#[cfg(feature = "network")]
pub fn fetch_key_by_email(email: &str) -> Result<Vec<u8>> {
let (local, domain) = parse_email(email)?;
let urls = wkd_urls(&local, &domain);
let client = reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.map_err(|e| Error::Network(e.to_string()))?;
let mut last_error = None;
for url in urls {
match client.get(&url).send() {
Ok(response) => {
if response.status().is_success() {
let bytes = read_response_limited(response)?;
let _ = parse_cert(&bytes)?;
return Ok(bytes);
}
}
Err(e) => {
last_error = Some(e.to_string());
}
}
}
Err(Error::KeyNotFound(format!(
"No key found for email '{}': {}",
email,
last_error.unwrap_or_else(|| "Not found".to_string())
)))
}
#[cfg(feature = "network")]
pub fn fetch_key_by_fingerprint(
fingerprint: &str,
keyserver: Option<&str>,
) -> Result<Vec<u8>> {
let server = keyserver.unwrap_or("https://keys.openpgp.org");
let url = format!("{}/vks/v1/by-fingerprint/{}", server, fingerprint.to_uppercase());
let client = reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.map_err(|e| Error::Network(e.to_string()))?;
let response = client.get(&url)
.send()
.map_err(|e| Error::Network(e.to_string()))?;
if !response.status().is_success() {
return Err(Error::KeyNotFound(format!(
"Key not found on keyserver: {}",
fingerprint
)));
}
let bytes = read_response_limited(response)?;
let (public_key, _) = parse_cert(&bytes)?;
let fetched_fp = fingerprint_to_hex(&public_key.primary_key);
if fetched_fp != fingerprint.to_uppercase() {
return Err(Error::KeyNotFound(format!(
"Fetched key fingerprint {} does not match requested {}",
fetched_fp, fingerprint
)));
}
Ok(bytes)
}
#[cfg(feature = "network")]
pub fn fetch_key_by_keyid(
key_id: &str,
keyserver: Option<&str>,
) -> Result<Vec<u8>> {
let server = keyserver.unwrap_or("https://keys.openpgp.org");
let url = format!("{}/vks/v1/by-keyid/{}", server, key_id.to_uppercase());
let client = reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.map_err(|e| Error::Network(e.to_string()))?;
let response = client.get(&url)
.send()
.map_err(|e| Error::Network(e.to_string()))?;
if !response.status().is_success() {
return Err(Error::KeyNotFound(format!(
"Key not found on keyserver: {}",
key_id
)));
}
let bytes = read_response_limited(response)?;
let (public_key, _) = parse_cert(&bytes)?;
let fetched_keyid = keyid_to_hex(&public_key.primary_key);
if fetched_keyid != key_id.to_uppercase() {
let subkey_match = public_key.public_subkeys.iter().any(|sk| {
keyid_to_hex(&sk.key) == key_id.to_uppercase()
});
if !subkey_match {
return Err(Error::KeyNotFound(format!(
"Fetched key ID {} does not match requested {}",
fetched_keyid, key_id
)));
}
}
Ok(bytes)
}
#[cfg(feature = "network")]
pub fn fetch_key_by_email_from_keyserver(
email: &str,
keyserver: Option<&str>,
) -> Result<Vec<u8>> {
let server = keyserver.unwrap_or("https://keys.openpgp.org");
let url = format!("{}/vks/v1/by-email/{}", server, email);
let client = reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.map_err(|e| Error::Network(e.to_string()))?;
let response = client.get(&url)
.send()
.map_err(|e| Error::Network(e.to_string()))?;
if !response.status().is_success() {
return Err(Error::KeyNotFound(format!(
"Key not found on keyserver for email: {}",
email
)));
}
let bytes = read_response_limited(response)?;
let _ = parse_cert(&bytes)?;
Ok(bytes)
}
#[cfg(feature = "network")]
fn read_response_limited(response: reqwest::blocking::Response) -> Result<Vec<u8>> {
if let Some(len) = response.content_length() {
if len > MAX_KEY_RESPONSE_SIZE {
return Err(Error::Network(format!(
"Response too large: {} bytes (max {})",
len, MAX_KEY_RESPONSE_SIZE
)));
}
}
let bytes = response.bytes()
.map_err(|e| Error::Network(e.to_string()))?;
if bytes.len() as u64 > MAX_KEY_RESPONSE_SIZE {
return Err(Error::Network(format!(
"Response too large: {} bytes (max {})",
bytes.len(), MAX_KEY_RESPONSE_SIZE
)));
}
Ok(bytes.to_vec())
}
fn parse_email(email: &str) -> Result<(String, String)> {
let parts: Vec<&str> = email.split('@').collect();
if parts.len() != 2 {
return Err(Error::InvalidInput(format!("Invalid email address: {}", email)));
}
Ok((parts[0].to_lowercase(), parts[1].to_lowercase()))
}
#[cfg(feature = "network")]
fn wkd_urls(local: &str, domain: &str) -> Vec<String> {
use sha1::{Sha1, Digest};
let hash = {
let mut hasher = Sha1::new();
hasher.update(local.as_bytes());
let result = hasher.finalize();
zbase32_encode(&result)
};
vec![
format!(
"https://openpgpkey.{domain}/.well-known/openpgpkey/{domain}/hu/{hash}?l={local}",
domain = domain,
hash = hash,
local = local
),
format!(
"https://{domain}/.well-known/openpgpkey/hu/{hash}?l={local}",
domain = domain,
hash = hash,
local = local
),
]
}
#[cfg(feature = "network")]
fn zbase32_encode(data: &[u8]) -> String {
const ALPHABET: &[u8] = b"ybndrfg8ejkmcpqxot1uwisza345h769";
let mut result = String::new();
let mut buffer: u64 = 0;
let mut bits_in_buffer = 0;
for &byte in data {
buffer = (buffer << 8) | byte as u64;
bits_in_buffer += 8;
while bits_in_buffer >= 5 {
bits_in_buffer -= 5;
let index = ((buffer >> bits_in_buffer) & 0x1f) as usize;
result.push(ALPHABET[index] as char);
}
}
if bits_in_buffer > 0 {
let index = ((buffer << (5 - bits_in_buffer)) & 0x1f) as usize;
result.push(ALPHABET[index] as char);
}
result
}
#[cfg(feature = "dane")]
fn openpgpkey_name(local: &str, domain: &str) -> String {
use sha2::{Sha256, Digest};
let mut hasher = Sha256::new();
hasher.update(local.as_bytes());
let hash = hasher.finalize();
let truncated = &hash[..28];
let hex = hex::encode(truncated);
format!("{}._openpgpkey.{}", hex, domain)
}
#[cfg(feature = "dane")]
fn get_system_resolver() -> String {
#[cfg(unix)]
{
if let Ok(contents) = std::fs::read_to_string("/etc/resolv.conf") {
for line in contents.lines() {
let line = line.trim();
if !line.starts_with('#') && line.starts_with("nameserver") {
if let Some(addr) = line.split_whitespace().nth(1) {
return format!("{}:53", addr);
}
}
}
}
}
#[cfg(target_os = "macos")]
{
if let Ok(output) = std::process::Command::new("scutil")
.arg("--dns")
.output()
{
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
let trimmed = line.trim();
if trimmed.starts_with("nameserver[") {
if let Some(addr) = trimmed.split(':').nth(1) {
let addr = addr.trim();
if !addr.is_empty() {
return format!("{}:53", addr);
}
}
}
}
}
}
#[cfg(target_os = "windows")]
{
if let Ok(output) = std::process::Command::new("powershell")
.args([
"-Command",
"(Get-DnsClientServerAddress -AddressFamily IPv4 | Select-Object -First 1).ServerAddresses[0]",
])
.output()
{
let addr = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !addr.is_empty() {
return format!("{}:53", addr);
}
}
}
"1.1.1.1:53".to_string()
}
#[cfg(feature = "dane")]
fn dns_query(name: &str, resolver: &str) -> Result<Vec<u8>> {
use hickory_proto::op::{Message, MessageType, OpCode, Query};
use hickory_proto::rr::{Name, RecordType};
use hickory_proto::serialize::binary::BinDecodable;
use std::net::UdpSocket;
use std::str::FromStr;
use std::time::Duration;
let dns_name = Name::from_str(name)
.map_err(|e| Error::Network(format!("Invalid DNS name '{}': {}", name, e)))?;
let mut msg = Message::new();
msg.set_id(rand::random::<u16>());
msg.set_message_type(MessageType::Query);
msg.set_op_code(OpCode::Query);
msg.set_recursion_desired(true);
let mut query = Query::new();
query.set_name(dns_name.clone());
query.set_query_type(RecordType::OPENPGPKEY);
msg.add_query(query);
use hickory_proto::op::Edns;
let mut edns = Edns::new();
edns.set_max_payload(4096);
edns.set_version(0);
msg.set_edns(edns);
let wire = msg.to_vec()
.map_err(|e| Error::Network(format!("Failed to serialize DNS query: {}", e)))?;
let resolver_addr: std::net::SocketAddr = resolver.parse()
.map_err(|e| Error::Network(format!("Invalid resolver address '{}': {}", resolver, e)))?;
let socket = UdpSocket::bind("0.0.0.0:0")
.map_err(|e| Error::Network(format!("Failed to bind UDP socket: {}", e)))?;
socket.set_read_timeout(Some(Duration::from_secs(10)))
.map_err(|e| Error::Network(format!("Failed to set socket timeout: {}", e)))?;
socket.send_to(&wire, resolver_addr)
.map_err(|e| Error::Network(format!("Failed to send DNS query: {}", e)))?;
let mut buf = vec![0u8; 65535];
let len = socket.recv(&mut buf)
.map_err(|e| Error::Network(format!("Failed to receive DNS response: {}", e)))?;
let response = Message::from_bytes(&buf[..len])
.map_err(|e| Error::Network(format!("Failed to parse DNS response: {}", e)))?;
if response.truncated() {
return dns_query_tcp(name, resolver, &wire);
}
use hickory_proto::op::ResponseCode;
match response.response_code() {
ResponseCode::NoError => {}
ResponseCode::NXDomain => {
return Err(Error::KeyNotFound(format!(
"No OPENPGPKEY DNS record found for {}", name
)));
}
code => {
return Err(Error::Network(format!(
"DNS query failed with response code: {}", code
)));
}
}
for record in response.answers() {
if let hickory_proto::rr::RData::OPENPGPKEY(ref key) = *record.data() {
return Ok(key.public_key().to_vec());
}
}
Err(Error::KeyNotFound(format!(
"No OPENPGPKEY record in DNS response for {}", name
)))
}
#[cfg(feature = "dane")]
fn dns_query_tcp(name: &str, resolver: &str, wire: &[u8]) -> Result<Vec<u8>> {
use hickory_proto::op::Message;
use hickory_proto::serialize::binary::BinDecodable;
use std::io::{Read, Write};
use std::net::TcpStream;
use std::time::Duration;
let resolver_addr: std::net::SocketAddr = resolver.parse()
.map_err(|e| Error::Network(format!("Invalid resolver address '{}': {}", resolver, e)))?;
let mut stream = TcpStream::connect_timeout(&resolver_addr, Duration::from_secs(10))
.map_err(|e| Error::Network(format!("TCP connection to DNS resolver failed: {}", e)))?;
stream.set_read_timeout(Some(Duration::from_secs(10)))
.map_err(|e| Error::Network(format!("Failed to set TCP timeout: {}", e)))?;
let len_bytes = (wire.len() as u16).to_be_bytes();
stream.write_all(&len_bytes)
.map_err(|e| Error::Network(format!("Failed to write DNS query length: {}", e)))?;
stream.write_all(wire)
.map_err(|e| Error::Network(format!("Failed to write DNS query: {}", e)))?;
let mut resp_len_buf = [0u8; 2];
stream.read_exact(&mut resp_len_buf)
.map_err(|e| Error::Network(format!("Failed to read DNS response length: {}", e)))?;
let resp_len = u16::from_be_bytes(resp_len_buf) as usize;
let mut resp_buf = vec![0u8; resp_len];
stream.read_exact(&mut resp_buf)
.map_err(|e| Error::Network(format!("Failed to read DNS response: {}", e)))?;
let response = Message::from_bytes(&resp_buf)
.map_err(|e| Error::Network(format!("Failed to parse DNS response: {}", e)))?;
for record in response.answers() {
if let hickory_proto::rr::RData::OPENPGPKEY(ref key) = *record.data() {
return Ok(key.public_key().to_vec());
}
}
Err(Error::KeyNotFound(format!(
"No OPENPGPKEY record in DNS response for {}", name
)))
}
#[cfg(feature = "dane")]
pub fn fetch_key_by_email_from_dane(
email: &str,
dns_resolver: Option<&str>,
) -> Result<Vec<u8>> {
let (local, domain) = parse_email(email)?;
let name = openpgpkey_name(&local, &domain);
let resolver = match dns_resolver {
Some(r) => r.to_string(),
None => get_system_resolver(),
};
let key_bytes = dns_query(&name, &resolver)?;
parse_cert(&key_bytes)?;
Ok(key_bytes)
}
#[cfg(test)]
mod tests {
#[cfg(feature = "network")]
use super::*;
#[test]
#[cfg(feature = "network")]
fn test_zbase32_encode() {
let input = b"test";
let encoded = zbase32_encode(input);
assert!(!encoded.is_empty());
}
#[test]
fn test_parse_email() {
let (local, domain) = parse_email("user@example.com").unwrap();
assert_eq!(local, "user");
assert_eq!(domain, "example.com");
assert!(parse_email("invalid").is_err());
}
#[test]
#[cfg(feature = "network")]
fn test_wkd_urls() {
let urls = wkd_urls("test", "example.com");
assert_eq!(urls.len(), 2);
assert!(urls[0].contains("openpgpkey.example.com"));
assert!(urls[1].contains("example.com/.well-known"));
}
#[test]
#[cfg(feature = "dane")]
fn test_openpgpkey_name() {
let name = openpgpkey_name("user", "example.com");
assert!(name.ends_with("._openpgpkey.example.com"));
let hash_part = name.split("._openpgpkey.").next().unwrap();
assert_eq!(hash_part.len(), 56, "Hash should be 56 hex chars (28 octets)");
assert!(hash_part.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
#[cfg(feature = "dane")]
fn test_openpgpkey_name_case_insensitive() {
let name1 = openpgpkey_name("user", "example.com");
let name2 = openpgpkey_name("user", "example.com");
assert_eq!(name1, name2);
}
#[test]
#[cfg(feature = "dane")]
fn test_dane_invalid_email() {
use crate::error::Error;
let result = fetch_key_by_email_from_dane("invalid", None);
assert!(result.is_err());
match result.unwrap_err() {
Error::InvalidInput(_) => {}
other => panic!("Expected InvalidInput, got: {}", other),
}
}
#[test]
#[cfg(feature = "dane")]
fn test_get_system_resolver() {
let resolver = get_system_resolver();
assert!(resolver.contains(':'), "Resolver should include port: {}", resolver);
assert!(resolver.ends_with(":53"), "Resolver should use port 53: {}", resolver);
}
}