Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[rust] Support providing tl_version + tl_headers via HTTP headers #280

Merged
merged 10 commits into from
May 24, 2024
11 changes: 6 additions & 5 deletions rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ license = "MIT OR Apache-2.0"
readme = "README.md"

[dependencies]
openssl = "0.10"
base64 = "0.22"
thiserror = "1.0"
anyhow = "1.0"
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
base64 = "0.22"
indexmap = "2.2"
openssl = "0.10"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
uuid = { version = "1.8", features = ["serde"] }
43 changes: 34 additions & 9 deletions rust/src/jws.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
use std::borrow::Cow;

use crate::http::HeaderName;
use indexmap::IndexMap;
use uuid::Uuid;

use crate::http::HeaderName;

/// `Tl-Signature` header.
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct JwsHeader<'a> {
/// Algorithm, should be `ES512`.
pub alg: Cow<'a, str>,
/// Siging key id.
pub kid: Cow<'a, str>,
pub alg: JwsAlgorithm,
/// Signing key id.
pub kid: Uuid,
/// Signing scheme version, e.g. `"2"`.
///
/// Empty implies v1, aka body-only signing.
#[serde(default)]
pub tl_version: Option<Cow<'a, str>>,
pub tl_version: Option<TlVersion>,
/// Comma separated ordered headers used in the signature.
#[serde(default)]
pub tl_headers: Option<String>,
Expand All @@ -25,7 +27,7 @@ pub struct JwsHeader<'a> {

impl<'a> JwsHeader<'a> {
pub(crate) fn new_v2(
kid: &'a str,
kid: Uuid,
headers: &IndexMap<HeaderName<'_>, &[u8]>,
jku: Option<&'a str>,
) -> Self {
Expand All @@ -37,11 +39,34 @@ impl<'a> JwsHeader<'a> {
all
});
Self {
alg: Cow::Borrowed("ES512"),
kid: Cow::Borrowed(kid),
tl_version: Some(Cow::Borrowed("2")),
alg: JwsAlgorithm::ES512,
kid,
tl_version: Some(TlVersion::V2),
tl_headers: Some(header_keys),
jku: jku.map(Cow::Borrowed),
}
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum TlVersion {
#[serde(rename = "1")]
V1,
#[serde(rename = "2")]
V2,
}

impl TlVersion {
pub const fn as_str(self) -> &'static str {
match self {
TlVersion::V1 => "1",
TlVersion::V2 => "2",
}
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum JwsAlgorithm {
#[serde(rename = "ES512")]
ES512,
}
11 changes: 6 additions & 5 deletions rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ mod verify;

use common::Unset;
pub use http::Method;
pub use jws::JwsHeader;
pub use jws::{JwsAlgorithm, JwsHeader, TlVersion};
pub use sign::{CustomSigner, Signer, SignerBuilder};
use uuid::Uuid;
use verify::PublicKey;
pub use verify::{CustomVerifier, Verifier, VerifierBuilder};

Expand All @@ -30,10 +31,10 @@ pub use verify::{CustomVerifier, Verifier, VerifierBuilder};
/// .sign()?;
/// # Ok(()) }
/// ```
pub fn sign_with_pem<'a>(
kid: &'a str,
private_key_pem: &'a [u8],
) -> SignerBuilder<'a, &'a str, &'a [u8], Unset, Unset, Unset> {
pub fn sign_with_pem(
kid: Uuid,
Haydabase marked this conversation as resolved.
Show resolved Hide resolved
private_key_pem: &[u8],
) -> SignerBuilder<'_, Uuid, &[u8], Unset, Unset, Unset> {
SignerBuilder::build_with_pem(kid, private_key_pem)
}

Expand Down
5 changes: 3 additions & 2 deletions rust/src/openssl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use openssl::{
nid::Nid,
pkey::{PKey, Private, Public},
};
use uuid::Uuid;

