sigh 1.0.3

HTTP signature generation and verification for ActivityPub, using OpenSSL
Documentation
use base64::prelude::{BASE64_STANDARD, Engine};
use http::{
    header::{HeaderMap, HeaderValue},
    Request,
    request::Parts,
};
use crate::{
    alg::Algorithm,
    Error,
    PrivateKey, PublicKey,
    signature_header::SignatureHeader,
};

/// Signature state for verifying a request
pub struct Signature<'a> {
    request_target: String,
    headers: HeaderMap,
    header: Option<SignatureHeader<'a>>,
}

impl<'a, B> From<&Request<B>> for Signature<'a> {
    fn from(req: &Request<B>) -> Self {
        let method = req.method().as_str().to_lowercase();
        let uri = req.uri();
        let request_target = match uri.query() {
            None =>
                format!("{} {}", method, uri.path()),
            Some(query) =>
                format!("{} {}?{}", method, uri.path(), query),
        };
        let headers = req.headers();
        Signature {
            request_target,
            headers: headers.clone(),
            header: None,
        }
    }
}

impl<'a> From<&Parts> for Signature<'a> {
    fn from(parts: &Parts) -> Self {
        let method = parts.method.as_str().to_lowercase();
        let uri = &parts.uri;
        let request_target = match uri.query() {
            None =>
                format!("{} {}", method, uri.path()),
            Some(query) =>
                format!("{} {}?{}", method, uri.path(), query),
        };
        Signature {
            request_target,
            headers: parts.headers.clone(),
            header: None,
        }
    }
}

impl<'a> Signature<'a> {
    fn header(&self) -> Result<SignatureHeader, Error> {
        match &self.header {
            Some(header) => Ok(header.clone()),
            None => self.headers.get("Signature")
                .ok_or(Error::SignatureHeaderMissing)?
                .to_str()
                .map_err(Error::HeaderValue)
                .and_then(SignatureHeader::parse),
        }
    }

    fn signing_string(&self) -> Result<String, Error> {
        let header = self.header()?;
        Ok(header.headers.iter()
             .enumerate()
             .map(|(i, key)| {
                 let key_s = key.to_lowercase();
                 let newline = if i == 0 {
                     ""
                 } else {
                     "\n"
                 };
                 let value = match *key {
                     "(request-target)" => &self.request_target,
                     "(created)" => header.other.iter()
                         .find(|(key, _)| *key == "created")
                         .map(|(_, value)| *value)
                         .unwrap_or(&""),
                     "(expires)" => header.other.iter()
                         .find(|(key, _)| *key == "expires")
                         .map(|(_, value)| *value)
                         .unwrap_or(&""),
                     _ => {
                         self.headers.get(*key)
                             .and_then(|value| value.to_str().ok())
                             .unwrap_or(&"")
                     }
                 };
                 format!("{}{}: {}", newline, key_s, value)
             }).collect())
    }

    /// Get the keyId of the key used for the signature
    pub fn key_id(&self) -> Option<&str> {
        self.header().ok()?.key_id
    }

    /// Get the HTTP headers that are used for the signing_string
    pub fn headers(&self) -> Option<Vec<&str>> {
        Some(self.header().ok()?.headers)
    }

    /// Verify a signature, should return `Ok(true)`
    pub fn verify(&self, public_key: &PublicKey) -> Result<bool, Error> {
        // TODO: verify created, expires
        // TODO: require minimal set of headers
        let signing_string = self.signing_string()?;
        let header = self.header()?;
        let alg = crate::alg::by_name(header.algorithm)
            .ok_or(Error::UnknownAlgorithm(header.algorithm.to_string()))?;
        let signature = header.signature_bytes()?;
        alg.verify(&public_key, signing_string.as_bytes(), &signature)
    }
}

/// Configuration for generating a signature
pub struct SigningConfig<'k, A: Algorithm> {
    algorithm: A,
    private_key: &'k PrivateKey,
    key_id: String,
    signed_headers: &'static [&'static str],
    /// Other fields such as `created`, and `expires`
    pub other: Vec<(String, String)>,
}

impl<'k, A: Algorithm> SigningConfig<'k, A> {
    /// Configure for `algorithm` with `private_key` identified by `key_id`
    pub fn new(algorithm: A, private_key: &'k PrivateKey, key_id: impl Into<String>) -> Self {
        SigningConfig {
            algorithm,
            private_key,
            key_id: key_id.into(),
            signed_headers: &[
                "(request-target)",
                "host", "date",
                "digest", "content-type"
            ],
            other: vec![],
        }
    }

