Files
nexus/rust/vendor/picky/src/http/http_signature.rs
T
Andy Dixon 50c0104d57 feat(core): RDP client on IronRDP
clients::rdp connects over any caller-dialled TCP stream (direct, proxy
or SSH tunnel), upgrades to TLS and logs on with NLA (CredSSP/NTLM),
optionally falling back to TLS-only logon for servers without NLA such
as xrdp (credentials are passed so they log straight in). The server
certificate is pinned through a caller check on its SHA256 fingerprint
before any credentials are sent. The desktop decodes into the VNC
client's RGBA framebuffer; input (IronRDP's scancode/Unicode/mouse
operations), Display Control resizes and the deactivation-reactivation
sequence run on the session thread.

IronRDP's releases need three vendored patches (vendor/README.md):
picky and sspi with their RustCrypto release-candidate pins relaxed so
they share russh's stable versions (their own suites, 402 tests, pass
against them), and ironrdp-session with two unreleased upstream bitmap
fixes (#1486 stride, #1524 RDP6 orientation) without which xrdp desktops
render sheared.

Verified live against xrdp (scottyhardy/docker-remote-desktop): logon,
full desktop rendering, keyboard and mouse input.
2026-10-12 10:50:24 +01:00

1314 lines
49 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use crate::hash::HashAlgorithm;
use crate::http::http_request::{HttpRequest, HttpRequestError};
use crate::key::{PrivateKey, PublicKey};
use crate::signature::{SignatureAlgorithm, SignatureError};
use base64::engine::general_purpose;
use base64::{DecodeError, Engine as _};
use std::borrow::Cow;
use std::cell::RefCell;
use std::collections::HashMap;
use std::fmt::{self, Debug};
use std::str::FromStr;
use thiserror::Error;
// === error type === //
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum HttpSignatureError {
/// couldn't decode base64
#[error("couldn't decode base64: {source}")]
Base64Decoding { source: DecodeError },
/// signature is not yet valid
#[error("signature is not yet valid (created: {created}, now: {now})")]
NotYetValid { created: u64, now: u64 },
/// signature expired
#[error("signature expired (not after: {not_after}, now: {now})")]
Expired { not_after: u64, now: u64 },
/// signature error occurred
#[error("signature error: {source}")]
Signature { source: SignatureError },
/// couldn't generate signing string
#[error("couldn't generate signing string: {source}")]
SigningStringGeneration { source: HttpRequestError },
/// invalid signing string
#[error("signing string invalid for line `{line}`")]
InvalidSigningString { line: String },
/// missing required builder argument
#[error("missing required builder argument `{arg}`")]
MissingBuilderArgument { arg: &'static str },
/// builder requires a non empty `headers` parameter
#[error("builder requires a non empty `headers` parameter")]
BuilderEmptyHeaders,
/// `headers` parameter shouldn't be provided when using builder with a pre-generated signing string
#[error("`headers` parameter shouldn't be provided when using builder with a pre-generated signing string")]
BuilderHeadersProvidedWithPreGenerated,
/// required parameter is missing from http signature string
#[error("required parameter is missing from http signature string: {parameter}")]
MissingRequiredParameter { parameter: &'static str },
/// a parameter is present but invalid
#[error("invalid parameter: {parameter}")]
InvalidParameter { parameter: &'static str },
/// incompatible 'algorithm' parameter with provided signature verification method
#[error("incompatible 'algorithm' parameter: {value:?}")]
IncompatibleAlgorithm { value: SignatureAlgorithm },
}
impl From<DecodeError> for HttpSignatureError {
fn from(e: DecodeError) -> Self {
Self::Base64Decoding { source: e }
}
}
impl From<SignatureError> for HttpSignatureError {
fn from(e: SignatureError) -> Self {
Self::Signature { source: e }
}
}
impl From<HttpRequestError> for HttpSignatureError {
fn from(e: HttpRequestError) -> Self {
Self::SigningStringGeneration { source: e }
}
}
// === header parameter ===
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum Header {
/// Lowercased HTTP header field name
Name(String),
/// Special `(request-target)` header field
RequestTarget,
/// Special `(created)` header field
Created,
/// Special `(expires)` header field
Expires,
}
impl Header {
pub const REQUEST_TARGET_STR: &'static str = "(request-target)";
pub const CREATED_STR: &'static str = "(created)";
pub const EXPIRES_STR: &'static str = "(expires)";
pub fn new_name(mut name: String) -> Self {
name.make_ascii_lowercase();
Self::Name(name)
}
pub fn as_str(&self) -> &str {
match self {
Header::Name(header_name) => header_name.as_str(),
Header::RequestTarget => Self::REQUEST_TARGET_STR,
Header::Created => Self::CREATED_STR,
Header::Expires => Self::EXPIRES_STR,
}
}
}
impl fmt::Display for Header {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl From<&str> for Header {
fn from(s: &str) -> Self {
match s {
Self::REQUEST_TARGET_STR => Self::RequestTarget,
Self::CREATED_STR => Self::Created,
Self::EXPIRES_STR => Self::Expires,
_ => Self::new_name(s.to_owned()),
}
}
}
// === signature algorithm === //
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum HttpSigAlgorithm {
Known(SignatureAlgorithm),
Custom(String),
}
impl HttpSigAlgorithm {
pub fn as_known(&self) -> Option<SignatureAlgorithm> {
if let Self::Known(algo) = self {
Some(*algo)
} else {
None
}
}
pub fn is_known(&self) -> bool {
self.as_known().is_some()
}
pub fn as_custom(&self) -> Option<&str> {
if let Self::Custom(name) = self {
Some(name.as_str())
} else {
None
}
}
pub fn is_custom(&self) -> bool {
self.as_custom().is_some()
}
pub fn as_str(&self) -> &str {
match self {
Self::Known(algo) => to_http_sig_algo_str(*algo),
Self::Custom(name) => name,
}
}
}
// === http signature ===
/// Contains signature parameters.
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct HttpSignature {
/// An opaque string that the server can
/// use to look up the component they need to validate the signature.
pub key_id: String,
/// In original string format, `headers` should be a lowercased, quoted list of HTTP header
/// fields, separated by a single space character.
///
/// For instance : `(request-target) (created) host date cache-control x-emptyheader x-example`.
pub headers: Vec<Header>,
/// The `created` field expresses when the signature was
/// created. The value MUST be a Unix timestamp integer value. A
/// signature with a `created` timestamp value that is in the future MUST
/// NOT be processed.
pub created: Option<u64>,
/// The `expires` field expresses when the signature ceases to
/// be valid. The value MUST be a Unix timestamp integer value. A
/// signature with an `expires` timestamp value that is in the past MUST
/// NOT be processed.
pub expires: Option<u64>,
/// Base 64 encoded digital signature, as described in RFC4648, Section 4. The
/// client uses the `algorithm` and `headers` signature parameters to
/// form a canonicalized `signing string`. This `signing string` is then
/// signed with the key associated with `key_id` and the algorithm
/// corresponding to `algorithm`. The `signature` parameter is then set
/// to the base 64 encoding of the signature.
pub signature: String,
/// Used to specify the signature string construction mechanism.
/// Implementers SHOULD derive the digital signature algorithm used by an implementation from
/// the key metadata identified by the `keyId` rather than from this field. If `algorithm`
/// is provided and differs from the key metadata identified by the `keyId`, for example
/// `rsa-sha256` but an EdDSA key is identified via `keyId`, then an implementation
/// MUST produce an error.
/// Note: as of draft 12 there is only one signature string construction mechanism. As such
/// this parameter is only used to hint the digital signature algorithm.
pub algorithm: Option<HttpSigAlgorithm>,
legacy: bool,
}
impl HttpSignature {
pub fn verifier(&self) -> HttpSignatureVerifier<'_> {
HttpSignatureVerifier {
http_signature: self,
inner: Default::default(),
}
}
pub fn to_signing_string(&self) -> String {
let mut acc = Vec::with_capacity(5);
if self.legacy {
acc.push(format!(
"{} {}={}",
HTTP_SIGNATURE_HEADER, HTTP_SIGNATURE_KEY_ID, self.key_id
));
} else {
acc.push(format!(
"{} {}=\"{}\"",
HTTP_SIGNATURE_HEADER, HTTP_SIGNATURE_KEY_ID, self.key_id
));
match &self.algorithm {
Some(HttpSigAlgorithm::Custom(algorithm_name)) => {
acc.push(format!("{HTTP_SIGNATURE_ALGORITHM}=\"{algorithm_name}\""));
}
Some(HttpSigAlgorithm::Known(algorithm)) => {
acc.push(format!(
"{}=\"{}\"",
HTTP_SIGNATURE_ALGORITHM,
to_http_sig_algo_str(*algorithm)
));
}
None => {}
}
}
if let Some(created) = self.created {
acc.push(format!("{HTTP_SIGNATURE_CREATED}={created}"));
}
if let Some(expires) = self.expires {
acc.push(format!("{HTTP_SIGNATURE_EXPIRES}={expires}"));
}
if self.legacy {
acc.push(format!(
"{}={}",
HTTP_SIGNATURE_HEADERS,
self.headers
.iter()
.map(|header| header.as_str())
.collect::<Vec<&str>>()
.join(" "),
));
acc.push(format!("{}={}", HTTP_SIGNATURE_SIGNATURE, self.signature));
} else {
acc.push(format!(
"{}=\"{}\"",
HTTP_SIGNATURE_HEADERS,
self.headers
.iter()
.map(|header| header.as_str())
.collect::<Vec<&str>>()
.join(" "),
));
acc.push(format!("{}=\"{}\"", HTTP_SIGNATURE_SIGNATURE, self.signature));
}
acc.join(",")
}
}
const HTTP_SIGNATURE_HEADER: &str = "Signature";
const HTTP_SIGNATURE_KEY_ID: &str = "keyId";
const HTTP_SIGNATURE_SIGNATURE: &str = "signature";
const HTTP_SIGNATURE_CREATED: &str = "created";
const HTTP_SIGNATURE_EXPIRES: &str = "expires";
const HTTP_SIGNATURE_HEADERS: &str = "headers";
const HTTP_SIGNATURE_ALGORITHM: &str = "algorithm";
impl fmt::Display for HttpSignature {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_signing_string())
}
}
impl FromStr for HttpSignature {
type Err = HttpSignatureError;
fn from_str(http_authorization_header: &str) -> Result<Self, Self::Err> {
let items = http_authorization_header
.trim_start_matches(HTTP_SIGNATURE_HEADER)
.split(',')
.collect::<Vec<&str>>();
let mut keys = HashMap::new();
for item in items {
if let Some(index) = item.find('=') {
let (key, value) = item.split_at(index);
let value = value[1..].trim().trim_matches('"');
keys.insert(key.trim(), value.trim().to_owned());
}
}
let headers = {
if let Some(headers_str) = keys.remove(HTTP_SIGNATURE_HEADERS) {
let headers_str_vec = headers_str.split(' ').collect::<Vec<&str>>();
let mut headers = Vec::with_capacity(headers_str_vec.len());
for header_str in headers_str_vec {
headers.push(Header::from(header_str));
}
headers
} else {
vec![]
}
};
let created = if let Some(created) = keys.remove(HTTP_SIGNATURE_CREATED) {
Some(
created
.parse::<u64>()
.map_err(|_| HttpSignatureError::InvalidParameter {
parameter: HTTP_SIGNATURE_CREATED,
})?,
)
} else {
None
};
let expires = if let Some(created) = keys.remove(HTTP_SIGNATURE_EXPIRES) {
Some(
created
.parse::<u64>()
.map_err(|_| HttpSignatureError::InvalidParameter {
parameter: HTTP_SIGNATURE_EXPIRES,
})?,
)
} else {
None
};
let algorithm = keys.remove(HTTP_SIGNATURE_ALGORITHM).map(|val| {
if let Some(algo) = from_http_sig_algo_str(&val) {
HttpSigAlgorithm::Known(algo)
} else {
HttpSigAlgorithm::Custom(val)
}
});
let signature = keys
.remove(HTTP_SIGNATURE_SIGNATURE)
.ok_or(HttpSignatureError::MissingRequiredParameter {
parameter: HTTP_SIGNATURE_SIGNATURE,
})?;
let legacy = !signature.contains(['/', '+']);
Ok(HttpSignature {
key_id: keys
.remove(HTTP_SIGNATURE_KEY_ID)
.ok_or(HttpSignatureError::MissingRequiredParameter {
parameter: HTTP_SIGNATURE_KEY_ID,
})?,
headers,
created,
expires,
signature,
algorithm,
legacy,
})
}
}
// === http signature builder === //
macro_rules! builder_argument_missing_err {
($field:ident) => {{
const _: fn() = || {
let HttpSignatureBuilderInner { $field: _, .. };
};
HttpSignatureError::MissingBuilderArgument {
arg: stringify!($field),
}
}};
}
#[derive(Clone)]
enum SigningStringGenMethod<'a> {
PreGenerated(&'a str),
FromHttpRequest(&'a dyn HttpRequest),
}
impl Debug for SigningStringGenMethod<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "SigningStringGenMethod::")?;
match self {
SigningStringGenMethod::PreGenerated(signing_string) => write!(f, "PreGenerated({signing_string})"),
SigningStringGenMethod::FromHttpRequest(_) => write!(f, "FromHttpRequest(...)"),
}
}
}
#[derive(Default, Clone, Debug)]
struct HttpSignatureBuilderInner<'a> {
key_id: Option<String>,
signature_method: Option<(&'a PrivateKey, SignatureAlgorithm)>,
created: Option<u64>,
expires: Option<u64>,
headers: Vec<Header>,
signing_string_generation: Option<SigningStringGenMethod<'a>>,
legacy: bool,
}
#[derive(Default, Clone, Debug)]
/// Utility to generate `HttpSignature`s
pub struct HttpSignatureBuilder<'a> {
inner: RefCell<HttpSignatureBuilderInner<'a>>,
}
impl<'a> HttpSignatureBuilder<'a> {
pub fn new() -> Self {
Self::default()
}
#[inline]
/// Required
pub fn key_id<S: Into<String>>(&self, key_id: S) -> &Self {
self.inner.borrow_mut().key_id = Some(key_id.into());
self
}
#[inline]
/// Required
pub fn signature_method(&self, private_key: &'a PrivateKey, signature_type: SignatureAlgorithm) -> &Self {
self.inner.borrow_mut().signature_method = Some((private_key, signature_type));
self
}
#[inline]
/// If generating signing string, at least one of `created`, `expires`, `request_target`
/// or `http_header` is required otherwise DO NOT provide.
pub fn created(&self, unix_timestamp: u64) -> &Self {
let mut inner_mut = self.inner.borrow_mut();
inner_mut.created = Some(unix_timestamp);
inner_mut.headers.push(Header::Created);
drop(inner_mut);
self
}
#[inline]
/// If generating signing string, at least one of `created`, `expires`, `request_target`
/// or `http_header` is required otherwise DO NOT provide.
pub fn expires(&self, unix_timestamp: u64) -> &Self {
let mut inner_mut = self.inner.borrow_mut();
inner_mut.expires = Some(unix_timestamp);
inner_mut.headers.push(Header::Expires);
drop(inner_mut);
self
}
#[inline]
/// If generating signing string, at least one of `created`, `expires`, `request_target`
/// or `http_header` is required otherwise DO NOT provide.
pub fn request_target(&self) -> &Self {
self.inner.borrow_mut().headers.push(Header::RequestTarget);
self
}
#[inline]
/// If generating signing string, at least one of `created`, `expires`, `request_target`
/// or `http_header` is required otherwise DO NOT provide.
pub fn http_header<S: Into<String>>(&self, header: S) -> &Self {
self.inner.borrow_mut().headers.push(Header::new_name(header.into()));
self
}
#[inline]
/// Required (alternative: `pre_generated_signing_string`).
pub fn generate_signing_string_using_http_request(&self, http_request: &'a dyn HttpRequest) -> &Self {
self.inner.borrow_mut().signing_string_generation = Some(SigningStringGenMethod::FromHttpRequest(http_request));
self
}
#[inline]
/// Required (alternative: `generate_signing_string_using_http_request`).
pub fn pre_generated_signing_string(&self, signing_string: &'a str) -> &Self {
self.inner.borrow_mut().signing_string_generation = Some(SigningStringGenMethod::PreGenerated(signing_string));
self
}
#[inline]
#[doc(hidden)]
pub fn legacy(&self) -> &Self {
self.inner.borrow_mut().legacy = true;
self
}
pub fn build(&self) -> Result<HttpSignature, HttpSignatureError> {
let mut inner = self.inner.borrow_mut();
let (private_key, signature_type) = {
inner
.signature_method
.take()
.ok_or(builder_argument_missing_err!(signature_method))?
};
let key_id = inner.key_id.take().ok_or(builder_argument_missing_err!(key_id))?;
let signing_string_generation = inner
.signing_string_generation
.take()
.ok_or(builder_argument_missing_err!(signing_string_generation))?;
let mut created = inner.created.take();
let mut expires = inner.expires.take();
let mut headers: Vec<Header> = inner.headers.drain(..).collect();
let legacy = inner.legacy;
drop(inner);
let signature_binary =
match signing_string_generation {
SigningStringGenMethod::PreGenerated(signing_string) => {
if !headers.is_empty() {
return Err(HttpSignatureError::BuilderHeadersProvidedWithPreGenerated);
}
// parse pre-generated signing string to fill our HttpSignature struct properly.
for line in signing_string.lines() {
let mut split = line.split(':');
let key = split.next().expect("there is always at least one element in the split");
if let Some(value) = split.next() {
match key {
Header::REQUEST_TARGET_STR => {
headers.push(Header::RequestTarget);
}
Header::CREATED_STR => {
headers.push(Header::Created);
created = Some(value.trim().parse().map_err(|_| {
HttpSignatureError::InvalidSigningString { line: line.to_owned() }
})?);
}
Header::EXPIRES_STR => {
headers.push(Header::Expires);
expires = Some(value.trim().parse().map_err(|_| {
HttpSignatureError::InvalidSigningString { line: line.to_owned() }
})?);
}
header_name => headers.push(Header::new_name(header_name.to_owned())),
}
} else {
return Err(HttpSignatureError::InvalidSigningString { line: line.to_owned() });
}
}
signature_type.sign(signing_string.as_bytes(), private_key)?
}
SigningStringGenMethod::FromHttpRequest(http_request) => {
// Generate signing string.
// See https://tools.ietf.org/html/draft-cavage-http-signatures-12#section-2.3
if headers.is_empty() {
return Err(HttpSignatureError::BuilderEmptyHeaders);
}
let mut acc = Vec::with_capacity(headers.len());
for header in &headers {
match header {
Header::Name(header_name) => {
let concatenated_values = http_request.get_header_concatenated_values(header_name)?;
if concatenated_values.is_empty() {
acc.push(format!("{}:", header_name.as_str()));
} else {
acc.push(format!("{}: {}", header_name.as_str(), concatenated_values));
}
}
Header::RequestTarget => {
acc.push(format!(
"{}: {} {}",
header.as_str(),
http_request.get_lowercased_method()?,
http_request.get_target()?
));
}
Header::Created => acc.push(format!(
"{}: {}",
header.as_str(),
created.expect("Some by builder construction")
)),
Header::Expires => acc.push(format!(
"{}: {}",
header.as_str(),
expires.expect("Some by builder construction")
)),
}
}
let signing_string = acc.join("\n");
signature_type.sign(signing_string.as_bytes(), private_key)?
}
};
Ok(HttpSignature {
key_id,
headers,
created,
expires,
signature: if legacy {
general_purpose::URL_SAFE_NO_PAD.encode(&signature_binary)
} else {
general_purpose::STANDARD.encode(&signature_binary)
},
algorithm: Some(HttpSigAlgorithm::Known(signature_type)),
legacy,
})
}
}
// === http signature verifier === //
macro_rules! verifier_argument_missing_err {
($field:ident) => {{
const _: fn() = || {
let HttpSignatureVerifierInner { $field: _, .. };
};
HttpSignatureError::MissingBuilderArgument {
arg: stringify!($field),
}
}};
}
#[derive(Default, Clone, Debug)]
struct HttpSignatureVerifierInner<'a> {
now: Option<u64>,
leeway: u64,
signature_method: Option<(&'a PublicKey, SignatureAlgorithm)>,
signing_string_generation: Option<SigningStringGenMethod<'a>>,
}
#[derive(Clone, Debug)]
/// Utility to verify `HttpSignature`s
pub struct HttpSignatureVerifier<'a> {
http_signature: &'a HttpSignature,
inner: RefCell<HttpSignatureVerifierInner<'a>>,
}
impl<'a> HttpSignatureVerifier<'a> {
#[inline]
/// Optional. Required only if http signature contains (expires) or (created) parameters.
pub fn now(&self, unix_timestamp: u64) -> &Self {
self.inner.borrow_mut().now = Some(unix_timestamp);
self
}
#[inline]
/// Optional. Add leeway to check expiration and creation times.
pub fn leeway(&self, leeway: u64) -> &Self {
self.inner.borrow_mut().leeway = leeway;
self
}
#[inline]
/// Required
pub fn signature_method(&self, public_key: &'a PublicKey, signature_type: SignatureAlgorithm) -> &Self {
self.inner.borrow_mut().signature_method = Some((public_key, signature_type));
self
}
#[inline]
/// Required (alternative: `pre_generated_signing_string`).
pub fn generate_signing_string_using_http_request(&self, http_request: &'a dyn HttpRequest) -> &Self {
self.inner.borrow_mut().signing_string_generation = Some(SigningStringGenMethod::FromHttpRequest(http_request));
self
}
#[inline]
/// Required (alternative: `generate_signing_string_using_http_request`).
pub fn pre_generated_signing_string(&self, signing_string: &'a str) -> &Self {
self.inner.borrow_mut().signing_string_generation = Some(SigningStringGenMethod::PreGenerated(signing_string));
self
}
pub fn verify(&self) -> Result<(), HttpSignatureError> {
let mut inner = self.inner.borrow_mut();
let (public_key, signature_type) = {
inner
.signature_method
.take()
.ok_or(verifier_argument_missing_err!(signature_method))?
};
// Sanity checks based on optional http signature parameter "algorithm"
if let Some(HttpSigAlgorithm::Known(http_sig_algo)) = self.http_signature.algorithm {
if http_sig_algo != signature_type || !is_algo_compatible_with_key(http_sig_algo, public_key) {
return Err(HttpSignatureError::IncompatibleAlgorithm { value: http_sig_algo });
}
}
let signing_string_generation = inner
.signing_string_generation
.take()
.ok_or(verifier_argument_missing_err!(signing_string_generation))?;
if let Some(expires) = self.http_signature.expires {
let now = inner.now.ok_or(verifier_argument_missing_err!(now))?;
if now - inner.leeway > expires {
return Err(HttpSignatureError::Expired {
not_after: expires,
now,
});
}
}
if let Some(created) = self.http_signature.created {
let now = inner.now.ok_or(verifier_argument_missing_err!(now))?;
if now + inner.leeway < created {
return Err(HttpSignatureError::NotYetValid { created, now });
}
}
drop(inner);
let signing_string = match signing_string_generation {
SigningStringGenMethod::PreGenerated(signing_string) => Cow::Borrowed(signing_string),
SigningStringGenMethod::FromHttpRequest(http_request) => {
let headers = if self.http_signature.headers.is_empty() {
&[Header::Created][..]
} else {
self.http_signature.headers.as_slice()
};
let mut acc = Vec::with_capacity(headers.len());
for header in headers {
match header {
Header::Name(header_name) => {
let concatenated_values = http_request.get_header_concatenated_values(header_name)?;
if concatenated_values.is_empty() {
acc.push(format!("{}:", header_name.as_str()));
} else {
acc.push(format!("{}: {}", header_name.as_str(), concatenated_values));
}
}
Header::RequestTarget => {
acc.push(format!(
"{}: {} {}",
header.as_str(),
http_request.get_lowercased_method()?,
http_request.get_target()?
));
}
Header::Created => acc.push(format!(
"{}: {}",
header.as_str(),
self.http_signature
.created
.ok_or(HttpSignatureError::MissingRequiredParameter {
parameter: HTTP_SIGNATURE_CREATED,
})?
)),
Header::Expires => acc.push(format!(
"{}: {}",
header.as_str(),
self.http_signature
.expires
.ok_or(HttpSignatureError::MissingRequiredParameter {
parameter: HTTP_SIGNATURE_EXPIRES,
})?
)),
}
}
Cow::Owned(acc.join("\n"))
}
};
let decoded_signature = if self.http_signature.legacy {
general_purpose::URL_SAFE_NO_PAD.decode(&self.http_signature.signature)?
} else {
general_purpose::STANDARD.decode(&self.http_signature.signature)?
};
signature_type.verify(public_key, signing_string.as_bytes(), &decoded_signature)?;
Ok(())
}
}
// === http signature algorithms === //
const HTTP_SIG_ALGO_RSA_MD5: &str = "rsa-md5";
const HTTP_SIG_ALGO_RSA_SHA_1: &str = "rsa-sha1";
const HTTP_SIG_ALGO_RSA_SHA_224: &str = "rsa-sha224";
const HTTP_SIG_ALGO_RSA_SHA_256: &str = "rsa-sha256";
const HTTP_SIG_ALGO_RSA_SHA_384: &str = "rsa-sha384";
const HTTP_SIG_ALGO_RSA_SHA_512: &str = "rsa-sha512";
const HTTP_SIG_ALGO_RSA_SHA2_224: &str = "rsa-sha2-224";
const HTTP_SIG_ALGO_RSA_SHA2_256: &str = "rsa-sha2-256";
const HTTP_SIG_ALGO_RSA_SHA2_384: &str = "rsa-sha2-384";
const HTTP_SIG_ALGO_RSA_SHA2_512: &str = "rsa-sha2-512";
const HTTP_SIG_ALGO_RSA_SHA3_384: &str = "rsa-sha3-384";
const HTTP_SIG_ALGO_RSA_SHA3_512: &str = "rsa-sha3-512";
const HTTP_SIG_ALGO_ECDSA_SHA_256: &str = "ecdsa-sha256";
const HTTP_SIG_ALGO_ECDSA_SHA_384: &str = "ecdsa-sha384";
const HTTP_SIG_ALGO_ED25519_SHA512: &str = "ed25519-sha512";
fn to_http_sig_algo_str(algo: SignatureAlgorithm) -> &'static str {
match algo {
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::MD5) => HTTP_SIG_ALGO_RSA_MD5,
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA1) => HTTP_SIG_ALGO_RSA_SHA_1,
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_224) => HTTP_SIG_ALGO_RSA_SHA_224,
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256) => HTTP_SIG_ALGO_RSA_SHA_256,
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_384) => HTTP_SIG_ALGO_RSA_SHA_384,
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_512) => HTTP_SIG_ALGO_RSA_SHA_512,
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA3_384) => HTTP_SIG_ALGO_RSA_SHA3_384,
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA3_512) => HTTP_SIG_ALGO_RSA_SHA3_512,
SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_256) => HTTP_SIG_ALGO_ECDSA_SHA_256,
SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_384) => HTTP_SIG_ALGO_ECDSA_SHA_384,
SignatureAlgorithm::Ed25519 => HTTP_SIG_ALGO_ED25519_SHA512,
SignatureAlgorithm::Ecdsa(_) => "ECDSA unsupported algorithm",
}
}
fn from_http_sig_algo_str(s: &str) -> Option<SignatureAlgorithm> {
match s {
HTTP_SIG_ALGO_RSA_MD5 => Some(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::MD5)),
HTTP_SIG_ALGO_RSA_SHA_1 => Some(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA1)),
HTTP_SIG_ALGO_RSA_SHA_224 | HTTP_SIG_ALGO_RSA_SHA2_224 => {
Some(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_224))
}
HTTP_SIG_ALGO_RSA_SHA_256 | HTTP_SIG_ALGO_RSA_SHA2_256 => {
Some(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256))
}
HTTP_SIG_ALGO_RSA_SHA_384 | HTTP_SIG_ALGO_RSA_SHA2_384 => {
Some(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_384))
}
HTTP_SIG_ALGO_RSA_SHA_512 | HTTP_SIG_ALGO_RSA_SHA2_512 => {
Some(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_512))
}
HTTP_SIG_ALGO_RSA_SHA3_384 => Some(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA3_384)),
HTTP_SIG_ALGO_RSA_SHA3_512 => Some(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA3_512)),
HTTP_SIG_ALGO_ECDSA_SHA_256 => Some(SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_256)),
HTTP_SIG_ALGO_ECDSA_SHA_384 => Some(SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_384)),
HTTP_SIG_ALGO_ED25519_SHA512 => Some(SignatureAlgorithm::Ed25519),
_ => None,
}
}
fn is_algo_compatible_with_key(algo: SignatureAlgorithm, key: &PublicKey) -> bool {
use picky_asn1_x509::oids::*;
let key_algo = Into::<String>::into(key.as_inner().algorithm.oid());
match algo {
// Currently, SignatureHashType only contains RSA methods, so this is a an auto-win
_ if key_algo == RSA_ENCRYPTION => true,
// Otherwise we need to check for specific hash algorithm
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA1) if key_algo == SHA1_WITH_RSA_ENCRYPTION => true,
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_224) if key_algo == SHA224_WITH_RSA_ENCRYPTION => true,
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256) if key_algo == SHA256_WITH_RSA_ENCRYPTION => true,
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_384) if key_algo == SHA384_WITH_RSA_ENCRYPTION => true,
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_512) if key_algo == SHA512_WITH_RSA_ENCRYPTION => true,
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA3_384) if key_algo == ID_RSASSA_PKCS1_V1_5_WITH_SHA3_384 => {
true
}
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA3_512) if key_algo == ID_RSASSA_PKCS1_V1_5_WITH_SHA3_512 => {
true
}
SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_256) if key_algo == ECDSA_WITH_SHA256 => true,
SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_384) if key_algo == ECDSA_WITH_SHA384 => true,
SignatureAlgorithm::Ed25519 if key_algo == ED25519 => true,
// Key metadata is incompatible with this algorithm
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pem::Pem;
use http::method::Method;
use http::{header, request};
use picky_asn1_x509::{AlgorithmIdentifier, SubjectPublicKeyInfo};
const HTTP_SIGNATURE_EXAMPLE: &str = "Signature keyId=\"my-rsa-key\",algorithm=\"rsa-sha256\"\
,created=1402170695,headers=\"(request-target) (created) date\",\
signature=\"bw579lDtTDsp7zif/F7Fy93KXrM6qUfCb43JMJtiL4+3nazIPlxcxVsRJEgZzK/QQPDoeUQ\
p4BYCzi2CbthYhHJMn/Wv008gNMcQQTuEw/KcnMrFWxqqUnVZQbCQvNai2y80WrBiOFZvN2VIdLUSO4SoIa\
OHvrvEoQhl3sqpv1z7yCVbQtJHwnPOWoy/11p+SU3X2ARJXN555q5wSn+DykM0Ohq1cXD84MHXP5ulI0Fa8\
4zQ5waxoXsieex4FI+zXSlngGmchBPXMUC437u2wXA1zLA4KGUL/uNScL1MKrTMqgV0MK4o6sR0LHOqHmIi\
MJ7h++UmOW/0Iw74CL2UGQ==\"";
const HTTP_SIGNATURE_WEIRD_FORMAT: &str = "Signature keyId = my-rsa-key ,created= \"1402170695\",\
,algorithm =\"rsa-sha256 \",headers=(request-target) (created) date ,\
signature=bw579lDtTDsp7zif/F7Fy93KXrM6qUfCb43JMJtiL4+3nazIPlxcxVsRJEgZzK/QQPDoeUQ\
p4BYCzi2CbthYhHJMn/Wv008gNMcQQTuEw/KcnMrFWxqqUnVZQbCQvNai2y80WrBiOFZvN2VIdLUSO4SoIa\
OHvrvEoQhl3sqpv1z7yCVbQtJHwnPOWoy/11p+SU3X2ARJXN555q5wSn+DykM0Ohq1cXD84MHXP5ulI0Fa8\
4zQ5waxoXsieex4FI+zXSlngGmchBPXMUC437u2wXA1zLA4KGUL/uNScL1MKrTMqgV0MK4o6sR0LHOqHmIi\
MJ7h++UmOW/0Iw74CL2UGQ==";
fn private_key_1() -> PrivateKey {
let pem = picky_test_data::RSA_2048_PK_7.parse::<Pem>().expect("pem 1");
PrivateKey::from_pem(&pem).expect("private key 1")
}
fn private_key_2() -> PrivateKey {
let pem = picky_test_data::RSA_2048_PK_1.parse::<Pem>().expect("pem 2");
PrivateKey::from_pem(&pem).expect("private key 2")
}
#[test]
fn sign() {
let private_key = private_key_1();
let http_signature_builder = HttpSignatureBuilder::new();
http_signature_builder
.key_id("my-rsa-key")
.signature_method(&private_key, SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256))
.request_target()
.created(1402170695)
.http_header("Date");
let req = request::Builder::new()
.method(Method::GET)
.uri("/foo")
.header(header::DATE, "Tue, 07 Jun 2014 20:51:35 GMT")
.header(header::CACHE_CONTROL, "max-age=60") // unused for signature
.header(header::CACHE_CONTROL, "must-revalidate") // unused for signature
.body(())
.expect("couldn't build request");
let (parts, _) = req.into_parts();
let http_signature = http_signature_builder
.clone()
.generate_signing_string_using_http_request(&parts)
.build()
.expect("couldn't generate http signature");
let http_signature_str = http_signature.to_signing_string();
pretty_assertions::assert_eq!(http_signature_str, HTTP_SIGNATURE_EXAMPLE);
// changing unused headers should not change signature
let req_2 = request::Builder::new()
.method(Method::GET)
.uri("/foo")
.header(header::DATE, "Tue, 07 Jun 2014 20:51:35 GMT")
.header(header::CACHE_CONTROL, "max-age=222") // unused for signature
.body(())
.expect("couldn't build request");
let (parts_2, _) = req_2.into_parts();
let http_signature_2 = http_signature_builder
.generate_signing_string_using_http_request(&parts_2)
.build()
.expect("couldn't generate http signature 2");
let http_signature_str_2 = http_signature_2.to_signing_string();
pretty_assertions::assert_eq!(http_signature_str_2, http_signature_str);
}
#[test]
fn verify() {
let req = request::Builder::new()
.method(Method::GET)
.uri("/foo")
.header(header::DATE, "Tue, 07 Jun 2014 20:51:35 GMT")
.header("something-else", "owowo") // unused for signature
.body(())
.expect("couldn't build request");
let (parts, _) = req.into_parts();
for http_signature in &[
HttpSignature::from_str(HTTP_SIGNATURE_EXAMPLE).expect("http signature example"),
HttpSignature::from_str(HTTP_SIGNATURE_WEIRD_FORMAT).expect("http signature weird format"),
] {
assert!(!http_signature.legacy);
http_signature
.verifier()
.now(1402170700)
.signature_method(
&private_key_1().to_public_key().unwrap(),
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256),
)
.generate_signing_string_using_http_request(&parts)
.verify()
.expect("couldn't verify");
}
}
#[test]
fn invalid_signature_err() {
let req = request::Builder::new()
.method(Method::GET)
.uri("/foo")
.header(header::DATE, "Tue, 07 Jun 2014 20:51:35 GMT")
.body(())
.expect("couldn't build request");
let (parts, _) = req.into_parts();
let http_signature = HttpSignatureBuilder::new()
.key_id("my-rsa-key")
.signature_method(
&private_key_1(),
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256),
)
.request_target()
.created(1402170695)
.expires(1402170705)
.http_header("Date")
.generate_signing_string_using_http_request(&parts)
.build()
.expect("couldn't generate http signature");
let err = http_signature
.verifier()
.now(1402170700)
.signature_method(
&private_key_2().to_public_key().unwrap(),
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256),
)
.generate_signing_string_using_http_request(&parts)
.verify()
.expect_err("verify");
assert_eq!(err.to_string(), "signature error: invalid signature");
let err = http_signature
.verifier()
.now(1402170700)
.signature_method(
&private_key_1().to_public_key().unwrap(),
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA1),
)
.generate_signing_string_using_http_request(&parts)
.verify()
.expect_err("verify");
assert_eq!(
err.to_string(),
"incompatible \'algorithm\' parameter: RsaPkcs1v15(SHA2_256)"
);
let err = http_signature
.verifier()
.now(1402170710)
.signature_method(
&private_key_1().to_public_key().unwrap(),
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256),
)
.generate_signing_string_using_http_request(&parts)
.verify()
.expect_err("verify");
assert_eq!(
err.to_string(),
"signature expired (not after: 1402170705, now: 1402170710)"
);
let err = http_signature
.verifier()
.now(1402170600)
.signature_method(
&private_key_1().to_public_key().unwrap(),
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256),
)
.generate_signing_string_using_http_request(&parts)
.verify()
.expect_err("verify");
assert_eq!(
err.to_string(),
"signature is not yet valid (created: 1402170695, now: 1402170600)"
);
let req_2 = request::Builder::new()
.method(Method::GET)
.uri("/foo")
.header(header::DATE, "Tue, 08 Jun 2014 20:51:35 GMT")
.body(())
.expect("couldn't build request");
let (parts_2, _) = req_2.into_parts();
let err = http_signature
.verifier()
.now(1402170700)
.signature_method(
&private_key_1().to_public_key().unwrap(),
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256),
)
.generate_signing_string_using_http_request(&parts_2)
.verify()
.expect_err("verify");
assert_eq!(err.to_string(), "signature error: invalid signature");
let mut invalid_algorithm_http_sig = http_signature;
invalid_algorithm_http_sig.algorithm = None;
let err = invalid_algorithm_http_sig
.verifier()
.now(1402170700)
.signature_method(
&private_key_1().to_public_key().unwrap(),
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA1),
)
.generate_signing_string_using_http_request(&parts)
.verify()
.expect_err("verify");
assert_eq!(err.to_string(), "signature error: invalid signature");
}
#[test]
fn sign_with_pre_generated_signing_string() {
let signing_string = "(request-target): get /foo\n(created): 1402170695\ndate: Tue, 07 Jun 2014 20:51:35 GMT";
let http_signature = HttpSignatureBuilder::new()
.key_id("my-rsa-key")
.signature_method(
&private_key_1(),
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256),
)
.pre_generated_signing_string(signing_string)
.build()
.expect("couldn't generate http signature using pre-generated signing string");
let http_signature_str = http_signature.to_signing_string();
assert_eq!(http_signature_str, HTTP_SIGNATURE_EXAMPLE);
}
#[test]
fn verify_with_pre_generated_signing_string() {
let signing_string = "(request-target): get /foo\n(created): 1402170695\ndate: Tue, 07 Jun 2014 20:51:35 GMT";
let http_signature = HttpSignature::from_str(HTTP_SIGNATURE_EXAMPLE).expect("http signature");
http_signature
.verifier()
.now(1402170700)
.signature_method(
&private_key_1().to_public_key().unwrap(),
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256),
)
.pre_generated_signing_string(signing_string)
.verify()
.expect("couldn't verify");
}
#[test]
fn verify_with_leeway() {
let signing_string = "(request-target): get /foo\n(created): 1402170695\ndate: Tue, 07 Jun 2014 20:51:35 GMT";
let http_signature = HttpSignature::from_str(HTTP_SIGNATURE_EXAMPLE).expect("http signature");
http_signature
.verifier()
.now(1402170690)
.leeway(10)
.signature_method(
&private_key_1().to_public_key().unwrap(),
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256),
)
.pre_generated_signing_string(signing_string)
.verify()
.expect("couldn't verify");
}
fn parse_err(http_signature: &str) -> String {
http_signature
.parse::<HttpSignature>()
.expect_err("no parse error")
.to_string()
}
#[test]
fn http_signature_parse_err() {
pretty_assertions::assert_eq!(
parse_err("Signature signature=\"some sig\""),
"required parameter is missing from http signature string: keyId"
);
pretty_assertions::assert_eq!(
parse_err(
"Signature keyId=\"my-rsa-key\", created=\"HQHQHQ\", \
signature=\"some sig\""
),
"invalid parameter: created"
);
}
const HTTP_SIGNATURE_LEGACY: &str = "Signature keyId=my-rsa-key,created=1402170695,\
headers=(request-target) (created) date,\
signature=bw579lDtTDsp7zif_F7Fy93KXrM6qUfCb43JMJtiL4-3nazIPlxcxVsRJEgZzK_QQPDoeUQp4B\
YCzi2CbthYhHJMn_Wv008gNMcQQTuEw_KcnMrFWxqqUnVZQbCQvNai2y80WrBiOFZvN2VIdLUSO4SoIaOHvr\
vEoQhl3sqpv1z7yCVbQtJHwnPOWoy_11p-SU3X2ARJXN555q5wSn-DykM0Ohq1cXD84MHXP5ulI0Fa84zQ5w\
axoXsieex4FI-zXSlngGmchBPXMUC437u2wXA1zLA4KGUL_uNScL1MKrTMqgV0MK4o6sR0LHOqHmIiMJ7h--\
UmOW_0Iw74CL2UGQ";
#[test]
fn legacy() {
let req = request::Builder::new()
.method(Method::GET)
.uri("/foo")
.header(header::DATE, "Tue, 07 Jun 2014 20:51:35 GMT")
.body(())
.expect("couldn't build request");
let (parts, _) = req.into_parts();
{
// sign
let private_key = private_key_1();
let http_signature = HttpSignatureBuilder::new()
.key_id("my-rsa-key")
.signature_method(&private_key, SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256))
.request_target()
.created(1402170695)
.generate_signing_string_using_http_request(&parts)
.http_header("Date")
.legacy()
.build()
.expect("build http signature");
pretty_assertions::assert_eq!(http_signature.to_signing_string(), HTTP_SIGNATURE_LEGACY);
}
{
// verify
let http_signature = HttpSignature::from_str(HTTP_SIGNATURE_LEGACY).expect("http signature legacy");
http_signature
.verifier()
.now(1402170700)
.signature_method(
&private_key_1().to_public_key().unwrap(),
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256),
)
.generate_signing_string_using_http_request(&parts)
.verify()
.expect("couldn't verify");
}
}
#[test]
fn incompatible_algorithm_err() {
let req = request::Builder::new()
.method(Method::GET)
.uri("/foo")
.header(header::DATE, "Tue, 07 Jun 2014 20:51:35 GMT")
.body(())
.expect("couldn't build request");
let (parts, _) = req.into_parts();
let private_key = private_key_1();
let http_signature = HttpSignatureBuilder::new()
.key_id("my-rsa-key")
.signature_method(&private_key, SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_384))
.request_target()
.created(1402170695)
.generate_signing_string_using_http_request(&parts)
.http_header("Date")
.build()
.expect("build http signature");
let mut spki = SubjectPublicKeyInfo::from(private_key_1().to_public_key().unwrap());
spki.algorithm = AlgorithmIdentifier::new_sha512_with_rsa_encryption();
let sha512_only_key = PublicKey::from(spki);
let err = http_signature
.verifier()
.now(1402170700)
.signature_method(
&sha512_only_key,
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_384),
)
.generate_signing_string_using_http_request(&parts)
.verify()
.expect_err("verify");
assert_eq!(
err.to_string(),
"incompatible 'algorithm' parameter: RsaPkcs1v15(SHA2_384)"
);
}
const HTTP_SIGNATURE_UNKNOWN_ALGO: &str = "Signature keyId=\"my-rsa-key\",algorithm=\"magical-algo\",\
headers=\"(request-target)\",signature=\"GARBAGE\"";
#[test]
fn unknown_algorithms_are_ignored() {
let http_signature = HttpSignature::from_str(HTTP_SIGNATURE_UNKNOWN_ALGO).expect("from str");
assert_eq!(http_signature.algorithm.unwrap().as_str(), "magical-algo");
}
}