-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #24 from junkurihara/feat/httpsig
feat: Add http message signature (RFC9421) based request authentication for allowed sources
- Loading branch information
Showing
46 changed files
with
3,788 additions
and
156 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
[package] | ||
name = "httpsig-registry" | ||
description = "Handler for endpoints that serves wire-formatted HTTPSig public keys" | ||
version.workspace = true | ||
authors.workspace = true | ||
homepage.workspace = true | ||
repository.workspace = true | ||
license.workspace = true | ||
readme.workspace = true | ||
categories.workspace = true | ||
keywords.workspace = true | ||
edition.workspace = true | ||
publish.workspace = true | ||
|
||
[dependencies] | ||
anyhow = { version = "1.0.83" } | ||
thiserror = { version = "1.0.60" } | ||
pulldown-cmark = { version = "0.10.3", default-features = false } | ||
http = { version = "1.1.0" } | ||
indexmap = { version = "2.2.6" } | ||
minisign-verify = { version = "0.2.1" } | ||
reqwest = { version = "0.12.4", default-features = false, features = [ | ||
"rustls-tls", | ||
"http2", | ||
"hickory-dns", | ||
] } | ||
futures = { version = "0.3.30", default-features = false, features = [ | ||
"std", | ||
"async-await", | ||
] } | ||
tokio = { version = "1.37.0", features = [ | ||
"net", | ||
"rt-multi-thread", | ||
"time", | ||
"sync", | ||
"macros", | ||
] } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
# List of endpoints serving wire-formatted HTTPSig public keys for MODoH | ||
|
||
## modoh01.typeq.org | ||
|
||
Below is the list of target domain names that are handled under the public keys of the above endpoint (For DHKex). Wildcard for prefix, i.e., `*.example.com` is acceptable. If not specified, it is identical to the endpoint domain. | ||
|
||
- modoh01.typeq.org | ||
|
||
## modoh02.typeq.org | ||
|
||
## modoh03.typeq.org | ||
|
||
## dnsauth.typeq.org |
4 changes: 4 additions & 0 deletions
4
httpsig-registry/registry-sample/httpsig-endpoints.md.minisig
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
untrusted comment: signature from rsign secret key | ||
RUQm8wdk0lJP8EHUuBcr5PqdYYiBqC7XVePhn4VByqvpWUHcEQ8RQ4DUA6WS11deeQmVqj6nX4IwDSpol6YaP4wxN1CyAbMu4QI= | ||
trusted comment: timestamp:1711525202 file:httpsig-endpoints.md prehashed | ||
l+/B9UT3aI65PbBVCl/ptq6jSbDj0bd/aT3C0F0dFfdoj32N/bjw7T9Hj8v2UKzb6lfx6mvUPM23j3pbtur9BA== |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
pub const HTTPSIG_CONFIGS_PATH: &str = "/.well-known/httpsigconfigs"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
use thiserror::Error; | ||
|
||
/// Describes things that can go wrong in registry handling | ||
#[derive(Debug, Error)] | ||
pub enum ModohRegistryError { | ||
/// Url parse error | ||
#[error("Url parse error")] | ||
FailToParseUrl, | ||
/// IO error | ||
#[error("IO error")] | ||
Io(#[from] std::io::Error), | ||
/// Reqwest error | ||
#[error("Reqwest error")] | ||
Reqwest(#[from] reqwest::Error), | ||
/// Minisign error | ||
#[error("Minisign error")] | ||
Minisign(#[from] minisign_verify::Error), | ||
} |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
mod constants; | ||
mod error; | ||
mod http_client; | ||
mod parse_md; | ||
|
||
use crate::{constants::HTTPSIG_CONFIGS_PATH, error::ModohRegistryError}; | ||
use minisign_verify::{PublicKey, Signature}; | ||
use std::{borrow::Cow, str::FromStr}; | ||
|
||
/* ------------------------------------------------ */ | ||
#[derive(Clone, Debug)] | ||
/// HTTP message signatures enabled domain information | ||
pub struct HttpSigDomainInfo { | ||
/// Configs endpoint | ||
pub configs_endpoint_uri: http::Uri, | ||
/// Domain name | ||
pub dh_signing_target_domain: String, | ||
} | ||
impl HttpSigDomainInfo { | ||
/// Create a new HttpSigDomainInfo | ||
pub fn new<'a, T: Into<Cow<'a, str>>>(configs_endpoint_domain: T, dh_signing_target_domain: Option<String>) -> Self { | ||
let configs_endpoint_uri: http::Uri = format!("https://{}{}", configs_endpoint_domain.into(), HTTPSIG_CONFIGS_PATH) | ||
.parse() | ||
.unwrap(); | ||
let dh_signing_target_domain = | ||
dh_signing_target_domain.unwrap_or_else(|| configs_endpoint_uri.authority().unwrap().to_string()); | ||
Self { | ||
configs_endpoint_uri, | ||
dh_signing_target_domain, | ||
} | ||
} | ||
|
||
/// Create a new HttpSigDomainInfo by fetching endpoint list in markdown format from `file://<abs_path>` or `https://<domain>/<path>` | ||
pub async fn new_from_registry_md<'a, T1, T2>(registry_uri: T1, minisign_base64_pk: T2) -> Result<Vec<Self>, ModohRegistryError> | ||
where | ||
T1: Into<Cow<'a, str>>, | ||
T2: Into<Cow<'a, str>>, | ||
{ | ||
// let registry_uri = registry_uri.into(); | ||
let reqwest_uri = reqwest::Url::from_str(®istry_uri.into()).map_err(|_| ModohRegistryError::FailToParseUrl)?; | ||
if !reqwest_uri.path().ends_with(".md") { | ||
return Err(ModohRegistryError::FailToParseUrl); | ||
} | ||
let (markdown_input, markdown_minisig_input) = match reqwest_uri.scheme() { | ||
"file" => { | ||
let markdown_path = reqwest_uri.to_file_path().map_err(|_| ModohRegistryError::FailToParseUrl)?; | ||
let markdown_sig_path = markdown_path.with_extension("md.minisig"); | ||
let markdown_input = std::fs::read_to_string(markdown_path)?; | ||
let markdown_minisig_input = std::fs::read_to_string(markdown_sig_path)?; | ||
(markdown_input, markdown_minisig_input) | ||
} | ||
"https" => { | ||
let mut reqwest_minisig_uri = reqwest_uri.clone(); | ||
reqwest_minisig_uri.set_path(&format!("{}.minisig", reqwest_uri.path())); | ||
let client = reqwest::Client::new(); | ||
let futs = vec![client.get(reqwest_uri).send(), client.get(reqwest_minisig_uri).send()]; | ||
let res = futures::future::join_all(futs) | ||
.await | ||
.into_iter() | ||
.collect::<Result<Vec<_>, _>>()?; | ||
let texts = futures::future::join_all(res.into_iter().map(|v| v.text())) | ||
.await | ||
.into_iter() | ||
.collect::<Result<Vec<_>, _>>()?; | ||
(texts[0].clone(), texts[1].clone()) | ||
} | ||
_ => return Err(ModohRegistryError::FailToParseUrl), | ||
}; | ||
|
||
let minisign_pk = minisign_base64_pk.into(); | ||
let pk = PublicKey::from_base64(&minisign_pk)?; | ||
let sig = Signature::decode(&markdown_minisig_input)?; | ||
pk.verify(markdown_input.as_bytes(), &sig, false)?; | ||
|
||
let parsed = parse_md::parse_md(markdown_input); | ||
Ok(parsed) | ||
} | ||
} | ||
|
||
/* ------------------------------------------------ */ | ||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
|
||
#[test] | ||
fn it_works() { | ||
let minisign_pk = "RWQm8wdk0lJP8AyGtShi96d72ZzkZnGX9gxR0F5EIWmMW2N25SDfzbrt"; | ||
let file_path = std::path::PathBuf::from("./registry-sample/httpsig-endpoints.md"); | ||
let file_path_minisig = std::path::PathBuf::from("./registry-sample/httpsig-endpoints.md.minisig"); | ||
let markdown_input = std::fs::read_to_string(file_path).unwrap(); | ||
let markdown_minisig_input = std::fs::read_to_string(file_path_minisig).unwrap(); | ||
let pk = PublicKey::from_base64(minisign_pk).unwrap(); | ||
let sig = Signature::decode(&markdown_minisig_input).unwrap(); | ||
let res = pk.verify(markdown_input.as_bytes(), &sig, false); | ||
assert!(res.is_ok()); | ||
|
||
let parsed = parse_md::parse_md(markdown_input); | ||
println!("{:#?}", parsed); | ||
} | ||
|
||
#[tokio::test] | ||
async fn test_from_uri() { | ||
let minisign_pk = "RWQm8wdk0lJP8AyGtShi96d72ZzkZnGX9gxR0F5EIWmMW2N25SDfzbrt"; | ||
|
||
let abs_path = std::path::PathBuf::from("./registry-sample/httpsig-endpoints.md") | ||
.canonicalize() | ||
.unwrap(); | ||
let string_path = format!("file://{}", abs_path.to_str().unwrap()); | ||
let res = HttpSigDomainInfo::new_from_registry_md(string_path, minisign_pk).await; | ||
println!("from file:\n{:#?}", res); | ||
|
||
let https_path = "https://filedn.com/lVEKDQEKcCIhnH516GYdXu0/modoh_httpsig_dev/httpsig-endpoints.md"; | ||
let res = HttpSigDomainInfo::new_from_registry_md(https_path, minisign_pk).await; | ||
println!("from https:\n{:#?}", res); | ||
assert!(res.is_ok()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
use crate::HttpSigDomainInfo; | ||
use indexmap::IndexMap; | ||
use pulldown_cmark::{Event, HeadingLevel, Parser, Tag, TagEnd}; | ||
use std::borrow::Cow; | ||
|
||
/// Parse the markdown and convert it to a vector of HttpSigDomainInfo | ||
pub(crate) fn parse_md<'a, T: Into<Cow<'a, str>>>(markdown_input: T) -> Vec<HttpSigDomainInfo> { | ||
let markdown_input = markdown_input.into(); | ||
let parser = Parser::new(markdown_input.as_ref()); | ||
|
||
type VisitingState = Option<u8>; | ||
const VISITING_H2: u8 = 0; | ||
const VISITING_LIST: u8 = 1; | ||
const VISITING_ITEM: u8 = 2; | ||
let mut visiting_state: VisitingState = None; | ||
let mut visiting_domain: Option<String> = None; | ||
let mut domain_map: IndexMap<String, Vec<String>> = IndexMap::new(); | ||
let mut textbuf = String::new(); | ||
for event in parser { | ||
match event { | ||
Event::Start(Tag::Heading { | ||
level: HeadingLevel::H2, .. | ||
}) => { | ||
visiting_state = Some(VISITING_H2); | ||
visiting_domain = None; | ||
textbuf.clear(); | ||
} | ||
Event::End(TagEnd::Heading(HeadingLevel::H2)) => { | ||
visiting_state = None; | ||
} | ||
Event::Start(Tag::List(_)) => { | ||
visiting_state = Some(VISITING_LIST); | ||
textbuf.clear(); | ||
} | ||
Event::End(TagEnd::List(_)) => { | ||
visiting_state = None; | ||
visiting_domain = None; | ||
textbuf.clear(); | ||
} | ||
Event::Start(Tag::Item) => { | ||
if matches!(visiting_state, Some(VISITING_LIST)) { | ||
visiting_state = Some(VISITING_ITEM); | ||
textbuf.clear(); | ||
} | ||
} | ||
Event::End(TagEnd::Item) => { | ||
if matches!(visiting_state, Some(VISITING_ITEM)) { | ||
visiting_state = Some(VISITING_LIST); | ||
textbuf.clear(); | ||
} | ||
} | ||
Event::Text(text) => match visiting_state { | ||
Some(VISITING_H2) => { | ||
visiting_domain = Some(text.to_string()); | ||
domain_map.entry(text.trim().to_string()).or_default(); | ||
textbuf.clear(); | ||
} | ||
Some(VISITING_ITEM) => { | ||
if let Some(domain) = &visiting_domain { | ||
textbuf.push_str(&text); | ||
let text = text.trim().to_string(); | ||
if text != "*" { | ||
domain_map.get_mut(domain).unwrap().push(textbuf.clone()); | ||
textbuf.clear(); | ||
} | ||
} | ||
} | ||
_ => (), | ||
}, | ||
_ => (), | ||
} | ||
} | ||
let domain_info_vec = domain_map | ||
.iter_mut() | ||
.map(|(k, v)| if v.is_empty() { (k, vec![k.clone()]) } else { (k, v.clone()) }) | ||
.flat_map(|(k, v)| { | ||
v.iter() | ||
.map(move |dh_target| HttpSigDomainInfo::new(k.clone(), Some(dh_target.clone()))) | ||
.collect::<Vec<_>>() | ||
}) | ||
.collect::<Vec<_>>(); | ||
|
||
domain_info_vec | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
[package] | ||
name = "httpsig-proto" | ||
description = "Wire format and protocol definition for HTTP message signature in (Mutualized) Oblivious DNS over HTTPS with Authorization" | ||
version.workspace = true | ||
authors.workspace = true | ||
homepage.workspace = true | ||
repository.workspace = true | ||
license.workspace = true | ||
readme.workspace = true | ||
categories.workspace = true | ||
keywords.workspace = true | ||
edition.workspace = true | ||
publish.workspace = true | ||
|
||
[dependencies] | ||
anyhow = "1.0.83" | ||
thiserror = "1.0.60" | ||
rand = "0.8.5" | ||
hpke = "0.11.0" | ||
bytes = "1.6.0" | ||
byteorder = "1.5.0" | ||
p256 = { version = "0.13.2" } | ||
elliptic-curve = { version = "0.13.8", features = ["ecdh"] } | ||
ed25519-compact = { version = "2.1.1" } | ||
digest = "0.10.7" | ||
sha2 = "0.10.8" | ||
hkdf = "0.12.4" | ||
httpsig = "0.0.15" |
Oops, something went wrong.