    /// Sign a request
    pub fn sign<B>(&self, request: &mut Request<B>) -> Result<(), Error> {
        let mut signature = Signature::from(&*request);
        signature.header = Some(SignatureHeader {
            key_id: Some(&self.key_id),
            algorithm: self.algorithm.name(),
            headers: self.signed_headers.iter().cloned().collect(),
            signature: &"-",
            other: self.other.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect(),
        });
        let signing_string = signature.signing_string()?;
        let value = self.algorithm.sign(&self.private_key, &signing_string.as_bytes())?;
        let value = BASE64_STANDARD.encode(value);
        let mut header = signature.header.unwrap();
        header.signature = &value;
        request.headers_mut().insert("signature", HeaderValue::from_str(&header.to_string()).map_err(Error::SerializeHeader)?);
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use http::Request;
    use crate::Key;
    use super::*;

    /// Real-world Mastodon 4.0 data
    #[test]
    fn verify_example_post() {
        let request = Request::builder()
            .method("POST")
            .uri("/test")
            .header("host", "relay.fedi.buzz")
            .header("date", "Wed, 07 Dec 2022 17:25:25 GMT")
            .header("digest", "SHA-256=Kr9tlIjunJw2X/ceUWcezSYxI+OTxQPxpyCrOS0yvLc=")
            .header("content-type", "application/activity+json")
            .header("signature", r#"keyId="https://c3d2.social/actor#main-key",algorithm="rsa-sha256",headers="(request-target) host date digest content-type",signature="jeZwvES9qqa6atwASUXHLSynt3rd8OhoNQvnjqhdYkChxahG0QnQDJQcFkEptyjVgODGOqEkdYuqwsJfCh0CLvLMPS0TBefyzFbTB+BVtIWcCANnCNLWlKup0aRqPoH9reN0NaEIqj8JqhN/Bhh2THJdHWAWexCnLQbiKQ2Dy+lk697wSTQ1H4sh8xd1ZtgCPXaoO3Q6oobuBs/d/hcKuxuPFHvikbtQaQfUQjG5MtDm994HkqpYx/+QMfYPw7lcQVStFZ3BbQgrfs4g83OPo2+uu6Q+KQ5ZxR6oHd9N3nmpZO2f+XBZ3j767kVgTnPrHAiqCGX7I3+M8PqAAWERYg==""#)
            .body(())
            .unwrap();
        let public_key = PublicKey::from_pem(b"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAulcRhqjl6GZG9l+Ye29J\ncOYSTpS+rvGvc4YQtIbd08P2jLaiw4k+Nj90sClLV5fQzNG5fo+S8dR85U6VqyL5\nGpixD6x0kuclyBjuTDxd9gh+voix5MVSFuOXM88X5z8glfkiQd/os7NmWgTM9mXI\nsy7q8ZwhaMmijEK2E53ms06yDAeaO3/uCcUt1+CRUOxCEiRf6nMo9SC3ceFG/uma\n/5ck8QgOcxRvCpfH+q25q7qVxDzeWDAfAXnyGybdxiNfJ/9qrCQ05o5BDI3s6ED0\nuPfZdThhEAM/5k3hozDTXZ5umVA9QsV53Kc73z8w7H1Rb+6acfRca+6kFlRdM3Gd\nMwIDAQAB\n-----END PUBLIC KEY-----\n").unwrap();

        let signature = Signature::from(&request);
        assert_eq!(signature.verify(&public_key).unwrap(), true);
    }

    fn test_sign<A: Algorithm>(algorithm: A) {
        let mut request = Request::builder()
            .method("POST")
            .uri("/test")
            .header("host", "example.com")
            .header("date", "Wed, 07 Dec 2022 17:25:25 GMT")
            .header("content-type", "application/activity+json")
            .body(())
            .unwrap();
        let (private_key, _) = algorithm.generate_keys().unwrap();
        SigningConfig::new(algorithm, &private_key, "key1")
            .sign(&mut request).unwrap();
        request.headers().get("signature").unwrap();
    }

    #[test]
    fn sign_rsa_sha256() {
        test_sign(crate::alg::RsaSha256);
    }

    #[test]
    fn sign_hs2019() {
        test_sign(crate::alg::Hs2019);
    }

    fn test_round_trip<A: Algorithm>(algorithm: A) {
        let mut request = Request::builder()
            .method("POST")
            .uri("/test")
            .header("host", "example.com")
            .header("date", "Wed, 07 Dec 2022 17:25:25 GMT")
            .header("content-type", "application/activity+json")
            .body(())
            .unwrap();
        let (private_key, public_key) = algorithm.generate_keys().unwrap();
        SigningConfig::new(algorithm, &private_key, "key1")
            .sign(&mut request).unwrap();

        let signature = Signature::from(&request);
        assert_eq!(signature.verify(&public_key).unwrap(), true);
    }

    #[test]
    fn round_trip_rsa_sha256() {
        test_round_trip(crate::alg::RsaSha256);
    }

    #[test]
    fn round_trip_hs2019() {
        test_round_trip(crate::alg::Hs2019);
    }
}