pub(crate) fn parse_ec_private_key(private_key: &[u8]) -> anyhow::Result<EcKey<Private>> {
let private_key = PKey::private_key_from_pem(private_key)?.ec_key()?;
Expand All @@ -31,7 +32,7 @@ pub(crate) fn parse_ec_public_key(public_key: &[u8]) -> anyhow::Result<EcKey<Pub

/// Read JWKs json then find & parse the JWK for the given `signature_kid`
pub(crate) fn find_and_parse_ec_jwk(
signature_kid: &str,
signature_kid: Uuid,
jwks: &[u8],
) -> anyhow::Result<EcKey<Public>> {
let jwks: Jwks = serde_json::from_slice(jwks)?;
Expand Down Expand Up @@ -91,7 +92,7 @@ struct Jwks {
#[derive(serde::Deserialize)]
struct Jwk {
#[serde(default)]
kid: String,
kid: Uuid,
#[serde(default)]
kty: String,
#[serde(default)]
Expand Down
3 changes: 2 additions & 1 deletion rust/src/sign/custom_signer.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::future::Future;

use indexmap::IndexMap;
use uuid::Uuid;

use crate::{base64::ToUrlSafeBase64, http::HeaderName, Error, JwsHeader};

Expand All @@ -23,7 +24,7 @@ use super::build_v2_signing_payload;
/// # Ok(()) }
/// ```
pub struct CustomSigner<'a> {
pub(crate) kid: &'a str,
pub(crate) kid: Uuid,
pub(crate) body: &'a [u8],
pub(crate) method: &'static str,
pub(crate) path: &'a str,
Expand Down
13 changes: 7 additions & 6 deletions rust/src/sign/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod signer_v1;

use indexmap::IndexMap;
use std::fmt;
use uuid::Uuid;

use crate::{base64::ToUrlSafeBase64, common::Unset, http::HeaderName, openssl, Error, Method};

Expand Down Expand Up @@ -55,8 +56,8 @@ impl<'a> SignerBuilder<'a, Unset, Unset, Unset, Unset, Unset> {
}
}

impl<'a> SignerBuilder<'a, &'a str, &'a [u8], Unset, Unset, Unset> {
pub fn build_with_pem(kid: &'a str, private_key: &'a [u8]) -> Self {
impl<'a> SignerBuilder<'a, Uuid, &'a [u8], Unset, Unset, Unset> {
pub fn build_with_pem(kid: Uuid, private_key: &'a [u8]) -> Self {
SignerBuilder {
kid,
private_key,
Expand All @@ -71,7 +72,7 @@ impl<'a> SignerBuilder<'a, &'a str, &'a [u8], Unset, Unset, Unset> {

impl<'a, Pk, Body, Method, Path> SignerBuilder<'a, Unset, Pk, Body, Method, Path> {
/// Add the private key kid.
pub fn kid(self, kid: &str) -> SignerBuilder<'a, &str, Pk, Body, Method, Path> {
pub fn kid(self, kid: Uuid) -> SignerBuilder<'a, Uuid, Pk, Body, Method, Path> {
SignerBuilder {
kid,
private_key: self.private_key,
Expand Down Expand Up @@ -183,7 +184,7 @@ impl<'a, K, Pk, Body, Method, Path> SignerBuilder<'a, K, Pk, Body, Method, Path>
}
}

impl<'a> SignerBuilder<'a, &'a str, Unset, &'a [u8], Method, &'a str> {
impl<'a> SignerBuilder<'a, Uuid, Unset, &'a [u8], Method, &'a str> {
/// Builds a [`CustomSigner`]
///
/// requires the kid, body, method, and path to be set to call this function.
Expand All @@ -200,7 +201,7 @@ impl<'a> SignerBuilder<'a, &'a str, Unset, &'a [u8], Method, &'a str> {
}
}

impl<'a> SignerBuilder<'a, &'a str, &'a [u8], &'a [u8], Unset, Unset> {
impl<'a> SignerBuilder<'a, Uuid, &'a [u8], &'a [u8], Unset, Unset> {
/// Build a V1 Signer see [`SignerV1`].
///
/// requires the private key, kid, and body to be set to call this function.
Expand All @@ -217,7 +218,7 @@ impl<'a> SignerBuilder<'a, &'a str, &'a [u8], &'a [u8], Unset, Unset> {
}
}

impl<'a> SignerBuilder<'a, &'a str, &'a [u8], &'a [u8], Method, &'a str> {
impl<'a> SignerBuilder<'a, Uuid, &'a [u8], &'a [u8], Method, &'a str> {
/// Build a V2 Signer see [`Signer`].
///
/// requires the private key, kid, body, method, and path to be set to call this function.
Expand Down
4 changes: 3 additions & 1 deletion rust/src/sign/signer_v1.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use uuid::Uuid;

use crate::{base64::ToUrlSafeBase64, openssl, Error};

/// Produce a JWS `Tl-Signature` v1 header value, signing just the request body.
Expand All @@ -7,7 +9,7 @@ use crate::{base64::ToUrlSafeBase64, openssl, Error};
/// In general full request signing should be preferred, see [`Signer::sign`].
pub struct SignerV1<'a> {
pub(crate) private_key: &'a [u8],
pub(crate) kid: &'a str,
pub(crate) kid: Uuid,
pub(crate) body: &'a [u8],
pub(crate) jws_jku: Option<&'a str>,
}
Expand Down
43 changes: 26 additions & 17 deletions rust/src/verify/custom_verifer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ use std::fmt;
use anyhow::anyhow;
use indexmap::{IndexMap, IndexSet};

use crate::{base64::ToUrlSafeBase64, http::HeaderName, sign::build_v2_signing_payload, Error};
use crate::{
base64::ToUrlSafeBase64, http::HeaderName, jws::TlVersion, sign::build_v2_signing_payload,
Error,
};

use super::{parse_tl_signature, ParsedTlSignature};

Expand Down Expand Up @@ -46,21 +49,28 @@ impl<'a> CustomVerifier<'a> {

let mut required_headers = self.required_headers.clone();

let version = jws_header.tl_version.map(|s| s.to_string()).or_else(|| {
let version_header_name = HeaderName("Tl-Signature-Version");
required_headers.insert(version_header_name);
self.get_header_string_value_safe(&version_header_name)
});
match version {
Some(version) if version != "2" => {
return Err(Error::JwsError(anyhow!("unexpected header tl_version")))
}
None => return Err(Error::JwsError(anyhow!("missing header tl_version"))),
_ => {}
}
let version = jws_header
.tl_version
.map(Ok)
.or_else(|| {
let version_header_name = HeaderName("Tl-Signature-Version");
required_headers.insert(version_header_name);
self.get_header_string_value_safe(&version_header_name)
.map(|v| match v.as_str() {
"2" => Ok(TlVersion::V2),
v => Err(Error::JwsError(anyhow!(
"unexpected header tl_version: {}",
v
))),
})
})
.ok_or(Error::JwsError(anyhow!("missing header tl_version")))??;

if jws_header.alg != "ES512" {
return Err(Error::JwsError(anyhow!("unexpected header alg")));
if version != TlVersion::V2 {
return Err(Error::JwsError(anyhow!(
"unexpected header tl_version: {}",
version.as_str()
)));
}

let included_header_names_csv = jws_header
Expand Down Expand Up @@ -134,8 +144,7 @@ impl<'a> CustomVerifier<'a> {
fn get_header_string_value_safe(&self, header_name: &HeaderName) -> Option<String> {
self.headers
.get(header_name)
.map(|h| std::str::from_utf8(h).ok())
.flatten()
.and_then(|h| std::str::from_utf8(h).ok())
.map(|s| s.to_string())
}
}
9 changes: 5 additions & 4 deletions rust/src/verify/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ use anyhow::anyhow;
use indexmap::{IndexMap, IndexSet};

use crate::{
base64::DecodeUrlSafeBase64, common::Unset, http::HeaderName, openssl, Error, JwsHeader, Method,
base64::DecodeUrlSafeBase64, common::Unset, http::HeaderName, jws::TlVersion, openssl, Error,
JwsHeader, Method,
};

pub use self::custom_verifer::CustomVerifier;
Expand Down Expand Up @@ -247,7 +248,7 @@ impl<'a> Verifier<'a> {
let public_key = match self.public_key {
PublicKey::Pem(pem) => openssl::parse_ec_public_key(pem),
PublicKey::Jwks(jwks) => {
openssl::find_and_parse_ec_jwk(&parsed_tl_signature.header.kid, jwks)
openssl::find_and_parse_ec_jwk(parsed_tl_signature.header.kid, jwks)
}
}
.map_err(Error::InvalidKey)?;
Expand All @@ -267,12 +268,12 @@ impl<'a> Verifier<'a> {
let parsed_tl_signature = parse_tl_signature(tl_signature)?;

match &parsed_tl_signature.header.tl_version {
Some(tl_version) if tl_version == "1" => VerifierV1 {
None | Some(TlVersion::V1) => VerifierV1 {
Haydabase marked this conversation as resolved.
Show resolved Hide resolved
public_key: self.public_key,
body: self.base.body,
}
.verify_parsed_body_only(parsed_tl_signature),
_ => self.verify_parsed(parsed_tl_signature),
Some(TlVersion::V2) => self.verify_parsed(parsed_tl_signature),
}
}
}
Expand Down
8 changes: 1 addition & 7 deletions rust/src/verify/verifier_v1.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
use std::fmt;

use anyhow::anyhow;

use crate::{base64::ToUrlSafeBase64, openssl, Error};

use super::{parse_tl_signature, ParsedTlSignature, PublicKey};
Expand Down Expand Up @@ -42,14 +40,10 @@ impl<'a> VerifierV1<'a> {

let public_key = match self.public_key {
PublicKey::Pem(pem) => openssl::parse_ec_public_key(pem),
PublicKey::Jwks(jwks) => openssl::find_and_parse_ec_jwk(&jws_header.kid, jwks),
PublicKey::Jwks(jwks) => openssl::find_and_parse_ec_jwk(jws_header.kid, jwks),
}
.map_err(Error::InvalidKey)?;

if jws_header.alg != "ES512" {
return Err(Error::JwsError(anyhow!("unexpected header alg")));
}

// v1 signature: body only
let payload = format!("{header_b64}.{}", self.body.to_url_safe_base64());
openssl::verify_es512(&public_key, payload.as_bytes(), &signature).map_err(Error::JwsError)
Expand Down
Loading