use std::fmt::Write;
use std::{ops::Deref, sync::Arc};
use jaws::token::{Signed, TokenFormat, Unsigned};
use reqwest::header::HeaderMap;
use serde::Serialize;
use super::fmt::HttpCase;
use super::jose::{AccountKeyIdentifier, Nonce, RequestHeader};
use super::AcmeError;
use super::Url;
use jaws::{fmt, Flat};
const CONTENT_JOSE: &str = "application/jose+json";
pub trait Encode {
fn encode(&self) -> Result<String, AcmeError>;
}
impl<T> Encode for T
where
T: Serialize,
{
fn encode(&self) -> Result<String, AcmeError> {
serde_json::to_string_pretty(&self).map_err(AcmeError::ser)
}
}
#[derive(Debug, Clone, Copy)]
pub enum Method<T> {
Get,
Post(T),
}
type Token<Payload, State> = jaws::Token<Payload, State, Flat>;
#[derive(Debug)]
#[doc(hidden)]
pub struct Identified<K> {
identifier: AccountKeyIdentifier,
key: Arc<K>,
}
impl<K> Clone for Identified<K> {
fn clone(&self) -> Self {
Self {
identifier: self.identifier.clone(),
key: self.key.clone(),
}
}
}
#[derive(Debug)]
#[doc(hidden)]
pub struct Signature<K> {
key: Arc<K>,
}
impl<K> Clone for Signature<K> {
fn clone(&self) -> Self {
Self {
key: self.key.clone(),
}
}
}
#[derive(Debug)]
pub enum Key<K> {
Identified(Identified<K>),
Signed(Signature<K>),
}
impl<K> Clone for Key<K> {
fn clone(&self) -> Self {
match self {
Key::Identified(Identified { identifier, key }) => Key::Identified(Identified {
identifier: identifier.clone(),
key: key.clone(),
}),
Key::Signed(Signature { key }) => Key::Signed(Signature { key: key.clone() }),
}
}
}
impl<K> Key<K>
where
K: jaws::algorithms::TokenSigner<jaws::SignatureBytes>,
{
#[allow(clippy::type_complexity)]
pub(crate) fn sign<P, Fmt>(
&self,
mut token: jaws::Token<P, Unsigned<RequestHeader>, Fmt>,
) -> Result<jaws::Token<P, Signed<RequestHeader, K>, Fmt>, jaws::token::TokenSigningError>
where
P: Serialize,
Fmt: TokenFormat,
{
match &self {
Key::Identified(Identified { identifier, key }) => {
*token.header_mut().key_id() = Some(AsRef::<str>::as_ref(&identifier).to_owned());
token.sign(key.deref())
}
Key::Signed(Signature { key }) => {
token.header_mut().key().derived();
token.sign(key.deref())
}
}
}
pub fn key(&self) -> &Arc<K> {
match self {
Key::Identified(Identified { identifier: _, key }) => key,
Key::Signed(Signature { key }) => key,
}
}
}
impl<K> From<(Arc<K>, Option<AccountKeyIdentifier>)> for Key<K> {
fn from((key, id): (Arc<K>, Option<AccountKeyIdentifier>)) -> Self {
match id {
Some(identifier) => Key::Identified(Identified { identifier, key }),
None => Key::Signed(Signature { key }),
}
}
}
impl<K> From<(Arc<K>, Url)> for Key<K> {
fn from((key, id): (Arc<K>, Url)) -> Self {
Key::Identified(Identified {
identifier: AccountKeyIdentifier::from(id),
key,
})
}
}
impl<K> From<Arc<K>> for Key<K> {
fn from(value: Arc<K>) -> Self {
Key::Signed(Signature { key: value })
}
}
impl<K> From<(Arc<K>, AccountKeyIdentifier)> for Key<K> {
fn from((key, identifier): (Arc<K>, AccountKeyIdentifier)) -> Self {
Key::Identified(Identified { identifier, key })
}
}
#[derive(Debug)]
pub struct Request<T, K> {
method: Method<T>,
url: Url,
key: Key<K>,
headers: HeaderMap,
}
impl<T, K> Clone for Request<T, K>
where
T: Clone,
{
fn clone(&self) -> Self {
Self {
method: self.method.clone(),
url: self.url.clone(),
key: self.key.clone(),
headers: self.headers.clone(),
}
}
}
impl<T, K> Request<T, K> {
fn new<KK>(method: Method<T>, url: Url, key: KK) -> Self
where
KK: Into<Key<K>>,
{
Self {
method,
url,
key: key.into(),
headers: Default::default(),
}
}
pub fn post<L>(payload: T, url: Url, key: L) -> Self
where
L: Into<Key<K>>,
{
Self::new(Method::Post(payload), url, key)
}
pub fn headers_mut(&mut self) -> &mut HeaderMap {
&mut self.headers
}
pub fn headers(&self) -> &HeaderMap {
&self.headers
}
pub fn with_url(mut self, url: Url) -> Self {
self.url = url;
self
}
pub fn with_key(mut self, key: Key<K>) -> Self {
self.key = key;
self
}
}
impl<K> Request<(), K> {
pub fn get<L>(url: Url, key: L) -> Self
where
L: Into<Key<K>>,
{
Self::new(Method::Get, url, key)
}
}
impl<T, K> Request<T, K>
where
T: Serialize,
K: jaws::algorithms::TokenSigner<jaws::SignatureBytes>,
{
fn token(&self, nonce: Nonce) -> Token<&T, Unsigned<RequestHeader>> {
let header = RequestHeader::new(self.url.clone(), Some(nonce));
match &self.method {
Method::Get => jaws::Token::empty(header, Flat),
Method::Post(payload) => jaws::Token::flat(header, payload),
}
}
fn signed_token(&self, nonce: Nonce) -> Result<Token<&T, Signed<RequestHeader, K>>, AcmeError> {
let token = self.token(nonce);
Ok(self.key.sign(token)?)
}
pub fn sign(&self, nonce: Nonce) -> Result<SignedRequest, AcmeError> {
let signed_token = self.signed_token(nonce)?;
let mut request = reqwest::Request::new(reqwest::Method::POST, self.url.clone().into());
*request.headers_mut() = self.headers.clone();
request
.headers_mut()
.insert(reqwest::header::CONTENT_TYPE, CONTENT_JOSE.parse().unwrap());
let body = serde_json::to_vec(&signed_token).map_err(AcmeError::ser)?;
*request.body_mut() = Some(body.into());
Ok(SignedRequest(request))
}
}
impl<T, K> Request<T, K> {
pub fn as_signed(&self) -> FormatSignedRequest<'_, T, K> {
let nonce = String::from("<nonce>").into();
FormatSignedRequest(self, nonce)
}
pub fn as_signed_with_nonce(&self, nonce: Nonce) -> FormatSignedRequest<'_, T, K> {
FormatSignedRequest(self, nonce)
}
fn acme_format_preamble<W: fmt::Write>(&self, f: &mut fmt::IndentWriter<'_, W>) -> fmt::Result {
let method = match &self.method {
Method::Get => "POST as GET",
Method::Post(_) => "POST",
};
let path = self.url.path();
writeln!(f, "{method} {path} HTTP/1.1")?;
if let Some(host) = self.url.host() {
writeln!(f, "{}: {}", reqwest::header::HOST.titlecase(), host)?;
}
writeln!(
f,
"{}: {}",
reqwest::header::CONTENT_TYPE.titlecase(),
CONTENT_JOSE
)?;
writeln!(f)?;
Ok(())
}
}
pub struct FormatSignedRequest<'r, T, K>(&'r Request<T, K>, Nonce);
impl<T, K> fmt::JWTFormat for FormatSignedRequest<'_, T, K>
where
T: Serialize,
K: jaws::algorithms::TokenSigner<jaws::SignatureBytes>,
{
fn fmt<W: std::fmt::Write>(&self, f: &mut fmt::IndentWriter<'_, W>) -> std::fmt::Result {
self.0.acme_format_preamble(f)?;
let signed = self.0.signed_token(self.1.clone()).unwrap();
<jaws::Token<&T, Signed<RequestHeader, K>, _> as fmt::JWTFormat>::fmt(&signed, f)
}
}
impl<T, K> fmt::JWTFormat for Request<T, K>
where
T: Serialize,
K: jaws::algorithms::TokenSigner<jaws::SignatureBytes>,
{
fn fmt<W: fmt::Write>(&self, f: &mut fmt::IndentWriter<'_, W>) -> fmt::Result {
self.acme_format_preamble(f)?;
let nonce = String::from("<nonce>").into();
let token = self.token(nonce);
<jaws::Token<&T, Unsigned<RequestHeader>, _> as fmt::JWTFormat>::fmt(&token, f)
}
}
pub struct SignedRequest(reqwest::Request);
impl SignedRequest {
pub(crate) fn into_inner(self) -> reqwest::Request {
self.0
}
}
impl From<SignedRequest> for reqwest::Request {
fn from(value: SignedRequest) -> Self {
value.0
}
}
#[cfg(test)]
mod test {
use ecdsa::SigningKey;
use p256::NistP256;
use serde_json::json;
use jaws::{Compact, JWTFormat, SignatureBytes};
use super::*;
#[test]
fn encode_via_serialize() {
let data = json!({
"foo": "bar",
"baz": ["qux", "gorb"]
});
let expected = serde_json::to_string_pretty(
&serde_json::from_str::<serde_json::Value>(crate::example!("json-object.json"))
.unwrap(),
)
.unwrap();
assert_eq!(data.encode().unwrap(), expected);
}
#[test]
fn key_builds_header() {
let key = crate::key!("ec-p255");
let url = "https://letsencrypt.test/new-orderz"
.parse::<Url>()
.unwrap();
let nonce: Nonce = String::from("<nonce>").into();
let mut token = jaws::Token::new(RequestHeader::new(url, Some(nonce)), &(), Compact);
token.header_mut().key().derived();
let signed = token
.sign::<SigningKey<NistP256>, SignatureBytes>(&key)
.unwrap();
let header = signed.header();
assert_eq!(
header.formatted().to_string(),
crate::example!("header-key.txt").trim()
);
}
#[test]
fn key_builds_header_with_id() {
let key = crate::key!("ec-p255");
let identifier = AccountKeyIdentifier::from(
"https://letsencrypt.test/account/foo-bar"
.parse::<Url>()
.unwrap(),
);
let url = "https://letsencrypt.test/new-orderz"
.parse::<Url>()
.unwrap();
let nonce: Nonce = String::from("<nonce>").into();
let mut token = jaws::Token::new(RequestHeader::new(url, Some(nonce)), &(), Compact);
*token.header_mut().key_id() = Some(identifier.to_string());
let signed = token
.sign::<SigningKey<NistP256>, SignatureBytes>(&key)
.unwrap();
let header = signed.header();
eprintln!("{}", header.formatted());
assert_eq!(
header.formatted().to_string(),
crate::example!("header-id.txt").trim()
);
}
#[test]
fn request_has_headers() {
let key = crate::key!("ec-p255");
let identifier = AccountKeyIdentifier::from(
"https://letsencrypt.test/account/foo-bar"
.parse::<Url>()
.unwrap(),
);
let url = "https://letsencrypt.test/new-orderz"
.parse::<Url>()
.unwrap();
let mut request = Request::get(url, (key, Some(identifier)));
request
.headers_mut()
.insert("X-Foo", "bar".parse().unwrap());
let signed = request.sign("foo".into()).unwrap();
assert_eq!(signed.0.headers().get("X-Foo").unwrap(), "bar");
assert_eq!(
signed.0.headers().get("Content-Type").unwrap(),
"application/jose+json"
);
}
}