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.
1314 lines
49 KiB
Rust
1314 lines
49 KiB
Rust
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");
|
||
}
|
||
}
|