Riley Apeldoorn
564771931f
* Reorganize the fetch component * Organize the server code a little more * Move verification to the server and clean it up * Improve the error handling around the fetch code
149 lines
5.4 KiB
Rust
149 lines
5.4 KiB
Rust
//! Verification of HTTP signatures.
|
|
|
|
use http::Request;
|
|
use puppy::fetch::{
|
|
signatures::{Private, Public, Signature, SigningKey, VerificationKey, Key},
|
|
FetchError,
|
|
};
|
|
use serde_json::{json, Value};
|
|
|
|
use puppy::config::Config;
|
|
|
|
/// Checks request signatures.
|
|
#[derive(Clone)]
|
|
pub struct Verifier {
|
|
actor_id: String,
|
|
key_id: String,
|
|
private: Private,
|
|
public: Public,
|
|
}
|
|
|
|
const VERIFIER_PATH: &str = "/s/request-verifier";
|
|
/// The path at which the request verification actor will present itself.
|
|
pub const VERIFIER_MOUNT: &[&str] = &["s", "request-verifier"];
|
|
|
|
/// A "verdict" about a signed request, passed by a [`Verifier`].
|
|
pub enum Verdict {
|
|
/// The signature checks out.
|
|
Verified(Signer),
|
|
/// The signature does not contain a signature header. This may be intentional, or a client error.
|
|
Unsigned,
|
|
/// The signature failed to verify due to an error related to the signature itself.
|
|
Rejected {
|
|
signature_str: String,
|
|
reason: String,
|
|
},
|
|
}
|
|
|
|
impl Verifier {
|
|
/// Get the JSON-LD representation of the verifier actor.
|
|
pub fn to_json_ld(&self) -> Value {
|
|
json!({
|
|
"@context": [
|
|
"https://www.w3.org/ns/activitystreams",
|
|
"https://w3id.org/security/v1",
|
|
],
|
|
"id": self.actor_id,
|
|
"name": "Public key fetcher",
|
|
"publicKey": {
|
|
"id": self.key_id,
|
|
"owner": self.actor_id,
|
|
"publicKeyPem": self.public.encode_pem()
|
|
},
|
|
"type": "Service",
|
|
})
|
|
}
|
|
/// Load the server's verifier actor.
|
|
///
|
|
/// Each server has one special actor for fetching public keys. Unlike all other objects,
|
|
/// acquiring that actor's JSON-LD representation does not require a request signature.
|
|
///
|
|
/// It doesn't have any data in the data store. Due to its exceptional nature, we just put
|
|
/// the private key in the [`state_dir`][Config::state_dir]. The very first time you load
|
|
/// the verifier, it generates the required private keys.
|
|
pub fn load(cfg: &Config) -> Verifier {
|
|
let Config { ap_domain, state_dir, .. } = cfg;
|
|
let key_path = format!("{state_dir}/fetcher.pem");
|
|
// Read the private key from the state directory, or generate a new one if it couldn't
|
|
// be read.
|
|
let private = Private::load(&key_path).unwrap_or_else(|| {
|
|
let (private, _) = Private::gen();
|
|
private.save(key_path);
|
|
private
|
|
});
|
|
Verifier {
|
|
actor_id: format!("https://{ap_domain}{VERIFIER_PATH}"),
|
|
key_id: format!("https://{ap_domain}{VERIFIER_PATH}#sig-key"),
|
|
public: private.get_public(),
|
|
private,
|
|
}
|
|
}
|
|
/// Does the HTTP signature verification process, and returns a "proof" of the signature in the form
|
|
/// of the [`Signer`], which contains information about who signed a particular request.
|
|
pub async fn verify<B>(&self, req: &Request<B>) -> Verdict {
|
|
// TODO: implement the whole verification thing as a middleware so we can intercept requests
|
|
// like these, instead of coupling this tightly with the router.
|
|
if req.uri().path() == VERIFIER_PATH {
|
|
// HACK: Allow access to the request verifier actor without checking the signature.
|
|
return Verdict::Unsigned;
|
|
}
|
|
|
|
let Some(header) = req.headers().get("signature") else {
|
|
return Verdict::Unsigned;
|
|
};
|
|
|
|
let signature_str = header
|
|
.to_str()
|
|
.expect("signature header value should be valid ascii")
|
|
.to_string();
|
|
|
|
let sig = match Signature::derive(&req) {
|
|
Err(error) => return Verdict::Rejected { signature_str, reason: error },
|
|
Ok(signature) => signature,
|
|
};
|
|
|
|
// Fetch the signer's public key using our private key.
|
|
let fetch_result = self.fetch_public_key(sig.key_id()).await;
|
|
let public_key = match fetch_result {
|
|
Ok(public_key) => public_key,
|
|
Err(err) => {
|
|
return Verdict::Rejected {
|
|
reason: format!("could not fetch public key: {err}"),
|
|
signature_str,
|
|
}
|
|
}
|
|
};
|
|
|
|
// TODO: verify digest also
|
|
if let Err(error) = public_key.verify(&sig) {
|
|
Verdict::Rejected { signature_str, reason: error }
|
|
} else {
|
|
Verdict::Verified(Signer { ap_id: public_key.owner })
|
|
}
|
|
}
|
|
/// Send a request to get the public key from an ID. This request will be signed with the
|
|
/// verifier actor's public key.
|
|
async fn fetch_public_key(&self, uri: &str) -> Result<VerificationKey, FetchError> {
|
|
let json = puppy::fetch::resolve(&self.signing_key(), uri).await?;
|
|
let Some(key) = Key::from_json(json) else {
|
|
return Err(FetchError::BadJson(
|
|
"invalid public key structure".to_string(),
|
|
));
|
|
};
|
|
Ok(key.upgrade())
|
|
}
|
|
/// Get the key that the verification actor signs requests with.
|
|
fn signing_key(&self) -> SigningKey {
|
|
Key {
|
|
id: self.key_id.clone(),
|
|
owner: self.actor_id.clone(),
|
|
inner: self.private.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// An ActivityPub actor that signed a request.
|
|
pub struct Signer {
|
|
/// The ActivityPub ID (a URL) of the signer of the request.
|
|
pub ap_id: String,
|
|
}
|