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.
This commit is contained in:
Vendored
+101
@@ -0,0 +1,101 @@
|
||||
//! Hash algorithms supported by picky
|
||||
|
||||
use digest::Digest;
|
||||
use picky_asn1_x509::ShaVariant;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
|
||||
/// unsupported algorithm
|
||||
#[derive(Debug)]
|
||||
pub struct UnsupportedHashAlgorithmError {
|
||||
pub algorithm: String,
|
||||
}
|
||||
|
||||
impl fmt::Display for UnsupportedHashAlgorithmError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "unsupported algorithm: {}", self.algorithm)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for UnsupportedHashAlgorithmError {}
|
||||
|
||||
/// Supported hash algorithms
|
||||
#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[non_exhaustive]
|
||||
pub enum HashAlgorithm {
|
||||
MD5,
|
||||
SHA1,
|
||||
SHA2_224,
|
||||
SHA2_256,
|
||||
SHA2_384,
|
||||
SHA2_512,
|
||||
SHA3_384,
|
||||
SHA3_512,
|
||||
}
|
||||
|
||||
impl TryFrom<HashAlgorithm> for ShaVariant {
|
||||
type Error = UnsupportedHashAlgorithmError;
|
||||
|
||||
fn try_from(v: HashAlgorithm) -> Result<ShaVariant, UnsupportedHashAlgorithmError> {
|
||||
match v {
|
||||
HashAlgorithm::MD5 => Ok(ShaVariant::MD5),
|
||||
HashAlgorithm::SHA1 => Ok(ShaVariant::SHA1),
|
||||
HashAlgorithm::SHA2_256 => Ok(ShaVariant::SHA2_256),
|
||||
HashAlgorithm::SHA2_384 => Ok(ShaVariant::SHA2_384),
|
||||
HashAlgorithm::SHA2_512 => Ok(ShaVariant::SHA2_512),
|
||||
HashAlgorithm::SHA3_384 => Ok(ShaVariant::SHA3_384),
|
||||
HashAlgorithm::SHA3_512 => Ok(ShaVariant::SHA3_512),
|
||||
_ => Err(UnsupportedHashAlgorithmError {
|
||||
algorithm: format!("{v:?}"),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<ShaVariant> for HashAlgorithm {
|
||||
type Error = UnsupportedHashAlgorithmError;
|
||||
|
||||
fn try_from(v: ShaVariant) -> Result<HashAlgorithm, UnsupportedHashAlgorithmError> {
|
||||
match v {
|
||||
ShaVariant::MD5 => Ok(HashAlgorithm::MD5),
|
||||
ShaVariant::SHA1 => Ok(HashAlgorithm::SHA1),
|
||||
ShaVariant::SHA2_256 => Ok(HashAlgorithm::SHA2_256),
|
||||
ShaVariant::SHA2_384 => Ok(HashAlgorithm::SHA2_384),
|
||||
ShaVariant::SHA2_512 => Ok(HashAlgorithm::SHA2_512),
|
||||
ShaVariant::SHA3_384 => Ok(HashAlgorithm::SHA3_384),
|
||||
ShaVariant::SHA3_512 => Ok(HashAlgorithm::SHA3_512),
|
||||
_ => Err(UnsupportedHashAlgorithmError {
|
||||
algorithm: format!("{v:?}"),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HashAlgorithm {
|
||||
pub fn digest(self, msg: &[u8]) -> Vec<u8> {
|
||||
match self {
|
||||
Self::MD5 => md5::Md5::digest(msg).as_slice().to_vec(),
|
||||
Self::SHA1 => sha1::Sha1::digest(msg).as_slice().to_vec(),
|
||||
Self::SHA2_224 => sha2::Sha224::digest(msg).as_slice().to_vec(),
|
||||
Self::SHA2_256 => sha2::Sha256::digest(msg).as_slice().to_vec(),
|
||||
Self::SHA2_384 => sha2::Sha384::digest(msg).as_slice().to_vec(),
|
||||
Self::SHA2_512 => sha2::Sha512::digest(msg).as_slice().to_vec(),
|
||||
Self::SHA3_384 => sha3::Sha3_384::digest(msg).as_slice().to_vec(),
|
||||
Self::SHA3_512 => sha3::Sha3_512::digest(msg).as_slice().to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn output_size(self) -> usize {
|
||||
match self {
|
||||
Self::MD5 => md5::Md5::output_size(),
|
||||
Self::SHA1 => sha1::Sha1::output_size(),
|
||||
Self::SHA2_224 => sha2::Sha224::output_size(),
|
||||
Self::SHA2_256 => sha2::Sha256::output_size(),
|
||||
Self::SHA2_384 => sha2::Sha384::output_size(),
|
||||
Self::SHA2_512 => sha2::Sha512::output_size(),
|
||||
Self::SHA3_384 => sha3::Sha3_384::output_size(),
|
||||
Self::SHA3_512 => sha3::Sha3_512::output_size(),
|
||||
}
|
||||
}
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
use std::borrow::Cow;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error, Clone)]
|
||||
#[non_exhaustive]
|
||||
pub enum HttpRequestError {
|
||||
/// couldn't convert a http header value to string
|
||||
#[error("couldn't convert http header value to string for header key {key}")]
|
||||
HeaderValueToStr { key: String },
|
||||
|
||||
/// unexpected error occurred
|
||||
#[error("unexpected error: {reason}")]
|
||||
Unexpected { reason: String },
|
||||
}
|
||||
|
||||
pub trait HttpRequest {
|
||||
fn get_header_concatenated_values<'a>(&'a self, header_name: &str) -> Result<Cow<'a, str>, HttpRequestError>;
|
||||
fn get_lowercased_method(&self) -> Result<Cow<'_, str>, HttpRequestError>;
|
||||
fn get_target(&self) -> Result<Cow<'_, str>, HttpRequestError>;
|
||||
}
|
||||
|
||||
#[cfg(feature = "http_trait_impl")]
|
||||
mod http_trait_impl {
|
||||
use super::*;
|
||||
|
||||
impl HttpRequest for http::request::Parts {
|
||||
fn get_header_concatenated_values<'a>(&'a self, header_name: &str) -> Result<Cow<'a, str>, HttpRequestError> {
|
||||
let mut values = Vec::new();
|
||||
let all_values = self.headers.get_all(header_name);
|
||||
for value in all_values {
|
||||
let value_str = value.to_str().map_err(|_| HttpRequestError::HeaderValueToStr {
|
||||
key: header_name.to_owned(),
|
||||
})?;
|
||||
values.push(value_str.trim());
|
||||
}
|
||||
Ok(Cow::Owned(values.join(", ")))
|
||||
}
|
||||
|
||||
fn get_lowercased_method(&self) -> Result<Cow<'_, str>, HttpRequestError> {
|
||||
Ok(Cow::Owned(self.method.as_str().to_lowercase()))
|
||||
}
|
||||
|
||||
fn get_target(&self) -> Result<Cow<'_, str>, HttpRequestError> {
|
||||
Ok(Cow::Borrowed(self.uri.path()))
|
||||
}
|
||||
}
|
||||
impl<T> HttpRequest for http::request::Request<T> {
|
||||
fn get_header_concatenated_values<'a>(&'a self, header_name: &str) -> Result<Cow<'a, str>, HttpRequestError> {
|
||||
let mut values = Vec::new();
|
||||
let all_values = self.headers().get_all(header_name);
|
||||
for value in all_values {
|
||||
let value_str = value.to_str().map_err(|_| HttpRequestError::HeaderValueToStr {
|
||||
key: header_name.to_owned(),
|
||||
})?;
|
||||
values.push(value_str.trim());
|
||||
}
|
||||
Ok(Cow::Owned(values.join(", ")))
|
||||
}
|
||||
|
||||
fn get_lowercased_method(&self) -> Result<Cow<'_, str>, HttpRequestError> {
|
||||
Ok(Cow::Owned(self.method().as_str().to_lowercase()))
|
||||
}
|
||||
|
||||
fn get_target(&self) -> Result<Cow<'_, str>, HttpRequestError> {
|
||||
Ok(Cow::Borrowed(self.uri().path()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use http::method::Method;
|
||||
use http::{header, request};
|
||||
|
||||
#[test]
|
||||
fn http_request_parts() {
|
||||
let req = request::Builder::new()
|
||||
.method(Method::GET)
|
||||
.uri("/foo")
|
||||
.header("Host", "example.org")
|
||||
.header(header::DATE, "Tue, 07 Jun 2014 20:51:35 GMT")
|
||||
.header("X-Example", " Example header with some whitespace. ")
|
||||
.header("X-EmptyHeader", "")
|
||||
.header(header::CACHE_CONTROL, "max-age=60")
|
||||
.header(header::CACHE_CONTROL, "must-revalidate")
|
||||
.body(())
|
||||
.expect("couldn't build request");
|
||||
|
||||
let (parts, _) = req.into_parts();
|
||||
|
||||
assert_eq!(parts.get_target().expect("target"), "/foo");
|
||||
assert_eq!(parts.get_lowercased_method().expect("method"), "get");
|
||||
assert_eq!(
|
||||
parts.get_header_concatenated_values("host").expect("host"),
|
||||
"example.org"
|
||||
);
|
||||
assert_eq!(
|
||||
parts.get_header_concatenated_values("date").expect("date"),
|
||||
"Tue, 07 Jun 2014 20:51:35 GMT"
|
||||
);
|
||||
assert_eq!(
|
||||
parts.get_header_concatenated_values("x-example").expect("example"),
|
||||
"Example header with some whitespace."
|
||||
);
|
||||
assert_eq!(
|
||||
parts.get_header_concatenated_values("X-EmptyHeader").expect("empty"),
|
||||
""
|
||||
);
|
||||
assert_eq!(
|
||||
parts
|
||||
.get_header_concatenated_values(header::CACHE_CONTROL.as_str())
|
||||
.expect("cache control"),
|
||||
"max-age=60, must-revalidate"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn http_request_request() {
|
||||
let req = request::Builder::new()
|
||||
.method(Method::GET)
|
||||
.uri("/foo")
|
||||
.header("Host", "example.org")
|
||||
.header(header::DATE, "Tue, 07 Jun 2014 20:51:35 GMT")
|
||||
.header("X-Example", " Example header with some whitespace. ")
|
||||
.header("X-EmptyHeader", "")
|
||||
.header(header::CACHE_CONTROL, "max-age=60")
|
||||
.header(header::CACHE_CONTROL, "must-revalidate")
|
||||
.body(())
|
||||
.expect("couldn't build request");
|
||||
|
||||
assert_eq!(req.get_target().expect("target"), "/foo");
|
||||
assert_eq!(req.get_lowercased_method().expect("method"), "get");
|
||||
assert_eq!(req.get_header_concatenated_values("host").expect("host"), "example.org");
|
||||
assert_eq!(
|
||||
req.get_header_concatenated_values("date").expect("date"),
|
||||
"Tue, 07 Jun 2014 20:51:35 GMT"
|
||||
);
|
||||
assert_eq!(
|
||||
req.get_header_concatenated_values("x-example").expect("example"),
|
||||
"Example header with some whitespace."
|
||||
);
|
||||
assert_eq!(req.get_header_concatenated_values("X-EmptyHeader").expect("empty"), "");
|
||||
assert_eq!(
|
||||
req.get_header_concatenated_values(header::CACHE_CONTROL.as_str())
|
||||
.expect("cache control"),
|
||||
"max-age=60, must-revalidate"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1313
File diff suppressed because it is too large
Load Diff
Vendored
+143
@@ -0,0 +1,143 @@
|
||||
//! Signing HTTP Messages
|
||||
//!
|
||||
//! This module provides an implementation of a subset of
|
||||
//! [draft-cavage-http-signatures-12 RFC](https://tools.ietf.org/html/draft-cavage-http-signatures-12).
|
||||
//!
|
||||
//! # Example
|
||||
//! ```
|
||||
//! use picky::{
|
||||
//! http::http_signature::{HttpSignatureBuilder, HttpSignature},
|
||||
//! signature::SignatureAlgorithm,
|
||||
//! hash::HashAlgorithm,
|
||||
//! key::PrivateKey,
|
||||
//! pem::parse_pem,
|
||||
//! };
|
||||
//! use http::{request, header::{self, HeaderName}, method::Method};
|
||||
//!
|
||||
//! // all you need to generate a http signature
|
||||
//!
|
||||
//! let private_rsa_key: &str = "-----BEGIN RSA PRIVATE KEY-----\n\
|
||||
//! MIIEpgIBAAKCAQEApDx0MjvRzmxYXKfqHy0gN1znX6rSU2EnsDTbZaU1UcsMmRNx\n\
|
||||
//! L+FqDNzwNutCSQlkujzR37+bHGTxOnRvvSG3lwRvDBZepYWPum9WDqa9T5gTS/Cj\n\
|
||||
//! luq/oSsOyt/tUDO/GcNPTTfUQlgtOZ+zRo6FA0rpAQ8CrQm7XzGQ0DMoDU1SVNnu\n\
|
||||
//! tFJowlece9Y4NtAfhA+kJ5IEmcE9AgwxJY/iCyCxUEBUe7biwbUafLdtA3+3S8Bu\n\
|
||||
//! hBXAr+1BING3qS0vl08+3eaFq5q7f7VwcYOhUmH13itqSGwDznCk4oDQl+qn9DQZ\n\
|
||||
//! X9/09KtsgxuIcozxj0RwGKX8qkz4TlAGJw+oNwIDAQABAoIBAQCN9IrimH3iFBfU\n\
|
||||
//! Dnb4d4KvF6gNMpMU6pbpYOZ51vBdQEolTX65yfZmI9mlPndOtcXQi51D7lNdmYo/\n\
|
||||
//! 4kBqk2giKfzpz7QDEYyHspAJnelnkKStMNPVMBZucc8ZX6+5cOCunfg/YBAhQCHm\n\
|
||||
//! +rh0Nd+WVvtKpPTFJ/JCd48Zxf3KcDZD+AsWTjPt4zte8KdcwxiD3MrFunxgeujX\n\
|
||||
//! n0U0/f7hvX/7JBQ20gu2tD9whEaS2Gn8E4WpEV8wC6Ah1pU9mZNZ0u8clW9SV0de\n\
|
||||
//! ay0mHw8y/Wx6rkEMvrecK6mWbwSQGfRq+crI9PCwA5wn/EZmpQrQs9r5MLtDKVsQ\n\
|
||||
//! r9axQrSRAoGBANB53u3mxY9ByiYGT5Ge/33+BjANXxmIPG0TWgV1D8MhwFiaiHF+\n\
|
||||
//! tiEzoz4vi23Q+GeHyoM1wxw8VurDX+vcIJbZ0dyGOM/6F0eago7ZAtvHMdUdahAO\n\
|
||||
//! X+klqG8kIysgFXSzaU2w76816iIaXiZlDZUghrnd3wmgu9jhl3HCUlltAoGBAMms\n\
|
||||
//! 2uufuk26nssF+woQuy017lgLUNFCRrO9F3iwIiyY5R/q372gsx8HVzjYGKY8CF6v\n\
|
||||
//! m6JFfxogp44ZcafYOeu+iXbqoCAK4BTdbFB7/D3rX7WgidaxUlLGoXsNFIUIVubR\n\
|
||||
//! jaRA7l3tl3fkpdqUAye6zosMKp2oybQLyX5hLAWzAoGBAJKhVUIA8W1cOaFbCPYE\n\
|
||||
//! XfExDQsZLI1ZvB5/4O47srVtdMsdDeC93b4mgqfHawr3UvAGm1KEKtIeQofmmP3c\n\
|
||||
//! mvNfCvNPWIA3h84uB6wPSKpqRUt+382hPqZOfVSGl1HKxCyL0AH78+lJQ39vCk94\n\
|
||||
//! /f+om/n46tnrupPFv+4cXi1VAoGBALzSSmYxtozwHZyYjOJvp9A8nltwvMov82J1\n\
|
||||
//! uHQW9OgsftnTXoh83Tg/9zoRmYKK0otUf7L+vnIIANjamb88g35ldu8P3bwicosW\n\
|
||||
//! hUMV0qVmqsWy+Vs5yooVzzsWlA+6LyMNMECJSqRGv3pRabesvQeFr7wgOAZE8hTQ\n\
|
||||
//! tGbPNBhhAoGBAIkXxIJT0OMKSt/A7wDE9wd3dtC8mbkqr5aZTvwiuD6OvNDdXb/J\n\
|
||||
//! i03ns56mIflifVLPYVmCEXdYIzSv7HfeR4d78bAvqiMFfnQ2PF3tuoKMSvQM0m8/\n\
|
||||
//! f3VhEFFMrUTTRMX/9PR0ITQtnZlWIDfBVgXPmWqTqCGOMYsRPv70LGse\n\
|
||||
//! -----END RSA PRIVATE KEY-----";
|
||||
//! let pem = parse_pem(private_rsa_key).expect("couldn't parse pem");
|
||||
//! let private_key = PrivateKey::from_pem(&pem).expect("couldn't parse private key");
|
||||
//!
|
||||
//! let req = request::Builder::new()
|
||||
//! .method(Method::GET)
|
||||
//! .uri("/foo")
|
||||
//! .header("Host", "example.org")
|
||||
//! .header(header::DATE, "Tue, 07 Jun 2014 20:51:35 GMT")
|
||||
//! .header("X-Example", " Example header with some whitespace. ")
|
||||
//! .header("X-EmptyHeader", "")
|
||||
//! .header(header::CACHE_CONTROL, "max-age=60")
|
||||
//! .header(header::CACHE_CONTROL, "must-revalidate")
|
||||
//! .body(())
|
||||
//! .expect("couldn't build request");
|
||||
//!
|
||||
//! let (parts, _) = req.into_parts();
|
||||
//!
|
||||
//! // generate http signature
|
||||
//!
|
||||
//! let http_signature = HttpSignatureBuilder::new()
|
||||
//! .key_id("my-rsa-key")
|
||||
//! .signature_method(&private_key, SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_224))
|
||||
//! // `picky::http::http_request::HttpRequest` trait is implemented for `http::request::Parts`
|
||||
//! // for `http` crate with `http_trait_impl` feature gate
|
||||
//! .generate_signing_string_using_http_request(&parts)
|
||||
//! .request_target()
|
||||
//! .created(1402170695)
|
||||
//! .http_header("host")
|
||||
//! .http_header(header::DATE.as_str())
|
||||
//! .http_header(header::CACHE_CONTROL.as_str())
|
||||
//! .http_header("x-emptyheader")
|
||||
//! .http_header("X-EXAMPLE")
|
||||
//! .build()
|
||||
//! .expect("couldn't generate http signature");
|
||||
//!
|
||||
//! let http_signature_str = http_signature.to_string();
|
||||
//!
|
||||
//! assert_eq!(
|
||||
//! http_signature_str,
|
||||
//! "Signature keyId=\"my-rsa-key\",algorithm=\"rsa-sha224\",created=1402170695,\
|
||||
//! headers=\"(request-target) (created) host date cache-control x-emptyheader x-example\",\
|
||||
//! signature=\"JueyecQbV5rQ3TI1EfqZRjAZMMOb4ABZNS0yDcBDgyfbOLORYipT2An2MCH8n/HequVJkEE\
|
||||
//! 86/vj9ZFLbyqFkV3a8uQGB6gaE79l9YNdzVeO5k7GBb1jskwBXnqVtGmn8aT2f+cJzkDtu6ptg+UtaU\
|
||||
//! ZOQKdutc8aHq1NCLwvqMbA410XP0pA5r/VTbMg/yW8rHguue0Trh0WYCw8zHfNuZtheWxvWGLdYxvC0\
|
||||
//! u5oJA0PdxFceqVd/304+RQsrDGLtX8J9vSeqEsQJfvswFyMTkdl1gDbP/YdXp7ADzc2D9IefT9zqvFd\
|
||||
//! yEDDXEXKmZm+22395xRtnFmeWXu/+PM6wg==\""
|
||||
//! );
|
||||
//!
|
||||
//! // parse a http signature and verify it
|
||||
//!
|
||||
//! let parsed_http_signature = http_signature_str.parse::<HttpSignature>()
|
||||
//! .expect("couldn't parse http signature");
|
||||
//!
|
||||
//! assert_eq!(parsed_http_signature, http_signature);
|
||||
//!
|
||||
//! parsed_http_signature.verifier()
|
||||
//! .signature_method(&private_key.to_public_key().unwrap(), SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_224))
|
||||
//! .generate_signing_string_using_http_request(&parts)
|
||||
//! .now(1402170695)
|
||||
//! .verify()
|
||||
//! .expect("couldn't verify signature");
|
||||
//!
|
||||
//! // alternatively you can provide a pre-generated signing string
|
||||
//!
|
||||
//! let signing_string =
|
||||
//! "(request-target): get /foo\n\
|
||||
//! (created): 1402170695\n\
|
||||
//! host: example.org\n\
|
||||
//! date: Tue, 07 Jun 2014 20:51:35 GMT\n\
|
||||
//! cache-control: max-age=60, must-revalidate\n\
|
||||
//! x-emptyheader:\n\
|
||||
//! x-example: Example header with some whitespace.";
|
||||
//!
|
||||
//! let http_signature_pre_generated = HttpSignatureBuilder::new()
|
||||
//! .key_id("my-rsa-key")
|
||||
//! .signature_method(&private_key, SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_224))
|
||||
//! .pre_generated_signing_string(signing_string)
|
||||
//! .build()
|
||||
//! .expect("couldn't generate http signature using pre-generated signing string");
|
||||
//!
|
||||
//! let http_signature_pre_generated_str = http_signature_pre_generated.to_string();
|
||||
//!
|
||||
//! assert_eq!(http_signature_pre_generated, http_signature);
|
||||
//! assert_eq!(http_signature_pre_generated_str, http_signature_str);
|
||||
//!
|
||||
//! parsed_http_signature.verifier()
|
||||
//! .signature_method(&private_key.to_public_key().unwrap(), SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_224))
|
||||
//! .pre_generated_signing_string(signing_string)
|
||||
//! .now(1402170695)
|
||||
//! .verify()
|
||||
//! .expect("couldn't verify signature using pre-generated signing string");
|
||||
//! ```
|
||||
|
||||
pub mod http_request;
|
||||
pub mod http_signature;
|
||||
|
||||
pub use http_request::HttpRequest;
|
||||
pub use http_signature::HttpSignature;
|
||||
Vendored
+1602
File diff suppressed because it is too large
Load Diff
Vendored
+767
@@ -0,0 +1,767 @@
|
||||
//! A JSON Web Key (JWK) is a JavaScript Object Notation (JSON) data structure that represents a cryptographic key.
|
||||
//!
|
||||
//! See [RFC7517](https://tools.ietf.org/html/rfc7517).
|
||||
|
||||
use crate::jose::jwe::{JweAlg, JweEnc};
|
||||
use crate::jose::jws::JwsAlg;
|
||||
use crate::key::ec::{EcdsaPublicKey, NamedEcCurve};
|
||||
use crate::key::ed::{EdPublicKey, NamedEdAlgorithm};
|
||||
use crate::key::{EcCurve, EdAlgorithm, PublicKey};
|
||||
use base64::engine::general_purpose;
|
||||
use base64::{DecodeError, Engine as _};
|
||||
use crypto_bigint::BoxedUint;
|
||||
use picky_asn1::wrapper::IntegerAsn1;
|
||||
use picky_asn1_x509::SubjectPublicKeyInfo;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
// === error type === //
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum JwkError {
|
||||
/// Json error
|
||||
#[error("JSON error: {source}")]
|
||||
Json { source: serde_json::Error },
|
||||
|
||||
/// couldn't decode base64
|
||||
#[error("couldn't decode base64: {source}")]
|
||||
Base64Decoding { source: DecodeError },
|
||||
|
||||
/// unsupported algorithm
|
||||
#[error("unsupported algorithm: {algorithm}")]
|
||||
UnsupportedAlgorithm { algorithm: &'static str },
|
||||
|
||||
#[error("invalid ec public key: {cause}")]
|
||||
InvalidEcPublicKey { cause: String },
|
||||
|
||||
#[error("invalid ec point coordinates in JWK")]
|
||||
InvalidEcPointCoordinates,
|
||||
|
||||
#[error("invalid ed public key: {cause}")]
|
||||
InvalidEdPublicKey { cause: String },
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for JwkError {
|
||||
fn from(e: serde_json::Error) -> Self {
|
||||
Self::Json { source: e }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DecodeError> for JwkError {
|
||||
fn from(e: DecodeError) -> Self {
|
||||
Self::Base64Decoding { source: e }
|
||||
}
|
||||
}
|
||||
|
||||
// === key type === //
|
||||
|
||||
/// Algorithm type for JWK
|
||||
///
|
||||
/// See [RFC7518 #6](https://tools.ietf.org/html/rfc7518#section-6.1)
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(tag = "kty")]
|
||||
pub enum JwkKeyType {
|
||||
/// Edwards curve-based cryptography
|
||||
///
|
||||
/// Defined by separate [RFC 8037](https://www.rfc-editor.org/rfc/rfc8037)
|
||||
#[serde(rename = "OKP")]
|
||||
Ed(JwkPublicEdKey),
|
||||
/// Elliptic Curve
|
||||
///
|
||||
/// Recommended+ by RFC
|
||||
#[serde(rename = "EC")]
|
||||
Ec(JwkPublicEcKey),
|
||||
/// Elliptic Curve
|
||||
///
|
||||
/// Required by RFC
|
||||
#[serde(rename = "RSA")]
|
||||
Rsa(JwkPublicRsaKey),
|
||||
/// Octet sequence (used to represent symmetric keys) (unsupported)
|
||||
///
|
||||
/// Required by RFC
|
||||
#[serde(rename = "oct")]
|
||||
Oct,
|
||||
}
|
||||
|
||||
impl JwkKeyType {
|
||||
/// Build a JWK key from RSA components.
|
||||
///
|
||||
/// Each argument is the unsigned big-endian representation as an octet sequence of the value.
|
||||
/// If a signed representation is provided, leading zero is removed for any number bigger than 0x7F.
|
||||
pub fn new_rsa_key(modulus: &[u8], public_exponent: &[u8]) -> Self {
|
||||
let modulus = h_strip_unrequired_leading_zero(modulus);
|
||||
let public_exponent = h_strip_unrequired_leading_zero(public_exponent);
|
||||
Self::Rsa(JwkPublicRsaKey {
|
||||
n: general_purpose::URL_SAFE_NO_PAD.encode(modulus),
|
||||
e: general_purpose::URL_SAFE_NO_PAD.encode(public_exponent),
|
||||
})
|
||||
}
|
||||
|
||||
/// Build a JWK key from EC components.
|
||||
///
|
||||
/// `x` and `y` are big-endian representation of the affine point coordinates.
|
||||
pub fn new_ec_key(curve: JwkEcPublicKeyCurve, x: &[u8], y: &[u8]) -> Self {
|
||||
let x = h_strip_unrequired_leading_zero(x);
|
||||
let y = h_strip_unrequired_leading_zero(y);
|
||||
Self::Ec(JwkPublicEcKey {
|
||||
crv: curve,
|
||||
x: general_purpose::URL_SAFE_NO_PAD.encode(x),
|
||||
y: general_purpose::URL_SAFE_NO_PAD.encode(y),
|
||||
})
|
||||
}
|
||||
|
||||
/// Build a JWK key from Edwards curve components.
|
||||
///
|
||||
/// `crv` is ed-based algorithm name.
|
||||
/// `x` is raw public key bytes.
|
||||
pub fn new_ed_key(crv: JwkEdPublicKeyAlgorithm, x: &[u8]) -> Self {
|
||||
Self::Ed(JwkPublicEdKey {
|
||||
crv,
|
||||
x: general_purpose::URL_SAFE_NO_PAD.encode(x),
|
||||
})
|
||||
}
|
||||
|
||||
/// Build a JWK key from RSA components already encoded following base64 url format.
|
||||
///
|
||||
/// Each argument is the unsigned big-endian representation as an octet sequence of the value.
|
||||
/// The octet sequence MUST utilize the minimum number of octets needed to represent the value.
|
||||
/// That is: **no leading zero** must be present.
|
||||
///
|
||||
/// See definition for term `Base64urlUInt` in [RFC7518 section 2](https://datatracker.ietf.org/doc/html/rfc7518#section-2)
|
||||
pub fn new_rsa_key_from_base64_url(modulus: String, public_exponent: String) -> Self {
|
||||
Self::Rsa(JwkPublicRsaKey {
|
||||
n: modulus,
|
||||
e: public_exponent,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn as_rsa(&self) -> Option<&JwkPublicRsaKey> {
|
||||
match self {
|
||||
JwkKeyType::Rsa(rsa) => Some(rsa),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_ec(&self) -> Option<&JwkPublicEcKey> {
|
||||
match self {
|
||||
JwkKeyType::Ec(ec) => Some(ec),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_ed(&self) -> Option<&JwkPublicEdKey> {
|
||||
match self {
|
||||
JwkKeyType::Ed(ed) => Some(ed),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_rsa(&self) -> bool {
|
||||
self.as_rsa().is_some()
|
||||
}
|
||||
|
||||
pub fn is_ec(&self) -> bool {
|
||||
self.as_ec().is_some()
|
||||
}
|
||||
|
||||
pub fn is_ed(&self) -> bool {
|
||||
self.as_ed().is_some()
|
||||
}
|
||||
}
|
||||
|
||||
/// Strips leading zero for any number bigger than 0x7F.
|
||||
fn h_strip_unrequired_leading_zero(value: &[u8]) -> &[u8] {
|
||||
if let [0x00, rest @ ..] = value { rest } else { value }
|
||||
}
|
||||
|
||||
/// Big integers from 0x00 to 0x7F are all base64-encoded using two ASCII characters ranging from "AA" to "fw".
|
||||
/// We know the required capacity is _exactly_ of one byte.
|
||||
/// The value 0 is valid and is represented as the array [0x00] ("AA").
|
||||
/// For numbers greater than 0x7F, logic is a bit more complex.
|
||||
/// There is no leading zero in JWK keys because _unsigned_ numbers are used.
|
||||
/// As such, there is no need to disambiguate the high-order bit (0x80)
|
||||
/// which is used as the sign bit for _signed_ numbers.
|
||||
/// The high-order bit is set when base64 encoding's leading character matches [g-z0-9_-].
|
||||
fn h_allocate_signed_big_int_buffer(base64_url_encoding: &str) -> Vec<u8> {
|
||||
match base64_url_encoding.chars().next() {
|
||||
// The leading zero is re-introduced for any number whose high-order bit is set
|
||||
Some('g'..='z' | '0'..='9' | '_' | '-') => vec![0],
|
||||
// Otherwise, there is nothing more to do
|
||||
_ => Vec::with_capacity(1),
|
||||
}
|
||||
}
|
||||
|
||||
// === public key use === //
|
||||
|
||||
/// Public Key Use, identifies the intended use of the public key.
|
||||
///
|
||||
/// See [RFC7517 #4](https://tools.ietf.org/html/rfc7517#section-4.2)
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum JwkPubKeyUse {
|
||||
#[serde(rename = "sig")]
|
||||
Signature,
|
||||
#[serde(rename = "enc")]
|
||||
Encryption,
|
||||
}
|
||||
|
||||
// === key operations === //
|
||||
|
||||
/// Key Operations, identifies the operation(s) for which the key is intended to be used.
|
||||
///
|
||||
/// See [RFC7517 #4](https://tools.ietf.org/html/rfc7517#section-4.3)
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum JwkKeyOps {
|
||||
#[serde(rename = "sign")]
|
||||
Sign,
|
||||
#[serde(rename = "verify")]
|
||||
Verify,
|
||||
#[serde(rename = "encrypt")]
|
||||
Encrypt,
|
||||
#[serde(rename = "decrypt")]
|
||||
Decrypt,
|
||||
#[serde(rename = "wrapKey")]
|
||||
WrapKey,
|
||||
#[serde(rename = "unwrapKey")]
|
||||
UnwrapKey,
|
||||
#[serde(rename = "deriveKey")]
|
||||
DeriveKey,
|
||||
#[serde(rename = "deriveBits")]
|
||||
DeriveBits,
|
||||
}
|
||||
|
||||
// === algorithms === //
|
||||
|
||||
/// JOSE algorithms names as defined by [RFC7518](https://tools.ietf.org/html/rfc7518)
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum Jwa {
|
||||
Sig(JwsAlg),
|
||||
Enc(JweEnc),
|
||||
CEKAlg(JweAlg),
|
||||
}
|
||||
|
||||
// === json web key === //
|
||||
|
||||
/// Represents a cryptographic key as defined by [RFC7517](https://tools.ietf.org/html/rfc7517).
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct Jwk {
|
||||
// -- specific to JWK -- //
|
||||
#[serde(flatten)]
|
||||
pub key: JwkKeyType,
|
||||
|
||||
/// Identifies the algorithm intended for use with the key.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub alg: Option<Jwa>,
|
||||
|
||||
/// Public Key Use
|
||||
///
|
||||
/// Intended use of the public key.
|
||||
#[serde(rename = "use", skip_serializing_if = "Option::is_none")]
|
||||
pub key_use: Option<JwkPubKeyUse>,
|
||||
|
||||
/// Key Operations
|
||||
///
|
||||
/// identifies the operation(s) for which the key is intended to be used.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub key_ops: Option<Vec<JwkKeyOps>>,
|
||||
|
||||
// -- common with all -- //
|
||||
/// Key ID Header
|
||||
///
|
||||
/// A hint indicating which key was used.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub kid: Option<String>,
|
||||
|
||||
/// X.509 URL Header
|
||||
///
|
||||
/// URI that refers to a resource for an X.509 public key certificate or certificate chain.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub x5u: Option<String>,
|
||||
|
||||
/// X.509 Certificate Chain
|
||||
///
|
||||
/// Chain of one or more PKIX certificates.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub x5c: Option<Vec<String>>,
|
||||
|
||||
/// X.509 Certificate SHA-1 Thumbprint
|
||||
///
|
||||
/// base64url-encoded SHA-1 thumbprint (a.k.a. digest) of the DER encoding of an X.509 certificate.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub x5t: Option<String>,
|
||||
|
||||
/// X.509 Certificate SHA-256 Thumbprint
|
||||
///
|
||||
/// base64url-encoded SHA-256 thumbprint (a.k.a. digest) of the DER encoding of an X.509 certificate.
|
||||
#[serde(rename = "x5t#S256", alias = "x5t#s256", skip_serializing_if = "Option::is_none")]
|
||||
pub x5t_s256: Option<String>,
|
||||
}
|
||||
|
||||
impl Jwk {
|
||||
pub fn new(key: JwkKeyType) -> Self {
|
||||
Jwk {
|
||||
key,
|
||||
alg: None,
|
||||
key_use: None,
|
||||
key_ops: None,
|
||||
kid: None,
|
||||
x5u: None,
|
||||
x5c: None,
|
||||
x5t: None,
|
||||
x5t_s256: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_json(json: &str) -> Result<Self, JwkError> {
|
||||
Ok(serde_json::from_str(json)?)
|
||||
}
|
||||
|
||||
pub fn from_public_key(public_key: &PublicKey) -> Result<Self, JwkError> {
|
||||
use picky_asn1::wrapper::BitStringAsn1Container;
|
||||
use picky_asn1_x509::PublicKey as SerdePublicKey;
|
||||
|
||||
match &public_key.as_inner().subject_public_key {
|
||||
SerdePublicKey::Rsa(BitStringAsn1Container(rsa)) => {
|
||||
let modulus = rsa.modulus.as_signed_bytes_be();
|
||||
let public_exponent = rsa.public_exponent.as_signed_bytes_be();
|
||||
Ok(Self::new(JwkKeyType::new_rsa_key(modulus, public_exponent)))
|
||||
}
|
||||
SerdePublicKey::Ec(_) => {
|
||||
let ec_key = EcdsaPublicKey::try_from(public_key)
|
||||
.map_err(|e| JwkError::InvalidEcPublicKey { cause: e.to_string() })?;
|
||||
|
||||
match ec_key.curve() {
|
||||
NamedEcCurve::Known(EcCurve::NistP256) => {
|
||||
let point = p256::Sec1Point::from_bytes(ec_key.encoded_point()).map_err(|_| {
|
||||
JwkError::InvalidEcPublicKey {
|
||||
cause: "invalid P-256 EC point encoding".to_string(),
|
||||
}
|
||||
})?;
|
||||
|
||||
match (point.x(), point.y()) {
|
||||
(Some(x), Some(y)) => Ok(Self::new(JwkKeyType::new_ec_key(
|
||||
JwkEcPublicKeyCurve::P256,
|
||||
x.as_slice(),
|
||||
y.as_slice(),
|
||||
))),
|
||||
_ => Err(JwkError::InvalidEcPublicKey {
|
||||
cause: "Invalid P-256 curve EC public point coordinates".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
NamedEcCurve::Known(EcCurve::NistP384) => {
|
||||
let point = p384::Sec1Point::from_bytes(ec_key.encoded_point()).map_err(|_| {
|
||||
JwkError::InvalidEcPublicKey {
|
||||
cause: "invalid P-384 EC point encoding".to_string(),
|
||||
}
|
||||
})?;
|
||||
|
||||
match (point.x(), point.y()) {
|
||||
(Some(x), Some(y)) => Ok(Self::new(JwkKeyType::new_ec_key(
|
||||
JwkEcPublicKeyCurve::P384,
|
||||
x.as_slice(),
|
||||
y.as_slice(),
|
||||
))),
|
||||
_ => Err(JwkError::InvalidEcPublicKey {
|
||||
cause: "Invalid P-384 curve EC public point coordinates".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
NamedEcCurve::Known(EcCurve::NistP521) => {
|
||||
let point = p521::Sec1Point::from_bytes(ec_key.encoded_point()).map_err(|_| {
|
||||
JwkError::InvalidEcPublicKey {
|
||||
cause: "invalid P-521 EC point encoding".to_string(),
|
||||
}
|
||||
})?;
|
||||
|
||||
match (point.x(), point.y()) {
|
||||
(Some(x), Some(y)) => Ok(Self::new(JwkKeyType::new_ec_key(
|
||||
JwkEcPublicKeyCurve::P521,
|
||||
x.as_slice(),
|
||||
y.as_slice(),
|
||||
))),
|
||||
_ => Err(JwkError::InvalidEcPublicKey {
|
||||
cause: "Invalid P-521 curve EC public point coordinates".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
NamedEcCurve::Unsupported(_) => Err(JwkError::UnsupportedAlgorithm {
|
||||
algorithm: "Unsupported EC curve",
|
||||
}),
|
||||
}
|
||||
}
|
||||
SerdePublicKey::Ed(_) => {
|
||||
let ed_key = EdPublicKey::try_from(public_key)
|
||||
.map_err(|e| JwkError::InvalidEdPublicKey { cause: e.to_string() })?;
|
||||
|
||||
let algorithm = match ed_key.algorithm() {
|
||||
NamedEdAlgorithm::Known(EdAlgorithm::Ed25519) => JwkEdPublicKeyAlgorithm::Ed25519,
|
||||
NamedEdAlgorithm::Known(EdAlgorithm::X25519) => JwkEdPublicKeyAlgorithm::X25519,
|
||||
NamedEdAlgorithm::Unsupported(_) => {
|
||||
return Err(JwkError::UnsupportedAlgorithm {
|
||||
algorithm: "Unsupported ED algorithm",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Self::new(JwkKeyType::new_ed_key(algorithm, ed_key.data())))
|
||||
}
|
||||
SerdePublicKey::Mldsa(_) => Err(JwkError::UnsupportedAlgorithm {
|
||||
algorithm: "JWK unsupported with MLDSA keys",
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> Result<String, JwkError> {
|
||||
Ok(serde_json::to_string(self)?)
|
||||
}
|
||||
|
||||
pub fn to_json_pretty(&self) -> Result<String, JwkError> {
|
||||
Ok(serde_json::to_string_pretty(self)?)
|
||||
}
|
||||
|
||||
pub fn to_public_key(&self) -> Result<PublicKey, JwkError> {
|
||||
match &self.key {
|
||||
JwkKeyType::Rsa(rsa) => {
|
||||
let modulus = IntegerAsn1::from_bytes_be_signed(rsa.modulus_signed_bytes_be()?);
|
||||
let public_exponent = IntegerAsn1::from_bytes_be_signed(rsa.public_exponent_signed_bytes_be()?);
|
||||
let spki = SubjectPublicKeyInfo::new_rsa_key(modulus, public_exponent);
|
||||
Ok(spki.into())
|
||||
}
|
||||
JwkKeyType::Ec(ec) => {
|
||||
let curve = match ec.crv {
|
||||
JwkEcPublicKeyCurve::P256 => EcCurve::NistP256,
|
||||
JwkEcPublicKeyCurve::P384 => EcCurve::NistP384,
|
||||
JwkEcPublicKeyCurve::P521 => EcCurve::NistP521,
|
||||
};
|
||||
|
||||
let x = BoxedUint::from_be_slice_vartime(&ec.x_signed_bytes_be()?);
|
||||
let y = BoxedUint::from_be_slice_vartime(&ec.y_signed_bytes_be()?);
|
||||
|
||||
PublicKey::from_ec_components(curve, &x, &y).map_err(|_| JwkError::InvalidEcPointCoordinates)
|
||||
}
|
||||
JwkKeyType::Ed(ed) => {
|
||||
let algorithm = match ed.crv {
|
||||
JwkEdPublicKeyAlgorithm::Ed25519 => Ok(EdAlgorithm::Ed25519),
|
||||
JwkEdPublicKeyAlgorithm::X25519 => Ok(EdAlgorithm::X25519),
|
||||
JwkEdPublicKeyAlgorithm::Ed448 => Err("ed448 algorithm"),
|
||||
JwkEdPublicKeyAlgorithm::X448 => Err("x448 algorithm"),
|
||||
}
|
||||
.map_err(|algorithm| JwkError::UnsupportedAlgorithm { algorithm })?;
|
||||
|
||||
Ok(PublicKey::from_ed_encoded_components(
|
||||
&algorithm.into(),
|
||||
ed.public_key_bytes()?.as_ref(),
|
||||
))
|
||||
}
|
||||
JwkKeyType::Oct => Err(JwkError::UnsupportedAlgorithm {
|
||||
algorithm: "octet sequence",
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === jwk set === //
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct JwkSet {
|
||||
pub keys: Vec<Jwk>,
|
||||
}
|
||||
|
||||
impl JwkSet {
|
||||
pub fn from_json(json: &str) -> Result<Self, JwkError> {
|
||||
Ok(serde_json::from_str(json)?)
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> Result<String, JwkError> {
|
||||
Ok(serde_json::to_string(self)?)
|
||||
}
|
||||
|
||||
pub fn to_json_pretty(&self) -> Result<String, JwkError> {
|
||||
Ok(serde_json::to_string_pretty(self)?)
|
||||
}
|
||||
}
|
||||
|
||||
// === public rsa key === //
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct JwkPublicRsaKey {
|
||||
n: String,
|
||||
e: String,
|
||||
}
|
||||
|
||||
impl JwkPublicRsaKey {
|
||||
pub fn modulus_signed_bytes_be(&self) -> Result<Vec<u8>, JwkError> {
|
||||
let mut buf = h_allocate_signed_big_int_buffer(&self.n);
|
||||
general_purpose::URL_SAFE_NO_PAD
|
||||
.decode_vec(&self.n, &mut buf)
|
||||
.map_err(JwkError::from)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
pub fn modulus_unsigned_bytes_be(&self) -> Result<Vec<u8>, JwkError> {
|
||||
general_purpose::URL_SAFE_NO_PAD.decode(&self.n).map_err(JwkError::from)
|
||||
}
|
||||
|
||||
pub fn public_exponent_signed_bytes_be(&self) -> Result<Vec<u8>, JwkError> {
|
||||
let mut buf = h_allocate_signed_big_int_buffer(&self.e);
|
||||
general_purpose::URL_SAFE_NO_PAD
|
||||
.decode_vec(&self.e, &mut buf)
|
||||
.map_err(JwkError::from)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
pub fn public_exponent_unsigned_bytes_be(&self) -> Result<Vec<u8>, JwkError> {
|
||||
general_purpose::URL_SAFE_NO_PAD.decode(&self.e).map_err(JwkError::from)
|
||||
}
|
||||
}
|
||||
|
||||
/// The key type of a JWK defined in
|
||||
/// [RFC 7518, section 6.1](https://tools.ietf.org/html/rfc7518#section-6.1).
|
||||
///
|
||||
/// Note that P521 is not supported yet for signning and verification.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum JwkEcPublicKeyCurve {
|
||||
#[serde(rename = "P-256")]
|
||||
P256,
|
||||
#[serde(rename = "P-384")]
|
||||
P384,
|
||||
#[serde(rename = "P-521")]
|
||||
P521,
|
||||
}
|
||||
|
||||
// === public ec key === //
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct JwkPublicEcKey {
|
||||
crv: JwkEcPublicKeyCurve,
|
||||
x: String,
|
||||
y: String,
|
||||
}
|
||||
|
||||
impl JwkPublicEcKey {
|
||||
pub fn x_signed_bytes_be(&self) -> Result<Vec<u8>, JwkError> {
|
||||
let mut buf = h_allocate_signed_big_int_buffer(&self.x);
|
||||
general_purpose::URL_SAFE_NO_PAD
|
||||
.decode_vec(&self.x, &mut buf)
|
||||
.map_err(JwkError::from)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
pub fn y_signed_bytes_be(&self) -> Result<Vec<u8>, JwkError> {
|
||||
let mut buf = h_allocate_signed_big_int_buffer(&self.y);
|
||||
general_purpose::URL_SAFE_NO_PAD
|
||||
.decode_vec(&self.y, &mut buf)
|
||||
.map_err(JwkError::from)?;
|
||||
Ok(buf)
|
||||
}
|
||||
}
|
||||
|
||||
/// Defined in [RFC 8037](https://tools.ietf.org/html/rfc8037)
|
||||
///
|
||||
/// Note that X25519, Ed448 and X448 are not yet supported by picky for jws/jwe.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum JwkEdPublicKeyAlgorithm {
|
||||
#[serde(rename = "Ed25519")]
|
||||
Ed25519,
|
||||
#[serde(rename = "Ed448")]
|
||||
Ed448,
|
||||
#[serde(rename = "X25519")]
|
||||
X25519,
|
||||
#[serde(rename = "X448")]
|
||||
X448,
|
||||
}
|
||||
|
||||
// === public ed key === //
|
||||
|
||||
/// Defined in [RFC 8037](https://tools.ietf.org/html/rfc8037)
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct JwkPublicEdKey {
|
||||
/// NOTE: "crv" defines not exactly the curve but the algorithm.
|
||||
crv: JwkEdPublicKeyAlgorithm,
|
||||
/// In contrast to EC keys, the `x` coordinate is an octet string, not an encoded big integer.
|
||||
x: String,
|
||||
}
|
||||
|
||||
impl JwkPublicEdKey {
|
||||
pub fn public_key_bytes(&self) -> Result<Vec<u8>, JwkError> {
|
||||
let mut buf = Vec::new();
|
||||
general_purpose::URL_SAFE_NO_PAD
|
||||
.decode_vec(&self.x, &mut buf)
|
||||
.map_err(JwkError::from)?;
|
||||
Ok(buf)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::jose::jws::JwsAlg;
|
||||
use crate::pem::Pem;
|
||||
use rstest::rstest;
|
||||
|
||||
const RSA_MODULUS: &str = "rpJjxW0nNZiq1mPC3ZAxqf9qNjmKurP7XuKrpWrfv3IOUldqChQVPNg8zCvDOMZIO-ZDuRmVH\
|
||||
EZ5E1vz5auHNACnpl6AvDGJ-4qyX42vfUDMNZx8i86d7bQpwJkO_MVMLj8qMGmTVbQ8zqVw2z\
|
||||
MyKUFfa2V83nvx2wz4FJh2Thw2uZX2P7h8nlDVSuXO0wJ_OY_2qtqRIAnNXMzL5BF5pEFh4hi\
|
||||
JIFiMTNkhVtUjT1QSB9E8DtDme8g4u769Oc0My45fgqSNE7kKKyaDhTfqSovyhj-qWiD-X_Gw\
|
||||
pWkW4ungpHzz_97-ZDB3yQ7AMwKAsw5EW2cMqseAp3f-kf159w";
|
||||
|
||||
const RSA_PUBLIC_EXPONENT: &str = "AQAB";
|
||||
|
||||
const X509_SHA1_THUMBPRINT: &str = "N3ORVnr9T6opxpS9iRbkKGwKiQI";
|
||||
|
||||
const X509_CERT_0: &str = "MIIDWjCCAkKgAwIBAgIUWRsBqKmpXGP/OwrwLWicwxhuCFowDQYJKoZIhvc\
|
||||
NAQELBQAwKjEoMCYGA1UEAwwfbG9naW4uZGV2b2x1dGlvbnMuY29tIEF1dG\
|
||||
hvcml0eTAeFw0xOTAzMTMxMzE1MzVaFw0yMDAzMTIxMzE1MzVaMCYxJDAiB\
|
||||
gNVBAMMG2xvZ2luLmRldm9sdXRpb25zLmNvbSBUb2tlbjCCASIwDQYJKoZI\
|
||||
hvcNAQEBBQADggEPADCCAQoCggEBAK6SY8VtJzWYqtZjwt2QMan/ajY5irq\
|
||||
z+17iq6Vq379yDlJXagoUFTzYPMwrwzjGSDvmQ7kZlRxGeRNb8+WrhzQAp6\
|
||||
ZegLwxifuKsl+Nr31AzDWcfIvOne20KcCZDvzFTC4/KjBpk1W0PM6lcNszM\
|
||||
ilBX2tlfN578dsM+BSYdk4cNrmV9j+4fJ5Q1UrlztMCfzmP9qrakSAJzVzM\
|
||||
y+QReaRBYeIYiSBYjEzZIVbVI09UEgfRPA7Q5nvIOLu+vTnNDMuOX4KkjRO\
|
||||
5Cismg4U36kqL8oY/qlog/l/xsKVpFuLp4KR88//e/mQwd8kOwDMCgLMORF\
|
||||
tnDKrHgKd3/pH9efcCAwEAAaN8MHowCQYDVR0TBAIwADAOBgNVHQ8BAf8EB\
|
||||
AMCBeAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQW\
|
||||
BBQQW2Cx8HUpXfFM3B76WzBb/BhCBDAfBgNVHSMEGDAWgBRWAUlOiE4Z3ww\
|
||||
aHgz284/sYB9NaDANBgkqhkiG9w0BAQsFAAOCAQEAkliCiJF9Z/Y57V6Rrn\
|
||||
gHCBBWtqR+N/A+KHQqWxP2MmJiHVBBnZAueVPsvykO+EfbazNEkUoPVKhUd\
|
||||
5NxEmTEMOBu9HUEzlmA5xDjl5xS7fejJIr7pgbxIup4m+DsNsPVnF1Snk56\
|
||||
F6660RhRb9fsHQ0pgvWuG+tQXJ4J1Zi0cp+xi4yze6hJGAyAqj6wU46AUiL\
|
||||
6kUr9GUVHqEsl5mNMIW18JT4KM/s5DWxFGO2soSTkaVHwGSkMBQSTgHMWs0\
|
||||
L3bBfimjw9FwjwwHAbe1W5QU6uVXGApuKANRsXxgCn566QkE/BuV3WVR6uy\
|
||||
n2P1J/vU9hxasgRIcjf3jHC4lGpew==";
|
||||
|
||||
const X509_CERT_1: &str = "MIIDRjCCAi6gAwIBAgIUUqhc3/U6OhKtEk1b8JfX3GL0FPYwDQYJKoZIhvc\
|
||||
NAQELBQAwKDEmMCQGA1UEAwwdbG9naW4uZGV2b2x1dGlvbnMuY29tIFJvb3\
|
||||
QgQ0EwHhcNMTkwMzEzMTMxNTM1WhcNMjAwMzEyMTMxNTM1WjAqMSgwJgYDV\
|
||||
QQDDB9sb2dpbi5kZXZvbHV0aW9ucy5jb20gQXV0aG9yaXR5MIIBIjANBgkq\
|
||||
hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlbRwXVPc/WH4t/Yti5qv24pAu8Q\
|
||||
m0eOVvbum23bYtfJDbCSDh7sY/vvQXgIkM8/0C3tFZ3XaXHbyDHAMn6OC+S\
|
||||
Obzs6SjpfKk9s69Yo/aWFl9oRnAK/+dZ0Y6MTdZO1w+PpR81q5QOFMLpWX1\
|
||||
YNdahaZec31sBmsHqlW04OrHUhGOTGdWNots9/PWvN//x++FL+Sqgh/jxF7\
|
||||
khbgfAuz1QKa8P0ZlE4cOcRIs5bSnUFwtoytKH02/YZnCJD7I/iXFuCPV/+\
|
||||
LZO6yobkTREE3npeXvAKr1OKF2F0JVORMhHiYyguh9t3bMwHTCFqmfQkIMD\
|
||||
GjaTJD7bd8y2Au+eDzgwIDAQABo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDV\
|
||||
R0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQUVgFJTohOGd8MGh4M9vOP7GAf\
|
||||
TWgwHwYDVR0jBBgwFoAU42BA1coGHUUPUSeacQfTzicjosgwDQYJKoZIhvc\
|
||||
NAQELBQADggEBAKyyDs+uIughmloEmlf8s1cSP8cLtC1Di2TfYSG0bpEM3B\
|
||||
EPTond/7ujDlv0eug9NRurvWd5v7bWvy9VlJo+x2rLBmkzaNcBSVHZ4UbFU\
|
||||
90MSvHjxNZ7VbUfbWsJVeaYHtqf1m3z0fYT0tUor3chD+wbSqraWw4+t54h\
|
||||
fJl22jExTWS9X0F5/Gf3LQOiOvtjHP+b3VkpXkEPIBbvIO/X6kgoGDLm/lA\
|
||||
IPdZmpI956z5+acLHu3AQkxNXQPzCjSSdJphLVU1XeHXOMWldVtE9BqSMVI\
|
||||
HZ6oCz/FtMA4F6R7WiVXXGR+ywRwFyeiFoRea2ImUK9TRWFsaXKeOBMm+TL\
|
||||
bk=";
|
||||
|
||||
const X509_CERT_2: &str = "MIIDRDCCAiygAwIBAgIUCAKwhsjTttdG4koEAV7zqlnI7wkwDQYJKoZIhvc\
|
||||
NAQELBQAwKDEmMCQGA1UEAwwdbG9naW4uZGV2b2x1dGlvbnMuY29tIFJvb3\
|
||||
QgQ0EwHhcNMTkwMzEzMTMxNTM1WhcNMjQwMzExMTMxNTM1WjAoMSYwJAYDV\
|
||||
QQDDB1sb2dpbi5kZXZvbHV0aW9ucy5jb20gUm9vdCBDQTCCASIwDQYJKoZI\
|
||||
hvcNAQEBBQADggEPADCCAQoCggEBANRZxxg9eTCMVr4DsIUcytQOLnlZ7tl\
|
||||
uliP+jM76mjJEuWqizHzZ1ZoPcEbdW9sV8kgWdPHL3KOlXAr0DEobnhQsNx\
|
||||
uzJ8B73TcV7AKp2HR+xCTKPEha1gVHgQMmzQyCIgLEsdcjhsFeFYqMflELZ\
|
||||
rMy+7DBSZWWf3wCnxiKbzTL01wKqylVWeSiXsniTpsoUSSk8Fe2/Li8dBMY\
|
||||
he1vTb57GI8ta24P4lfJv6CPTNTVsr+6ue3lRuY/UIMNTybhBSc00qbuo0K\
|
||||
ahWHyzDgY+iNEaALbyWeNOoTBQIO8lp4mhHcO/Znh2PxdqCi/FSCB2+A1Xd\
|
||||
uOArn+MKegU5aVJN0CAwEAAaNmMGQwEgYDVR0TAQH/BAgwBgEB/wIBAjAOB\
|
||||
gNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFONgQNXKBh1FD1EnmnEH084nI6LI\
|
||||
MB8GA1UdIwQYMBaAFONgQNXKBh1FD1EnmnEH084nI6LIMA0GCSqGSIb3DQE\
|
||||
BCwUAA4IBAQB+v34Vk/+qQgA7eWlczWNVWM0J67om+QwtMEo+VgzE2OHNID\
|
||||
2o5QXsxcck0j8dANutkoqsUXpos/RG+QPNng5RBWA/sWUYWdfwZgrE30rBK\
|
||||
waP8Yi8gVsZpz3/RClbPcfkUXI12ANw3bRI1TscOK165p1TV6nmeEus5LZq\
|
||||
CJV37/WRt47CccsDNZaqSN7T5lQ045jsZVYpfgx/I1l9Q/fICrTOFwqYbXJ\
|
||||
9DTe1v8C+LFbtTNcEzRGwZefLTNH2yuZjGy1/t4+cnmFJUzmC4abOoZcpkr\
|
||||
z6U68caCbQA+wdmFs4XaO2bFaiyM+m0LVMOQfLuX/0RZc2KB7fAbb7oHQl";
|
||||
|
||||
fn get_jwk_set() -> JwkSet {
|
||||
JwkSet {
|
||||
keys: vec![Jwk {
|
||||
alg: Some(Jwa::Sig(JwsAlg::RS256)),
|
||||
key_ops: Some(vec![JwkKeyOps::Verify]),
|
||||
kid: Some("bG9naW4uZGV2b2x1dGlvbnMuY29tIFRva2VuLk1hciAxMyAxMzoxNTozNSAyMDE5IEdNVA".to_owned()),
|
||||
x5t: Some(X509_SHA1_THUMBPRINT.to_owned()),
|
||||
x5c: Some(vec![
|
||||
X509_CERT_0.to_owned(),
|
||||
X509_CERT_1.to_owned(),
|
||||
X509_CERT_2.to_owned(),
|
||||
]),
|
||||
..Jwk::new(JwkKeyType::new_rsa_key_from_base64_url(
|
||||
RSA_MODULUS.into(),
|
||||
RSA_PUBLIC_EXPONENT.into(),
|
||||
))
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rsa_key() {
|
||||
let expected = get_jwk_set();
|
||||
let decoded = JwkSet::from_json(picky_test_data::JOSE_JWK_SET).unwrap();
|
||||
pretty_assertions::assert_eq!(decoded, expected);
|
||||
|
||||
let encoded = expected.to_json_pretty().unwrap();
|
||||
let decoded = JwkSet::from_json(&encoded).unwrap();
|
||||
pretty_assertions::assert_eq!(decoded, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(picky_test_data::JOSE_JWK_EC_P256_JSON)]
|
||||
#[case(picky_test_data::JOSE_JWK_EC_P384_JSON)]
|
||||
#[case(picky_test_data::JOSE_JWK_EC_P521_JSON)]
|
||||
fn ecdsa_key_roundtrip(#[case] json: &str) {
|
||||
let decoded = Jwk::from_json(json).unwrap();
|
||||
let encoded = decoded.to_json().unwrap();
|
||||
pretty_assertions::assert_eq!(encoded, json);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(picky_test_data::JOSE_JWK_ED25519_JSON)]
|
||||
#[case(picky_test_data::JOSE_JWK_X25519_JSON)]
|
||||
fn ed_key_roundtrip(#[case] json: &str) {
|
||||
let decoded = Jwk::from_json(json).unwrap();
|
||||
let encoded = decoded.to_json().unwrap();
|
||||
pretty_assertions::assert_eq!(encoded, json);
|
||||
}
|
||||
|
||||
const PUBLIC_KEY_PEM: &str = r#"-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA61BjmfXGEvWmegnBGSuS
|
||||
+rU9soUg2FnODva32D1AqhwdziwHINFaD1MVlcrYG6XRKfkcxnaXGfFDWHLEvNBS
|
||||
EVCgJjtHAGZIm5GL/KA86KDp/CwDFMSwluowcXwDwoyinmeOY9eKyh6aY72xJh7n
|
||||
oLBBq1N0bWi1e2i+83txOCg4yV2oVXhBo8pYEJ8LT3el6Smxol3C1oFMVdwPgc0v
|
||||
Tl25XucMcG/ALE/KNY6pqC2AQ6R2ERlVgPiUWOPatVkt7+Bs3h5Ramxh7XjBOXeu
|
||||
lmCpGSynXNcpZ/06+vofGi/2MlpQZNhHAo8eayMp6FcvNucIpUndo1X8dKMv3Y26
|
||||
ZQIDAQAB
|
||||
-----END PUBLIC KEY-----"#;
|
||||
|
||||
#[test]
|
||||
fn x509_and_jwk_conversion_rsa() {
|
||||
let initial_key = PublicKey::from_pem(&PUBLIC_KEY_PEM.parse::<Pem>().expect("pem")).expect("public key");
|
||||
let jwk = Jwk::from_public_key(&initial_key).unwrap();
|
||||
if let JwkKeyType::Rsa(rsa_key) = &jwk.key {
|
||||
let modulus = general_purpose::URL_SAFE_NO_PAD.decode(&rsa_key.n).unwrap();
|
||||
assert_ne!(modulus[0], 0x00);
|
||||
let public_exponent = general_purpose::URL_SAFE_NO_PAD.decode(&rsa_key.e).unwrap();
|
||||
assert_ne!(public_exponent[0], 0x00);
|
||||
} else {
|
||||
panic!("Unexpected key type");
|
||||
}
|
||||
let from_jwk_key = jwk.to_public_key().unwrap();
|
||||
assert_eq!(from_jwk_key, initial_key);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(picky_test_data::EC_NIST256_PK_1_PUB)]
|
||||
#[case(picky_test_data::EC_NIST384_PK_1_PUB)]
|
||||
fn x509_and_jwk_conversion_ec(#[case] pem: &str) {
|
||||
let initial_key = PublicKey::from_pem(&pem.parse::<Pem>().expect("pem")).expect("public key");
|
||||
let jwk = Jwk::from_public_key(&initial_key).unwrap();
|
||||
if let JwkKeyType::Ec(rsa_key) = &jwk.key {
|
||||
let x = general_purpose::URL_SAFE_NO_PAD.decode(&rsa_key.x).unwrap();
|
||||
assert_ne!(x[0], 0x00);
|
||||
let y = general_purpose::URL_SAFE_NO_PAD.decode(&rsa_key.y).unwrap();
|
||||
assert_ne!(y[0], 0x00);
|
||||
} else {
|
||||
panic!("Unexpected key type");
|
||||
}
|
||||
let from_jwk_key = jwk.to_public_key().unwrap();
|
||||
assert_eq!(from_jwk_key, initial_key);
|
||||
}
|
||||
}
|
||||
Vendored
+668
@@ -0,0 +1,668 @@
|
||||
//! JSON Web Signature (JWS) represents content secured with digital signatures or Message Authentication Codes (MACs) using JSON-based data structures.
|
||||
//!
|
||||
//! See [RFC7515](https://tools.ietf.org/html/rfc7515).
|
||||
|
||||
use crate::hash::HashAlgorithm;
|
||||
use crate::jose::jwk::Jwk;
|
||||
use crate::key::{EcCurve, PrivateKey, PublicKey};
|
||||
use crate::signature::{SignatureAlgorithm, SignatureError};
|
||||
use base64::engine::general_purpose;
|
||||
use base64::{DecodeError, Engine as _};
|
||||
use picky_asn1::wrapper::IntegerAsn1;
|
||||
use picky_asn1_x509::signature::EcdsaSignatureValue;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use thiserror::Error;
|
||||
|
||||
// === error type === //
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum JwsError {
|
||||
/// RSA error
|
||||
#[error("RSA error: {context}")]
|
||||
Rsa { context: String },
|
||||
|
||||
/// Json error
|
||||
#[error("JSON error: {source}")]
|
||||
Json { source: serde_json::Error },
|
||||
|
||||
/// signature error
|
||||
#[error("signature error: {source}")]
|
||||
Signature { source: SignatureError },
|
||||
|
||||
/// invalid token encoding
|
||||
#[error("input isn't a valid token string: {input}")]
|
||||
InvalidEncoding { input: String },
|
||||
|
||||
/// couldn't decode base64
|
||||
#[error("couldn't decode base64: {source}")]
|
||||
Base64Decoding { source: DecodeError },
|
||||
|
||||
/// input isn't valid utf8
|
||||
#[error("input isn't valid utf8: {source}, input: {input:?}")]
|
||||
InvalidUtf8 {
|
||||
source: std::string::FromUtf8Error,
|
||||
input: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<rsa::errors::Error> for JwsError {
|
||||
fn from(e: rsa::errors::Error) -> Self {
|
||||
Self::Rsa { context: e.to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for JwsError {
|
||||
fn from(e: serde_json::Error) -> Self {
|
||||
Self::Json { source: e }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SignatureError> for JwsError {
|
||||
fn from(e: SignatureError) -> Self {
|
||||
Self::Signature { source: e }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DecodeError> for JwsError {
|
||||
fn from(e: DecodeError) -> Self {
|
||||
Self::Base64Decoding { source: e }
|
||||
}
|
||||
}
|
||||
|
||||
// === JWS algorithms === //
|
||||
|
||||
mod jws_alg {
|
||||
// We use #[deprecated] for `JwsAlg::ED22519` to tell users that this algorithm is invalid
|
||||
// (`JwsAlg::EdDSA` should be used instead) but we should keep it inside enum to allow decoding
|
||||
// of invalid tokens generated by other libraries such as `golang-jws`. However, to avoid
|
||||
// compilation warnings caused by enum derives, we need to allow deprecated items inside this
|
||||
// module.
|
||||
#![allow(deprecated)]
|
||||
|
||||
use super::*;
|
||||
|
||||
/// `alg` header parameter values for JWS
|
||||
///
|
||||
/// [JSON Web Algorithms (JWA) draft-ietf-jose-json-web-algorithms-40 #3](https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40#section-3.1)
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum JwsAlg {
|
||||
/// HMAC using SHA-256 (unsupported)
|
||||
///
|
||||
/// Required by RFC
|
||||
HS256,
|
||||
|
||||
/// HMAC using SHA-384 (unsupported)
|
||||
HS384,
|
||||
|
||||
/// HMAC using SHA-512 (unsupported)
|
||||
HS512,
|
||||
|
||||
/// RSASSA-PKCS-v1_5 using SHA-256
|
||||
///
|
||||
/// Recommended by RFC
|
||||
RS256,
|
||||
|
||||
/// RSASSA-PKCS-v1_5 using SHA-384
|
||||
RS384,
|
||||
|
||||
/// RSASSA-PKCS-v1_5 using SHA-512
|
||||
RS512,
|
||||
|
||||
/// ECDSA using P-256 and SHA-256
|
||||
///
|
||||
/// Recommended+ by RFC
|
||||
ES256,
|
||||
|
||||
/// ECDSA using P-384 and SHA-384
|
||||
ES384,
|
||||
|
||||
/// ECDSA using P-521 and SHA-512
|
||||
ES512,
|
||||
|
||||
/// RSASSA-PSS using SHA-256 and MGF1 with SHA-256 (unsupported)
|
||||
PS256,
|
||||
|
||||
/// RSASSA-PSS using SHA-384 and MGF1 with SHA-384 (unsupported)
|
||||
PS384,
|
||||
|
||||
/// RSASSA-PSS using SHA-512 and MGF1 with SHA-512 (unsupported)
|
||||
PS512,
|
||||
|
||||
/// EdDSA using Ed25519/Ed448
|
||||
EdDSA,
|
||||
|
||||
/// [DO NOT USE] EdDSA using Ed25519
|
||||
///
|
||||
/// This value is used by some popular libraries (e.g. `golang-jwt) instead of `EdDSA` due to
|
||||
/// mistake in the implementation. This value is deprecated and should not be used.
|
||||
#[deprecated(note = "You should not use this value, but it may appear in the wild")]
|
||||
ED25519,
|
||||
}
|
||||
}
|
||||
// Hack to localize #![allow(deprecated)]
|
||||
pub use jws_alg::JwsAlg;
|
||||
|
||||
impl TryFrom<SignatureAlgorithm> for JwsAlg {
|
||||
type Error = SignatureError;
|
||||
|
||||
fn try_from(v: SignatureAlgorithm) -> Result<Self, Self::Error> {
|
||||
match v {
|
||||
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256) => Ok(Self::RS256),
|
||||
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_384) => Ok(Self::RS384),
|
||||
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_512) => Ok(Self::RS512),
|
||||
SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_256) => Ok(Self::ES256),
|
||||
SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_384) => Ok(Self::ES384),
|
||||
SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_512) => Ok(Self::ES512),
|
||||
SignatureAlgorithm::Ed25519 => Ok(Self::EdDSA),
|
||||
unsupported => Err(SignatureError::UnsupportedAlgorithm {
|
||||
algorithm: format!("{unsupported:?}"),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<JwsAlg> for SignatureAlgorithm {
|
||||
type Error = SignatureError;
|
||||
|
||||
fn try_from(v: JwsAlg) -> Result<Self, Self::Error> {
|
||||
match v {
|
||||
JwsAlg::RS256 => Ok(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256)),
|
||||
JwsAlg::RS384 => Ok(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_384)),
|
||||
JwsAlg::RS512 => Ok(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_512)),
|
||||
JwsAlg::ES256 => Ok(SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_256)),
|
||||
JwsAlg::ES384 => Ok(SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_384)),
|
||||
JwsAlg::ES512 => Ok(SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_512)),
|
||||
JwsAlg::EdDSA => Ok(SignatureAlgorithm::Ed25519),
|
||||
#[allow(deprecated)]
|
||||
JwsAlg::ED25519 => Ok(SignatureAlgorithm::Ed25519),
|
||||
unsupported => Err(SignatureError::UnsupportedAlgorithm {
|
||||
algorithm: format!("{unsupported:?}"),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === JWS header === //
|
||||
|
||||
/// JOSE header of a JWS
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct JwsHeader {
|
||||
// -- specific to JWS -- //
|
||||
/// Algorithm Header
|
||||
///
|
||||
/// identifies the cryptographic algorithm used to secure the JWS.
|
||||
pub alg: JwsAlg,
|
||||
|
||||
// -- common with JWE -- //
|
||||
/// JWK Set URL
|
||||
///
|
||||
/// URI that refers to a resource for a set of JSON-encoded public keys,
|
||||
/// one of which corresponds to the key used to digitally sign the JWS.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub jku: Option<String>,
|
||||
|
||||
/// JSON Web Key
|
||||
///
|
||||
/// The public key that corresponds to the key used to digitally sign the JWS.
|
||||
/// This key is represented as a JSON Web Key (JWK).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub jwk: Option<Jwk>,
|
||||
|
||||
/// Type header
|
||||
///
|
||||
/// Used by JWS applications to declare the media type [IANA.MediaTypes] of this complete JWS.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub typ: Option<String>,
|
||||
|
||||
/// Content Type header
|
||||
///
|
||||
/// Used by JWS applications to declare the media type [IANA.MediaTypes] of the secured content (the payload).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cty: Option<String>,
|
||||
|
||||
// -- common with all -- //
|
||||
/// Key ID Header
|
||||
///
|
||||
/// A hint indicating which key was used.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub kid: Option<String>,
|
||||
|
||||
/// X.509 URL Header
|
||||
///
|
||||
/// URI that refers to a resource for an X.509 public key certificate or certificate chain.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub x5u: Option<String>,
|
||||
|
||||
/// X.509 Certificate Chain
|
||||
///
|
||||
/// Chain of one or more PKIX certificates.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub x5c: Option<Vec<String>>,
|
||||
|
||||
/// X.509 Certificate SHA-1 Thumbprint
|
||||
///
|
||||
/// base64url-encoded SHA-1 thumbprint (a.k.a. digest) of the DER encoding of an X.509 certificate.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub x5t: Option<String>,
|
||||
|
||||
/// X.509 Certificate SHA-256 Thumbprint
|
||||
///
|
||||
/// base64url-encoded SHA-256 thumbprint (a.k.a. digest) of the DER encoding of an X.509 certificate.
|
||||
#[serde(rename = "x5t#S256", alias = "x5t#s256", skip_serializing_if = "Option::is_none")]
|
||||
pub x5t_s256: Option<String>,
|
||||
|
||||
// -- extra parameters -- //
|
||||
/// Additional header parameters (both public and private)
|
||||
#[serde(flatten)]
|
||||
pub additional: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
impl JwsHeader {
|
||||
pub fn new(alg: JwsAlg) -> Self {
|
||||
Self {
|
||||
alg,
|
||||
jku: None,
|
||||
jwk: None,
|
||||
typ: None,
|
||||
cty: None,
|
||||
kid: None,
|
||||
x5u: None,
|
||||
x5c: None,
|
||||
x5t: None,
|
||||
x5t_s256: None,
|
||||
additional: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_cty(alg: JwsAlg, cty: impl Into<String>) -> Self {
|
||||
Self {
|
||||
cty: Some(cty.into()),
|
||||
..Self::new(alg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === json web signature === //
|
||||
|
||||
/// Provides an API to sign any kind of data (binary). JSON claims are part of `Jwt` only.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Jws {
|
||||
pub header: JwsHeader,
|
||||
pub payload: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Jws {
|
||||
pub fn new(alg: JwsAlg, payload: Vec<u8>) -> Self {
|
||||
Self {
|
||||
header: JwsHeader::new(alg),
|
||||
payload,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encode(&self, private_key: &PrivateKey) -> Result<String, JwsError> {
|
||||
let header_base64 = general_purpose::URL_SAFE_NO_PAD.encode(serde_json::to_vec(&self.header)?);
|
||||
let payload_base64 = general_purpose::URL_SAFE_NO_PAD.encode(&self.payload);
|
||||
let header_and_payload = [header_base64, payload_base64].join(".");
|
||||
let signature_algo = SignatureAlgorithm::try_from(self.header.alg)?;
|
||||
let signature = signature_algo.sign(header_and_payload.as_bytes(), private_key)?;
|
||||
|
||||
let signature = match self.header.alg {
|
||||
// EC sugnatures have special encoding rules (RFC 7518, section 3.4)
|
||||
JwsAlg::ES256 | JwsAlg::ES384 | JwsAlg::ES512 => {
|
||||
// Parse signature as ASN.1 DER sequence
|
||||
let signature: EcdsaSignatureValue =
|
||||
picky_asn1_der::from_bytes(&signature).map_err(|e| SignatureError::Ec {
|
||||
context: format!("Invalid EC DER signature encoding: {e}"),
|
||||
})?;
|
||||
|
||||
let curve = match self.header.alg {
|
||||
JwsAlg::ES256 => EcCurve::NistP256,
|
||||
JwsAlg::ES384 => EcCurve::NistP384,
|
||||
JwsAlg::ES512 => EcCurve::NistP521,
|
||||
_ => unreachable!("Checked in match above"),
|
||||
};
|
||||
|
||||
let signature_component_size = curve.field_bytes_size();
|
||||
|
||||
let r = signature.r.as_unsigned_bytes_be();
|
||||
let s = signature.s.as_unsigned_bytes_be();
|
||||
|
||||
// We should add zero padding (leading zeros) for R & S components to match the
|
||||
// size of the curve, as ASN.1 DER encoding removes leading zeros.
|
||||
let mut jws_signature = Vec::with_capacity(signature_component_size * 2);
|
||||
|
||||
let r_padding = signature_component_size - r.len();
|
||||
(0..r_padding).for_each(|_| jws_signature.push(0));
|
||||
jws_signature.extend_from_slice(r);
|
||||
|
||||
let s_padding = signature_component_size - s.len();
|
||||
(0..s_padding).for_each(|_| jws_signature.push(0));
|
||||
jws_signature.extend_from_slice(s);
|
||||
|
||||
jws_signature
|
||||
}
|
||||
_ => signature,
|
||||
};
|
||||
|
||||
let signature_base64 = general_purpose::URL_SAFE_NO_PAD.encode(signature);
|
||||
Ok([header_and_payload, signature_base64].join("."))
|
||||
}
|
||||
|
||||
/// Verifies signature and returns decoded JWS payload.
|
||||
pub fn decode(encoded_token: &str, public_key: &PublicKey) -> Result<Self, JwsError> {
|
||||
RawJws::decode(encoded_token).and_then(|raw_jws| raw_jws.verify(public_key))
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw low-level interface to the yet to be verified JWS token.
|
||||
///
|
||||
/// This is useful to inspect the structure before performing further processing.
|
||||
/// For most usecases, use `Jws` directly.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RawJws<'repr> {
|
||||
pub compact_repr: Cow<'repr, str>,
|
||||
pub header: JwsHeader,
|
||||
payload: Vec<u8>,
|
||||
pub signature: Vec<u8>,
|
||||
}
|
||||
|
||||
/// An owned `RawJws` for convenience.
|
||||
pub type OwnedRawJws = RawJws<'static>;
|
||||
|
||||
impl<'repr> RawJws<'repr> {
|
||||
/// Decodes a JWS in compact representation.
|
||||
pub fn decode(compact_repr: impl Into<Cow<'repr, str>>) -> Result<Self, JwsError> {
|
||||
decode_impl(compact_repr.into())
|
||||
}
|
||||
|
||||
/// Peeks the payload before signature verification.
|
||||
pub fn peek_payload(&self) -> &[u8] {
|
||||
&self.payload
|
||||
}
|
||||
|
||||
/// Verifies signature and returns a verified `Jws` structure.
|
||||
pub fn verify(self, public_key: &PublicKey) -> Result<Jws, JwsError> {
|
||||
verify_signature(&self.compact_repr, public_key, self.header.alg)?;
|
||||
Ok(self.discard_signature())
|
||||
}
|
||||
|
||||
/// Discards the signature without verifying it and hands a `Jws` structure.
|
||||
///
|
||||
/// Generally, you should not do that.
|
||||
pub fn discard_signature(self) -> Jws {
|
||||
Jws {
|
||||
header: self.header,
|
||||
payload: self.payload,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_impl(compact_repr: Cow<'_, str>) -> Result<RawJws<'_>, JwsError> {
|
||||
let first_dot_idx = compact_repr.find('.').ok_or_else(|| JwsError::InvalidEncoding {
|
||||
input: compact_repr.clone().into_owned(),
|
||||
})?;
|
||||
|
||||
let last_dot_idx = compact_repr.rfind('.').ok_or_else(|| JwsError::InvalidEncoding {
|
||||
input: compact_repr.clone().into_owned(),
|
||||
})?;
|
||||
|
||||
if first_dot_idx == last_dot_idx || compact_repr.starts_with('.') || compact_repr.ends_with('.') {
|
||||
return Err(JwsError::InvalidEncoding {
|
||||
input: compact_repr.into_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
let header_json = general_purpose::URL_SAFE_NO_PAD.decode(&compact_repr[..first_dot_idx])?;
|
||||
let header = serde_json::from_slice::<JwsHeader>(&header_json)?;
|
||||
|
||||
let signature = general_purpose::URL_SAFE_NO_PAD.decode(&compact_repr[last_dot_idx + 1..])?;
|
||||
|
||||
let payload = general_purpose::URL_SAFE_NO_PAD.decode(&compact_repr[first_dot_idx + 1..last_dot_idx])?;
|
||||
|
||||
Ok(RawJws {
|
||||
compact_repr,
|
||||
header,
|
||||
payload,
|
||||
signature,
|
||||
})
|
||||
}
|
||||
|
||||
/// JWS verification primitive
|
||||
pub fn verify_signature(encoded_token: &str, public_key: &PublicKey, algorithm: JwsAlg) -> Result<(), JwsError> {
|
||||
let last_dot_idx = encoded_token.rfind('.').ok_or_else(|| JwsError::InvalidEncoding {
|
||||
input: encoded_token.to_owned(),
|
||||
})?;
|
||||
|
||||
if encoded_token.ends_with('.') {
|
||||
return Err(JwsError::InvalidEncoding {
|
||||
input: encoded_token.to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
let signature = general_purpose::URL_SAFE_NO_PAD.decode(&encoded_token[last_dot_idx + 1..])?;
|
||||
let signature_algo = SignatureAlgorithm::try_from(algorithm)?;
|
||||
|
||||
let signature = match algorithm {
|
||||
// Special decoding rules for ECDSA
|
||||
JwsAlg::ES256 | JwsAlg::ES384 | JwsAlg::ES512 => {
|
||||
let curve = match algorithm {
|
||||
JwsAlg::ES256 => EcCurve::NistP256,
|
||||
JwsAlg::ES384 => EcCurve::NistP384,
|
||||
JwsAlg::ES512 => EcCurve::NistP521,
|
||||
_ => unreachable!("Checked in match above"),
|
||||
};
|
||||
|
||||
let component_size = curve.field_bytes_size();
|
||||
let jws_encoded_signature_size = component_size * 2;
|
||||
|
||||
if signature.len() != jws_encoded_signature_size {
|
||||
return Err(SignatureError::Ec {
|
||||
context: format!(
|
||||
"Invalid JWS EC signature size. Expected: {}; Actual: {}",
|
||||
jws_encoded_signature_size,
|
||||
signature.len()
|
||||
),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
|
||||
let (r, s) = signature.split_at(component_size);
|
||||
|
||||
let signature = EcdsaSignatureValue {
|
||||
r: IntegerAsn1::from_bytes_be_unsigned(r.to_vec()),
|
||||
s: IntegerAsn1::from_bytes_be_unsigned(s.to_vec()),
|
||||
};
|
||||
|
||||
picky_asn1_der::to_vec(&signature).map_err(|e| SignatureError::Ec {
|
||||
context: format!("Failed to encode EC signature to DER format: {e}"),
|
||||
})?
|
||||
}
|
||||
_ => signature,
|
||||
};
|
||||
|
||||
signature_algo.verify(public_key, &encoded_token.as_bytes()[..last_dot_idx], &signature)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::pem::Pem;
|
||||
use rstest::rstest;
|
||||
|
||||
const PAYLOAD: &str = r#"{"sub":"1234567890","name":"John Doe","admin":true,"iat":1516239022}"#;
|
||||
|
||||
fn get_private_key_1() -> PrivateKey {
|
||||
let pk_pem = picky_test_data::RSA_2048_PK_1.parse::<Pem>().unwrap();
|
||||
PrivateKey::from_pem(&pk_pem).unwrap()
|
||||
}
|
||||
|
||||
fn get_private_key_2() -> PrivateKey {
|
||||
let pk_pem = picky_test_data::RSA_2048_PK_7.parse::<Pem>().unwrap();
|
||||
PrivateKey::from_pem(&pk_pem).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_rsa_sha256() {
|
||||
let jwt = Jws {
|
||||
header: JwsHeader {
|
||||
typ: Some(String::from("JWT")),
|
||||
..JwsHeader::new(JwsAlg::RS256)
|
||||
},
|
||||
payload: PAYLOAD.as_bytes().to_vec(),
|
||||
};
|
||||
let encoded = jwt.encode(&get_private_key_1()).unwrap();
|
||||
assert_eq!(encoded, picky_test_data::JOSE_JWT_SIG_EXAMPLE);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(JwsAlg::ES256, picky_test_data::EC_NIST256_PK_1)]
|
||||
#[case(JwsAlg::ES384, picky_test_data::EC_NIST384_PK_1)]
|
||||
#[case(JwsAlg::ES512, picky_test_data::EC_NIST521_PK_1)]
|
||||
fn ecdsa_sign_verify(#[case] alg: JwsAlg, #[case] key_pem: &str) {
|
||||
let key = PrivateKey::from_pem_str(key_pem).unwrap();
|
||||
|
||||
let jwt = Jws {
|
||||
header: JwsHeader {
|
||||
typ: Some(String::from("JWT")),
|
||||
..JwsHeader::new(alg)
|
||||
},
|
||||
payload: PAYLOAD.as_bytes().to_vec(),
|
||||
};
|
||||
|
||||
// Check encode + sign
|
||||
let encoded = jwt.encode(&key).unwrap();
|
||||
|
||||
// Check decode + verify
|
||||
let jws = RawJws::decode(&encoded).unwrap();
|
||||
jws.clone().verify(&key.to_public_key().unwrap()).unwrap();
|
||||
|
||||
assert_eq!(&jws.header, &jwt.header);
|
||||
assert_eq!(&jws.payload, &jwt.payload);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(picky_test_data::EC_NIST256_PK_1, picky_test_data::JOSE_JWT_SIG_ES256)]
|
||||
#[case(picky_test_data::EC_NIST384_PK_1, picky_test_data::JOSE_JWT_SIG_ES384)]
|
||||
#[case(picky_test_data::EC_NIST521_PK_1, picky_test_data::JOSE_JWT_SIG_ES512)]
|
||||
fn ecdsa_parse_and_verify(#[case] key_pem: &str, #[case] signature: &str) {
|
||||
let key = PrivateKey::from_pem_str(key_pem).unwrap();
|
||||
|
||||
// Check decode + verify
|
||||
let jws = RawJws::decode(signature).unwrap();
|
||||
jws.clone().verify(&key.to_public_key().unwrap()).unwrap();
|
||||
}
|
||||
|
||||
const JWT_ED25519_BODY: &str = r#"{"username":"kataras"}"#;
|
||||
const JWT_ED25519_GO_BODY: &str = r#"{"foo":"bar"}"#;
|
||||
|
||||
/// Validate that invalid header with alg set to "ED25519" could be parsed by picky
|
||||
#[rstest]
|
||||
#[allow(deprecated)]
|
||||
#[case(
|
||||
picky_test_data::JOSE_JWT_SIG_ED25519_GO_PRIVATE_KEY,
|
||||
picky_test_data::JOSE_JWT_SIG_ED25519_GO,
|
||||
JwsAlg::ED25519,
|
||||
JWT_ED25519_GO_BODY
|
||||
)]
|
||||
#[case(
|
||||
picky_test_data::JOSE_JWT_SIG_ED25519_PRIVATE_KEY,
|
||||
picky_test_data::JOSE_JWT_SIG_ED25519,
|
||||
JwsAlg::EdDSA,
|
||||
JWT_ED25519_BODY
|
||||
)]
|
||||
fn ed25519_algorithm(#[case] key: &str, #[case] encoded_expected: &str, #[case] alg: JwsAlg, #[case] body: &str) {
|
||||
let key = PrivateKey::from_pem_str(key).unwrap();
|
||||
|
||||
let jwt = Jws {
|
||||
header: JwsHeader {
|
||||
typ: Some(String::from("JWT")),
|
||||
..JwsHeader::new(alg)
|
||||
},
|
||||
payload: body.as_bytes().to_vec(),
|
||||
};
|
||||
|
||||
// Check encode + sign
|
||||
let encoded = jwt.encode(&key).unwrap();
|
||||
assert_eq!(encoded, encoded_expected);
|
||||
|
||||
// Check decode + verify
|
||||
let jws = RawJws::decode(&encoded).unwrap();
|
||||
jws.verify(&key.to_public_key().unwrap()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_rsa_sha256() {
|
||||
let public_key = get_private_key_1().to_public_key().unwrap();
|
||||
let jwt = Jws::decode(picky_test_data::JOSE_JWT_SIG_EXAMPLE, &public_key).unwrap();
|
||||
assert_eq!(jwt.payload.as_slice(), PAYLOAD.as_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_rsa_sha256_delayed_signature_check() {
|
||||
let jws = RawJws::decode(picky_test_data::JOSE_JWT_SIG_EXAMPLE).unwrap();
|
||||
println!("{}", String::from_utf8_lossy(&jws.payload));
|
||||
assert_eq!(jws.peek_payload(), PAYLOAD.as_bytes());
|
||||
|
||||
let public_key = get_private_key_2().to_public_key().unwrap();
|
||||
let err = jws.verify(&public_key).err().unwrap();
|
||||
assert_eq!(err.to_string(), "signature error: invalid signature");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_rsa_sha256_invalid_signature_err() {
|
||||
let public_key = get_private_key_2().to_public_key().unwrap();
|
||||
let err = Jws::decode(picky_test_data::JOSE_JWT_SIG_EXAMPLE, &public_key)
|
||||
.err()
|
||||
.unwrap();
|
||||
assert_eq!(err.to_string(), "signature error: invalid signature");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_invalid_base64_err() {
|
||||
let public_key = get_private_key_1().to_public_key().unwrap();
|
||||
let err = Jws::decode("aieoè~†.tésp.à", &public_key).err().unwrap();
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"couldn\'t decode base64: Invalid symbol 195, offset 4."
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_invalid_json_err() {
|
||||
let public_key = get_private_key_1().to_public_key().unwrap();
|
||||
|
||||
let err = Jws::decode("abc.abc.abc", &public_key).err().unwrap();
|
||||
assert_eq!(err.to_string(), "JSON error: expected value at line 1 column 1");
|
||||
|
||||
let err = Jws::decode("eyAiYWxnIjogIkhTMjU2IH0K.abc.abc", &public_key)
|
||||
.err()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"JSON error: control character (\\u0000-\\u001F) \
|
||||
found while parsing a string at line 2 column 0"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_invalid_encoding_err() {
|
||||
let public_key = get_private_key_1().to_public_key().unwrap();
|
||||
|
||||
let err = Jws::decode(".abc.abc", &public_key).err().unwrap();
|
||||
assert_eq!(err.to_string(), "input isn\'t a valid token string: .abc.abc");
|
||||
|
||||
let err = Jws::decode("abc.abc.", &public_key).err().unwrap();
|
||||
assert_eq!(err.to_string(), "input isn\'t a valid token string: abc.abc.");
|
||||
|
||||
let err = Jws::decode("abc.abc", &public_key).err().unwrap();
|
||||
assert_eq!(err.to_string(), "input isn\'t a valid token string: abc.abc");
|
||||
|
||||
let err = Jws::decode("abc", &public_key).err().unwrap();
|
||||
assert_eq!(err.to_string(), "input isn\'t a valid token string: abc");
|
||||
}
|
||||
}
|
||||
Vendored
+690
@@ -0,0 +1,690 @@
|
||||
use super::jwe::Jwe;
|
||||
use crate::jose::jwe::{JweAlg, JweEnc, JweError, JweHeader};
|
||||
use crate::jose::jws::{Jws, JwsAlg, JwsError, JwsHeader};
|
||||
use crate::key::{PrivateKey, PublicKey};
|
||||
use core::fmt;
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use thiserror::Error;
|
||||
|
||||
// === error type === //
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum JwtError {
|
||||
/// JWS error
|
||||
#[error("JWS error: {source}")]
|
||||
Jws { source: JwsError },
|
||||
|
||||
/// JWE error
|
||||
#[error("JWE error: {source}")]
|
||||
Jwe { source: JweError },
|
||||
|
||||
/// Json error
|
||||
#[error("JSON error: {source}")]
|
||||
Json { source: serde_json::Error },
|
||||
|
||||
/// registered claim type is invalid
|
||||
#[error("registered claim `{claim}` has invalid type")]
|
||||
InvalidRegisteredClaimType { claim: &'static str },
|
||||
|
||||
/// a required claim is missing
|
||||
#[error("required claim `{claim}` is missing")]
|
||||
RequiredClaimMissing { claim: &'static str },
|
||||
|
||||
/// token not yet valid
|
||||
#[error("token not yet valid (not before: {}, now: {} [leeway: {}])", not_before, now.numeric_date, now.leeway)]
|
||||
NotYetValid { not_before: i64, now: JwtDate },
|
||||
|
||||
/// token expired
|
||||
#[error("token expired (not after: {}, now: {} [leeway: {}])", not_after, now.numeric_date, now.leeway)]
|
||||
Expired { not_after: i64, now: JwtDate },
|
||||
|
||||
/// validator is invalid
|
||||
#[error("invalid validator: {description}")]
|
||||
InvalidValidator { description: &'static str },
|
||||
}
|
||||
|
||||
impl From<JwsError> for JwtError {
|
||||
fn from(s: JwsError) -> Self {
|
||||
Self::Jws { source: s }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for JwtError {
|
||||
fn from(e: serde_json::Error) -> Self {
|
||||
Self::Json { source: e }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JweError> for JwtError {
|
||||
fn from(s: JweError) -> Self {
|
||||
Self::Jwe { source: s }
|
||||
}
|
||||
}
|
||||
|
||||
// === Validation states === //
|
||||
|
||||
pub struct CheckedState<C> {
|
||||
pub claims: C,
|
||||
}
|
||||
|
||||
impl<C> Clone for CheckedState<C>
|
||||
where
|
||||
C: Clone,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
claims: self.claims.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> fmt::Debug for CheckedState<C>
|
||||
where
|
||||
C: fmt::Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "ValidatedClaims({:?})", self.claims)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct UncheckedState {
|
||||
payload: Vec<u8>,
|
||||
}
|
||||
|
||||
// === JWT date === //
|
||||
|
||||
/// Represent date as defined by [RFC7519](https://tools.ietf.org/html/rfc7519#section-2).
|
||||
///
|
||||
/// A leeway can be configured to account clock skew when comparing with another date.
|
||||
/// Should be small (less than 120).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct JwtDate {
|
||||
pub numeric_date: i64,
|
||||
pub leeway: u16,
|
||||
}
|
||||
|
||||
impl JwtDate {
|
||||
pub const fn new(numeric_date: i64) -> Self {
|
||||
Self {
|
||||
numeric_date,
|
||||
leeway: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn new_with_leeway(numeric_date: i64, leeway: u16) -> Self {
|
||||
Self { numeric_date, leeway }
|
||||
}
|
||||
|
||||
pub const fn is_before(&self, other_numeric_date: i64) -> bool {
|
||||
self.numeric_date <= other_numeric_date + self.leeway as i64
|
||||
}
|
||||
|
||||
pub const fn is_before_strict(&self, other_numeric_date: i64) -> bool {
|
||||
self.numeric_date < other_numeric_date + self.leeway as i64
|
||||
}
|
||||
|
||||
pub const fn is_after(&self, other_numeric_date: i64) -> bool {
|
||||
self.numeric_date >= other_numeric_date - self.leeway as i64
|
||||
}
|
||||
|
||||
pub const fn is_after_strict(&self, other_numeric_date: i64) -> bool {
|
||||
self.numeric_date > other_numeric_date - self.leeway as i64
|
||||
}
|
||||
}
|
||||
|
||||
// === validator === //
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum CheckStrictness {
|
||||
Ignored,
|
||||
Optional,
|
||||
Required,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct JwtValidator {
|
||||
current_date: Option<JwtDate>,
|
||||
expiration_claim: CheckStrictness,
|
||||
not_before_claim: CheckStrictness,
|
||||
}
|
||||
|
||||
pub const NO_CHECK_VALIDATOR: JwtValidator = JwtValidator::no_check();
|
||||
|
||||
impl JwtValidator {
|
||||
/// Check signature and the registered exp and nbf claims. If a claim is missing token is rejected.
|
||||
pub const fn strict(current_date: JwtDate) -> Self {
|
||||
Self {
|
||||
current_date: Some(current_date),
|
||||
expiration_claim: CheckStrictness::Required,
|
||||
not_before_claim: CheckStrictness::Required,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check signature and the registered exp and nbf claims. Token isn't rejected if a claim is missing.
|
||||
pub const fn lenient(current_date: JwtDate) -> Self {
|
||||
Self {
|
||||
current_date: Some(current_date),
|
||||
expiration_claim: CheckStrictness::Optional,
|
||||
not_before_claim: CheckStrictness::Optional,
|
||||
}
|
||||
}
|
||||
|
||||
/// No check.
|
||||
pub const fn no_check() -> Self {
|
||||
Self {
|
||||
current_date: None,
|
||||
expiration_claim: CheckStrictness::Ignored,
|
||||
not_before_claim: CheckStrictness::Ignored,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current_date(self, current_date: JwtDate) -> Self {
|
||||
Self {
|
||||
current_date: Some(current_date),
|
||||
expiration_claim: CheckStrictness::Required,
|
||||
not_before_claim: CheckStrictness::Required,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expiration_check_required(self) -> Self {
|
||||
Self {
|
||||
expiration_claim: CheckStrictness::Required,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expiration_check_optional(self) -> Self {
|
||||
Self {
|
||||
expiration_claim: CheckStrictness::Optional,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expiration_check_ignored(self) -> Self {
|
||||
Self {
|
||||
expiration_claim: CheckStrictness::Ignored,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn not_before_check_required(self) -> Self {
|
||||
Self {
|
||||
not_before_claim: CheckStrictness::Required,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn not_before_check_optional(self) -> Self {
|
||||
Self {
|
||||
not_before_claim: CheckStrictness::Optional,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn not_before_check_ignored(self) -> Self {
|
||||
Self {
|
||||
not_before_claim: CheckStrictness::Ignored,
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === JWT === //
|
||||
|
||||
const JWT_TYPE: &str = "JWT";
|
||||
const EXPIRATION_TIME_CLAIM: &str = "exp";
|
||||
const NOT_BEFORE_CLAIM: &str = "nbf";
|
||||
|
||||
pub struct Jwt<H, State> {
|
||||
pub header: H,
|
||||
pub state: State,
|
||||
}
|
||||
|
||||
pub type JwtSig = Jwt<JwsHeader, UncheckedState>;
|
||||
pub type CheckedJwtSig<C> = Jwt<JwsHeader, CheckedState<C>>;
|
||||
pub type JwtEnc = Jwt<JweHeader, UncheckedState>;
|
||||
pub type CheckedJwtEnc<C> = Jwt<JweHeader, CheckedState<C>>;
|
||||
|
||||
impl<H, State> Clone for Jwt<H, State>
|
||||
where
|
||||
H: Clone,
|
||||
State: Clone,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
header: self.header.clone(),
|
||||
state: self.state.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<H, State> fmt::Debug for Jwt<H, State>
|
||||
where
|
||||
H: fmt::Debug,
|
||||
State: fmt::Debug,
|
||||
{
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
fmt.debug_struct("Jwt")
|
||||
.field("header", &self.header)
|
||||
.field("state", &self.state)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<H, C> Jwt<H, CheckedState<C>> {
|
||||
pub fn new_with_header(header: H, claims: C) -> Self {
|
||||
Jwt {
|
||||
header,
|
||||
state: CheckedState { claims },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> CheckedJwtSig<C> {
|
||||
pub fn new(alg: JwsAlg, claims: C) -> Self {
|
||||
Jwt {
|
||||
header: JwsHeader {
|
||||
typ: Some(JWT_TYPE.to_owned()),
|
||||
..JwsHeader::new(alg)
|
||||
},
|
||||
state: CheckedState { claims },
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_cty(alg: JwsAlg, cty: impl Into<String>, claims: C) -> Self {
|
||||
Jwt {
|
||||
header: JwsHeader {
|
||||
typ: Some(JWT_TYPE.to_owned()),
|
||||
..JwsHeader::new_with_cty(alg, cty)
|
||||
},
|
||||
state: CheckedState { claims },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> CheckedJwtSig<C>
|
||||
where
|
||||
C: Serialize,
|
||||
{
|
||||
pub fn encode(self, private_key: &PrivateKey) -> Result<String, JwtError> {
|
||||
let jws = Jws {
|
||||
header: self.header,
|
||||
payload: serde_json::to_vec(&self.state.claims)?,
|
||||
};
|
||||
let encoded = jws.encode(private_key)?;
|
||||
Ok(encoded)
|
||||
}
|
||||
}
|
||||
|
||||
impl JwtSig {
|
||||
pub fn encode(self, private_key: &PrivateKey) -> Result<String, JwtError> {
|
||||
let jws = Jws {
|
||||
header: self.header,
|
||||
payload: self.state.payload,
|
||||
};
|
||||
let encoded = jws.encode(private_key)?;
|
||||
Ok(encoded)
|
||||
}
|
||||
}
|
||||
|
||||
impl JwtSig {
|
||||
/// Verifies signature and returns decoded JWS payload.
|
||||
pub fn decode(encoded_token: &str, public_key: &PublicKey) -> Result<Self, JwtError> {
|
||||
let jws = Jws::decode(encoded_token, public_key)?;
|
||||
Ok(Self::from(jws))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Jws> for JwtSig {
|
||||
fn from(jws: Jws) -> Self {
|
||||
Self {
|
||||
header: jws.header,
|
||||
state: UncheckedState { payload: jws.payload },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> CheckedJwtEnc<C> {
|
||||
pub fn new(alg: JweAlg, enc: JweEnc, claims: C) -> Self {
|
||||
Jwt {
|
||||
header: JweHeader {
|
||||
typ: Some(JWT_TYPE.to_owned()),
|
||||
..JweHeader::new(alg, enc)
|
||||
},
|
||||
state: CheckedState { claims },
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_cty(alg: JweAlg, enc: JweEnc, cty: impl Into<String>, claims: C) -> Self {
|
||||
Jwt {
|
||||
header: JweHeader {
|
||||
typ: Some(JWT_TYPE.to_owned()),
|
||||
..JweHeader::new_with_cty(alg, enc, cty)
|
||||
},
|
||||
state: CheckedState { claims },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> CheckedJwtEnc<C>
|
||||
where
|
||||
C: Serialize,
|
||||
{
|
||||
/// Encode with CEK encrypted and included in the token using asymmetric cryptography.
|
||||
pub fn encode(self, asymmetric_key: &PublicKey) -> Result<String, JwtError> {
|
||||
let jwe = Jwe {
|
||||
header: self.header,
|
||||
payload: serde_json::to_vec(&self.state.claims)?,
|
||||
};
|
||||
let encoded = jwe.encode(asymmetric_key)?;
|
||||
Ok(encoded)
|
||||
}
|
||||
|
||||
/// Encode with provided CEK (a symmetric key). This will ignore `alg` value and override it with "dir".
|
||||
pub fn encode_direct(self, cek: &[u8]) -> Result<String, JweError> {
|
||||
let jwe = Jwe {
|
||||
header: self.header,
|
||||
payload: serde_json::to_vec(&self.state.claims)?,
|
||||
};
|
||||
let encoded = jwe.encode_direct(cek)?;
|
||||
Ok(encoded)
|
||||
}
|
||||
}
|
||||
|
||||
impl JwtEnc {
|
||||
/// Decode using asymmetric cryptography.
|
||||
pub fn decode(encoded_token: &str, key: &PrivateKey) -> Result<Self, JwtError> {
|
||||
let jwe = Jwe::decode(encoded_token, key)?;
|
||||
Ok(Self::from(jwe))
|
||||
}
|
||||
|
||||
/// Decode with provided CEK (a symmetric key).
|
||||
pub fn decode_direct(encoded_token: &str, cek: &[u8]) -> Result<Self, JwtError> {
|
||||
let jwe = Jwe::decode_direct(encoded_token, cek)?;
|
||||
Ok(Self::from(jwe))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Jwe> for JwtEnc {
|
||||
fn from(jwe: Jwe) -> Self {
|
||||
Self {
|
||||
header: jwe.header,
|
||||
state: UncheckedState { payload: jwe.payload },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<H> Jwt<H, UncheckedState> {
|
||||
/// Validate JWT claims using validator and convert payload to a user-defined typed struct.
|
||||
pub fn validate<C>(self, validator: &JwtValidator) -> Result<Jwt<H, CheckedState<C>>, JwtError>
|
||||
where
|
||||
C: DeserializeOwned,
|
||||
{
|
||||
Ok(Jwt {
|
||||
header: self.header,
|
||||
state: CheckedState {
|
||||
claims: h_decode_and_validate_claims(&self.state.payload, validator)?,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn h_decode_and_validate_claims<C: DeserializeOwned>(
|
||||
claims_json: &[u8],
|
||||
validator: &JwtValidator,
|
||||
) -> Result<C, JwtError> {
|
||||
let claims = match (
|
||||
&validator.current_date,
|
||||
validator.not_before_claim,
|
||||
validator.expiration_claim,
|
||||
) {
|
||||
(None, CheckStrictness::Required, _) | (None, _, CheckStrictness::Required) => {
|
||||
return Err(JwtError::InvalidValidator {
|
||||
description: "current date is missing",
|
||||
});
|
||||
}
|
||||
(Some(current_date), nbf_strictness, exp_strictness) => {
|
||||
let claims = serde_json::from_slice::<serde_json::Value>(claims_json)?;
|
||||
|
||||
let nbf_opt = claims.get(NOT_BEFORE_CLAIM);
|
||||
match (nbf_strictness, nbf_opt) {
|
||||
(CheckStrictness::Ignored, _) | (CheckStrictness::Optional, None) => {}
|
||||
(CheckStrictness::Required, None) => {
|
||||
return Err(JwtError::RequiredClaimMissing {
|
||||
claim: NOT_BEFORE_CLAIM,
|
||||
});
|
||||
}
|
||||
(_, Some(nbf)) => {
|
||||
let nbf_i64 = nbf.as_i64().ok_or(JwtError::InvalidRegisteredClaimType {
|
||||
claim: NOT_BEFORE_CLAIM,
|
||||
})?;
|
||||
if !current_date.is_after(nbf_i64) {
|
||||
return Err(JwtError::NotYetValid {
|
||||
not_before: nbf_i64,
|
||||
now: current_date.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let exp_opt = claims.get(EXPIRATION_TIME_CLAIM);
|
||||
match (exp_strictness, exp_opt) {
|
||||
(CheckStrictness::Ignored, _) | (CheckStrictness::Optional, None) => {}
|
||||
(CheckStrictness::Required, None) => {
|
||||
return Err(JwtError::RequiredClaimMissing {
|
||||
claim: EXPIRATION_TIME_CLAIM,
|
||||
});
|
||||
}
|
||||
(_, Some(exp)) => {
|
||||
let exp_i64 = exp.as_i64().ok_or(JwtError::InvalidRegisteredClaimType {
|
||||
claim: EXPIRATION_TIME_CLAIM,
|
||||
})?;
|
||||
if !current_date.is_before_strict(exp_i64) {
|
||||
return Err(JwtError::Expired {
|
||||
not_after: exp_i64,
|
||||
now: current_date.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
serde_json::value::from_value(claims)?
|
||||
}
|
||||
(None, _, _) => serde_json::from_slice(claims_json)?,
|
||||
};
|
||||
|
||||
Ok(claims)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::jose::jws::RawJws;
|
||||
use crate::pem::Pem;
|
||||
use serde::Deserialize;
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
struct MyClaims {
|
||||
sub: Cow<'static, str>,
|
||||
name: Cow<'static, str>,
|
||||
admin: bool,
|
||||
iat: i32,
|
||||
}
|
||||
|
||||
const fn get_strongly_typed_claims() -> MyClaims {
|
||||
MyClaims {
|
||||
sub: Cow::Borrowed("1234567890"),
|
||||
name: Cow::Borrowed("John Doe"),
|
||||
admin: true,
|
||||
iat: 1516239022,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_private_key_1() -> PrivateKey {
|
||||
let pk_pem = picky_test_data::RSA_2048_PK_1.parse::<Pem>().unwrap();
|
||||
PrivateKey::from_pkcs8(pk_pem.data()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_jws_rsa_sha256() {
|
||||
let claims = get_strongly_typed_claims();
|
||||
let jwt = CheckedJwtSig::new(JwsAlg::RS256, claims);
|
||||
let encoded = jwt.encode(&get_private_key_1()).unwrap();
|
||||
assert_eq!(encoded, picky_test_data::JOSE_JWT_SIG_EXAMPLE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_jws_rsa_sha256() {
|
||||
let public_key = get_private_key_1().to_public_key().unwrap();
|
||||
let jwt = JwtSig::decode(picky_test_data::JOSE_JWT_SIG_EXAMPLE, &public_key)
|
||||
.unwrap()
|
||||
.validate::<MyClaims>(&JwtValidator::no_check())
|
||||
.unwrap();
|
||||
assert_eq!(jwt.state.claims, get_strongly_typed_claims());
|
||||
|
||||
// exp and nbf claims aren't present but this should pass with lenient validator
|
||||
let now = JwtDate::new(0);
|
||||
JwtSig::decode(picky_test_data::JOSE_JWT_SIG_EXAMPLE, &public_key)
|
||||
.unwrap()
|
||||
.validate::<MyClaims>(&JwtValidator::lenient(now))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_jws_invalid_validator_err() {
|
||||
let public_key = get_private_key_1().to_public_key().unwrap();
|
||||
let validator = JwtValidator::no_check()
|
||||
.expiration_check_required()
|
||||
.not_before_check_optional();
|
||||
let err = JwtSig::decode(picky_test_data::JOSE_JWT_SIG_EXAMPLE, &public_key)
|
||||
.unwrap()
|
||||
.validate::<MyClaims>(&validator)
|
||||
.err()
|
||||
.unwrap();
|
||||
assert_eq!(err.to_string(), "invalid validator: current date is missing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_jws_required_claim_missing_err() {
|
||||
let public_key = get_private_key_1().to_public_key().unwrap();
|
||||
let now = JwtDate::new(0);
|
||||
let validator = JwtValidator::strict(now);
|
||||
let err = JwtSig::decode(picky_test_data::JOSE_JWT_SIG_EXAMPLE, &public_key)
|
||||
.unwrap()
|
||||
.validate::<MyClaims>(&validator)
|
||||
.err()
|
||||
.unwrap();
|
||||
assert_eq!(err.to_string(), "required claim `nbf` is missing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_jws_rsa_sha256_using_json_value_claims() {
|
||||
let public_key = get_private_key_1().to_public_key().unwrap();
|
||||
let validator = JwtValidator::no_check();
|
||||
let jwt = JwtSig::decode(picky_test_data::JOSE_JWT_SIG_EXAMPLE, &public_key)
|
||||
.unwrap()
|
||||
.validate::<serde_json::Value>(&validator)
|
||||
.unwrap();
|
||||
assert_eq!(jwt.state.claims["sub"].as_str().expect("sub"), "1234567890");
|
||||
assert_eq!(jwt.state.claims["name"].as_str().expect("name"), "John Doe");
|
||||
assert!(jwt.state.claims["admin"].as_bool().expect("sub"));
|
||||
assert_eq!(jwt.state.claims["iat"].as_i64().expect("iat"), 1516239022);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jwe_direct_aes_256_gcm() {
|
||||
let claims = get_strongly_typed_claims();
|
||||
let key = crate::hash::HashAlgorithm::SHA2_256.digest(b"magic_password");
|
||||
let jwt = CheckedJwtEnc::new(JweAlg::Direct, JweEnc::Aes256Gcm, claims);
|
||||
let encoded = jwt.encode_direct(&key).unwrap();
|
||||
let decoded = JwtEnc::decode_direct(&encoded, &key)
|
||||
.unwrap()
|
||||
.validate::<MyClaims>(&NO_CHECK_VALIDATOR)
|
||||
.unwrap();
|
||||
assert_eq!(decoded.state.claims, get_strongly_typed_claims());
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct MyExpirableClaims {
|
||||
exp: i64,
|
||||
nbf: i64,
|
||||
msg: String,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_jws_not_expired() {
|
||||
let public_key = get_private_key_1().to_public_key().unwrap();
|
||||
|
||||
let jwt = JwtSig::decode(picky_test_data::JOSE_JWT_SIG_WITH_EXP, &public_key)
|
||||
.unwrap()
|
||||
.validate::<MyExpirableClaims>(&JwtValidator::strict(JwtDate::new(1545263999)))
|
||||
.expect("couldn't decode jwt without leeway");
|
||||
|
||||
assert_eq!(jwt.state.claims.exp, 1545264000);
|
||||
assert_eq!(jwt.state.claims.nbf, 1545263000);
|
||||
assert_eq!(jwt.state.claims.msg, "THIS IS TIME SENSITIVE DATA");
|
||||
|
||||
// alternatively, a leeway can account for small clock skew
|
||||
JwtSig::decode(picky_test_data::JOSE_JWT_SIG_WITH_EXP, &public_key)
|
||||
.unwrap()
|
||||
.validate::<MyExpirableClaims>(&JwtValidator::strict(JwtDate::new_with_leeway(1545264001, 10)))
|
||||
.expect("couldn't decode jwt with leeway for exp");
|
||||
|
||||
JwtSig::decode(picky_test_data::JOSE_JWT_SIG_WITH_EXP, &public_key)
|
||||
.unwrap()
|
||||
.validate::<MyExpirableClaims>(&JwtValidator::strict(JwtDate::new_with_leeway(1545262999, 10)))
|
||||
.expect("couldn't decode jwt with leeway for nbf");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_jws_invalid_date_err() {
|
||||
let public_key = get_private_key_1().to_public_key().unwrap();
|
||||
|
||||
let err = JwtSig::decode(picky_test_data::JOSE_JWT_SIG_WITH_EXP, &public_key)
|
||||
.unwrap()
|
||||
.validate::<MyExpirableClaims>(&JwtValidator::strict(JwtDate::new(1545264001)))
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"token expired (not after: 1545264000, now: 1545264001 [leeway: 0])"
|
||||
);
|
||||
|
||||
let err = JwtSig::decode(picky_test_data::JOSE_JWT_SIG_WITH_EXP, &public_key)
|
||||
.unwrap()
|
||||
.validate::<MyExpirableClaims>(&JwtValidator::strict(JwtDate::new_with_leeway(1545262998, 1)))
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"token not yet valid (not before: 1545263000, now: 1545262998 [leeway: 1])"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_step_cli_generated_token() {
|
||||
#[derive(Deserialize)]
|
||||
struct SomeJetClaims {
|
||||
jet_ap: String,
|
||||
prx_usr: String,
|
||||
nbf: i64,
|
||||
}
|
||||
|
||||
let jws = RawJws::decode("eyJhbGciOiJSUzI1NiIsImtpZCI6InUzQkF1b3lrZ21FY0F2Z21ydm5PVWxNZUYxN2JjS09EbGYweFdHcDhMY2MiLCJ0eXAiOiJKV1QifQ.eyJpYXQiOjE2NTkxMTg1NjMsImpldF9hcCI6InJkcCIsImp0aSI6IjY1YjkwZmQwMjM2YWU3Mjg1OWE1YWZlZTM3MTEzOTdjOWU4NTI1YzA4YzIyNjE4N2NlNjJjOWQwNTEzNDUzOTUiLCJuYmYiOjE2NTkxMTg1NjMsInByeF91c3IiOiJ1c2VybmFtZSJ9.MzULmkNyVY48nOgN7zbtN9q8Ni8JRavpkbw34aD-lMfqJzl5pFEJQPV9G1iM1HCbcMPRJfMDjVP31dAHOVtsu-gqGRx9qw1ogpNffcJI0nh5-VPPnqBbT5u8H2rJ7WeXO5kx4KAnD2Fbc45Nb6YEM-f_s9RyFipub0LI5AwiUHcbicJno0Lxz0dFKMiSA4cTNOe22vY7STf-E52LnsdHhnTt3JKDPP-7i5FzL1wOdBHzvxhRpyLqNU1kcSXrV_1L07XekeR6Kp3JoWaaJsIWm1Sk27W13Q575gS0a9OJgGX0bumq9fCneOJgLU8HrelUP8-qRM2IaGV81NRAr5HasQ")
|
||||
.map(RawJws::discard_signature)
|
||||
.map(JwtSig::from)
|
||||
.unwrap()
|
||||
.validate::<SomeJetClaims>(&JwtValidator::no_check())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(jws.state.claims.jet_ap, "rdp");
|
||||
assert_eq!(jws.state.claims.prx_usr, "username");
|
||||
assert_eq!(jws.state.claims.nbf, 1659118563);
|
||||
}
|
||||
}
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
//! JOSE framework subset implementation.
|
||||
//!
|
||||
//! A Json Web Token (JWT) comes in two flavors, roughly:
|
||||
//! - Json Web Encryption (JWE), used to transfer data securely
|
||||
//! - Json Web Signature (JWS), used to assert one's identity
|
||||
//!
|
||||
//! Common part is known as the "JOSE header".
|
||||
//!
|
||||
//! JSON Web Key (JWK) are used to represent cryptographic keys using JSON.
|
||||
|
||||
pub mod jwe;
|
||||
pub mod jwk;
|
||||
pub mod jws;
|
||||
pub mod jwt;
|
||||
Vendored
+396
@@ -0,0 +1,396 @@
|
||||
use crate::key::{KeyError, PrivateKey, PrivateKeyKind, PublicKey};
|
||||
use crate::oid::ObjectIdentifier;
|
||||
|
||||
use picky_asn1::wrapper::BitStringAsn1;
|
||||
use picky_asn1_x509::{EcParameters, oids};
|
||||
use std::fmt::Display;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct EcdsaKeypair {
|
||||
curve: NamedEcCurve,
|
||||
private_key: Vec<u8>,
|
||||
public_key: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl EcdsaKeypair {
|
||||
pub fn curve(&self) -> &NamedEcCurve {
|
||||
&self.curve
|
||||
}
|
||||
|
||||
pub fn secret(&self) -> &[u8] {
|
||||
&self.private_key
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EcdsaKeypair {
|
||||
fn drop(&mut self) {
|
||||
self.private_key.zeroize();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum EcComponent<'a> {
|
||||
PointX(&'a [u8]),
|
||||
PointY(&'a [u8]),
|
||||
Secret(&'a [u8]),
|
||||
}
|
||||
|
||||
/// Elliptic curve name to use for curve operations which require curve-specific arithmetic.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum EcCurve {
|
||||
/// NIST P-256 curve (secp256r1)
|
||||
NistP256,
|
||||
/// NIST P-384 curve (secp384r1)
|
||||
NistP384,
|
||||
/// NIST P-521 curve (secp521r1)
|
||||
NistP521,
|
||||
}
|
||||
|
||||
impl EcCurve {
|
||||
/// Get size of field compoennet in bytes (e.g. X and Y point values, Secret key,
|
||||
/// R and S signature values)
|
||||
pub(crate) fn field_bytes_size(self) -> usize {
|
||||
match self {
|
||||
EcCurve::NistP256 => {
|
||||
use p256::elliptic_curve::FieldBytesSize;
|
||||
use p256::elliptic_curve::array::typenum::Unsigned;
|
||||
<FieldBytesSize<p256::NistP256> as Unsigned>::USIZE
|
||||
}
|
||||
EcCurve::NistP384 => {
|
||||
use p384::elliptic_curve::FieldBytesSize;
|
||||
use p384::elliptic_curve::array::typenum::Unsigned;
|
||||
<FieldBytesSize<p384::NistP384> as Unsigned>::USIZE
|
||||
}
|
||||
EcCurve::NistP521 => {
|
||||
use p521::elliptic_curve::FieldBytesSize;
|
||||
use p521::elliptic_curve::array::typenum::Unsigned;
|
||||
<FieldBytesSize<p521::NistP521> as Unsigned>::USIZE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// We need to validate input data sizes to prevent panics in the underlying `generic_array`
|
||||
/// library code.
|
||||
pub(crate) fn validate_component<'a>(&self, component: EcComponent<'a>) -> Result<&'a [u8], KeyError> {
|
||||
let (buffer, error_message) = match component {
|
||||
EcComponent::PointX(buf) => (buf, "Invalid `point.x` component size"),
|
||||
EcComponent::PointY(buf) => (buf, "Invalid `point.y` component size"),
|
||||
EcComponent::Secret(buf) => (buf, "Invalid `secret` component size"),
|
||||
};
|
||||
|
||||
if buffer.len() != self.field_bytes_size() {
|
||||
return Err(KeyError::EC {
|
||||
context: error_message.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes the curve type of an ECDSA keypair
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum NamedEcCurve {
|
||||
Known(EcCurve),
|
||||
Unsupported(ObjectIdentifier),
|
||||
}
|
||||
|
||||
impl Display for EcCurve {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::NistP256 => write!(f, "NIST-P256"),
|
||||
Self::NistP384 => write!(f, "NIST-P384"),
|
||||
Self::NistP521 => write!(f, "NIST-P521"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for NamedEcCurve {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Known(curve) => curve.fmt(f),
|
||||
Self::Unsupported(oid) => {
|
||||
let oid: String = oid.into();
|
||||
write!(f, "Unsupported(OID: {oid})")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'_ ObjectIdentifier> for NamedEcCurve {
|
||||
fn from(value: &ObjectIdentifier) -> Self {
|
||||
let oid: String = value.into();
|
||||
match oid.as_str() {
|
||||
oids::SECP256R1 => NamedEcCurve::Known(EcCurve::NistP256),
|
||||
oids::SECP384R1 => NamedEcCurve::Known(EcCurve::NistP384),
|
||||
oids::SECP521R1 => NamedEcCurve::Known(EcCurve::NistP521),
|
||||
_ => NamedEcCurve::Unsupported(value.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NamedEcCurve> for ObjectIdentifier {
|
||||
fn from(value: NamedEcCurve) -> Self {
|
||||
match value {
|
||||
NamedEcCurve::Known(curve) => match curve {
|
||||
EcCurve::NistP256 => oids::secp256r1(),
|
||||
EcCurve::NistP384 => oids::secp384r1(),
|
||||
EcCurve::NistP521 => oids::secp521r1(),
|
||||
},
|
||||
NamedEcCurve::Unsupported(oid) => oid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a PrivateKey> for EcdsaKeypair {
|
||||
type Error = KeyError;
|
||||
|
||||
fn try_from(v: &'a PrivateKey) -> Result<Self, Self::Error> {
|
||||
match &v.kind {
|
||||
PrivateKeyKind::Ec {
|
||||
public_key,
|
||||
private_key,
|
||||
curve_oid,
|
||||
..
|
||||
} => Ok(Self {
|
||||
curve: NamedEcCurve::from(curve_oid),
|
||||
private_key: private_key.clone(),
|
||||
public_key: public_key.clone(),
|
||||
}),
|
||||
_ => Err(KeyError::EC {
|
||||
context: "EC keypair cannot be built from Non-EC private key".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn calculate_public_ec_key(
|
||||
curve_oid: &ObjectIdentifier,
|
||||
private_key: &[u8],
|
||||
compress: bool,
|
||||
) -> Result<Option<Vec<u8>>, KeyError> {
|
||||
let curve = NamedEcCurve::from(curve_oid);
|
||||
|
||||
match curve {
|
||||
NamedEcCurve::Known(EcCurve::NistP256) => {
|
||||
use p256::elliptic_curve::sec1::ToSec1Point as _;
|
||||
|
||||
let private_key_validated = EcCurve::NistP256.validate_component(EcComponent::Secret(private_key))?;
|
||||
|
||||
let secret_bytes =
|
||||
p256::elliptic_curve::array::Array::try_from(private_key_validated).map_err(|_| KeyError::EC {
|
||||
context: format!(
|
||||
"validated private key is the not right length(expected: {}, actual: {})",
|
||||
EcCurve::NistP256.field_bytes_size(),
|
||||
private_key_validated.len()
|
||||
),
|
||||
})?;
|
||||
let secret_key = p256::SecretKey::from_bytes(&secret_bytes).map_err(|_| KeyError::EC {
|
||||
context: "Failed to construct P256 SecretKey from private key bytes".to_string(),
|
||||
})?;
|
||||
|
||||
// Calculate public key from secret key
|
||||
let public_key = secret_key.public_key().as_affine().to_sec1_point(compress);
|
||||
|
||||
Ok(Some(public_key.to_bytes().to_vec()))
|
||||
}
|
||||
NamedEcCurve::Known(EcCurve::NistP384) => {
|
||||
use p384::elliptic_curve::sec1::ToSec1Point as _;
|
||||
|
||||
let private_key_validated = EcCurve::NistP384.validate_component(EcComponent::Secret(private_key))?;
|
||||
|
||||
let secret_bytes =
|
||||
p384::elliptic_curve::array::Array::try_from(private_key_validated).map_err(|_| KeyError::EC {
|
||||
context: format!(
|
||||
"validated private key is the not right length(expected: {}, actual: {})",
|
||||
EcCurve::NistP384.field_bytes_size(),
|
||||
private_key_validated.len()
|
||||
),
|
||||
})?;
|
||||
let secret_key = p384::SecretKey::from_bytes(&secret_bytes).map_err(|_| KeyError::EC {
|
||||
context: "Failed to construct P384 SecretKey from private key bytes".to_string(),
|
||||
})?;
|
||||
|
||||
// Calculate public key from secret key
|
||||
let public_key = secret_key.public_key().as_affine().to_sec1_point(compress);
|
||||
|
||||
Ok(Some(public_key.to_bytes().to_vec()))
|
||||
}
|
||||
NamedEcCurve::Known(EcCurve::NistP521) => {
|
||||
use p521::elliptic_curve::sec1::ToSec1Point as _;
|
||||
|
||||
let private_key_validated = EcCurve::NistP521.validate_component(EcComponent::Secret(private_key))?;
|
||||
|
||||
let secret_bytes =
|
||||
p521::elliptic_curve::array::Array::try_from(private_key_validated).map_err(|_| KeyError::EC {
|
||||
context: format!(
|
||||
"validated private key is the not right length(expected: {}, actual: {})",
|
||||
EcCurve::NistP521.field_bytes_size(),
|
||||
private_key_validated.len()
|
||||
),
|
||||
})?;
|
||||
let secret_key = p521::SecretKey::from_bytes(&secret_bytes).map_err(|_| KeyError::EC {
|
||||
context: "Failed to construct P521 SecretKey from private key bytes".to_string(),
|
||||
})?;
|
||||
|
||||
// Calculate public key from secret key
|
||||
let public_key = secret_key.public_key().as_affine().to_sec1_point(compress);
|
||||
|
||||
Ok(Some(public_key.to_bytes().to_vec()))
|
||||
}
|
||||
NamedEcCurve::Unsupported(_) => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct EcdsaPublicKey<'a> {
|
||||
data: &'a [u8],
|
||||
curve: NamedEcCurve,
|
||||
}
|
||||
|
||||
impl EcdsaPublicKey<'_> {
|
||||
pub fn curve(&self) -> &NamedEcCurve {
|
||||
&self.curve
|
||||
}
|
||||
|
||||
pub fn encoded_point(&self) -> &[u8] {
|
||||
self.data
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a PublicKey> for EcdsaPublicKey<'a> {
|
||||
type Error = KeyError;
|
||||
|
||||
fn try_from(v: &'a PublicKey) -> Result<Self, Self::Error> {
|
||||
use picky_asn1_x509::PublicKey as InnerPublicKey;
|
||||
|
||||
let curve_oid = match &v.as_inner().algorithm.parameters() {
|
||||
picky_asn1_x509::AlgorithmIdentifierParameters::Ec(EcParameters::NamedCurve(curve_oid)) => {
|
||||
curve_oid.0.clone()
|
||||
}
|
||||
_ => {
|
||||
return Err(KeyError::EC {
|
||||
context: "EC public key cannot be constructed from non-EC public key".to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
match &v.as_inner().subject_public_key {
|
||||
InnerPublicKey::Rsa(_) => Err(KeyError::EC {
|
||||
context: "EC public key cannot be constructed from RSA public key".to_string(),
|
||||
}),
|
||||
InnerPublicKey::Ec(BitStringAsn1(bitstring)) => {
|
||||
let data = bitstring.payload_view();
|
||||
|
||||
Ok(EcdsaPublicKey {
|
||||
data,
|
||||
curve: NamedEcCurve::from(&curve_oid),
|
||||
})
|
||||
}
|
||||
InnerPublicKey::Ed(_) => Err(KeyError::EC {
|
||||
context: "EC public key cannot be constructed from ED25519 public key".to_string(),
|
||||
}),
|
||||
InnerPublicKey::Mldsa(_) => Err(KeyError::EC {
|
||||
context: "EC public key cannot be constructed from MLDSA public key".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a EcdsaKeypair> for EcdsaPublicKey<'a> {
|
||||
type Error = KeyError;
|
||||
|
||||
fn try_from(v: &'a EcdsaKeypair) -> Result<Self, Self::Error> {
|
||||
match v.public_key.as_ref() {
|
||||
Some(key) => Ok(Self {
|
||||
data: key.as_slice(),
|
||||
curve: v.curve.clone(),
|
||||
}),
|
||||
None => Err(KeyError::EC {
|
||||
context: "EC public key cannot be constructed from EC private key without public key".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rstest::*;
|
||||
|
||||
const RSA_PUBLIC_KEY_PEM: &str = "-----BEGIN RSA PUBLIC KEY-----\n\
|
||||
MIIBCgKCAQEA61BjmfXGEvWmegnBGSuS+rU9soUg2FnODva32D1AqhwdziwHINFa\n\
|
||||
D1MVlcrYG6XRKfkcxnaXGfFDWHLEvNBSEVCgJjtHAGZIm5GL/KA86KDp/CwDFMSw\n\
|
||||
luowcXwDwoyinmeOY9eKyh6aY72xJh7noLBBq1N0bWi1e2i+83txOCg4yV2oVXhB\n\
|
||||
o8pYEJ8LT3el6Smxol3C1oFMVdwPgc0vTl25XucMcG/ALE/KNY6pqC2AQ6R2ERlV\n\
|
||||
gPiUWOPatVkt7+Bs3h5Ramxh7XjBOXeulmCpGSynXNcpZ/06+vofGi/2MlpQZNhH\n\
|
||||
Ao8eayMp6FcvNucIpUndo1X8dKMv3Y26ZQIDAQAB\n\
|
||||
-----END RSA PUBLIC KEY-----";
|
||||
|
||||
#[rstest]
|
||||
#[case(picky_test_data::EC_NIST256_DER_PK_1)]
|
||||
#[case(picky_test_data::EC_NIST384_DER_PK_1)]
|
||||
#[case(picky_test_data::EC_NIST521_DER_PK_1)]
|
||||
#[case(picky_test_data::EC_NIST256_PK_1)] // PKCS8
|
||||
fn private_key_from_ec_pem(#[case] key_pem: &str) {
|
||||
PrivateKey::from_pem_str(key_pem).unwrap();
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(picky_test_data::EC_NIST256_NOPUBLIC_DER_PK_1)]
|
||||
#[case(picky_test_data::EC_NIST384_NOPUBLIC_DER_PK_1)]
|
||||
#[case(picky_test_data::EC_NIST521_NOPUBLIC_DER_PK_1)]
|
||||
fn ecdsa_private_key_without_public(#[case] key_pem: &str) {
|
||||
// This should succeed for supported curves
|
||||
let key = PrivateKey::from_pem_str(key_pem).unwrap();
|
||||
key.to_public_key().unwrap().to_pem_str().unwrap();
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
// Known curves
|
||||
#[case(picky_test_data::EC_NIST256_PK_1_PUB)]
|
||||
#[case(picky_test_data::EC_NIST384_PK_1_PUB)]
|
||||
#[case(picky_test_data::EC_NIST521_PK_1_PUB)]
|
||||
// Unsupported curve, should still work as long as pem contains the public key
|
||||
// (in that case no arithmetic operations are performed on the key)
|
||||
#[case(picky_test_data::EC_PUBLIC_KEY_SECP256K1_PEM)]
|
||||
fn ecdsa_public_valid_key_conversions(#[case] key_pem: &str) {
|
||||
let pk: &PublicKey = &PublicKey::from_pem_str(key_pem).unwrap();
|
||||
let epk: Result<EcdsaPublicKey, KeyError> = pk.try_into();
|
||||
assert!(epk.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ecdsa_public_invalid_key_conversions() {
|
||||
// PEM public key conversion fails with an error
|
||||
let pk: &PublicKey = &PublicKey::from_pem_str(RSA_PUBLIC_KEY_PEM).unwrap();
|
||||
let epk: Result<EcdsaPublicKey, KeyError> = pk.try_into();
|
||||
assert!(epk.is_err());
|
||||
assert!(matches!(epk, Err(KeyError::EC { context: _ })));
|
||||
|
||||
// TODO: add check for attempted conversion from ED keys - which are not supported yet
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(picky_test_data::EC_NIST256_DER_PK_1, NamedEcCurve::Known(EcCurve::NistP256))]
|
||||
#[case(picky_test_data::EC_NIST384_DER_PK_1, NamedEcCurve::Known(EcCurve::NistP384))]
|
||||
#[case(picky_test_data::EC_NIST521_DER_PK_1, NamedEcCurve::Known(EcCurve::NistP521))]
|
||||
fn ecdsa_key_pair_from_ec_private_key(#[case] key: &str, #[case] curve: NamedEcCurve) {
|
||||
let pk = PrivateKey::from_pem_str(key).unwrap();
|
||||
let pair = EcdsaKeypair::try_from(&pk).unwrap();
|
||||
assert_eq!(curve, pair.curve);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ring_ecdsa_pkcs8_keys_could_be_parsed() {
|
||||
let algo = &ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING;
|
||||
let rng = ring::rand::SystemRandom::new();
|
||||
let pkcs8_bytes = ring::signature::EcdsaKeyPair::generate_pkcs8(algo, &rng).unwrap();
|
||||
// Validate that missing `parameters` field from ECPriavteKeyInfo is handled correctly.
|
||||
// rings skips it during pkcs8 serialization
|
||||
let key = PrivateKey::from_pkcs8(&pkcs8_bytes).unwrap();
|
||||
let pair = EcdsaKeypair::try_from(&key).unwrap();
|
||||
|
||||
assert_eq!(pair.curve(), &NamedEcCurve::Known(EcCurve::NistP256))
|
||||
}
|
||||
}
|
||||
Vendored
+240
@@ -0,0 +1,240 @@
|
||||
use crate::key::{KeyError, PrivateKey, PrivateKeyKind, PublicKey};
|
||||
use crate::oid::ObjectIdentifier;
|
||||
|
||||
use picky_asn1::wrapper::BitStringAsn1;
|
||||
use picky_asn1_x509::oids;
|
||||
use std::fmt::Display;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
pub(crate) const X25519_FIELD_ELEMENT_SIZE: usize = 32;
|
||||
pub(crate) type X25519FieldElement = [u8; X25519_FIELD_ELEMENT_SIZE];
|
||||
|
||||
/// Name of supported Curve25519 and Curve448 based algorithms.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum EdAlgorithm {
|
||||
/// Curve25519-based EdDSA algorithm
|
||||
Ed25519,
|
||||
/// Curve25519-based ECDH algorithm (mainly used for jwe key agreement)
|
||||
X25519,
|
||||
// (Unsupported) Ed448 -- Curve448-based EdDSA algorithm
|
||||
// (Unsupported) X448 -- Curve448-based ECDH algorithm (mainly used for jwe key agreement)
|
||||
}
|
||||
|
||||
// Describes Edwards curve-based EC algorithm
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum NamedEdAlgorithm {
|
||||
Known(EdAlgorithm),
|
||||
Unsupported(ObjectIdentifier),
|
||||
}
|
||||
|
||||
impl Display for NamedEdAlgorithm {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
NamedEdAlgorithm::Known(alg) => write!(f, "{alg}"),
|
||||
NamedEdAlgorithm::Unsupported(oid) => {
|
||||
// We don't support Ed448 and X448 algorithms, but we can still print their named
|
||||
// representation of OID to make errrs more readable.
|
||||
if oid == &oids::ed448() {
|
||||
write!(f, "Ed448")
|
||||
} else if oid == &oids::x448() {
|
||||
write!(f, "X448")
|
||||
} else {
|
||||
let oid: String = oid.into();
|
||||
write!(f, "Unsupported(OID: {oid})")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for EdAlgorithm {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Ed25519 => write!(f, "Ed25519"),
|
||||
Self::X25519 => write!(f, "X25519"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'_ ObjectIdentifier> for NamedEdAlgorithm {
|
||||
fn from(value: &'_ ObjectIdentifier) -> Self {
|
||||
let oid: String = value.into();
|
||||
match oid.as_str() {
|
||||
oids::ED25519 => NamedEdAlgorithm::Known(EdAlgorithm::Ed25519),
|
||||
oids::X25519 => NamedEdAlgorithm::Known(EdAlgorithm::X25519),
|
||||
_ => NamedEdAlgorithm::Unsupported(value.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EdAlgorithm> for ObjectIdentifier {
|
||||
fn from(value: EdAlgorithm) -> Self {
|
||||
match value {
|
||||
EdAlgorithm::Ed25519 => oids::ed25519(),
|
||||
EdAlgorithm::X25519 => oids::x25519(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NamedEdAlgorithm> for ObjectIdentifier {
|
||||
fn from(value: NamedEdAlgorithm) -> Self {
|
||||
match value {
|
||||
NamedEdAlgorithm::Known(alg) => alg.into(),
|
||||
NamedEdAlgorithm::Unsupported(oid) => oid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct EdKeypair {
|
||||
algorithm: NamedEdAlgorithm,
|
||||
private_key: Vec<u8>,
|
||||
public_key: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl EdKeypair {
|
||||
pub fn algorithm(&self) -> &NamedEdAlgorithm {
|
||||
&self.algorithm
|
||||
}
|
||||
|
||||
pub fn secret(&self) -> &[u8] {
|
||||
&self.private_key
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EdKeypair {
|
||||
fn drop(&mut self) {
|
||||
self.private_key.zeroize();
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a PrivateKey> for EdKeypair {
|
||||
type Error = KeyError;
|
||||
|
||||
fn try_from(value: &'a PrivateKey) -> Result<Self, Self::Error> {
|
||||
match &value.kind {
|
||||
PrivateKeyKind::Ed {
|
||||
public_key,
|
||||
private_key,
|
||||
algorithm_oid,
|
||||
} => Ok(Self {
|
||||
algorithm: NamedEdAlgorithm::from(algorithm_oid),
|
||||
private_key: private_key.clone(),
|
||||
public_key: public_key.clone(),
|
||||
}),
|
||||
_ => Err(KeyError::ED {
|
||||
context: "Ed keypair cannot be constructed from non-Ed private key".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct EdPublicKey<'a> {
|
||||
data: &'a [u8],
|
||||
algorithm: NamedEdAlgorithm,
|
||||
}
|
||||
|
||||
impl EdPublicKey<'_> {
|
||||
pub fn algorithm(&self) -> &NamedEdAlgorithm {
|
||||
&self.algorithm
|
||||
}
|
||||
|
||||
pub fn data(&self) -> &[u8] {
|
||||
self.data
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a EdKeypair> for EdPublicKey<'a> {
|
||||
type Error = KeyError;
|
||||
|
||||
fn try_from(v: &'a EdKeypair) -> Result<Self, Self::Error> {
|
||||
match v.public_key.as_ref() {
|
||||
Some(key) => Ok(Self {
|
||||
data: key.as_slice(),
|
||||
algorithm: v.algorithm.clone(),
|
||||
}),
|
||||
None => Err(KeyError::ED {
|
||||
context: "Ed public key cannot be constructed from Ed private key without public key".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a PublicKey> for EdPublicKey<'a> {
|
||||
type Error = KeyError;
|
||||
|
||||
fn try_from(v: &'a PublicKey) -> Result<Self, Self::Error> {
|
||||
use picky_asn1_x509::PublicKey as InnerPublicKey;
|
||||
|
||||
let oid = v.as_inner().algorithm.oid();
|
||||
|
||||
match &v.as_inner().subject_public_key {
|
||||
InnerPublicKey::Rsa(_) => Err(KeyError::ED {
|
||||
context: "Ed public key cannot be constructed from RSA public key".to_string(),
|
||||
}),
|
||||
InnerPublicKey::Ec(_) => Err(KeyError::ED {
|
||||
context: "Ed public key cannot be constructed from Ec public key".to_string(),
|
||||
}),
|
||||
InnerPublicKey::Ed(BitStringAsn1(bitstring)) => {
|
||||
let data = bitstring.payload_view();
|
||||
|
||||
Ok(EdPublicKey {
|
||||
data,
|
||||
algorithm: NamedEdAlgorithm::from(oid),
|
||||
})
|
||||
}
|
||||
InnerPublicKey::Mldsa(_) => Err(KeyError::ED {
|
||||
context: "Ed public key cannot be constructed from Mldsa public key".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::key::{PrivateKey, PublicKey};
|
||||
use rstest::rstest;
|
||||
|
||||
#[rstest]
|
||||
#[case(picky_test_data::ED25519_PEM_PK_1)]
|
||||
#[case(picky_test_data::X25519_PEM_PK_1)]
|
||||
// Although X448 and ED448 are not supported, we should still be able to decode and encode them
|
||||
#[case(picky_test_data::ED448_PEM_PK_1)]
|
||||
#[case(picky_test_data::X448_PEM_PK_1)]
|
||||
fn private_key_roundtrip(#[case] key_pem: &str) {
|
||||
let decoded = PrivateKey::from_pem_str(key_pem).unwrap();
|
||||
let encoded = decoded.to_pem_str().unwrap();
|
||||
assert_eq!(encoded.as_str(), key_pem);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(picky_test_data::ED25519_PEM_PK_1_PUB)]
|
||||
#[case(picky_test_data::X25519_PEM_PK_1_PUB)]
|
||||
// Although X448 and ED448 are not supported, we should still be able to decode and encode them
|
||||
#[case(picky_test_data::ED448_PEM_PK_1_PUB)]
|
||||
#[case(picky_test_data::X448_PEM_PK_1_PUB)]
|
||||
fn public_key_roundtrip(#[case] key_pem: &str) {
|
||||
let decoded = PublicKey::from_pem_str(key_pem).unwrap();
|
||||
let encoded = decoded.to_pem_str().unwrap();
|
||||
assert_eq!(encoded.as_str(), key_pem);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(picky_test_data::ED25519_PEM_PK_1, picky_test_data::ED25519_PEM_PK_1_PUB)]
|
||||
#[case(picky_test_data::X25519_PEM_PK_1, picky_test_data::X25519_PEM_PK_1_PUB)]
|
||||
fn extract_public_key(#[case] key_pem: &str, #[case] expected_public_pem: &str) {
|
||||
let private = PrivateKey::from_pem_str(key_pem).unwrap();
|
||||
let public = private.to_public_key().unwrap();
|
||||
let public_expected = PublicKey::from_pem_str(expected_public_pem).unwrap();
|
||||
assert_eq!(public, public_expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(picky_test_data::ED448_PEM_PK_1)]
|
||||
#[case(picky_test_data::X448_PEM_PK_1)]
|
||||
fn extract_public_key_for_unsupported_algorithm_fails(#[case] key_pem: &str) {
|
||||
let private = PrivateKey::from_pem_str(key_pem).unwrap();
|
||||
assert!(private.to_public_key().is_err());
|
||||
}
|
||||
}
|
||||
Vendored
+1299
File diff suppressed because it is too large
Load Diff
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
//! [](https://crates.io/crates/picky)
|
||||
//! [](https://docs.rs/picky)
|
||||
//! 
|
||||
//! # picky
|
||||
//!
|
||||
//! Portable X.509, PKI, JOSE and HTTP signature implementation.
|
||||
|
||||
#[cfg(feature = "http_signature")]
|
||||
pub mod http;
|
||||
|
||||
#[cfg(feature = "jose")]
|
||||
pub mod jose;
|
||||
|
||||
#[cfg(feature = "x509")]
|
||||
pub mod x509;
|
||||
|
||||
#[cfg(feature = "ssh")]
|
||||
pub mod ssh;
|
||||
|
||||
#[cfg(feature = "pkcs12")]
|
||||
pub mod pkcs12;
|
||||
|
||||
#[cfg(feature = "putty")]
|
||||
pub mod putty;
|
||||
|
||||
pub mod hash;
|
||||
pub mod key;
|
||||
pub mod pem;
|
||||
pub mod signature;
|
||||
|
||||
pub use picky_asn1_x509::{AlgorithmIdentifier, oid, oids};
|
||||
Vendored
+292
@@ -0,0 +1,292 @@
|
||||
//! Privacy-Enhanced Mail (PEM) format utilities
|
||||
//!
|
||||
//! Based on the RFC-7468
|
||||
//! ([Textual Encodings of PKIX, PKCS, and CMS Structures](https://tools.ietf.org/html/rfc7468)).
|
||||
|
||||
use base64::engine::general_purpose;
|
||||
use base64::{DecodeError, Engine as _};
|
||||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
use std::io::BufRead;
|
||||
use std::str::FromStr;
|
||||
use thiserror::Error;
|
||||
|
||||
const PEM_HEADER_START: &str = "-----BEGIN";
|
||||
const PEM_FOOTER_START: &str = "-----END";
|
||||
const PEM_DASHES_BOUNDARIES: &str = "-----";
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum PemError {
|
||||
/// header not found
|
||||
#[error("header not found")]
|
||||
HeaderNotFound,
|
||||
|
||||
/// invalid pem header
|
||||
#[error("invalid pem header")]
|
||||
InvalidHeader,
|
||||
|
||||
/// footer not found
|
||||
#[error("footer not found")]
|
||||
FooterNotFound,
|
||||
|
||||
/// couldn't decode base64
|
||||
#[error("couldn't decode base64: {source}")]
|
||||
Base64Decoding { source: DecodeError },
|
||||
}
|
||||
|
||||
/// Privacy-Enhanced Mail (PEM) format structured representation
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Pem<'a> {
|
||||
label: String,
|
||||
data: Cow<'a, [u8]>,
|
||||
}
|
||||
|
||||
impl<'a> Pem<'a> {
|
||||
pub fn new<S: Into<String>, D: Into<Cow<'a, [u8]>>>(label: S, data: D) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
data: data.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(&self) -> &str {
|
||||
&self.label
|
||||
}
|
||||
|
||||
pub fn data(&self) -> &[u8] {
|
||||
&self.data
|
||||
}
|
||||
|
||||
pub fn into_data(self) -> Cow<'a, [u8]> {
|
||||
self.data
|
||||
}
|
||||
}
|
||||
|
||||
impl Pem<'static> {
|
||||
pub fn read_from(reader: &mut impl BufRead) -> Result<Self, PemError> {
|
||||
read_pem(reader)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Pem<'static> {
|
||||
type Err = PemError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
parse_pem(s.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Pem<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
writeln!(f, "{} {}-----", PEM_HEADER_START, self.label)?;
|
||||
|
||||
let encoded = general_purpose::STANDARD.encode(&self.data);
|
||||
let bytes = encoded.as_bytes();
|
||||
for chunk in bytes.chunks(64) {
|
||||
let chunk = std::str::from_utf8(chunk).map_err(|_| fmt::Error)?;
|
||||
writeln!(f, "{chunk}")?;
|
||||
}
|
||||
|
||||
write!(f, "{} {}-----", PEM_FOOTER_START, self.label)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Pem<'_>> for String {
|
||||
fn from(pem: Pem<'_>) -> Self {
|
||||
pem.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a PEM-encoded stream from a [u8] representation
|
||||
///
|
||||
/// If the input contains line ending characters (`\r`, `\n`), a copy of input
|
||||
/// is allocated striping these. If you can strip these with minimal data copy
|
||||
/// you should do it beforehand.
|
||||
pub fn parse_pem<T: ?Sized + AsRef<[u8]>>(input: &T) -> Result<Pem<'static>, PemError> {
|
||||
parse_pem_impl(input.as_ref())
|
||||
}
|
||||
|
||||
fn parse_pem_impl(input: &[u8]) -> Result<Pem<'static>, PemError> {
|
||||
let header_start_idx = h_find(input, PEM_HEADER_START.as_bytes()).ok_or(PemError::HeaderNotFound)?;
|
||||
|
||||
let label_start_idx = header_start_idx + PEM_HEADER_START.len();
|
||||
let label_end_idx = h_find(&input[label_start_idx..], b"-").ok_or(PemError::InvalidHeader)? + label_start_idx;
|
||||
let label = String::from_utf8_lossy(&input[label_start_idx..label_end_idx])
|
||||
.trim()
|
||||
.to_owned();
|
||||
|
||||
let header_end_idx = h_find(&input[label_end_idx..], PEM_DASHES_BOUNDARIES.as_bytes())
|
||||
.ok_or(PemError::InvalidHeader)?
|
||||
+ label_end_idx
|
||||
+ PEM_DASHES_BOUNDARIES.len();
|
||||
|
||||
let footer_start_idx =
|
||||
h_find(&input[header_end_idx..], PEM_FOOTER_START.as_bytes()).ok_or(PemError::FooterNotFound)? + header_end_idx;
|
||||
|
||||
let raw_data = &input[header_end_idx..footer_start_idx];
|
||||
|
||||
let data = if h_find(raw_data, b"\n").is_some() {
|
||||
// Line ending characters should be striped... Sadly, this means we need to copy and allocate.
|
||||
let striped_raw_data: Vec<u8> = raw_data
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|&byte| byte != b'\r' && byte != b'\n')
|
||||
.collect();
|
||||
general_purpose::STANDARD
|
||||
.decode(striped_raw_data)
|
||||
.map_err(|source| PemError::Base64Decoding { source })?
|
||||
} else {
|
||||
// Can be decoded as is!
|
||||
general_purpose::STANDARD
|
||||
.decode(raw_data)
|
||||
.map_err(|source| PemError::Base64Decoding { source })?
|
||||
};
|
||||
|
||||
Ok(Pem {
|
||||
label,
|
||||
data: Cow::Owned(data),
|
||||
})
|
||||
}
|
||||
|
||||
fn h_find(buffer: &[u8], value: &[u8]) -> Option<usize> {
|
||||
buffer.windows(value.len()).position(|window| window == value)
|
||||
}
|
||||
|
||||
/// Parse a PEM-encoded stream from a BufRead object.
|
||||
///
|
||||
/// Maybe slower than the AsRef<[u8]>-based implementation because additional copies are incurred,
|
||||
/// but in most cases it's probably easier to work with and not that bad anyway.
|
||||
pub fn read_pem(reader: &mut impl BufRead) -> Result<Pem<'static>, PemError> {
|
||||
let mut buf = Vec::with_capacity(1024);
|
||||
|
||||
// skip until start of header
|
||||
h_read_until(reader, PEM_HEADER_START.as_bytes(), &mut buf).ok_or(PemError::HeaderNotFound)?;
|
||||
buf.clear();
|
||||
|
||||
// read until end of header
|
||||
h_read_until(reader, PEM_DASHES_BOUNDARIES.as_bytes(), &mut buf).ok_or(PemError::InvalidHeader)?;
|
||||
let buf_utf8 = core::str::from_utf8(&buf).map_err(|_| PemError::InvalidHeader)?;
|
||||
let label = buf_utf8.trim_end_matches(PEM_DASHES_BOUNDARIES).trim().to_owned();
|
||||
buf.clear();
|
||||
|
||||
// read to footer
|
||||
h_read_until(reader, PEM_FOOTER_START.as_bytes(), &mut buf).ok_or(PemError::FooterNotFound)?;
|
||||
let base64_data: Vec<u8> = h_trim_end_matches(&buf, PEM_FOOTER_START.as_bytes())
|
||||
.iter()
|
||||
.cloned()
|
||||
.filter(|&byte| byte != b'\r' && byte != b'\n')
|
||||
.collect();
|
||||
let data = general_purpose::STANDARD
|
||||
.decode(base64_data)
|
||||
.map_err(|source| PemError::Base64Decoding { source })?;
|
||||
|
||||
// read until end of footer
|
||||
h_read_until(reader, PEM_DASHES_BOUNDARIES.as_bytes(), &mut buf).ok_or(PemError::FooterNotFound)?;
|
||||
|
||||
Ok(Pem {
|
||||
label,
|
||||
data: Cow::Owned(data),
|
||||
})
|
||||
}
|
||||
|
||||
// Helper to read until some pattern is matched. Returns None on any error
|
||||
// (cannot be copy pasted for any purpose and should stay private!).
|
||||
fn h_read_until(reader: &mut impl BufRead, pat: &[u8], buf: &mut Vec<u8>) -> Option<usize> {
|
||||
let mut read = 0;
|
||||
let first_delim = *pat.first()?;
|
||||
'outer: loop {
|
||||
read += reader.read_until(first_delim, buf).ok()?;
|
||||
|
||||
for &next_delim in &pat[1..] {
|
||||
let mut next = [0];
|
||||
reader.read_exact(&mut next).ok()?;
|
||||
buf.push(next[0]);
|
||||
read += 1;
|
||||
|
||||
if next[0] != next_delim {
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
|
||||
break Some(read);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to trim trailing characters matching the given pattern for bytes slice
|
||||
fn h_trim_end_matches<'a>(slice: &'a [u8], pat: &[u8]) -> &'a [u8] {
|
||||
for (&slice_elem, &pat_elem) in slice.iter().rev().zip(pat.iter().rev()) {
|
||||
if slice_elem != pat_elem {
|
||||
return slice; // pattern doesn't match, return all the slice
|
||||
}
|
||||
}
|
||||
|
||||
// pattern did match, return sub-slice
|
||||
&slice[..slice.len() - pat.len()]
|
||||
}
|
||||
|
||||
/// Build a PEM-encoded structure into a String.
|
||||
pub fn to_pem<S, T>(label: S, data: &T) -> String
|
||||
where
|
||||
S: Into<String>,
|
||||
T: ?Sized + AsRef<[u8]>,
|
||||
{
|
||||
Pem::new(label, data.as_ref()).to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::BufReader;
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
let pem_from_bytes = parse_pem(picky_test_data::PEM_BYTES).unwrap();
|
||||
assert_eq!(pem_from_bytes.label, "CERTIFICATE");
|
||||
|
||||
let pem_from_str = picky_test_data::PEM_STR.parse::<Pem>().unwrap();
|
||||
pretty_assertions::assert_eq!(pem_from_bytes, pem_from_str);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reader_based() {
|
||||
let mut reader = BufReader::new(picky_test_data::PEM_BYTES);
|
||||
|
||||
let pem_from_reader = read_pem(&mut reader).unwrap();
|
||||
assert_eq!(pem_from_reader.label, "CERTIFICATE");
|
||||
|
||||
let pem_from_str = picky_test_data::PEM_STR.parse::<Pem>().unwrap();
|
||||
pretty_assertions::assert_eq!(pem_from_reader, pem_from_str);
|
||||
}
|
||||
|
||||
// This test should not run on Windows. writeln! add `/r` ending character to Pem in String format on Windows targets.
|
||||
#[cfg(not(windows))]
|
||||
#[test]
|
||||
fn to_string() {
|
||||
let pem = picky_test_data::PEM_STR.parse::<Pem>().unwrap();
|
||||
let reconverted_pem = pem.to_string();
|
||||
pretty_assertions::assert_eq!(reconverted_pem, picky_test_data::PEM_STR);
|
||||
}
|
||||
|
||||
const FLATTENED_PEM: &str = "-----BEGIN GARBAGE-----R0FSQkFHRQo=-----END GARBAGE-----";
|
||||
|
||||
#[test]
|
||||
fn flattened() {
|
||||
FLATTENED_PEM.parse::<Pem>().unwrap();
|
||||
read_pem(&mut BufReader::new(FLATTENED_PEM.as_bytes())).unwrap();
|
||||
}
|
||||
|
||||
const MULTIPLE_PEM: &str = "-----BEGIN GARBAGE1-----R0FSQkFHRQo=-----END GARBAGE1-----\
|
||||
-----BEGIN GARBAGE2-----R0FSQkFHRQo=-----END GARBAGE2-----";
|
||||
|
||||
#[test]
|
||||
fn multiple() {
|
||||
// reading multiple PEM from some bytes stream is easier with read-based API
|
||||
let mut reader = BufReader::new(MULTIPLE_PEM.as_bytes());
|
||||
let pem1 = read_pem(&mut reader).unwrap();
|
||||
assert_eq!(pem1.label, "GARBAGE1");
|
||||
let pem2 = read_pem(&mut reader).unwrap();
|
||||
assert_eq!(pem2.label, "GARBAGE2");
|
||||
}
|
||||
}
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
use crate::pkcs12::Pkcs12Error;
|
||||
use picky_asn1::restricted_string::BmpString;
|
||||
use picky_asn1::wrapper::OctetStringAsn1;
|
||||
use picky_asn1_der::Asn1RawDer;
|
||||
use picky_asn1_x509::oid::ObjectIdentifier;
|
||||
use picky_asn1_x509::pkcs12::Pkcs12Attribute as Pkcs12AttributeAsn1;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Represents a PKCS#12 attributes which can be used to store additional information about safe
|
||||
/// bag contents (e.g. private key or certificate).
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Pkcs12Attribute {
|
||||
kind: Pkcs12AttributeKind,
|
||||
inner: Pkcs12AttributeAsn1,
|
||||
}
|
||||
|
||||
impl Pkcs12Attribute {
|
||||
pub(crate) fn from_asn1(asn1: Pkcs12AttributeAsn1) -> Self {
|
||||
let kind = match &asn1 {
|
||||
Pkcs12AttributeAsn1::FriendlyName(value) => Pkcs12AttributeKind::FriendlyName(value.clone()),
|
||||
Pkcs12AttributeAsn1::LocalKeyId(value) => Pkcs12AttributeKind::LocalKeyId(value.0.clone()),
|
||||
Pkcs12AttributeAsn1::Unknown { oid, value } => Pkcs12AttributeKind::Custom(CustomPkcs12Attribute {
|
||||
oid: oid.clone(),
|
||||
value: value.clone(),
|
||||
}),
|
||||
};
|
||||
|
||||
Self { kind, inner: asn1 }
|
||||
}
|
||||
|
||||
/// Creates a new `friendly name` attribute. This attribute is used to store a human-readable
|
||||
/// name of the safe bag contents (e.g. certificate name).
|
||||
pub fn new_friendly_name(value: BmpString) -> Self {
|
||||
let kind = Pkcs12AttributeKind::FriendlyName(value);
|
||||
let inner = kind.to_inner();
|
||||
Self { kind, inner }
|
||||
}
|
||||
|
||||
/// Creates a new `local key id` attribute. This attribute is used to indicate relation between
|
||||
/// private key and certificate (when set to same value on both objects).
|
||||
pub fn new_local_key_id(value: impl Into<Vec<u8>>) -> Self {
|
||||
let kind = Pkcs12AttributeKind::LocalKeyId(value.into());
|
||||
let inner = kind.to_inner();
|
||||
Self { kind, inner }
|
||||
}
|
||||
|
||||
/// Create a new custom attribute (e.g. Microsoft-specific attributes).
|
||||
pub fn new_custom(attr: CustomPkcs12Attribute) -> Self {
|
||||
let kind = Pkcs12AttributeKind::Custom(attr);
|
||||
let inner = kind.to_inner();
|
||||
Self { kind, inner }
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> &Pkcs12AttributeKind {
|
||||
&self.kind
|
||||
}
|
||||
|
||||
pub fn inner(&self) -> &Pkcs12AttributeAsn1 {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Pkcs12AttributeKind {
|
||||
FriendlyName(BmpString),
|
||||
LocalKeyId(Vec<u8>),
|
||||
Custom(CustomPkcs12Attribute),
|
||||
}
|
||||
|
||||
impl Pkcs12AttributeKind {
|
||||
pub(crate) fn to_inner(&self) -> Pkcs12AttributeAsn1 {
|
||||
match self {
|
||||
Self::FriendlyName(value) => Pkcs12AttributeAsn1::FriendlyName(value.clone()),
|
||||
Self::LocalKeyId(value) => Pkcs12AttributeAsn1::LocalKeyId(OctetStringAsn1::from(value.clone())),
|
||||
Self::Custom(value) => Pkcs12AttributeAsn1::Unknown {
|
||||
oid: value.oid.clone(),
|
||||
value: value.value.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Pkcs12AttributeAsn1> for Pkcs12AttributeKind {
|
||||
fn from(value: Pkcs12AttributeAsn1) -> Self {
|
||||
match value {
|
||||
Pkcs12AttributeAsn1::FriendlyName(value) => Self::FriendlyName(value),
|
||||
Pkcs12AttributeAsn1::LocalKeyId(value) => Self::LocalKeyId(value.0),
|
||||
Pkcs12AttributeAsn1::Unknown { oid, value } => CustomPkcs12Attribute { oid, value }.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CustomPkcs12Attribute> for Pkcs12AttributeKind {
|
||||
fn from(value: CustomPkcs12Attribute) -> Self {
|
||||
Self::Custom(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CustomPkcs12Attribute {
|
||||
oid: ObjectIdentifier,
|
||||
value: Vec<Asn1RawDer>,
|
||||
}
|
||||
|
||||
impl CustomPkcs12Attribute {
|
||||
/// Get attribute oid.
|
||||
pub fn oid(&self) -> &ObjectIdentifier {
|
||||
&self.oid
|
||||
}
|
||||
|
||||
pub fn new_empty(oid: ObjectIdentifier) -> Self {
|
||||
Self { oid, value: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn has_value(&self) -> bool {
|
||||
!self.value.is_empty()
|
||||
}
|
||||
|
||||
pub fn new_raw(oid: ObjectIdentifier, value: Vec<Asn1RawDer>) -> Self {
|
||||
Self { oid, value }
|
||||
}
|
||||
|
||||
/// Creates a new custom attribute from any DER-serializable value. Ut is advised to use types
|
||||
/// from `picky-asn1-der` crate.
|
||||
pub fn new_single_value<T: Serialize>(oid: ObjectIdentifier, value: &T) -> Result<Self, Pkcs12Error> {
|
||||
let encoded = picky_asn1_der::to_vec(value)?;
|
||||
Ok(Self {
|
||||
oid,
|
||||
value: vec![Asn1RawDer(encoded)],
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a new custom attribute from multiple any DER-serializable value (PKCS#12 allows
|
||||
/// attribute to have list of values). It is advised to use types from `picky-asn1-der` crate.
|
||||
pub fn new_multiple_values<'a, T: Serialize + 'a>(
|
||||
oid: ObjectIdentifier,
|
||||
values: impl IntoIterator<Item = &'a T>,
|
||||
) -> Result<Self, Pkcs12Error> {
|
||||
let mut encoded_values = Vec::new();
|
||||
for value in values {
|
||||
let encoded = picky_asn1_der::to_vec(value)?;
|
||||
encoded_values.push(Asn1RawDer(encoded));
|
||||
}
|
||||
Ok(Self {
|
||||
oid,
|
||||
value: encoded_values,
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert inner value to any DER-deserializable value. It is advised to use types from
|
||||
/// `picky-asn1-der` crate.
|
||||
pub fn to_single_value<'a, T: Deserialize<'a>>(&'a self) -> Result<T, Pkcs12Error> {
|
||||
if self.value.len() != 1 {
|
||||
return Err(Pkcs12Error::UnexpectedAttributeValuesCount {
|
||||
expected: 1,
|
||||
actual: self.value.len(),
|
||||
});
|
||||
}
|
||||
let deserialized = picky_asn1_der::from_bytes(&self.value[0].0)?;
|
||||
Ok(deserialized)
|
||||
}
|
||||
|
||||
/// Convert inner value to multiple DER-deserializable values. It is advised to use types from
|
||||
/// `picky-asn1-der` crate.
|
||||
pub fn to_multiple_values<'a, T: Deserialize<'a>>(&'a self) -> Result<Vec<T>, Pkcs12Error> {
|
||||
let values_result: Result<Vec<_>, _> = self
|
||||
.value
|
||||
.iter()
|
||||
.map(|value| picky_asn1_der::from_bytes(&value.0))
|
||||
.collect();
|
||||
values_result.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn raw_data(&self) -> &[Asn1RawDer] {
|
||||
&self.value
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::*;
|
||||
use expect_test::expect;
|
||||
|
||||
fn fake_oid() -> ObjectIdentifier {
|
||||
"1.3.6.1.4.1.311.17.1".to_string().try_into().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_custom_attribute_roundtrip() {
|
||||
let value = BmpString::from_str("Microsoft Software Key Storage Provider").unwrap();
|
||||
let attr = CustomPkcs12Attribute::new_single_value(fake_oid(), &value).unwrap();
|
||||
let decoded = attr.to_single_value::<BmpString>().unwrap();
|
||||
assert_eq!(decoded, value);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_custom_attribute_roundtrip_no_value() {
|
||||
let attr = CustomPkcs12Attribute::new_empty(fake_oid());
|
||||
assert!(!attr.has_value());
|
||||
let decoded = attr.to_single_value::<BmpString>();
|
||||
expect![[r#"
|
||||
Err(
|
||||
UnexpectedAttributeValuesCount {
|
||||
expected: 1,
|
||||
actual: 0,
|
||||
},
|
||||
)
|
||||
"#]]
|
||||
.assert_debug_eq(&decoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_custom_attribute_roundtrip_too_many_values() {
|
||||
let value = vec![OctetStringAsn1(vec![0x01]), OctetStringAsn1(vec![0x02])];
|
||||
let attr = CustomPkcs12Attribute::new_multiple_values(fake_oid(), &value).unwrap();
|
||||
let decoded = attr.to_single_value::<BmpString>();
|
||||
expect![[r#"
|
||||
Err(
|
||||
UnexpectedAttributeValuesCount {
|
||||
expected: 1,
|
||||
actual: 2,
|
||||
},
|
||||
)
|
||||
"#]]
|
||||
.assert_debug_eq(&decoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_custom_attributes_roundtrip() {
|
||||
let value = vec![
|
||||
OctetStringAsn1(vec![0x01, 0x02, 0x03]),
|
||||
OctetStringAsn1(vec![0x04, 0x05, 0x06]),
|
||||
];
|
||||
|
||||
let attr = CustomPkcs12Attribute::new_multiple_values(fake_oid(), &value).unwrap();
|
||||
let decoded = attr.to_multiple_values::<OctetStringAsn1>().unwrap();
|
||||
assert_eq!(decoded, value);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_custom_attributes_empty_list_allowed() {
|
||||
let value: Vec<OctetStringAsn1> = vec![];
|
||||
let attr = CustomPkcs12Attribute::new_multiple_values(fake_oid(), &value).unwrap();
|
||||
let decoded = attr.to_multiple_values::<OctetStringAsn1>().unwrap();
|
||||
assert_eq!(decoded, value);
|
||||
}
|
||||
}
|
||||
+674
@@ -0,0 +1,674 @@
|
||||
use crate::pkcs12::{Pbkdf1Usage, Pkcs12Error, Pkcs12HashAlgorithm, pbkdf1};
|
||||
use picky_asn1::restricted_string::BmpString;
|
||||
use picky_asn1::wrapper::OctetStringAsn1;
|
||||
pub use picky_asn1_x509::pkcs12::Pbes1AlgorithmKind as Pbes1Cipher;
|
||||
use picky_asn1_x509::pkcs12::{
|
||||
Pbes1Params as Pbes1ParamsAsn1, Pbes2AesCbcEncryption as Pbes2AesCbcEncryptionAsn1,
|
||||
Pbes2EncryptionScheme as Pbes2EncryptionSchemeAsn1, Pbes2KeyDerivationFunc as Pbes2KeyDerivationFuncAsn1,
|
||||
Pbes2Params as Pbes2ParamsAsn1, Pbkdf2Params as Pbkdf2ParamsAsn1, Pbkdf2Prf as Pbkdf2PrfAsn1,
|
||||
Pbkdf2SaltSource as Pbkdf2SaltSourceAsn1, Pkcs12EncryptionAlgorithm as Pkcs12EncryptionAsn1,
|
||||
};
|
||||
use rand::rngs::{StdRng, SysRng};
|
||||
use rand_core::SeedableRng as _;
|
||||
use std::str::FromStr as _;
|
||||
|
||||
/// Same default KDF iterations as in OpenSSL
|
||||
const DEFAULT_KDF_ITERATIONS: usize = 2048;
|
||||
const DEFAULT_SALT_SIZE: usize = 8;
|
||||
const AES_BLOCK_SIZE: usize = 16;
|
||||
|
||||
/// Crypto operations context for PFX file parsing/building. Contains password inside as a secure
|
||||
/// string and RNG.
|
||||
pub struct Pkcs12CryptoContext {
|
||||
password: zeroize::Zeroizing<String>,
|
||||
rng: Box<dyn rand_core::Rng>,
|
||||
}
|
||||
|
||||
impl Pkcs12CryptoContext {
|
||||
/// Creates new context with given password and default
|
||||
pub fn new_with_password(password: &str) -> Result<Self, Pkcs12Error> {
|
||||
Ok(Self {
|
||||
password: password.to_string().into(),
|
||||
rng: Box::new(StdRng::try_from_rng(&mut SysRng)?),
|
||||
})
|
||||
}
|
||||
|
||||
/// Sets RNG for this context
|
||||
pub fn with_rng(mut self, rng: impl rand::CryptoRng + 'static) -> Self {
|
||||
self.rng = Box::new(rng);
|
||||
self
|
||||
}
|
||||
|
||||
/// Creates new context with empty password and default RNG
|
||||
pub fn new_without_password() -> Result<Self, Pkcs12Error> {
|
||||
Ok(Self {
|
||||
password: String::new().into(),
|
||||
rng: Box::new(StdRng::try_from_rng(&mut SysRng)?),
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns password in PBES1 password representation - UCS2 encoded string with null terminator
|
||||
pub(crate) fn password_bytes_pbes1(&self) -> Result<zeroize::Zeroizing<Vec<u8>>, Pkcs12Error> {
|
||||
let mut bmp = zeroize::Zeroizing::new(BmpString::from_str(&self.password)?.into_bytes());
|
||||
bmp.extend_from_slice(&[0, 0]);
|
||||
|
||||
Ok(bmp)
|
||||
}
|
||||
|
||||
/// Returns password in PBES2 password representation - UTF8 encoded string
|
||||
pub(crate) fn password_bytes_pbes2(&self) -> &[u8] {
|
||||
self.password.as_bytes()
|
||||
}
|
||||
|
||||
pub(crate) fn generate_bytes(&mut self, len: usize) -> Vec<u8> {
|
||||
let mut data = vec![0u8; len];
|
||||
self.rng.fill_bytes(&mut data);
|
||||
data
|
||||
}
|
||||
}
|
||||
|
||||
/// This type holds all information required to encrypt/decrypt data in PKCS#12 file, including
|
||||
/// encryption algorithm, salt, IV and KDF iterations.
|
||||
///
|
||||
/// This type is not cloneable, because it is intended to be used only once for each encryptable
|
||||
/// object. If for some reason you need to have exactly the same salt/IV values for multiple
|
||||
/// encryptable objects, you should create crypto context with custom RNG set to same seed and
|
||||
/// create multiple Pkcs12Encryption objects from it.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct Pkcs12Encryption {
|
||||
kind: Pkcs12EncryptionKind,
|
||||
inner: Pkcs12EncryptionAsn1,
|
||||
}
|
||||
|
||||
impl Pkcs12Encryption {
|
||||
/// Clone-like operation only could be performed internally when cloning higher level structures.
|
||||
/// Pkcs12Encryption non-cloneable nature is intentional to provide hint to the user that this
|
||||
/// structure should be used only once for each encryptable object.
|
||||
pub(crate) fn duplicate(&self) -> Self {
|
||||
Self {
|
||||
kind: self.kind.clone(),
|
||||
inner: self.inner.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_asn1(inner: Pkcs12EncryptionAsn1) -> Result<Self, Pkcs12Error> {
|
||||
let kind = match &inner {
|
||||
Pkcs12EncryptionAsn1::Pbes1 { kind, params } => Pkcs12EncryptionKind::Pbes1(Pbes1Encryption {
|
||||
cipher: *kind,
|
||||
kdf_iterations: Some(params.iterations),
|
||||
}),
|
||||
Pkcs12EncryptionAsn1::Pbes2(Pbes2ParamsAsn1 {
|
||||
key_derivation_func:
|
||||
Pbes2KeyDerivationFuncAsn1::Pbkdf2(Pbkdf2ParamsAsn1 {
|
||||
iteration_count, prf, ..
|
||||
}),
|
||||
encryption_scheme: Pbes2EncryptionSchemeAsn1::AesCbc { kind, .. },
|
||||
}) => {
|
||||
let hmac_kdf = match &prf {
|
||||
Some(algorithm) => Pkcs12HashAlgorithm::from_asn1_pbkdf2_prf(algorithm)?,
|
||||
None => Pkcs12HashAlgorithm::Sha1,
|
||||
};
|
||||
Pkcs12EncryptionKind::Pbes2(Pbes2Encryption {
|
||||
cipher: (*kind).into(),
|
||||
hmac_kdf,
|
||||
kdf_iterations: Some(*iteration_count),
|
||||
})
|
||||
}
|
||||
_ => Pkcs12EncryptionKind::Unknown,
|
||||
};
|
||||
|
||||
Ok(Self { kind, inner })
|
||||
}
|
||||
|
||||
/// Create new legacy PBES1 encryption (Not recommended for new files)
|
||||
pub fn new_pbes1(encryption: Pbes1Encryption, context: &mut Pkcs12CryptoContext) -> Self {
|
||||
let salt = context.generate_bytes(DEFAULT_SALT_SIZE);
|
||||
let inner = Pkcs12EncryptionAsn1::Pbes1 {
|
||||
kind: encryption.cipher,
|
||||
params: Pbes1ParamsAsn1 {
|
||||
salt: OctetStringAsn1(salt),
|
||||
iterations: encryption.kdf_iterations.unwrap_or(DEFAULT_KDF_ITERATIONS as u32),
|
||||
},
|
||||
};
|
||||
|
||||
Self {
|
||||
kind: Pkcs12EncryptionKind::Pbes1(encryption),
|
||||
inner,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create new PBES2 encryption (It is advised to use PBES2 for new files)
|
||||
pub fn new_pbes2(encryption: Pbes2Encryption, context: &mut Pkcs12CryptoContext) -> Self {
|
||||
let iv = context.generate_bytes(AES_BLOCK_SIZE);
|
||||
let encryption_scheme = Pbes2EncryptionSchemeAsn1::AesCbc {
|
||||
kind: encryption.cipher.into(),
|
||||
iv: OctetStringAsn1(iv),
|
||||
};
|
||||
|
||||
// Skip serialization if set to SHA1 as specified in RFC
|
||||
let prf = match Pbkdf2PrfAsn1::from(encryption.hmac_kdf) {
|
||||
Pbkdf2PrfAsn1::HmacWithSha1 => None,
|
||||
value => Some(value),
|
||||
};
|
||||
|
||||
let kdf_params = Pbkdf2ParamsAsn1 {
|
||||
salt: Pbkdf2SaltSourceAsn1::Specified(OctetStringAsn1(context.generate_bytes(DEFAULT_SALT_SIZE))),
|
||||
iteration_count: encryption.kdf_iterations.unwrap_or(DEFAULT_KDF_ITERATIONS as u32),
|
||||
// key length is not set by most implementations
|
||||
key_length: None,
|
||||
prf,
|
||||
};
|
||||
|
||||
let pbes2_params = Pbes2ParamsAsn1 {
|
||||
key_derivation_func: Pbes2KeyDerivationFuncAsn1::Pbkdf2(kdf_params),
|
||||
encryption_scheme,
|
||||
};
|
||||
|
||||
let inner = Pkcs12EncryptionAsn1::Pbes2(pbes2_params);
|
||||
|
||||
Self {
|
||||
kind: Pkcs12EncryptionKind::Pbes2(encryption),
|
||||
inner,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parsed encryption representation
|
||||
pub fn kind(&self) -> &Pkcs12EncryptionKind {
|
||||
&self.kind
|
||||
}
|
||||
|
||||
pub fn inner(&self) -> &Pkcs12EncryptionAsn1 {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
pub(crate) fn decrypt(&self, data: &[u8], context: &Pkcs12CryptoContext) -> Result<Vec<u8>, Pkcs12Error> {
|
||||
match self.inner() {
|
||||
Pkcs12EncryptionAsn1::Pbes1 { kind, params } => {
|
||||
let password = context.password_bytes_pbes1()?;
|
||||
decrypt_pbes1(
|
||||
*kind,
|
||||
password.as_slice(),
|
||||
params.salt.as_slice(),
|
||||
params.iterations as usize,
|
||||
data,
|
||||
)
|
||||
}
|
||||
Pkcs12EncryptionAsn1::Pbes2(params) => {
|
||||
let password = context.password_bytes_pbes2();
|
||||
decrypt_pbes2(params, password, data)
|
||||
}
|
||||
Pkcs12EncryptionAsn1::Unknown(raw) => {
|
||||
let oid = raw.algorithm().clone();
|
||||
Err(Pkcs12Error::NotSupportedAlgorithm {
|
||||
algorithm: super::UnsupportedPkcs12Algorithm::Oid(oid),
|
||||
context: "decryption".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn encrypt(&self, data: &[u8], context: &Pkcs12CryptoContext) -> Result<Vec<u8>, Pkcs12Error> {
|
||||
match self.inner() {
|
||||
Pkcs12EncryptionAsn1::Pbes1 { kind, params } => {
|
||||
let password = context.password_bytes_pbes1()?;
|
||||
encrypt_pbes1(
|
||||
*kind,
|
||||
password.as_slice(),
|
||||
params.salt.as_slice(),
|
||||
params.iterations as usize,
|
||||
data,
|
||||
)
|
||||
}
|
||||
Pkcs12EncryptionAsn1::Pbes2(params) => {
|
||||
let password = context.password_bytes_pbes2();
|
||||
encrypt_pbes2(params, password, data)
|
||||
}
|
||||
Pkcs12EncryptionAsn1::Unknown(raw) => {
|
||||
let oid = raw.algorithm().clone();
|
||||
Err(Pkcs12Error::NotSupportedAlgorithm {
|
||||
algorithm: super::UnsupportedPkcs12Algorithm::Oid(oid),
|
||||
context: "encryption".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Supported PKCS12 encryption algorithm descriptor.
|
||||
/// If parsed PFX file contains unknown encryption algorithm, [`Pkcs12EncryptionKind::Unknown`]
|
||||
/// variant is returned instead. If such encryption is encountered, then we can't use encrypted PFX
|
||||
/// nodes, but unencrypted data still could be extracted.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Pkcs12EncryptionKind {
|
||||
Pbes1(Pbes1Encryption),
|
||||
Pbes2(Pbes2Encryption),
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// PBES1 encryption descriptor.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Pbes1Encryption {
|
||||
cipher: Pbes1Cipher,
|
||||
kdf_iterations: Option<u32>,
|
||||
}
|
||||
|
||||
impl Pbes1Encryption {
|
||||
/// Creates new PBES1 encryption descriptor with given cipher algorithm.
|
||||
pub fn new(cipher: Pbes1Cipher) -> Self {
|
||||
Self {
|
||||
cipher,
|
||||
kdf_iterations: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets KDF iteraions count to `iterations`. If not set, [`DEFAULT_KDF_ITERATIONS`] is used
|
||||
/// instead.
|
||||
pub fn with_kdf_iterations(mut self, iterations: u32) -> Self {
|
||||
self.kdf_iterations = Some(iterations);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// PBES2 encryption descriptor.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Pbes2Encryption {
|
||||
cipher: Pbes2Cipher,
|
||||
hmac_kdf: Pkcs12HashAlgorithm,
|
||||
kdf_iterations: Option<u32>,
|
||||
}
|
||||
|
||||
impl Pbes2Encryption {
|
||||
/// Creates new PBES2 encryption descriptor with given kdf and cipher algorithms.
|
||||
pub fn new(cipher: Pbes2Cipher, hmac_kdf: Pkcs12HashAlgorithm) -> Self {
|
||||
Self {
|
||||
cipher,
|
||||
hmac_kdf,
|
||||
kdf_iterations: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets KDF iteraions count to `iterations`. If not set, [`DEFAULT_KDF_ITERATIONS`] is used
|
||||
/// instead.
|
||||
pub fn with_kdf_iterations(mut self, iterations: u32) -> Self {
|
||||
self.kdf_iterations = Some(iterations);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// PBES2 cipher algorithm
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Pbes2Cipher {
|
||||
Aes128Cbc,
|
||||
Aes192Cbc,
|
||||
Aes256Cbc,
|
||||
}
|
||||
|
||||
impl Pbes2Cipher {
|
||||
/// Returns cipher key size in bytes.
|
||||
pub fn key_size(self) -> usize {
|
||||
match self {
|
||||
Self::Aes128Cbc => 16,
|
||||
Self::Aes192Cbc => 24,
|
||||
Self::Aes256Cbc => 32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Pbes2Cipher> for Pbes2AesCbcEncryptionAsn1 {
|
||||
fn from(value: Pbes2Cipher) -> Self {
|
||||
match value {
|
||||
Pbes2Cipher::Aes128Cbc => Self::Aes128,
|
||||
Pbes2Cipher::Aes192Cbc => Self::Aes192,
|
||||
Pbes2Cipher::Aes256Cbc => Self::Aes256,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Pbes2AesCbcEncryptionAsn1> for Pbes2Cipher {
|
||||
fn from(value: Pbes2AesCbcEncryptionAsn1) -> Self {
|
||||
match value {
|
||||
Pbes2AesCbcEncryptionAsn1::Aes128 => Self::Aes128Cbc,
|
||||
Pbes2AesCbcEncryptionAsn1::Aes192 => Self::Aes192Cbc,
|
||||
Pbes2AesCbcEncryptionAsn1::Aes256 => Self::Aes256Cbc,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Pkcs12HashAlgorithm> for Pbkdf2PrfAsn1 {
|
||||
fn from(value: Pkcs12HashAlgorithm) -> Self {
|
||||
match value {
|
||||
Pkcs12HashAlgorithm::Sha1 => Self::HmacWithSha1,
|
||||
Pkcs12HashAlgorithm::Sha224 => Self::HmacWithSha224,
|
||||
Pkcs12HashAlgorithm::Sha256 => Self::HmacWithSha256,
|
||||
Pkcs12HashAlgorithm::Sha384 => Self::HmacWithSha384,
|
||||
Pkcs12HashAlgorithm::Sha512 => Self::HmacWithSha512,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Pbes2CipherInputs {
|
||||
key: Vec<u8>,
|
||||
iv: Vec<u8>,
|
||||
cipher: Pbes2Cipher,
|
||||
}
|
||||
|
||||
fn prepare_pbes2_cipher_inputs(
|
||||
params: &Pbes2ParamsAsn1,
|
||||
password: &[u8],
|
||||
cipher_context: &str,
|
||||
) -> Result<Pbes2CipherInputs, Pkcs12Error> {
|
||||
let (salt, kdf_iterations, prf) = match ¶ms.key_derivation_func {
|
||||
Pbes2KeyDerivationFuncAsn1::Pbkdf2(kdf) => {
|
||||
let salt = match &kdf.salt {
|
||||
Pbkdf2SaltSourceAsn1::Specified(salt) => salt.0.clone(),
|
||||
Pbkdf2SaltSourceAsn1::OtherSource(raw) => {
|
||||
let oid = raw.algorithm().clone();
|
||||
return Err(Pkcs12Error::NotSupportedAlgorithm {
|
||||
algorithm: super::UnsupportedPkcs12Algorithm::Oid(oid),
|
||||
context: format!("pbes2 {cipher_context} (kdf salt source)"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let prf = match &kdf.prf {
|
||||
None => Pkcs12HashAlgorithm::Sha1,
|
||||
Some(Pbkdf2PrfAsn1::HmacWithSha1) => Pkcs12HashAlgorithm::Sha1,
|
||||
Some(Pbkdf2PrfAsn1::HmacWithSha224) => Pkcs12HashAlgorithm::Sha224,
|
||||
Some(Pbkdf2PrfAsn1::HmacWithSha256) => Pkcs12HashAlgorithm::Sha256,
|
||||
Some(Pbkdf2PrfAsn1::HmacWithSha384) => Pkcs12HashAlgorithm::Sha384,
|
||||
Some(Pbkdf2PrfAsn1::HmacWithSha512) => Pkcs12HashAlgorithm::Sha512,
|
||||
Some(Pbkdf2PrfAsn1::Unknown(raw)) => {
|
||||
let oid = raw.algorithm().clone();
|
||||
return Err(Pkcs12Error::NotSupportedAlgorithm {
|
||||
algorithm: super::UnsupportedPkcs12Algorithm::Oid(oid),
|
||||
context: format!("pbes2 {cipher_context} (kdf prf)"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
(salt, kdf.iteration_count, prf)
|
||||
}
|
||||
Pbes2KeyDerivationFuncAsn1::Unknown(raw) => {
|
||||
let oid = raw.algorithm().clone();
|
||||
return Err(Pkcs12Error::NotSupportedAlgorithm {
|
||||
algorithm: super::UnsupportedPkcs12Algorithm::Oid(oid),
|
||||
context: format!("pbes2 {cipher_context} (kdf)"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let (cipher, iv) = match ¶ms.encryption_scheme {
|
||||
Pbes2EncryptionSchemeAsn1::AesCbc { kind, iv } => (Pbes2Cipher::from(*kind), iv.0.clone()),
|
||||
Pbes2EncryptionSchemeAsn1::Unknown(raw) => {
|
||||
let oid = raw.algorithm().clone();
|
||||
return Err(Pkcs12Error::NotSupportedAlgorithm {
|
||||
algorithm: super::UnsupportedPkcs12Algorithm::Oid(oid),
|
||||
context: format!("pbes2 {cipher_context} (cipher)"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let calculate_kdf = match prf {
|
||||
Pkcs12HashAlgorithm::Sha1 => pbkdf2::pbkdf2_hmac::<sha1::Sha1>,
|
||||
Pkcs12HashAlgorithm::Sha224 => pbkdf2::pbkdf2_hmac::<sha2::Sha224>,
|
||||
Pkcs12HashAlgorithm::Sha256 => pbkdf2::pbkdf2_hmac::<sha2::Sha256>,
|
||||
Pkcs12HashAlgorithm::Sha384 => pbkdf2::pbkdf2_hmac::<sha2::Sha384>,
|
||||
Pkcs12HashAlgorithm::Sha512 => pbkdf2::pbkdf2_hmac::<sha2::Sha512>,
|
||||
};
|
||||
|
||||
let mut key = vec![0u8; cipher.key_size()];
|
||||
calculate_kdf(password, salt.as_slice(), kdf_iterations, key.as_mut_slice());
|
||||
|
||||
Ok(Pbes2CipherInputs { key, iv, cipher })
|
||||
}
|
||||
|
||||
fn decrypt_pbes2(params: &Pbes2ParamsAsn1, password: &[u8], data: &[u8]) -> Result<Vec<u8>, Pkcs12Error> {
|
||||
let Pbes2CipherInputs { key, iv, cipher } = prepare_pbes2_cipher_inputs(params, password, "decryption")?;
|
||||
|
||||
use aes::cipher::BlockModeDecrypt;
|
||||
use cbc::Decryptor;
|
||||
use cbc::cipher::KeyIvInit;
|
||||
use cbc::cipher::block_padding::Pkcs7;
|
||||
|
||||
let decrypted = match cipher {
|
||||
Pbes2Cipher::Aes128Cbc => {
|
||||
use aes::Aes128;
|
||||
type Aes128Cbc = Decryptor<Aes128>;
|
||||
|
||||
let aes = Aes128Cbc::new_from_slices(key.as_slice(), iv.as_slice()).map_err(|_| Pkcs12Error::Pbes2 {
|
||||
context: "AES128 decryptor initialization failed".to_string(),
|
||||
})?;
|
||||
|
||||
aes.decrypt_padded_vec::<Pkcs7>(data).map_err(|_| Pkcs12Error::Pbes2 {
|
||||
context: "AES128 decryption with padding failed".to_string(),
|
||||
})?
|
||||
}
|
||||
Pbes2Cipher::Aes192Cbc => {
|
||||
use aes::Aes192;
|
||||
type Aes192Cbc = Decryptor<Aes192>;
|
||||
|
||||
let aes = Aes192Cbc::new_from_slices(key.as_slice(), iv.as_slice()).map_err(|_| Pkcs12Error::Pbes2 {
|
||||
context: "AES192 decryptor initialization failed".to_string(),
|
||||
})?;
|
||||
|
||||
aes.decrypt_padded_vec::<Pkcs7>(data).map_err(|_| Pkcs12Error::Pbes2 {
|
||||
context: "AES192 decryption with padding failed".to_string(),
|
||||
})?
|
||||
}
|
||||
Pbes2Cipher::Aes256Cbc => {
|
||||
use aes::Aes256;
|
||||
type Aes256Cbc = Decryptor<Aes256>;
|
||||
|
||||
let aes = Aes256Cbc::new_from_slices(key.as_slice(), iv.as_slice()).map_err(|_| Pkcs12Error::Pbes2 {
|
||||
context: "AES256 decryptor initialization failed".to_string(),
|
||||
})?;
|
||||
|
||||
aes.decrypt_padded_vec::<Pkcs7>(data).map_err(|_| Pkcs12Error::Pbes2 {
|
||||
context: "AES256 decryption with padding failed".to_string(),
|
||||
})?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(decrypted)
|
||||
}
|
||||
|
||||
fn encrypt_pbes2(params: &Pbes2ParamsAsn1, password: &[u8], data: &[u8]) -> Result<Vec<u8>, Pkcs12Error> {
|
||||
let Pbes2CipherInputs { key, iv, cipher } = prepare_pbes2_cipher_inputs(params, password, "encryption")?;
|
||||
|
||||
use aes::cipher::BlockModeEncrypt;
|
||||
use cbc::Encryptor;
|
||||
use cbc::cipher::KeyIvInit;
|
||||
use cbc::cipher::block_padding::Pkcs7;
|
||||
|
||||
let encrypted = match cipher {
|
||||
Pbes2Cipher::Aes128Cbc => {
|
||||
use aes::Aes128;
|
||||
type Aes128Cbc = Encryptor<Aes128>;
|
||||
|
||||
let aes = Aes128Cbc::new_from_slices(key.as_slice(), iv.as_slice()).map_err(|_| Pkcs12Error::Pbes2 {
|
||||
context: "AES128 encryptor initialization failed".to_string(),
|
||||
})?;
|
||||
|
||||
aes.encrypt_padded_vec::<Pkcs7>(data)
|
||||
}
|
||||
Pbes2Cipher::Aes192Cbc => {
|
||||
use aes::Aes192;
|
||||
type Aes192Cbc = Encryptor<Aes192>;
|
||||
|
||||
let aes = Aes192Cbc::new_from_slices(key.as_slice(), iv.as_slice()).map_err(|_| Pkcs12Error::Pbes2 {
|
||||
context: "AES192 encryptor initialization failed".to_string(),
|
||||
})?;
|
||||
|
||||
aes.encrypt_padded_vec::<Pkcs7>(data)
|
||||
}
|
||||
Pbes2Cipher::Aes256Cbc => {
|
||||
use aes::Aes256;
|
||||
type Aes256Cbc = Encryptor<Aes256>;
|
||||
|
||||
let aes = Aes256Cbc::new_from_slices(key.as_slice(), iv.as_slice()).map_err(|_| Pkcs12Error::Pbes2 {
|
||||
context: "AES256 encryptor initialization failed".to_string(),
|
||||
})?;
|
||||
|
||||
aes.encrypt_padded_vec::<Pkcs7>(data)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(encrypted)
|
||||
}
|
||||
|
||||
fn generate_pbes1_key_and_iv(
|
||||
cipher: Pbes1Cipher,
|
||||
password: &[u8],
|
||||
salt: &[u8],
|
||||
kdf_iterations: usize,
|
||||
) -> (Vec<u8>, Vec<u8>) {
|
||||
let (key_size, iv_size) = match cipher {
|
||||
Pbes1Cipher::ShaAnd40BitRc2Cbc => (5, 8),
|
||||
Pbes1Cipher::ShaAnd3Key3DesCbc => (24, 8),
|
||||
};
|
||||
|
||||
let key = pbkdf1(
|
||||
Pkcs12HashAlgorithm::Sha1,
|
||||
password,
|
||||
salt,
|
||||
kdf_iterations,
|
||||
Pbkdf1Usage::Key,
|
||||
key_size,
|
||||
);
|
||||
let iv = pbkdf1(
|
||||
Pkcs12HashAlgorithm::Sha1,
|
||||
password,
|
||||
salt,
|
||||
kdf_iterations,
|
||||
Pbkdf1Usage::Iv,
|
||||
iv_size,
|
||||
);
|
||||
|
||||
(key, iv)
|
||||
}
|
||||
|
||||
fn encrypt_pbes1(
|
||||
scheme: Pbes1Cipher,
|
||||
password: &[u8],
|
||||
salt: &[u8],
|
||||
kdf_iterations: usize,
|
||||
data: &[u8],
|
||||
) -> Result<Vec<u8>, Pkcs12Error> {
|
||||
use cbc::Encryptor;
|
||||
use cbc::cipher::block_padding::Pkcs7;
|
||||
use cbc::cipher::{BlockModeEncrypt, KeyIvInit};
|
||||
|
||||
let (dk, iv) = generate_pbes1_key_and_iv(scheme, password, salt, kdf_iterations);
|
||||
|
||||
match scheme {
|
||||
Pbes1Cipher::ShaAnd40BitRc2Cbc => {
|
||||
use rc2::Rc2;
|
||||
type Rc2Cbc = Encryptor<Rc2>;
|
||||
|
||||
let rc2 = Rc2Cbc::new_from_slices(&dk, &iv).map_err(|_| Pkcs12Error::Pbes1 {
|
||||
context: "RC2 encryption initialization failed".to_string(),
|
||||
})?;
|
||||
Ok(rc2.encrypt_padded_vec::<Pkcs7>(data))
|
||||
}
|
||||
Pbes1Cipher::ShaAnd3Key3DesCbc => {
|
||||
use des::TdesEde3;
|
||||
type TDesCbc = Encryptor<TdesEde3>;
|
||||
|
||||
let tdes = TDesCbc::new_from_slices(&dk, &iv).map_err(|_| Pkcs12Error::Pbes1 {
|
||||
context: "3DES encryptor initialization failed".to_string(),
|
||||
})?;
|
||||
Ok(tdes.encrypt_padded_vec::<Pkcs7>(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn decrypt_pbes1(
|
||||
scheme: Pbes1Cipher,
|
||||
password: &[u8],
|
||||
salt: &[u8],
|
||||
kdf_iterations: usize,
|
||||
data: &[u8],
|
||||
) -> Result<Vec<u8>, Pkcs12Error> {
|
||||
use cbc::Decryptor;
|
||||
use cbc::cipher::block_padding::Pkcs7;
|
||||
use cbc::cipher::{BlockModeDecrypt, KeyIvInit};
|
||||
|
||||
let (dk, iv) = generate_pbes1_key_and_iv(scheme, password, salt, kdf_iterations);
|
||||
|
||||
match scheme {
|
||||
Pbes1Cipher::ShaAnd40BitRc2Cbc => {
|
||||
use rc2::Rc2;
|
||||
type Rc2Cbc = Decryptor<Rc2>;
|
||||
|
||||
let rc2 = Rc2Cbc::new_from_slices(&dk, &iv).map_err(|_| Pkcs12Error::Pbes1 {
|
||||
context: "RC2 decryptor initialization failed".to_string(),
|
||||
})?;
|
||||
rc2.decrypt_padded_vec::<Pkcs7>(data).map_err(|_| Pkcs12Error::Pbes1 {
|
||||
context: "RC2 decryption with padding failed".to_string(),
|
||||
})
|
||||
}
|
||||
Pbes1Cipher::ShaAnd3Key3DesCbc => {
|
||||
use des::TdesEde3;
|
||||
type TDesCbc = Decryptor<TdesEde3>;
|
||||
|
||||
let tdes = TDesCbc::new_from_slices(&dk, &iv).map_err(|_| Pkcs12Error::Pbes1 {
|
||||
context: "3DES decryptor initialization failed".to_string(),
|
||||
})?;
|
||||
tdes.decrypt_padded_vec::<Pkcs7>(data).map_err(|_| Pkcs12Error::Pbes1 {
|
||||
context: "3DES decryption with padding failed".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rstest::rstest;
|
||||
|
||||
#[test]
|
||||
fn pbes1_3des_roundtrip() {
|
||||
let password = b"\0a\0b\0c\0\0";
|
||||
let salt = (0..8).collect::<Vec<u8>>();
|
||||
let iterations = 2000;
|
||||
let data = (0..123).collect::<Vec<u8>>();
|
||||
let encrypted = encrypt_pbes1(Pbes1Cipher::ShaAnd3Key3DesCbc, password, &salt, iterations, &data).unwrap();
|
||||
let decrypted = decrypt_pbes1(Pbes1Cipher::ShaAnd3Key3DesCbc, password, &salt, iterations, &encrypted).unwrap();
|
||||
assert_eq!(decrypted, data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pbes1_rc2_roundtrip() {
|
||||
let password = b"\0b\0c\0a\0\0";
|
||||
let salt = (0..8).collect::<Vec<u8>>();
|
||||
let iterations = 2048;
|
||||
let data = (0..124).collect::<Vec<u8>>();
|
||||
let encrypted = encrypt_pbes1(Pbes1Cipher::ShaAnd40BitRc2Cbc, password, &salt, iterations, &data).unwrap();
|
||||
let decrypted = decrypt_pbes1(Pbes1Cipher::ShaAnd40BitRc2Cbc, password, &salt, iterations, &encrypted).unwrap();
|
||||
assert_eq!(decrypted, data);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(Pbes2AesCbcEncryptionAsn1::Aes128)]
|
||||
#[case(Pbes2AesCbcEncryptionAsn1::Aes192)]
|
||||
#[case(Pbes2AesCbcEncryptionAsn1::Aes256)]
|
||||
fn pbes2_aes256_roundtrip(#[case] aes: Pbes2AesCbcEncryptionAsn1) {
|
||||
let password = b"test";
|
||||
let data = (0..123).collect::<Vec<u8>>();
|
||||
let params = Pbes2ParamsAsn1 {
|
||||
key_derivation_func: Pbes2KeyDerivationFuncAsn1::Pbkdf2(Pbkdf2ParamsAsn1 {
|
||||
salt: Pbkdf2SaltSourceAsn1::Specified(OctetStringAsn1((0..8).collect())),
|
||||
iteration_count: 2000,
|
||||
key_length: None,
|
||||
prf: Some(Pbkdf2PrfAsn1::HmacWithSha256),
|
||||
}),
|
||||
encryption_scheme: Pbes2EncryptionSchemeAsn1::AesCbc {
|
||||
kind: aes,
|
||||
iv: OctetStringAsn1((0..16).collect()),
|
||||
},
|
||||
};
|
||||
let encrypted = encrypt_pbes2(¶ms, password, &data).unwrap();
|
||||
let decrypted = decrypt_pbes2(¶ms, password, &encrypted).unwrap();
|
||||
assert_eq!(decrypted, data);
|
||||
}
|
||||
}
|
||||
Vendored
+196
@@ -0,0 +1,196 @@
|
||||
use crate::pkcs12::{Pbkdf1Usage, Pkcs12CryptoContext, Pkcs12Error, Pkcs12HashAlgorithm, pbkdf1};
|
||||
use hmac::KeyInit;
|
||||
use picky_asn1::wrapper::OctetStringAsn1;
|
||||
use picky_asn1_x509::pkcs12::{MacData as MacDataAsn1, Pkcs12DigestInfo as Pkcs12DigestInfoAsn1};
|
||||
use thiserror::Error;
|
||||
|
||||
const DEFAULT_MAC_KDF_ITERATIONS: u32 = 1;
|
||||
const DEFAULT_SALT_SIZE: usize = 20;
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum Pkcs12MacError {
|
||||
#[error("Invalid hmac input size")]
|
||||
InvalidHmacInputSize,
|
||||
#[error("MAC validation failed (wrong password or corrupted data)")]
|
||||
MacValidation,
|
||||
}
|
||||
|
||||
/// HMAC algorithm parameters (used for PFX integrity data)
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Pkcs12MacAlgorithmHmac {
|
||||
hash_algorithm: Pkcs12HashAlgorithm,
|
||||
iterations: Option<u32>,
|
||||
}
|
||||
|
||||
impl Pkcs12MacAlgorithmHmac {
|
||||
pub fn new(hash_algorithm: Pkcs12HashAlgorithm) -> Self {
|
||||
Self {
|
||||
hash_algorithm,
|
||||
iterations: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_iterations(mut self, iterations: u32) -> Self {
|
||||
self.iterations = Some(iterations);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn hash_algorithm(&self) -> Pkcs12HashAlgorithm {
|
||||
self.hash_algorithm
|
||||
}
|
||||
|
||||
pub fn iterations(&self) -> Option<u32> {
|
||||
self.iterations
|
||||
}
|
||||
}
|
||||
|
||||
/// Parsed MAC algorithm parameters
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Pkcs12MacAlgorithm {
|
||||
Hmac(Pkcs12MacAlgorithmHmac),
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// Parsed PFX MAC data
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Pkcs12MacData {
|
||||
algorithm: Pkcs12MacAlgorithm,
|
||||
inner: MacDataAsn1,
|
||||
}
|
||||
|
||||
impl Pkcs12MacData {
|
||||
pub(crate) fn from_asn1(inner: MacDataAsn1, skip_unknown_hash_algorithm: bool) -> Result<Self, Pkcs12Error> {
|
||||
let hash_algorithm = match Pkcs12HashAlgorithm::from_asn1_digest_algorithm(&inner.mac.digest_algorithm) {
|
||||
Ok(algorithm) => algorithm,
|
||||
Err(_) if skip_unknown_hash_algorithm => {
|
||||
return Ok(Self {
|
||||
algorithm: Pkcs12MacAlgorithm::Unknown,
|
||||
inner,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
let kdf_iterations = inner.iterations.unwrap_or(DEFAULT_MAC_KDF_ITERATIONS);
|
||||
|
||||
Ok(Self {
|
||||
algorithm: Pkcs12MacAlgorithm::Hmac(Pkcs12MacAlgorithmHmac {
|
||||
hash_algorithm,
|
||||
iterations: Some(kdf_iterations),
|
||||
}),
|
||||
inner,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn new_hmac(
|
||||
algorithm: Pkcs12MacAlgorithmHmac,
|
||||
context: &mut Pkcs12CryptoContext,
|
||||
data: &[u8],
|
||||
) -> Result<Self, Pkcs12Error> {
|
||||
let hash_algorithm = algorithm.hash_algorithm;
|
||||
let kdf_iterations = algorithm.iterations.unwrap_or(DEFAULT_MAC_KDF_ITERATIONS);
|
||||
let salt = context.generate_bytes(DEFAULT_SALT_SIZE);
|
||||
|
||||
// PKCS12 MAC uses BMPString as password representation
|
||||
let password = context.password_bytes_pbes1()?;
|
||||
|
||||
let digest = Self::calculate_digest(
|
||||
hash_algorithm,
|
||||
kdf_iterations,
|
||||
password.as_slice(),
|
||||
salt.as_slice(),
|
||||
data,
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
algorithm: Pkcs12MacAlgorithm::Hmac(algorithm),
|
||||
inner: MacDataAsn1 {
|
||||
mac: Pkcs12DigestInfoAsn1 {
|
||||
digest_algorithm: hash_algorithm.into(),
|
||||
digest: OctetStringAsn1(digest),
|
||||
},
|
||||
salt: OctetStringAsn1(salt),
|
||||
iterations: Some(kdf_iterations),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn validate(&self, context: &Pkcs12CryptoContext, data: &[u8]) -> Result<(), Pkcs12Error> {
|
||||
let hash_algorithm = Pkcs12HashAlgorithm::from_asn1_digest_algorithm(&self.inner.mac.digest_algorithm)?;
|
||||
let kdf_iterations = self.inner.iterations.unwrap_or(DEFAULT_MAC_KDF_ITERATIONS);
|
||||
let salt = self.inner.salt.0.as_slice();
|
||||
|
||||
// PKCS12 MAC uses BMPString as password representation
|
||||
let password = context.password_bytes_pbes1()?;
|
||||
|
||||
let digest = Self::calculate_digest(hash_algorithm, kdf_iterations, password.as_slice(), salt, data)?;
|
||||
|
||||
if digest == self.inner.mac.digest.0.as_slice() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Pkcs12MacError::MacValidation.into())
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_digest(
|
||||
hash_algorithm: Pkcs12HashAlgorithm,
|
||||
kdf_iterations: u32,
|
||||
password: &[u8],
|
||||
salt: &[u8],
|
||||
data: &[u8],
|
||||
) -> Result<Vec<u8>, Pkcs12Error> {
|
||||
let key = pbkdf1(
|
||||
hash_algorithm,
|
||||
password,
|
||||
salt,
|
||||
kdf_iterations as usize,
|
||||
Pbkdf1Usage::Mac,
|
||||
hash_algorithm.digest_size(),
|
||||
);
|
||||
|
||||
use hmac::Mac;
|
||||
|
||||
let map_hmac_err = |_| Pkcs12MacError::InvalidHmacInputSize;
|
||||
|
||||
let mac = match hash_algorithm {
|
||||
Pkcs12HashAlgorithm::Sha1 => {
|
||||
let mut hmac = hmac::Hmac::<sha1::Sha1>::new_from_slice(&key).map_err(map_hmac_err)?;
|
||||
hmac.update(data);
|
||||
hmac.finalize().into_bytes().to_vec()
|
||||
}
|
||||
Pkcs12HashAlgorithm::Sha224 => {
|
||||
let mut hmac = hmac::Hmac::<sha2::Sha224>::new_from_slice(&key).map_err(map_hmac_err)?;
|
||||
hmac.update(data);
|
||||
hmac.finalize().into_bytes().to_vec()
|
||||
}
|
||||
Pkcs12HashAlgorithm::Sha256 => {
|
||||
let mut hmac = hmac::Hmac::<sha2::Sha256>::new_from_slice(&key).map_err(map_hmac_err)?;
|
||||
hmac.update(data);
|
||||
hmac.finalize().into_bytes().to_vec()
|
||||
}
|
||||
Pkcs12HashAlgorithm::Sha384 => {
|
||||
let mut hmac = hmac::Hmac::<sha2::Sha384>::new_from_slice(&key).map_err(map_hmac_err)?;
|
||||
hmac.update(data);
|
||||
hmac.finalize().into_bytes().to_vec()
|
||||
}
|
||||
Pkcs12HashAlgorithm::Sha512 => {
|
||||
let mut hmac = hmac::Hmac::<sha2::Sha512>::new_from_slice(&key).map_err(map_hmac_err)?;
|
||||
hmac.update(data);
|
||||
hmac.finalize().into_bytes().to_vec()
|
||||
}
|
||||
};
|
||||
|
||||
Ok(mac)
|
||||
}
|
||||
|
||||
pub fn inner(&self) -> &MacDataAsn1 {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
/// Parsed MAC algorithm parameters
|
||||
pub fn algorithm(&self) -> &Pkcs12MacAlgorithm {
|
||||
&self.algorithm
|
||||
}
|
||||
}
|
||||
Vendored
+614
@@ -0,0 +1,614 @@
|
||||
//! This module provides types for parsing and building PKCS#12 files (PFX).
|
||||
//!
|
||||
//! # PFX file structure
|
||||
//!
|
||||
//! PFX files are constructed from `safe contents` on the highest level, which are containers for
|
||||
//! various types of `safe bags`. They could be encrypted or not and contain one or more `safe bags`.
|
||||
//!
|
||||
//! `Safe bags` are polymorphic containers for various types of data, such as private keys, certificates,
|
||||
//! custom data (`secrets`), CRLs, etc. They could also be nested, which means that one `safe bag` could
|
||||
//! contain multiple `safe bags` inside, making it possible to represent complex tree-like structures.
|
||||
//!
|
||||
//! Each `safe bag` could also contain `attributes`, which are key-value pairs of various types of data.
|
||||
//! High-level API defines only `friendly name` and `local key id` attributes, but it is possible to
|
||||
//! parse/build any custom attributes as well.
|
||||
//!
|
||||
//! There are also special `safe bags` called `shrouded key bags`, which are basically encrypted
|
||||
//! PKCS8 private keys. Encryption on `safe bag` level is performed to allow PFX files that have
|
||||
//! unencrypted certificates and encrypted private keys, to be preprocessed by the software without
|
||||
//! knowing PFX password (e.g. check if PFX contains private key for the certificate or not). Usually
|
||||
//! relations between contained certificates and private keys are marked with `local key id` attribute
|
||||
//! which is set to the same value for both certificate and private key.
|
||||
//!
|
||||
//! # Parsing
|
||||
//!
|
||||
//! Almost any kind of PKCS#12 object specified by [RFC](https://datatracker.ietf.org/doc/html/rfc7292)
|
||||
//! could be parsed or constructed. However, only the most used PFX types are wrapped in high level
|
||||
//! convenient API such as private keys, certificates, nested pfx nodes, etc. If parsed
|
||||
//! data was in fact some rarely used PFX type, it is still possible to access it via low level API
|
||||
//! via `.inner()` method usually returns ASN.1 DER encoded data (`picky-asn1-x509` crate types).
|
||||
//!
|
||||
//! Parsing process could be controlled even more with [`Pkcs12ParsingParams`], which allows to skip
|
||||
//! some parsing errors (e.g. Skip failed mapping of private key ASN.1 structure to high-level picky
|
||||
//! wrapper)
|
||||
//!
|
||||
//! Parsing of PFX file is simple as calling [`Pfx::from_der`] method with required params
|
||||
//! (crypto context which holds PFX password, parsing params and data itself), after which its safe
|
||||
//! bags could be inspected and data extracted from PFX.
|
||||
//!
|
||||
//! # Building
|
||||
//!
|
||||
//! Building process of PFX files is down-to-top, in contrast to parsing. It starts with
|
||||
//! building of required safe bags/attributes structures, wrapping them inside safe contents
|
||||
//! (encrypted or unencrypted) and then wrapping them in PFX structure. This allows to keep API
|
||||
//! flexible while representing Pfx file both after parsing and building as the same type ([`Pfx])
|
||||
//!
|
||||
//! # Encryption
|
||||
//!
|
||||
//! - It is advised to always use Pbes2 AES-based encryption for PFX files. The only use case for new
|
||||
//! PFX files with Pbes1 encryption is to support legacy software that does not support Pbes2.
|
||||
//! - Usually PFX files without passwords are actually encrypted with empty-string passwords if
|
||||
//! generated by modern software by default. (e.g. certmgr or OpenSSL). However, it is possible to
|
||||
//! create PFX files without any encryption and MAC in picky if PFX will be used just as a plain
|
||||
//! container for certificates and private keys and stored somewhere securely (e.g. wrapped in
|
||||
//! another encryption layer).
|
||||
|
||||
mod attribute;
|
||||
mod encryption;
|
||||
mod mac;
|
||||
mod pbkdf1;
|
||||
mod safe_bag;
|
||||
mod safe_contents;
|
||||
|
||||
use picky_asn1::restricted_string::CharSetError;
|
||||
use picky_asn1_der::Asn1RawDer;
|
||||
use picky_asn1_x509::oid::ObjectIdentifier;
|
||||
use picky_asn1_x509::pkcs12::{
|
||||
AuthenticatedSafeContentInfo as AuthenticatedSafeContentInfoAsn1,
|
||||
ParsedAuthenticatedSafeDataRepr as ParsedAuthenticatedSafeDataReprAsn1, Pbkdf2Prf as Pbkdf2PrfAsn1, Pfx as PfxAsn1,
|
||||
Pkcs12DigestAlgorithm as Pkcs12DigestAlgorithmAsn1, Pkcs12DigestAlgorithm,
|
||||
RawAuthenticatedSafeContentInfo as RawAuthenticatedSafeContentInfoAsn1, RawPfx as RawPfxAsn1,
|
||||
SafeContentsContentInfo as SafeContentsContentInfoAsn1,
|
||||
};
|
||||
use std::fmt::Display;
|
||||
use thiserror::Error;
|
||||
|
||||
pub(crate) use pbkdf1::{Pbkdf1Usage, pbkdf1};
|
||||
|
||||
pub use attribute::{CustomPkcs12Attribute, Pkcs12Attribute, Pkcs12AttributeKind};
|
||||
pub use encryption::{
|
||||
Pbes1Cipher, Pbes1Encryption, Pbes2Cipher, Pbes2Encryption, Pkcs12CryptoContext, Pkcs12Encryption,
|
||||
Pkcs12EncryptionKind,
|
||||
};
|
||||
pub use mac::{Pkcs12MacAlgorithm, Pkcs12MacAlgorithmHmac, Pkcs12MacData, Pkcs12MacError};
|
||||
pub use safe_bag::{SafeBag, SafeBagKind, SecretSafeBag};
|
||||
pub use safe_contents::{SafeContents, SafeContentsKind};
|
||||
|
||||
const PFX_VERSION: u8 = 3;
|
||||
|
||||
/// Parsed PFX (PKCS12 archive). See module docs for more info on PFX file structure and API usage.
|
||||
#[derive(Debug)]
|
||||
pub struct Pfx {
|
||||
safe_contents: Vec<SafeContents>,
|
||||
mac_data: Option<Pkcs12MacData>,
|
||||
/// Pre-serialized auth safe data. Just an optimization to avoid re-serializing the auth safe
|
||||
/// data if it was already serialized for the MAC calculation.
|
||||
auth_safe_data: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl Pfx {
|
||||
/// Create new PFX file with HMAC MAC algorithm. Usually this is the default in modern software
|
||||
pub fn new_with_hmac(
|
||||
safe_contents: Vec<SafeContents>,
|
||||
mac: Pkcs12MacAlgorithmHmac,
|
||||
crypto_context: &mut Pkcs12CryptoContext,
|
||||
) -> Result<Self, Pkcs12Error> {
|
||||
let safe_contents_asn1 = safe_contents.iter().map(|sc| sc.inner().clone()).collect::<Vec<_>>();
|
||||
|
||||
let serialized_auth_safe = picky_asn1_der::to_vec(&safe_contents_asn1)?;
|
||||
let mac_data = Pkcs12MacData::new_hmac(mac, crypto_context, &serialized_auth_safe)?;
|
||||
|
||||
Ok(Self {
|
||||
safe_contents,
|
||||
mac_data: Some(mac_data),
|
||||
auth_safe_data: Some(serialized_auth_safe),
|
||||
})
|
||||
}
|
||||
|
||||
/// Create new PFX file without MAC algorithm if needed for some reason (e.g. MAC is performed
|
||||
/// on the higher level)
|
||||
pub fn new_without_mac(safe_contents: Vec<SafeContents>) -> Self {
|
||||
Self {
|
||||
safe_contents,
|
||||
mac_data: None,
|
||||
auth_safe_data: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialize PFX file to DER bytes
|
||||
pub fn to_der(&self) -> Result<Vec<u8>, Pkcs12Error> {
|
||||
match &self.auth_safe_data {
|
||||
Some(auth_safe) => {
|
||||
let pfx_asn1 = RawPfxAsn1 {
|
||||
version: PFX_VERSION,
|
||||
auth_safe: RawAuthenticatedSafeContentInfoAsn1::Data(Asn1RawDer(auth_safe.clone())),
|
||||
mac_data: self.mac_data.as_ref().map(|mac_data| mac_data.inner().clone()),
|
||||
};
|
||||
|
||||
picky_asn1_der::to_vec(&pfx_asn1).map_err(Into::into)
|
||||
}
|
||||
None => {
|
||||
let pfx_asn1 = PfxAsn1 {
|
||||
version: PFX_VERSION,
|
||||
auth_safe: AuthenticatedSafeContentInfoAsn1::<ParsedAuthenticatedSafeDataReprAsn1>::Data(
|
||||
self.safe_contents
|
||||
.iter()
|
||||
.map(|sc| sc.inner().clone())
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
mac_data: self.mac_data.as_ref().map(|mac_data| mac_data.inner().clone()),
|
||||
};
|
||||
|
||||
picky_asn1_der::to_vec(&pfx_asn1).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a PKCS12 archive (PFX) from its DER representation.
|
||||
pub fn from_der(
|
||||
data: &[u8],
|
||||
crypto_context: &Pkcs12CryptoContext,
|
||||
parsing_params: &Pkcs12ParsingParams,
|
||||
) -> Result<Self, Pkcs12Error> {
|
||||
let (auth_safe, mac_data) = if parsing_params.skip_mac_validation {
|
||||
let pfx_asn1: PfxAsn1 = picky_asn1_der::from_bytes(data)?;
|
||||
if pfx_asn1.version != PFX_VERSION {
|
||||
return Err(Pkcs12Error::InvalidVersion(pfx_asn1.version));
|
||||
}
|
||||
let mac_data = pfx_asn1
|
||||
.mac_data
|
||||
.map(|asn1| Pkcs12MacData::from_asn1(asn1, true))
|
||||
.transpose()?;
|
||||
|
||||
let auth_safe = match pfx_asn1.auth_safe {
|
||||
AuthenticatedSafeContentInfoAsn1::Data(data) => data,
|
||||
AuthenticatedSafeContentInfoAsn1::Unknown { content_type, .. } => {
|
||||
return Err(Pkcs12Error::InvalidAuthenticatedSafeContentType(content_type.into()));
|
||||
}
|
||||
};
|
||||
|
||||
(auth_safe, mac_data)
|
||||
} else {
|
||||
let pfx_asn1: RawPfxAsn1 = picky_asn1_der::from_bytes(data)?;
|
||||
if pfx_asn1.version != PFX_VERSION {
|
||||
return Err(Pkcs12Error::InvalidVersion(pfx_asn1.version));
|
||||
}
|
||||
|
||||
let mac_data = pfx_asn1
|
||||
.mac_data
|
||||
.map(|asn1| Pkcs12MacData::from_asn1(asn1, false))
|
||||
.transpose()?;
|
||||
|
||||
let auth_safe = match pfx_asn1.auth_safe {
|
||||
AuthenticatedSafeContentInfoAsn1::Data(data) => {
|
||||
if let Some(mac_data) = &mac_data {
|
||||
mac_data.validate(crypto_context, data.0.as_slice())?;
|
||||
}
|
||||
|
||||
let parsed: Vec<SafeContentsContentInfoAsn1> = picky_asn1_der::from_bytes(data.0.as_slice())?;
|
||||
parsed
|
||||
}
|
||||
AuthenticatedSafeContentInfoAsn1::Unknown { content_type, .. } => {
|
||||
return Err(Pkcs12Error::InvalidAuthenticatedSafeContentType(content_type.into()));
|
||||
}
|
||||
};
|
||||
|
||||
(auth_safe, mac_data)
|
||||
};
|
||||
|
||||
auth_safe
|
||||
.into_iter()
|
||||
.map(|safe_contents| SafeContents::from_asn1(safe_contents, crypto_context, parsing_params))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map(|safe_contents| Self {
|
||||
safe_contents,
|
||||
mac_data,
|
||||
auth_safe_data: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Inspect parsed PFX data
|
||||
pub fn safe_contents(&self) -> &[SafeContents] {
|
||||
&self.safe_contents
|
||||
}
|
||||
|
||||
/// Inspect parsed MAC data
|
||||
pub fn mac_data(&self) -> Option<&Pkcs12MacData> {
|
||||
self.mac_data.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
/// Parameters which control some aspects of PFX file parsing process
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Pkcs12ParsingParams {
|
||||
/// Continue parsing if conversion to high level picky data structure fails (e.g. due to
|
||||
/// unsupported private key or certificate kind)
|
||||
pub skip_soft_parsing_errors: bool,
|
||||
|
||||
/// Continue parsing if decryption fails and keep data in encrypted form
|
||||
pub skip_decryption_errors: bool,
|
||||
|
||||
/// Continue parsing if MAC validation fails.
|
||||
///
|
||||
/// This is useful for parsing available unencrypted data from
|
||||
/// password-protected PFX files. Also could be useful if PFX integrity has been intentionally
|
||||
/// violated for testing purposes.
|
||||
pub skip_mac_validation: bool,
|
||||
}
|
||||
|
||||
/// Hashing algorithm used for MAC or KDF in PFX file
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Pkcs12HashAlgorithm {
|
||||
Sha1,
|
||||
Sha224,
|
||||
Sha256,
|
||||
Sha384,
|
||||
Sha512,
|
||||
}
|
||||
|
||||
impl Pkcs12HashAlgorithm {
|
||||
pub(crate) fn pbkdf1_u_bits(self) -> usize {
|
||||
match self {
|
||||
Self::Sha1 => 160,
|
||||
Self::Sha224 => 224,
|
||||
Self::Sha256 => 256,
|
||||
Self::Sha384 => 384,
|
||||
Self::Sha512 => 512,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn pbkdf1_v_bits(self) -> usize {
|
||||
match self {
|
||||
Self::Sha1 => 512,
|
||||
Self::Sha224 => 512,
|
||||
Self::Sha256 => 512,
|
||||
Self::Sha384 => 1024,
|
||||
Self::Sha512 => 1024,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn digest_size(self) -> usize {
|
||||
match self {
|
||||
Self::Sha1 => 20,
|
||||
Self::Sha224 => 28,
|
||||
Self::Sha256 => 32,
|
||||
Self::Sha384 => 48,
|
||||
Self::Sha512 => 64,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_asn1_pbkdf2_prf(value: &Pbkdf2PrfAsn1) -> Result<Self, Pkcs12Error> {
|
||||
let algorithm = match value {
|
||||
Pbkdf2PrfAsn1::HmacWithSha1 => Pkcs12HashAlgorithm::Sha1,
|
||||
Pbkdf2PrfAsn1::HmacWithSha224 => Pkcs12HashAlgorithm::Sha224,
|
||||
Pbkdf2PrfAsn1::HmacWithSha256 => Pkcs12HashAlgorithm::Sha256,
|
||||
Pbkdf2PrfAsn1::HmacWithSha384 => Pkcs12HashAlgorithm::Sha384,
|
||||
Pbkdf2PrfAsn1::HmacWithSha512 => Pkcs12HashAlgorithm::Sha512,
|
||||
Pbkdf2PrfAsn1::Unknown(raw) => {
|
||||
let oid = raw.algorithm().clone();
|
||||
return Err(Pkcs12Error::NotSupportedAlgorithm {
|
||||
algorithm: UnsupportedPkcs12Algorithm::Oid(oid),
|
||||
context: "Crypto operation (pbkdf2 prf algorithm)".to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Ok(algorithm)
|
||||
}
|
||||
|
||||
pub(crate) fn from_asn1_digest_algorithm(value: &Pkcs12DigestAlgorithmAsn1) -> Result<Self, Pkcs12Error> {
|
||||
let algorithm = match value {
|
||||
Pkcs12DigestAlgorithm::Sha1 => Pkcs12HashAlgorithm::Sha1,
|
||||
Pkcs12DigestAlgorithm::Sha224 => Pkcs12HashAlgorithm::Sha224,
|
||||
Pkcs12DigestAlgorithm::Sha256 => Pkcs12HashAlgorithm::Sha256,
|
||||
Pkcs12DigestAlgorithm::Sha384 => Pkcs12HashAlgorithm::Sha384,
|
||||
Pkcs12DigestAlgorithm::Sha512 => Pkcs12HashAlgorithm::Sha512,
|
||||
Pkcs12DigestAlgorithm::Unknown(raw) => {
|
||||
let oid = raw.algorithm().clone();
|
||||
return Err(Pkcs12Error::NotSupportedAlgorithm {
|
||||
algorithm: UnsupportedPkcs12Algorithm::Oid(oid),
|
||||
context: "MAC calculation (pbkdf1 prf algorithm)".to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Ok(algorithm)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Pkcs12HashAlgorithm> for Pkcs12DigestAlgorithm {
|
||||
fn from(value: Pkcs12HashAlgorithm) -> Self {
|
||||
match value {
|
||||
Pkcs12HashAlgorithm::Sha1 => Self::Sha1,
|
||||
Pkcs12HashAlgorithm::Sha224 => Self::Sha224,
|
||||
Pkcs12HashAlgorithm::Sha256 => Self::Sha256,
|
||||
Pkcs12HashAlgorithm::Sha384 => Self::Sha384,
|
||||
Pkcs12HashAlgorithm::Sha512 => Self::Sha512,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Pkcs12Error {
|
||||
#[error("Not supported algorithm `{algorithm}` in context of {context}")]
|
||||
NotSupportedAlgorithm {
|
||||
algorithm: UnsupportedPkcs12Algorithm,
|
||||
context: String,
|
||||
},
|
||||
#[error("Failed to perform PBES1 crypto operation: {context}")]
|
||||
Pbes1 { context: String },
|
||||
#[error("Failed to perform PBES2 crypto operation: {context}")]
|
||||
Pbes2 { context: String },
|
||||
#[error(transparent)]
|
||||
CharSet(#[from] CharSetError),
|
||||
#[error(transparent)]
|
||||
Mac(#[from] mac::Pkcs12MacError),
|
||||
#[error("Invalid ASN.1 DER encoding")]
|
||||
Asn1Der(#[from] picky_asn1_der::Asn1DerError),
|
||||
#[error(transparent)]
|
||||
Key(#[from] crate::key::KeyError),
|
||||
#[error(transparent)]
|
||||
Certificate(#[from] crate::x509::certificate::CertError),
|
||||
#[error(transparent)]
|
||||
RandError(#[from] rand::rngs::SysError),
|
||||
#[error("Not supported or invalid PFX version: {0}")]
|
||||
InvalidVersion(u8),
|
||||
#[error("Invalid PFX AuthenticatedSafe content type: {0}")]
|
||||
InvalidAuthenticatedSafeContentType(UnsupportedPkcs12Algorithm),
|
||||
#[error("Unexpected attribute values count. Expected: `{expected}`, got: `{actual}`")]
|
||||
UnexpectedAttributeValuesCount { expected: usize, actual: usize },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum UnsupportedPkcs12Algorithm {
|
||||
Named(&'static str),
|
||||
Oid(ObjectIdentifier),
|
||||
}
|
||||
|
||||
impl From<&'static str> for UnsupportedPkcs12Algorithm {
|
||||
fn from(name: &'static str) -> Self {
|
||||
Self::Named(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ObjectIdentifier> for UnsupportedPkcs12Algorithm {
|
||||
fn from(oid: ObjectIdentifier) -> Self {
|
||||
Self::Oid(oid)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for UnsupportedPkcs12Algorithm {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
UnsupportedPkcs12Algorithm::Named(name) => f.write_str(name),
|
||||
UnsupportedPkcs12Algorithm::Oid(oid) => {
|
||||
write!(f, "OID({})", format_oid(oid))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Pkcs12Error {
|
||||
pub fn unsupported_algorithm(algorithm: impl Into<UnsupportedPkcs12Algorithm>, context: impl Into<String>) -> Self {
|
||||
Self::NotSupportedAlgorithm {
|
||||
algorithm: algorithm.into(),
|
||||
context: context.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn format_oid(oid: &ObjectIdentifier) -> String {
|
||||
let oid_str: String = oid.clone().into();
|
||||
format!("OID({oid_str})")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::key::PrivateKey;
|
||||
use crate::pem::Pem;
|
||||
use rstest::rstest;
|
||||
|
||||
#[test]
|
||||
fn pfx_certmgr_aes256() {
|
||||
let encoded = picky_test_data::CERTMGR_AES256;
|
||||
let crypto_context = Pkcs12CryptoContext::new_with_password("test").unwrap();
|
||||
let _decoded = Pfx::from_der(encoded, &crypto_context, &Pkcs12ParsingParams::default()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pfx_certmgr_3des() {
|
||||
let encoded = picky_test_data::CERTMGR_3DES;
|
||||
let crypto_context = Pkcs12CryptoContext::new_with_password("test").unwrap();
|
||||
let _decoded = Pfx::from_der(encoded, &crypto_context, &Pkcs12ParsingParams::default()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pfx_certmgr_rc2() {
|
||||
let encoded = picky_test_data::LEAF_PASSWORD_IS_ABC;
|
||||
let crypto_context = Pkcs12CryptoContext::new_with_password("abc").unwrap();
|
||||
let _decoded = Pfx::from_der(encoded, &crypto_context, &Pkcs12ParsingParams::default()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pfx_certmgr_rc2_empty_pass() {
|
||||
let encoded = picky_test_data::LEAF_EMPTY_PASSWORD;
|
||||
let crypto_context = Pkcs12CryptoContext::new_without_password().unwrap();
|
||||
let _decoded = Pfx::from_der(encoded, &crypto_context, &Pkcs12ParsingParams::default()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pfx_openssl_aes_empty_pass() {
|
||||
let encoded = picky_test_data::OPENSSL_NOCRYPT;
|
||||
let crypto_context = Pkcs12CryptoContext::new_without_password().unwrap();
|
||||
let _decoded = Pfx::from_der(encoded, &crypto_context, &Pkcs12ParsingParams::default()).unwrap();
|
||||
}
|
||||
|
||||
fn stable_rand() -> impl rand_core::CryptoRng {
|
||||
use rand_core::SeedableRng as _;
|
||||
rand_chacha::ChaChaRng::seed_from_u64(42)
|
||||
}
|
||||
|
||||
fn build_cert_bags() -> [SafeBag; 3] {
|
||||
let leaf = crate::x509::Cert::from_der(picky_test_data::ASSERT_LEAF).unwrap();
|
||||
let intermediate = crate::x509::Cert::from_der(picky_test_data::ASSERT_INTERMEDIATE).unwrap();
|
||||
let root = crate::x509::Cert::from_der(picky_test_data::ASSERT_ROOT).unwrap();
|
||||
|
||||
let leaf_cert_bag = SafeBag::new_certificate(leaf, build_leaf_attributes()).unwrap();
|
||||
|
||||
let intermediate_cert_bag = SafeBag::new_certificate(
|
||||
intermediate,
|
||||
vec![Pkcs12Attribute::new_friendly_name(
|
||||
"PICKY_INTERMEDIATE".parse().unwrap(),
|
||||
)],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let root_cert_bag = SafeBag::new_certificate(
|
||||
root,
|
||||
vec![Pkcs12Attribute::new_friendly_name("PICKY_ROOT".parse().unwrap())],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
[leaf_cert_bag, intermediate_cert_bag, root_cert_bag]
|
||||
}
|
||||
|
||||
fn build_leaf_attributes() -> Vec<Pkcs12Attribute> {
|
||||
vec![
|
||||
Pkcs12Attribute::new_local_key_id([0x01, 0x00, 0x00, 0x00]),
|
||||
Pkcs12Attribute::new_friendly_name("PICKY_LEAF".parse().unwrap()),
|
||||
]
|
||||
}
|
||||
|
||||
fn make_crypto_context(password: Option<&str>) -> Pkcs12CryptoContext {
|
||||
if let Some(password) = password {
|
||||
Pkcs12CryptoContext::new_with_password(password)
|
||||
.unwrap()
|
||||
.with_rng(stable_rand())
|
||||
} else {
|
||||
Pkcs12CryptoContext::new_without_password()
|
||||
.unwrap()
|
||||
.with_rng(stable_rand())
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_pfx(der_data: &[u8], password: Option<&str>) {
|
||||
let crypto_context = make_crypto_context(password);
|
||||
|
||||
// Check that we can decode PFX encoded by picky itself
|
||||
let _decoded = Pfx::from_der(der_data, &crypto_context, &Pkcs12ParsingParams::default()).unwrap();
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let temp_path = tempfile::NamedTempFile::new().unwrap().into_temp_path();
|
||||
|
||||
std::fs::write(&temp_path, der_data).unwrap();
|
||||
|
||||
let certutil_args = vec![
|
||||
"-dump".to_string(),
|
||||
"-p".to_string(),
|
||||
password.unwrap_or("").to_string(),
|
||||
temp_path.to_str().unwrap().to_string(),
|
||||
];
|
||||
|
||||
let certutil_output = std::process::Command::new("certutil")
|
||||
.args(certutil_args)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
certutil_output.status.success(),
|
||||
"certutil failed: {}",
|
||||
String::from_utf8_lossy(&certutil_output.stdout)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn encryption_3des(crypto_context: &mut Pkcs12CryptoContext) -> Pkcs12Encryption {
|
||||
Pkcs12Encryption::new_pbes1(Pbes1Encryption::new(Pbes1Cipher::ShaAnd3Key3DesCbc), crypto_context)
|
||||
}
|
||||
|
||||
fn encryption_rc2(crypto_context: &mut Pkcs12CryptoContext) -> Pkcs12Encryption {
|
||||
Pkcs12Encryption::new_pbes1(Pbes1Encryption::new(Pbes1Cipher::ShaAnd40BitRc2Cbc), crypto_context)
|
||||
}
|
||||
|
||||
fn encryption_aes256(crypto_context: &mut Pkcs12CryptoContext) -> Pkcs12Encryption {
|
||||
Pkcs12Encryption::new_pbes2(
|
||||
Pbes2Encryption::new(Pbes2Cipher::Aes256Cbc, Pkcs12HashAlgorithm::Sha256),
|
||||
crypto_context,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn leaf_private_key_rsa() -> PrivateKey {
|
||||
let pem = picky_test_data::RSA_2048_PK_3.parse::<Pem>().unwrap();
|
||||
PrivateKey::from_pkcs8(pem.data()).unwrap()
|
||||
}
|
||||
|
||||
type EncryptionFn = fn(&mut Pkcs12CryptoContext) -> Pkcs12Encryption;
|
||||
|
||||
#[rstest]
|
||||
#[case(encryption_3des, Some("test"), Pkcs12HashAlgorithm::Sha1)]
|
||||
#[case(encryption_rc2, Some("test"), Pkcs12HashAlgorithm::Sha1)]
|
||||
#[case(encryption_aes256, Some("test"), Pkcs12HashAlgorithm::Sha256)]
|
||||
#[case(encryption_3des, None, Pkcs12HashAlgorithm::Sha1)]
|
||||
// RC2 uses same KDF as 3DES so we could skip case of RC2 without password
|
||||
#[case(encryption_aes256, None, Pkcs12HashAlgorithm::Sha256)]
|
||||
fn build_pfx_encrypted(
|
||||
#[case] encryption_fn: EncryptionFn,
|
||||
#[case] password: Option<&'static str>,
|
||||
#[case] hmac_algorithm: Pkcs12HashAlgorithm,
|
||||
) {
|
||||
let leaf_key = leaf_private_key_rsa();
|
||||
|
||||
let [leaf_cert_bag, intermediate_cert_bag, root_cert_bag] = build_cert_bags();
|
||||
|
||||
let mut crypto_context = make_crypto_context(password);
|
||||
|
||||
let key_encryption = encryption_fn(&mut crypto_context);
|
||||
|
||||
let leaf_key_bag =
|
||||
SafeBag::new_encrypted_key(leaf_key, build_leaf_attributes(), key_encryption, &crypto_context).unwrap();
|
||||
|
||||
let cert_encryption = encryption_fn(&mut crypto_context);
|
||||
|
||||
let cert_safe_contents = SafeContents::new_encrypted(
|
||||
vec![leaf_cert_bag, intermediate_cert_bag, root_cert_bag],
|
||||
cert_encryption,
|
||||
&crypto_context,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let key_safe_contents = SafeContents::new(vec![leaf_key_bag]);
|
||||
|
||||
let secret_safe_bag = SafeBag::new_secret(
|
||||
SecretSafeBag::new(picky_asn1_x509::oids::content_info_type_data(), &42u8).unwrap(),
|
||||
vec![Pkcs12Attribute::new_custom(
|
||||
CustomPkcs12Attribute::new_single_value(picky_asn1_x509::oids::content_info_type_data(), &256u64)
|
||||
.unwrap(),
|
||||
)],
|
||||
);
|
||||
|
||||
let secret_safe_contents = SafeContents::new(vec![secret_safe_bag]);
|
||||
|
||||
let pfx = Pfx::new_with_hmac(
|
||||
vec![cert_safe_contents, key_safe_contents, secret_safe_contents],
|
||||
Pkcs12MacAlgorithmHmac::new(hmac_algorithm),
|
||||
&mut crypto_context,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let der_data = pfx.to_der().unwrap();
|
||||
|
||||
validate_pfx(&der_data, password);
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
use crate::pkcs12::Pkcs12HashAlgorithm;
|
||||
|
||||
pub enum Pbkdf1Usage {
|
||||
Key,
|
||||
Iv,
|
||||
Mac,
|
||||
}
|
||||
|
||||
impl Pbkdf1Usage {
|
||||
fn to_id_byte(&self) -> u8 {
|
||||
match self {
|
||||
Pbkdf1Usage::Key => 1,
|
||||
Pbkdf1Usage::Iv => 2,
|
||||
Pbkdf1Usage::Mac => 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// PBKDF1 implementation for PKCS#12 as defined in [RFC](https://datatracker.ietf.org/doc/html/rfc7292#appendix-B.2)
|
||||
pub fn pbkdf1(
|
||||
hash: Pkcs12HashAlgorithm,
|
||||
password: &[u8],
|
||||
salt: &[u8],
|
||||
kdf_iterations: usize,
|
||||
usage: Pbkdf1Usage,
|
||||
output_size: usize,
|
||||
) -> Vec<u8> {
|
||||
let u = hash.pbkdf1_u_bits() / 8;
|
||||
let v = hash.pbkdf1_v_bits() / 8;
|
||||
|
||||
let hash_round = match hash {
|
||||
Pkcs12HashAlgorithm::Sha1 => pbkdf1_hash_round::<sha1::Sha1>,
|
||||
Pkcs12HashAlgorithm::Sha224 => pbkdf1_hash_round::<sha2::Sha224>,
|
||||
Pkcs12HashAlgorithm::Sha256 => pbkdf1_hash_round::<sha2::Sha256>,
|
||||
Pkcs12HashAlgorithm::Sha384 => pbkdf1_hash_round::<sha2::Sha384>,
|
||||
Pkcs12HashAlgorithm::Sha512 => pbkdf1_hash_round::<sha2::Sha512>,
|
||||
};
|
||||
|
||||
// Construct "diversifier" string
|
||||
let d = vec![usage.to_id_byte(); v];
|
||||
|
||||
let expanded_length = |len: usize| v * len.div_ceil(v);
|
||||
|
||||
// Expand salt and password length to multiple of V
|
||||
let expanded_salt = salt.iter().cycle().take(expanded_length(salt.len()));
|
||||
let expanded_password = password.iter().cycle().take(expanded_length(password.len()));
|
||||
|
||||
// I = S || P
|
||||
let mut key_material: Vec<u8> = expanded_salt.chain(expanded_password).cloned().collect();
|
||||
|
||||
let c = output_size.div_ceil(u);
|
||||
|
||||
let mut output: Vec<u8> = vec![];
|
||||
|
||||
// Temporary buffer for key blocks produced by SHA1
|
||||
let mut key_block = vec![];
|
||||
|
||||
let mut b = vec![];
|
||||
|
||||
for _ in 1..c {
|
||||
hash_round(&d, &key_material, kdf_iterations, &mut key_block);
|
||||
output.extend_from_slice(&key_block);
|
||||
|
||||
// Create concatenated string B of length V
|
||||
b.clear();
|
||||
b.extend(key_block.iter().cycle().take(v).copied());
|
||||
|
||||
// Pretty convoluted operation which is defined in RFC as follows:
|
||||
//
|
||||
// C. Treating I as a concatenation I_0, I_1, ..., I_(k-1) of v-bit
|
||||
// blocks, where k=ceiling(s/v)+ceiling(p/v), modify I by
|
||||
// setting I_j=(I_j+B+1) mod 2^v for each j.
|
||||
//
|
||||
// Implementation of this part has been borrowed from [p12 crate](https://github.com/hjiayz/p12)
|
||||
let b_iter = b.iter().rev().cycle().take(key_material.len());
|
||||
let i_b_iter = key_material.iter_mut().rev().zip(b_iter);
|
||||
let mut inc = 1u8;
|
||||
for (i3, (ii, bi)) in i_b_iter.enumerate() {
|
||||
if (i3 % v) == 0 {
|
||||
inc = 1;
|
||||
}
|
||||
let (ii2, inc2) = ii.overflowing_add(*bi);
|
||||
let (ii3, inc3) = ii2.overflowing_add(inc);
|
||||
inc = (inc2 || inc3) as u8;
|
||||
*ii = ii3;
|
||||
}
|
||||
}
|
||||
|
||||
hash_round(&d, &key_material, kdf_iterations, &mut key_block);
|
||||
output.extend_from_slice(&key_block);
|
||||
|
||||
// Truncate to output_size
|
||||
output.resize(output_size, 0);
|
||||
output
|
||||
}
|
||||
|
||||
fn pbkdf1_hash_round<H: digest::Digest + digest::FixedOutputReset>(
|
||||
d: &[u8],
|
||||
i: &[u8],
|
||||
iterations: usize,
|
||||
output_buffer: &mut Vec<u8>,
|
||||
) {
|
||||
let mut hasher = H::new();
|
||||
output_buffer.clear();
|
||||
output_buffer.extend_from_slice(d);
|
||||
output_buffer.extend_from_slice(i);
|
||||
|
||||
for _ in 0..iterations {
|
||||
digest::Digest::update(&mut hasher, &output_buffer);
|
||||
let hash = hasher.finalize_reset();
|
||||
output_buffer.clear();
|
||||
output_buffer.extend_from_slice(&hash[..]);
|
||||
}
|
||||
}
|
||||
+331
@@ -0,0 +1,331 @@
|
||||
use crate::key::PrivateKey;
|
||||
use crate::pkcs12::{Pkcs12Attribute, Pkcs12CryptoContext, Pkcs12Encryption, Pkcs12Error, Pkcs12ParsingParams};
|
||||
use crate::x509::Cert;
|
||||
use picky_asn1::wrapper::OctetStringAsn1;
|
||||
use picky_asn1_der::Asn1RawDer;
|
||||
use picky_asn1_x509::oid::ObjectIdentifier;
|
||||
use picky_asn1_x509::pkcs12::{
|
||||
CertificateBag as CertificateBagAsn1, EncryptedKeyBag as EncryptedKeyBagAsn1,
|
||||
Pkcs12Attribute as Pkcs12AttributeAsn1, SafeBag as SafeBagAsn1, SafeBagKind as SafeBagKindAsn1,
|
||||
SafeContents as SafeContentsAsn1, SecretBag as SecretBagAsn1,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// PFX safe bag, see module docs for more information
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SafeBag {
|
||||
kind: SafeBagKind,
|
||||
attributes: Vec<Pkcs12Attribute>,
|
||||
inner: SafeBagAsn1,
|
||||
}
|
||||
|
||||
impl SafeBag {
|
||||
/// Create new safe bag holding a private key
|
||||
pub fn new_key(key: PrivateKey, attributes: Vec<Pkcs12Attribute>) -> Result<Self, Pkcs12Error> {
|
||||
// Convert to `PrivateKeyInfo` structure in DER representation
|
||||
let der_data = key.to_pkcs8()?;
|
||||
|
||||
let inner = SafeBagAsn1 {
|
||||
kind: SafeBagKindAsn1::Key(Asn1RawDer(der_data)),
|
||||
attributes: attributes_to_asn1(&attributes),
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
kind: SafeBagKind::PrivateKey(key),
|
||||
attributes,
|
||||
inner,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create new safe bag with encrypted key. Note that attributes are not encrypted.
|
||||
pub fn new_encrypted_key(
|
||||
key: PrivateKey,
|
||||
attributes: Vec<Pkcs12Attribute>,
|
||||
encryption: Pkcs12Encryption,
|
||||
crypto_context: &Pkcs12CryptoContext,
|
||||
) -> Result<Self, Pkcs12Error> {
|
||||
let der_data = key.to_pkcs8()?;
|
||||
let encrypted = encryption.encrypt(&der_data, crypto_context)?;
|
||||
|
||||
let inner = SafeBagAsn1 {
|
||||
kind: SafeBagKindAsn1::EncryptedKey(EncryptedKeyBagAsn1 {
|
||||
algorithm: encryption.inner().clone(),
|
||||
encrypted_data: OctetStringAsn1(encrypted),
|
||||
}),
|
||||
attributes: attributes_to_asn1(&attributes),
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
kind: SafeBagKind::EncryptedPrivateKey { encryption, key },
|
||||
attributes,
|
||||
inner,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create new safe bag with certificate
|
||||
pub fn new_certificate(cert: Cert, attributes: Vec<Pkcs12Attribute>) -> Result<Self, Pkcs12Error> {
|
||||
let der_data = cert.to_der()?;
|
||||
|
||||
let inner = SafeBagAsn1 {
|
||||
kind: SafeBagKindAsn1::Certificate(CertificateBagAsn1::X509(OctetStringAsn1(der_data))),
|
||||
attributes: attributes_to_asn1(&attributes),
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
kind: SafeBagKind::Certificate(cert),
|
||||
attributes,
|
||||
inner,
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates new [`SecretSafeBag`] bag
|
||||
pub fn new_secret(secret: SecretSafeBag, attributes: Vec<Pkcs12Attribute>) -> Self {
|
||||
let inner = SafeBagAsn1 {
|
||||
kind: SafeBagKindAsn1::Secret(SecretBagAsn1 {
|
||||
type_id: secret.oid.clone(),
|
||||
value: secret.data.clone(),
|
||||
}),
|
||||
attributes: attributes_to_asn1(&attributes),
|
||||
};
|
||||
|
||||
Self {
|
||||
kind: SafeBagKind::Secret(secret),
|
||||
attributes,
|
||||
inner,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates safe bag with nested safe bag list.
|
||||
pub fn new_nested(safe_bags: Vec<SafeBag>, attributes: Vec<Pkcs12Attribute>) -> Self {
|
||||
let safe_contents = SafeContentsAsn1(safe_bags.iter().map(|sb| sb.inner.clone()).collect());
|
||||
|
||||
let inner = SafeBagAsn1 {
|
||||
kind: SafeBagKindAsn1::SafeContents(safe_contents),
|
||||
attributes: attributes_to_asn1(&attributes),
|
||||
};
|
||||
|
||||
Self {
|
||||
kind: SafeBagKind::Nested(safe_bags),
|
||||
attributes,
|
||||
inner,
|
||||
}
|
||||
}
|
||||
|
||||
/// PKCS#12 allows for arbitrary SafeBags to be included in the PKCS#12 file as long as they
|
||||
/// a unique OID.
|
||||
pub fn new_custom(oid: ObjectIdentifier, value: Asn1RawDer, attributes: Vec<Pkcs12Attribute>) -> Self {
|
||||
let inner = SafeBagAsn1 {
|
||||
kind: SafeBagKindAsn1::Unknown { type_id: oid, value },
|
||||
attributes: attributes_to_asn1(&attributes),
|
||||
};
|
||||
|
||||
Self {
|
||||
kind: SafeBagKind::Unknown,
|
||||
attributes,
|
||||
inner,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_asn1(
|
||||
safe_bag: SafeBagAsn1,
|
||||
crypto_context: &Pkcs12CryptoContext,
|
||||
parsing_params: &Pkcs12ParsingParams,
|
||||
) -> Result<Self, Pkcs12Error> {
|
||||
let attributes = safe_bag
|
||||
.attributes
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(Pkcs12Attribute::from_asn1)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let to_unparsed = |inner, attributes| Self {
|
||||
kind: SafeBagKind::Unknown,
|
||||
attributes,
|
||||
inner,
|
||||
};
|
||||
|
||||
let kind = match &safe_bag.kind {
|
||||
SafeBagKindAsn1::Key(Asn1RawDer(der_data)) => {
|
||||
let key = match PrivateKey::from_pkcs8(&der_data) {
|
||||
Ok(key) => key,
|
||||
Err(_) if parsing_params.skip_soft_parsing_errors => {
|
||||
return Ok(to_unparsed(safe_bag, attributes));
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
|
||||
SafeBagKind::PrivateKey(key)
|
||||
}
|
||||
SafeBagKindAsn1::EncryptedKey(encrypted_key) => {
|
||||
let encryption = match Pkcs12Encryption::from_asn1(encrypted_key.algorithm.clone()) {
|
||||
Ok(encryption) => encryption,
|
||||
Err(_) if parsing_params.skip_decryption_errors => {
|
||||
return Ok(to_unparsed(safe_bag, attributes));
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
let der_data = match encryption.decrypt(&encrypted_key.encrypted_data.0, crypto_context) {
|
||||
Ok(der_data) => der_data,
|
||||
Err(_) if parsing_params.skip_decryption_errors => {
|
||||
return Ok(to_unparsed(safe_bag, attributes));
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
let key = match PrivateKey::from_pkcs8(&der_data) {
|
||||
Ok(key) => key,
|
||||
Err(_) if parsing_params.skip_soft_parsing_errors => {
|
||||
return Ok(to_unparsed(safe_bag, attributes));
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
|
||||
SafeBagKind::EncryptedPrivateKey { encryption, key }
|
||||
}
|
||||
SafeBagKindAsn1::Certificate(CertificateBagAsn1::X509(OctetStringAsn1(der_data))) => {
|
||||
let cert = match Cert::from_der(&der_data) {
|
||||
Ok(cert) => cert,
|
||||
Err(_) if parsing_params.skip_soft_parsing_errors => {
|
||||
return Ok(to_unparsed(safe_bag, attributes));
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
|
||||
SafeBagKind::Certificate(cert)
|
||||
}
|
||||
SafeBagKindAsn1::Secret(SecretBagAsn1 { type_id, value }) => {
|
||||
let secret = SecretSafeBag {
|
||||
oid: type_id.clone(),
|
||||
data: value.clone(),
|
||||
};
|
||||
|
||||
SafeBagKind::Secret(secret)
|
||||
}
|
||||
SafeBagKindAsn1::SafeContents(safe_contents) => {
|
||||
let safe_bags = safe_contents
|
||||
.0
|
||||
.iter()
|
||||
.map(|sb| Self::from_asn1(sb.clone(), crypto_context, parsing_params))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
SafeBagKind::Nested(safe_bags)
|
||||
}
|
||||
SafeBagKindAsn1::Crl(_)
|
||||
| SafeBagKindAsn1::Certificate(CertificateBagAsn1::Unknown { .. })
|
||||
| SafeBagKindAsn1::Unknown { .. } => {
|
||||
return Ok(to_unparsed(safe_bag, attributes));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
kind,
|
||||
attributes,
|
||||
inner: safe_bag,
|
||||
})
|
||||
}
|
||||
|
||||
/// Adds a PKCS12 attribute to this safe bag.
|
||||
///
|
||||
/// Note that there is an additional performance cost: the inner DER representation must be updated.
|
||||
pub fn add_attribute(&mut self, attribute: Pkcs12Attribute) {
|
||||
self.attributes.push(attribute);
|
||||
self.inner.attributes = attributes_to_asn1(&self.attributes);
|
||||
}
|
||||
|
||||
pub fn attributes(&self) -> &[Pkcs12Attribute] {
|
||||
&self.attributes
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> &SafeBagKind {
|
||||
&self.kind
|
||||
}
|
||||
|
||||
pub fn into_kind(self) -> SafeBagKind {
|
||||
self.kind
|
||||
}
|
||||
|
||||
pub fn inner(&self) -> &SafeBagAsn1 {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> SafeBagAsn1 {
|
||||
self.inner
|
||||
}
|
||||
}
|
||||
|
||||
/// Parsed safe bag representation.
|
||||
#[derive(Debug)]
|
||||
pub enum SafeBagKind {
|
||||
PrivateKey(PrivateKey),
|
||||
EncryptedPrivateKey {
|
||||
encryption: Pkcs12Encryption,
|
||||
key: PrivateKey,
|
||||
},
|
||||
Certificate(Cert),
|
||||
Secret(SecretSafeBag),
|
||||
Nested(Vec<SafeBag>),
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl Clone for SafeBagKind {
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
Self::PrivateKey(key) => Self::PrivateKey(key.clone()),
|
||||
Self::EncryptedPrivateKey { encryption, key } => Self::EncryptedPrivateKey {
|
||||
encryption: encryption.duplicate(),
|
||||
key: key.clone(),
|
||||
},
|
||||
Self::Certificate(cert) => Self::Certificate(cert.clone()),
|
||||
Self::Secret(secret) => Self::Secret(secret.clone()),
|
||||
Self::Nested(nested) => Self::Nested(nested.clone()),
|
||||
Self::Unknown => Self::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Secret bag which could contain any user-defined data, as long as it could be DER-encoded.
|
||||
/// It is advised to use types from `picky-asn1-der` crate.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SecretSafeBag {
|
||||
oid: ObjectIdentifier,
|
||||
data: Asn1RawDer,
|
||||
}
|
||||
|
||||
impl SecretSafeBag {
|
||||
pub fn new_raw(oid: ObjectIdentifier, data: Asn1RawDer) -> Self {
|
||||
Self { oid, data }
|
||||
}
|
||||
|
||||
/// Create new secret bag from serializable data.
|
||||
pub fn new<T: Serialize>(oid: ObjectIdentifier, value: &T) -> Result<Self, Pkcs12Error> {
|
||||
let encoded = picky_asn1_der::to_vec(value)?;
|
||||
Ok(Self {
|
||||
oid,
|
||||
data: Asn1RawDer(encoded),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn oid(&self) -> &ObjectIdentifier {
|
||||
&self.oid
|
||||
}
|
||||
|
||||
pub fn raw_data(&self) -> &[u8] {
|
||||
&self.data.0
|
||||
}
|
||||
|
||||
/// Get secret bag data as deserialized type.
|
||||
pub fn get_data<'a, T: Deserialize<'a>>(&'a self) -> Result<T, Pkcs12Error> {
|
||||
let deserialized = picky_asn1_der::from_bytes(&self.data.0)?;
|
||||
Ok(deserialized)
|
||||
}
|
||||
}
|
||||
|
||||
fn attributes_to_asn1(attributes: &[Pkcs12Attribute]) -> Option<Vec<Pkcs12AttributeAsn1>> {
|
||||
if attributes.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(attributes.iter().map(|a| a.inner().clone()).collect())
|
||||
}
|
||||
}
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
use crate::pkcs12::{Pkcs12CryptoContext, Pkcs12Encryption, Pkcs12Error, Pkcs12ParsingParams, SafeBag};
|
||||
use picky_asn1::wrapper::OctetStringAsn1;
|
||||
use picky_asn1_x509::pkcs12::{
|
||||
EncryptedSafeContents as EncryptedSafeContentsAsn1, SafeContents as SafeContentsAsn1,
|
||||
SafeContentsContentInfo as SafeContentsContentInfoAsn1,
|
||||
};
|
||||
|
||||
/// Top-level PFX container object, which holds list of safe bags and could be encrypted or not.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SafeContents {
|
||||
kind: SafeContentsKind,
|
||||
inner: SafeContentsContentInfoAsn1,
|
||||
}
|
||||
|
||||
impl SafeContents {
|
||||
pub(crate) fn from_asn1(
|
||||
inner: SafeContentsContentInfoAsn1,
|
||||
crypto_context: &Pkcs12CryptoContext,
|
||||
parsing_params: &Pkcs12ParsingParams,
|
||||
) -> Result<Self, Pkcs12Error> {
|
||||
let to_unparsed = |inner| Self {
|
||||
kind: SafeContentsKind::Unknown,
|
||||
inner,
|
||||
};
|
||||
|
||||
let kind = match &inner {
|
||||
SafeContentsContentInfoAsn1::Data(data) => {
|
||||
let safe_bags = data
|
||||
.0
|
||||
.iter()
|
||||
.map(|sb| SafeBag::from_asn1(sb.clone(), crypto_context, parsing_params))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Self {
|
||||
kind: SafeContentsKind::SafeBags(safe_bags),
|
||||
inner,
|
||||
}
|
||||
}
|
||||
SafeContentsContentInfoAsn1::EncryptedData(encrypted) => {
|
||||
let encryption = match Pkcs12Encryption::from_asn1(encrypted.algorithm.clone()) {
|
||||
Ok(encryption) => encryption,
|
||||
Err(_) if parsing_params.skip_decryption_errors => {
|
||||
return Ok(to_unparsed(inner));
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
let encrypted_content = match encrypted.encrypted_content.as_ref() {
|
||||
Some(content) => content.0.as_slice(),
|
||||
None => {
|
||||
return Ok(Self {
|
||||
// No content to decrypt
|
||||
kind: SafeContentsKind::EncryptedSafeBags {
|
||||
encryption,
|
||||
safe_bags: vec![],
|
||||
},
|
||||
inner,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let decrypted = match encryption.decrypt(encrypted_content, crypto_context) {
|
||||
Ok(decrypted) => decrypted,
|
||||
Err(_) if parsing_params.skip_decryption_errors => {
|
||||
return Ok(to_unparsed(inner));
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
let safe_bags_asn1 = match picky_asn1_der::from_bytes::<SafeContentsAsn1>(&decrypted) {
|
||||
Ok(safe_contents) => safe_contents.0,
|
||||
Err(_) if parsing_params.skip_decryption_errors => {
|
||||
return Ok(to_unparsed(inner));
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
|
||||
let safe_bags = safe_bags_asn1
|
||||
.into_iter()
|
||||
.map(|sb| SafeBag::from_asn1(sb, crypto_context, parsing_params))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Self {
|
||||
kind: SafeContentsKind::EncryptedSafeBags { encryption, safe_bags },
|
||||
inner,
|
||||
}
|
||||
}
|
||||
SafeContentsContentInfoAsn1::Unknown { .. } => return Ok(to_unparsed(inner)),
|
||||
};
|
||||
|
||||
Ok(kind)
|
||||
}
|
||||
|
||||
pub fn new(safe_bags: Vec<SafeBag>) -> Self {
|
||||
let safe_contents = SafeContentsAsn1(safe_bags.iter().map(|sb| sb.inner().clone()).collect());
|
||||
Self {
|
||||
kind: SafeContentsKind::SafeBags(safe_bags),
|
||||
inner: SafeContentsContentInfoAsn1::Data(safe_contents),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_encrypted(
|
||||
safe_bags: Vec<SafeBag>,
|
||||
encryption: Pkcs12Encryption,
|
||||
crypto_context: &Pkcs12CryptoContext,
|
||||
) -> Result<Self, Pkcs12Error> {
|
||||
let safe_contents = SafeContentsAsn1(safe_bags.iter().map(|sb| sb.inner().clone()).collect());
|
||||
let der_data = picky_asn1_der::to_vec(&safe_contents)?;
|
||||
let encrypted = encryption.encrypt(&der_data, crypto_context)?;
|
||||
|
||||
let inner = SafeContentsContentInfoAsn1::EncryptedData(EncryptedSafeContentsAsn1 {
|
||||
algorithm: encryption.inner().clone(),
|
||||
encrypted_content: Some(OctetStringAsn1(encrypted)),
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
kind: SafeContentsKind::EncryptedSafeBags { encryption, safe_bags },
|
||||
inner,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> &SafeContentsKind {
|
||||
&self.kind
|
||||
}
|
||||
|
||||
pub fn into_kind(self) -> SafeContentsKind {
|
||||
self.kind
|
||||
}
|
||||
|
||||
pub fn inner(&self) -> &SafeContentsContentInfoAsn1 {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> SafeContentsContentInfoAsn1 {
|
||||
self.inner
|
||||
}
|
||||
}
|
||||
|
||||
// Clippy triggers lint because of relatively big `Pkcs12Encryption` (~200 bytes) in comparison with
|
||||
// `SafeContentsKind::Unknown` (0 bytes), but just to keep it consistent with other enums, we do allow
|
||||
// such difference in size.
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug)]
|
||||
pub enum SafeContentsKind {
|
||||
SafeBags(Vec<SafeBag>),
|
||||
EncryptedSafeBags {
|
||||
encryption: Pkcs12Encryption,
|
||||
safe_bags: Vec<SafeBag>,
|
||||
},
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl Clone for SafeContentsKind {
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
Self::SafeBags(bags) => Self::SafeBags(bags.clone()),
|
||||
Self::EncryptedSafeBags { encryption, safe_bags } => Self::EncryptedSafeBags {
|
||||
encryption: encryption.duplicate(),
|
||||
safe_bags: safe_bags.clone(),
|
||||
},
|
||||
Self::Unknown => Self::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+53
@@ -0,0 +1,53 @@
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum PuttyError {
|
||||
#[error("end of input")]
|
||||
EndOfInput,
|
||||
#[error("invalid input")]
|
||||
InvalidInput {
|
||||
context: &'static str,
|
||||
expected: &'static str,
|
||||
actual: String,
|
||||
},
|
||||
#[error("invalid key value format")]
|
||||
InvalidKeyValueFormat,
|
||||
#[error("AES encryption failed")]
|
||||
Aes,
|
||||
#[error("invalid argon2 params")]
|
||||
Argon2,
|
||||
#[error("MAC validation failed (wrong password or corrupted data)")]
|
||||
MacValidation,
|
||||
#[error("public and private key mismatch")]
|
||||
PublicAndPrivateKeyMismatch,
|
||||
#[error("invalid private key data")]
|
||||
InvalidPrivateKeyData,
|
||||
#[error("invalid public key data")]
|
||||
InvalidPublicKeyData,
|
||||
#[error("invalid public key container")]
|
||||
InvalidPublicKeyContainer,
|
||||
#[error("invalid public key comment")]
|
||||
InvalidPublicKeyComment,
|
||||
#[error("private key is already decrypted")]
|
||||
AlreadyDecrypted,
|
||||
#[error("private key is already encrypted")]
|
||||
AlreadyEncrypted,
|
||||
#[error("private key decryption is required prior to this operation")]
|
||||
Encrypted,
|
||||
#[error("unsupported feature")]
|
||||
NotSupported { feature: &'static str },
|
||||
#[error("RSA params precomputation failed")]
|
||||
RsaPrecompute,
|
||||
#[error("RSA primes count should be exactly 2")]
|
||||
RsaInvalidPrimesCount { count: usize },
|
||||
#[error(transparent)]
|
||||
SshPublicKey(#[from] crate::ssh::public_key::SshPublicKeyError),
|
||||
#[error(transparent)]
|
||||
SshPivateKey(#[from] crate::ssh::private_key::SshPrivateKeyError),
|
||||
#[error(transparent)]
|
||||
KeyError(#[from] crate::key::KeyError),
|
||||
#[error(transparent)]
|
||||
IoError(#[from] std::io::Error),
|
||||
#[error(transparent)]
|
||||
OutIsTooSmallError(#[from] inout::OutIsTooSmallError),
|
||||
#[error(transparent)]
|
||||
RandError(#[from] rand::rngs::SysError),
|
||||
}
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
macro_rules! ppk_const {
|
||||
($name:ident, $key:expr) => {
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct $name;
|
||||
|
||||
impl std::str::FromStr for $name {
|
||||
type Err = $crate::putty::key_value::PpkValueParsingError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if s == $key {
|
||||
Ok(Self)
|
||||
} else {
|
||||
Err($crate::putty::key_value::PpkValueParsingError {
|
||||
expected: $key,
|
||||
actual: s.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for $name {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str($key)
|
||||
}
|
||||
}
|
||||
|
||||
impl PpkLiteral for $name {
|
||||
fn context() -> &'static str {
|
||||
stringify!($name)
|
||||
}
|
||||
|
||||
fn as_static_str(&self) -> &'static str {
|
||||
$key
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! impl_ppk_enum_expected_str {
|
||||
($first:expr, $($keys:expr),+) => {
|
||||
concat!($first, ", ", $($keys),+)
|
||||
};
|
||||
($first:expr) => {
|
||||
$first
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! ppk_enum {
|
||||
($name:ident, $($variant:ident => $key:expr),+) => {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum $name {
|
||||
$($variant),+
|
||||
}
|
||||
|
||||
impl std::str::FromStr for $name {
|
||||
type Err = $crate::putty::key_value::PpkValueParsingError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
$($key => Ok(Self::$variant),)+
|
||||
_ => Err($crate::putty::key_value::PpkValueParsingError {
|
||||
expected: concat!("[", impl_ppk_enum_expected_str!($($key),+), "]"),
|
||||
actual: s.to_string()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for $name {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
$(Self::$variant => f.write_str($key),)+
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PpkLiteral for $name {
|
||||
fn context() -> &'static str {
|
||||
stringify!($name)
|
||||
}
|
||||
|
||||
fn as_static_str(&self) -> &'static str {
|
||||
match self {
|
||||
$(Self::$variant => $key,)+
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! ppk_generic_value {
|
||||
($name:ident, $type:ident) => {
|
||||
pub struct $name($type);
|
||||
|
||||
impl std::str::FromStr for $name {
|
||||
type Err = $crate::putty::key_value::PpkValueParsingError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
s.parse()
|
||||
.map(Self)
|
||||
.map_err(|_| $crate::putty::key_value::PpkValueParsingError {
|
||||
expected: concat!("<valid ", stringify!($type), " value>"),
|
||||
actual: s.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for $name {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<$type> for $name {
|
||||
fn from(value: $type) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<$name> for $type {
|
||||
fn from(value: $name) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! ppk_key_value {
|
||||
($name:ident, $key:ident, $value:ident) => {
|
||||
pub struct $name;
|
||||
|
||||
impl $crate::putty::key_value::PpkKeyValue for $name {
|
||||
type Key = $key;
|
||||
type Value = $value;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! ppk_multiline_key_value {
|
||||
($name:ident, $key:ident, $value:ident) => {
|
||||
pub struct $name;
|
||||
|
||||
impl $crate::putty::key_value::PpkMultilineKeyValue for $name {
|
||||
type Key = $key;
|
||||
type Value = $value;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use {
|
||||
impl_ppk_enum_expected_str, ppk_const, ppk_enum, ppk_generic_value, ppk_key_value, ppk_multiline_key_value,
|
||||
};
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
//! This module provides a set of traits and macros and types to parse and write PuTTY key-value
|
||||
//! format in strongly-typed manner.
|
||||
|
||||
mod macros;
|
||||
mod reader;
|
||||
mod writer;
|
||||
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_ENGINE;
|
||||
|
||||
use self::macros::*;
|
||||
|
||||
pub(crate) use reader::PuttyKvReader;
|
||||
pub(crate) use writer::PuttyKvWriter;
|
||||
|
||||
pub struct PpkValueParsingError {
|
||||
pub expected: &'static str,
|
||||
pub actual: String,
|
||||
}
|
||||
|
||||
const KV_DELIMITER: &str = ": ";
|
||||
|
||||
/// Trait for keys/values that could be represented as singular or set of static strings.
|
||||
pub(crate) trait PpkLiteral {
|
||||
fn context() -> &'static str;
|
||||
fn as_static_str(&self) -> &'static str;
|
||||
}
|
||||
|
||||
/// Trait for key-value pairs that use multiline format.
|
||||
pub(crate) trait PpkMultilineKeyValue {
|
||||
type Key: FromStr<Err = PpkValueParsingError> + ToString + PpkLiteral;
|
||||
type Value: FromStr<Err = PpkValueParsingError> + ToString;
|
||||
}
|
||||
|
||||
/// Trait for key-value pairs that use single-line format.
|
||||
pub(crate) trait PpkKeyValue {
|
||||
type Key: FromStr<Err = PpkValueParsingError> + ToString + PpkLiteral;
|
||||
type Value: FromStr<Err = PpkValueParsingError> + ToString;
|
||||
}
|
||||
|
||||
/// Wrapper type for base64 multiline data inside PPK file.
|
||||
pub(crate) struct Base64PpkValue(Vec<u8>);
|
||||
|
||||
impl FromStr for Base64PpkValue {
|
||||
type Err = PpkValueParsingError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
BASE64_ENGINE
|
||||
.decode(s)
|
||||
.map_err(|_| PpkValueParsingError {
|
||||
expected: "<valid base64 data>",
|
||||
actual: s.to_string(),
|
||||
})
|
||||
.map(Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Base64PpkValue {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", BASE64_ENGINE.encode(&self.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for Base64PpkValue {
|
||||
fn from(value: Vec<u8>) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Base64PpkValue> for Vec<u8> {
|
||||
fn from(value: Base64PpkValue) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper type for hex-string multiline data inside PPK file.
|
||||
pub(crate) struct HexPpkValue(Vec<u8>);
|
||||
|
||||
impl FromStr for HexPpkValue {
|
||||
type Err = PpkValueParsingError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
hex::decode(s)
|
||||
.map_err(|_| PpkValueParsingError {
|
||||
expected: "<valid hex data>",
|
||||
actual: s.to_string(),
|
||||
})
|
||||
.map(Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for HexPpkValue {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", hex::encode(&self.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for HexPpkValue {
|
||||
fn from(value: Vec<u8>) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HexPpkValue> for Vec<u8> {
|
||||
fn from(value: HexPpkValue) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
// Key value type definitions
|
||||
|
||||
ppk_enum!(
|
||||
PpkVersionKey,
|
||||
V2 => "PuTTY-User-Key-File-2",
|
||||
V3 => "PuTTY-User-Key-File-3"
|
||||
);
|
||||
ppk_enum!(
|
||||
PpkKeyAlgorithmValue,
|
||||
Rsa => "ssh-rsa",
|
||||
Dss => "ssh-dss",
|
||||
EcdsaSha2Nistp256 => "ecdsa-sha2-nistp256",
|
||||
EcdsaSha2Nistp384 => "ecdsa-sha2-nistp384",
|
||||
EcdsaSha2Nistp521 => "ecdsa-sha2-nistp521",
|
||||
Ed25519 => "ssh-ed25519",
|
||||
Ed448 => "ssh-ed448"
|
||||
);
|
||||
ppk_key_value!(PpkHeader, PpkVersionKey, PpkKeyAlgorithmValue);
|
||||
|
||||
ppk_const!(PpkEncryptionKey, "Encryption");
|
||||
ppk_enum!(
|
||||
PpkEncryptionValue,
|
||||
None => "none",
|
||||
Aes256Cbc => "aes256-cbc"
|
||||
);
|
||||
ppk_key_value!(PpkEncryption, PpkEncryptionKey, PpkEncryptionValue);
|
||||
|
||||
ppk_const!(PpkCommentKey, "Comment");
|
||||
ppk_generic_value!(PpkCommentValue, String);
|
||||
ppk_key_value!(PpkComment, PpkCommentKey, PpkCommentValue);
|
||||
|
||||
ppk_const!(PpkPublicLinesKey, "Public-Lines");
|
||||
ppk_multiline_key_value!(PpkPublicLines, PpkPublicLinesKey, Base64PpkValue);
|
||||
|
||||
ppk_const!(PpkKeyDerivationKey, "Key-Derivation");
|
||||
ppk_enum!(
|
||||
Argon2FlavourValue,
|
||||
Argon2d => "Argon2d",
|
||||
Argon2i => "Argon2i",
|
||||
Argon2id => "Argon2id"
|
||||
);
|
||||
ppk_key_value!(PpkKeyDerivation, PpkKeyDerivationKey, Argon2FlavourValue);
|
||||
ppk_const!(PpkArgon2MemoryKey, "Argon2-Memory");
|
||||
ppk_generic_value!(PpkArgon2MemoryValue, u32);
|
||||
ppk_key_value!(PpkArgon2Memory, PpkArgon2MemoryKey, PpkArgon2MemoryValue);
|
||||
ppk_const!(PpkArgon2PassesKey, "Argon2-Passes");
|
||||
ppk_generic_value!(PpkArgon2PassesValue, u32);
|
||||
ppk_key_value!(PpkArgon2Passes, PpkArgon2PassesKey, PpkArgon2PassesValue);
|
||||
ppk_const!(PpkArgon2ParallelismKey, "Argon2-Parallelism");
|
||||
ppk_generic_value!(PpkArgon2ParallelismValue, u32);
|
||||
ppk_key_value!(PpkArgon2Parallelism, PpkArgon2ParallelismKey, PpkArgon2ParallelismValue);
|
||||
ppk_const!(PpkArgon2SaltKey, "Argon2-Salt");
|
||||
ppk_key_value!(PpkArgon2Salt, PpkArgon2SaltKey, HexPpkValue);
|
||||
|
||||
ppk_const!(PpkPrivateLinesKey, "Private-Lines");
|
||||
ppk_multiline_key_value!(PpkPrivateLines, PpkPrivateLinesKey, Base64PpkValue);
|
||||
|
||||
ppk_const!(PpkPrivateMacKey, "Private-MAC");
|
||||
ppk_key_value!(PpkPrivateMac, PpkPrivateMacKey, HexPpkValue);
|
||||
|
||||
impl From<Argon2FlavourValue> for argon2::Algorithm {
|
||||
fn from(value: Argon2FlavourValue) -> Self {
|
||||
match value {
|
||||
Argon2FlavourValue::Argon2d => argon2::Algorithm::Argon2d,
|
||||
Argon2FlavourValue::Argon2i => argon2::Algorithm::Argon2i,
|
||||
Argon2FlavourValue::Argon2id => argon2::Algorithm::Argon2id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PpkKeyAlgorithmValue {
|
||||
pub fn key_mpint_values_count(&self) -> usize {
|
||||
match self {
|
||||
PpkKeyAlgorithmValue::Rsa => 4,
|
||||
PpkKeyAlgorithmValue::Dss
|
||||
| PpkKeyAlgorithmValue::EcdsaSha2Nistp256
|
||||
| PpkKeyAlgorithmValue::EcdsaSha2Nistp384
|
||||
| PpkKeyAlgorithmValue::EcdsaSha2Nistp521
|
||||
| PpkKeyAlgorithmValue::Ed25519
|
||||
| PpkKeyAlgorithmValue::Ed448 => 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
use super::{KV_DELIMITER, PpkKeyValue, PpkLiteral, PpkMultilineKeyValue, PpkValueParsingError};
|
||||
use crate::putty::PuttyError;
|
||||
|
||||
/// Reader for PPK key-value format.
|
||||
pub struct PuttyKvReader<'a> {
|
||||
input: std::str::Lines<'a>,
|
||||
}
|
||||
|
||||
impl<'a> PuttyKvReader<'a> {
|
||||
pub fn from_str(input: &'a str) -> Self {
|
||||
Self { input: input.lines() }
|
||||
}
|
||||
|
||||
pub fn next_value<T: PpkKeyValue>(&mut self) -> Result<T::Value, PuttyError> {
|
||||
let (_, value) = self.next_key_value::<T>()?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub fn next_key_value<T: PpkKeyValue>(&mut self) -> Result<(T::Key, T::Value), PuttyError> {
|
||||
let line = self.input.next().ok_or(PuttyError::EndOfInput)?;
|
||||
let (key, value) = line.split_once(KV_DELIMITER).ok_or(PuttyError::InvalidKeyValueFormat)?;
|
||||
|
||||
let parsed_key = key
|
||||
.parse()
|
||||
.map_err(|e: PpkValueParsingError| PuttyError::InvalidInput {
|
||||
context: T::Key::context(),
|
||||
expected: e.expected,
|
||||
actual: e.actual,
|
||||
})?;
|
||||
|
||||
let parsed_value = value
|
||||
.parse()
|
||||
.map_err(|e: PpkValueParsingError| PuttyError::InvalidInput {
|
||||
context: T::Key::context(),
|
||||
expected: e.expected,
|
||||
actual: e.actual,
|
||||
})?;
|
||||
|
||||
Ok((parsed_key, parsed_value))
|
||||
}
|
||||
|
||||
pub fn next_multiline_value<T: PpkMultilineKeyValue>(&mut self) -> Result<T::Value, PuttyError> {
|
||||
let (_, value) = self.next_multiline_key_value::<T>()?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub fn next_multiline_key_value<T: PpkMultilineKeyValue>(&mut self) -> Result<(T::Key, T::Value), PuttyError> {
|
||||
let line = self.input.next().ok_or(PuttyError::EndOfInput)?;
|
||||
let (key, value) = line.split_once(KV_DELIMITER).ok_or(PuttyError::InvalidKeyValueFormat)?;
|
||||
|
||||
// Parse key early to check if it's valid before reading multiline value
|
||||
let parsed_key: T::Key = key
|
||||
.parse()
|
||||
.map_err(|e: PpkValueParsingError| PuttyError::InvalidInput {
|
||||
context: T::Key::context(),
|
||||
expected: e.expected,
|
||||
actual: e.actual,
|
||||
})?;
|
||||
|
||||
// NOTE: u16 is enough for multiline fields, as in PPK format they are storing
|
||||
// base64-encoded private/public key data, and 65535 lines is more than enough for any
|
||||
// supported PPK key type.
|
||||
let lines_count: u16 = value.parse().map_err(|_| PuttyError::InvalidInput {
|
||||
context: T::Key::context(),
|
||||
expected: "<valid lines count>",
|
||||
actual: value.to_string(),
|
||||
})?;
|
||||
|
||||
let mut encoded = String::new();
|
||||
|
||||
for _ in 0..lines_count {
|
||||
// NOTE: we do not preserve newlines for multiline fields as in PPK they are
|
||||
// only used to split base64-encoded data into lines.
|
||||
encoded.push_str(self.input.next().ok_or(PuttyError::EndOfInput)?);
|
||||
}
|
||||
|
||||
let parsed_value = encoded
|
||||
.parse()
|
||||
.map_err(|e: PpkValueParsingError| PuttyError::InvalidInput {
|
||||
context: T::Key::context(),
|
||||
expected: e.expected,
|
||||
actual: e.actual,
|
||||
})?;
|
||||
|
||||
Ok((parsed_key, parsed_value))
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
use super::{KV_DELIMITER, PpkKeyValue, PpkLiteral as _, PpkMultilineKeyValue};
|
||||
|
||||
/// Writer for PPK key-value format.
|
||||
pub(crate) struct PuttyKvWriter {
|
||||
output: String,
|
||||
line_end: &'static str,
|
||||
}
|
||||
|
||||
impl PuttyKvWriter {
|
||||
pub fn new(crlf: bool) -> Self {
|
||||
Self {
|
||||
output: String::new(),
|
||||
line_end: if crlf { "\r\n" } else { "\n" },
|
||||
}
|
||||
}
|
||||
|
||||
fn write_key_value_impl(&mut self, key: &str, value: &str) {
|
||||
self.output.push_str(key);
|
||||
self.output.push_str(KV_DELIMITER);
|
||||
self.output.push_str(value);
|
||||
self.output.push_str(self.line_end);
|
||||
}
|
||||
|
||||
pub fn write_value<T: PpkKeyValue>(&mut self, value: T::Value)
|
||||
where
|
||||
T::Key: Default,
|
||||
{
|
||||
self.write_key_value::<T>(T::Key::default(), value);
|
||||
}
|
||||
|
||||
pub fn write_key_value<T: PpkKeyValue>(&mut self, key: T::Key, value: T::Value) {
|
||||
self.write_key_value_impl(key.as_static_str(), &value.to_string());
|
||||
}
|
||||
|
||||
pub fn write_multiline_value<T: PpkMultilineKeyValue>(&mut self, value: T::Value)
|
||||
where
|
||||
T::Key: Default,
|
||||
{
|
||||
self.write_multiline_key_value::<T>(T::Key::default(), value);
|
||||
}
|
||||
|
||||
pub fn write_multiline_key_value<T: PpkMultilineKeyValue>(&mut self, key: T::Key, value: T::Value) {
|
||||
// PuTTY uses a maximum of 64 characters per line
|
||||
const MAX_CHARS_PER_LINE: usize = 64;
|
||||
|
||||
let value = value.to_string();
|
||||
|
||||
let lines_count = value.len() / MAX_CHARS_PER_LINE + (value.len() % MAX_CHARS_PER_LINE != 0) as usize;
|
||||
|
||||
self.write_key_value_impl(key.as_static_str(), &lines_count.to_string());
|
||||
|
||||
let mut value_remaining = value.as_str();
|
||||
|
||||
while !value_remaining.is_empty() {
|
||||
let line_len = value_remaining.len().min(MAX_CHARS_PER_LINE);
|
||||
let (line, remaining) = value_remaining.split_at(line_len);
|
||||
value_remaining = remaining;
|
||||
|
||||
self.output.push_str(line);
|
||||
self.output.push_str(self.line_end);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish(self) -> String {
|
||||
self.output
|
||||
}
|
||||
}
|
||||
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
//! PuTTY key format described in [Appendix C][1] of the PuTTY User Manual.
|
||||
//!
|
||||
//! Both private([`Ppk`]) and public([`PuttyPublicKey`]) keys are supported.
|
||||
//!
|
||||
//! [1]: https://the.earth.li/~sgtatham/putty/0.75/htmldoc/AppendixC.html#ppk
|
||||
|
||||
mod error;
|
||||
mod key_value;
|
||||
mod ppk;
|
||||
mod private_key;
|
||||
mod public_key;
|
||||
|
||||
pub use error::PuttyError;
|
||||
pub use key_value::{
|
||||
Argon2FlavourValue as Argon2Flavour, PpkKeyAlgorithmValue as PpkKeyAlgorithm, PpkVersionKey as PpkVersion,
|
||||
};
|
||||
pub use ppk::{Argon2Params, Ppk, PpkEncryptionConfig, PpkEncryptionConfigBuilder};
|
||||
pub use public_key::PuttyPublicKey;
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
//! AES encryption and decryption utilities.
|
||||
|
||||
use crate::putty::PuttyError;
|
||||
use aes::cipher::KeyIvInit;
|
||||
use aes::cipher::block_padding::NoPadding;
|
||||
use cbc::cipher::{BlockModeDecrypt, BlockModeEncrypt};
|
||||
use inout::InOutBufReserved;
|
||||
use rand_core::Rng;
|
||||
|
||||
pub const KEY_SIZE: usize = 32;
|
||||
pub const BLOCK_SIZE: usize = 16;
|
||||
|
||||
/// Adds padding to the message if it is not a multiple of the AES block size.
|
||||
pub fn make_padding<R: Rng>(mut message: Vec<u8>, mut rng: R) -> Vec<u8> {
|
||||
if message.len() % BLOCK_SIZE != 0 {
|
||||
let unpadded_size = message.len();
|
||||
let padding_size = BLOCK_SIZE - (unpadded_size % BLOCK_SIZE);
|
||||
|
||||
message.resize(unpadded_size + padding_size, 0);
|
||||
rng.fill_bytes(&mut message[unpadded_size..]);
|
||||
}
|
||||
|
||||
message
|
||||
}
|
||||
|
||||
/// Encrypts the message in-place using AES-256 in CBC mode.
|
||||
pub fn encrypt(message: &mut [u8], key: &[u8], iv: &[u8]) -> Result<(), PuttyError> {
|
||||
let encryptor = cbc::Encryptor::<aes::Aes256>::new_from_slices(key, iv).map_err(|_| PuttyError::Aes)?;
|
||||
|
||||
let inout = InOutBufReserved::from_mut_slice(message, message.len())?;
|
||||
encryptor
|
||||
.encrypt_padded_inout::<NoPadding>(inout)
|
||||
.map_err(|_| PuttyError::Aes)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decrypts the message in-place using AES-256 in CBC mode.
|
||||
pub fn decrypt(message: &mut [u8], key: &[u8], iv: &[u8]) -> Result<(), PuttyError> {
|
||||
let decryptor = cbc::Decryptor::<aes::Aes256>::new_from_slices(key, iv).map_err(|_| PuttyError::Aes)?;
|
||||
|
||||
let _ = decryptor
|
||||
.decrypt_padded_inout::<NoPadding>(message.into())
|
||||
.map_err(|_| PuttyError::Aes);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
//! PPK encoding and decoding functions.
|
||||
|
||||
use crate::putty::key_value::{
|
||||
Base64PpkValue, HexPpkValue, PpkArgon2Memory, PpkArgon2MemoryValue, PpkArgon2Parallelism,
|
||||
PpkArgon2ParallelismValue, PpkArgon2Passes, PpkArgon2PassesValue, PpkArgon2Salt, PpkComment, PpkCommentValue,
|
||||
PpkEncryption, PpkEncryptionValue, PpkHeader, PpkKeyDerivation, PpkPrivateLines, PpkPrivateMac, PpkPublicLines,
|
||||
PpkVersionKey, PuttyKvReader, PuttyKvWriter,
|
||||
};
|
||||
use crate::putty::ppk::encryption::PpkEncryptionKind;
|
||||
use crate::putty::{Argon2Params, Ppk, PuttyError};
|
||||
use std::str::FromStr;
|
||||
|
||||
impl FromStr for Ppk {
|
||||
type Err = PuttyError;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
let mut reader = PuttyKvReader::from_str(input);
|
||||
|
||||
let (version, algorithm) = reader.next_key_value::<PpkHeader>()?;
|
||||
let encryption = reader.next_value::<PpkEncryption>()?;
|
||||
let comment = reader.next_value::<PpkComment>()?;
|
||||
let public_key = reader.next_multiline_value::<PpkPublicLines>()?;
|
||||
|
||||
let encryption = match encryption {
|
||||
PpkEncryptionValue::None => None,
|
||||
PpkEncryptionValue::Aes256Cbc if version == PpkVersionKey::V2 => Some(PpkEncryptionKind::Aes256CbcV2),
|
||||
PpkEncryptionValue::Aes256Cbc => {
|
||||
let argon2_flavor = reader.next_value::<PpkKeyDerivation>()?;
|
||||
let argon2_memory = reader.next_value::<PpkArgon2Memory>()?;
|
||||
let argon2_passes = reader.next_value::<PpkArgon2Passes>()?;
|
||||
let argon2_parallelism = reader.next_value::<PpkArgon2Parallelism>()?;
|
||||
let argon2_salt = reader.next_value::<PpkArgon2Salt>()?;
|
||||
|
||||
Some(PpkEncryptionKind::Aes256CbcV3(Argon2Params {
|
||||
flavor: argon2_flavor,
|
||||
memory: argon2_memory.into(),
|
||||
passes: argon2_passes.into(),
|
||||
parallelism: argon2_parallelism.into(),
|
||||
salt: argon2_salt.into(),
|
||||
}))
|
||||
}
|
||||
};
|
||||
|
||||
let private_key = reader.next_multiline_value::<PpkPrivateLines>()?;
|
||||
let mac = reader.next_value::<PpkPrivateMac>()?;
|
||||
|
||||
let ppk = Ppk {
|
||||
version,
|
||||
algorithm,
|
||||
encryption,
|
||||
comment: comment.into(),
|
||||
public_key: public_key.into(),
|
||||
private_key: private_key.into(),
|
||||
mac: mac.into(),
|
||||
};
|
||||
|
||||
// Validate MAC for file integrity check
|
||||
if ppk.encryption.is_none() {
|
||||
let mac = ppk.calculate_unencrypted_mac(ppk.private_key.as_slice())?;
|
||||
|
||||
if mac.as_slice() != ppk.mac.as_slice() {
|
||||
return Err(PuttyError::MacValidation);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ppk)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ppk {
|
||||
/// Encodes the PPK key to a string.
|
||||
pub fn to_string(&self) -> Result<String, PuttyError> {
|
||||
// NOTE: V2 uses CRLF line endings, V3 uses LF
|
||||
let crlf = match self.version {
|
||||
PpkVersionKey::V2 => true,
|
||||
PpkVersionKey::V3 => false,
|
||||
};
|
||||
|
||||
let mut writer = PuttyKvWriter::new(crlf);
|
||||
|
||||
writer.write_key_value::<PpkHeader>(self.version, self.algorithm);
|
||||
writer.write_value::<PpkEncryption>((self.encryption.as_ref()).into());
|
||||
writer.write_value::<PpkComment>(PpkCommentValue::from(self.comment.clone()));
|
||||
writer.write_multiline_value::<PpkPublicLines>(Base64PpkValue::from(self.public_key.clone()));
|
||||
match &self.encryption {
|
||||
Some(PpkEncryptionKind::Aes256CbcV3(argon2)) => {
|
||||
writer.write_value::<PpkKeyDerivation>(argon2.flavor);
|
||||
writer.write_value::<PpkArgon2Memory>(PpkArgon2MemoryValue::from(argon2.memory));
|
||||
writer.write_value::<PpkArgon2Passes>(PpkArgon2PassesValue::from(argon2.passes));
|
||||
writer.write_value::<PpkArgon2Parallelism>(PpkArgon2ParallelismValue::from(argon2.parallelism));
|
||||
writer.write_value::<PpkArgon2Salt>(HexPpkValue::from(argon2.salt.clone()));
|
||||
}
|
||||
None | Some(PpkEncryptionKind::Aes256CbcV2) => {}
|
||||
}
|
||||
|
||||
writer.write_multiline_value::<PpkPrivateLines>(Base64PpkValue::from(self.private_key.clone()));
|
||||
writer.write_value::<PpkPrivateMac>(HexPpkValue::from(self.mac.clone()));
|
||||
|
||||
Ok(writer.finish())
|
||||
}
|
||||
}
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
//! PPK encryption/decryption types and fucntions
|
||||
|
||||
use crate::putty::PuttyError;
|
||||
use crate::putty::key_value::{Argon2FlavourValue, PpkEncryptionValue, PpkVersionKey};
|
||||
use crate::putty::ppk::kdf::{self, KeyMaterialV2};
|
||||
use crate::putty::ppk::{Argon2Params, Ppk, aes as ppk_aes};
|
||||
use crate::ssh::decode::SshReadExt;
|
||||
use rand::rngs::{StdRng, SysRng};
|
||||
use rand_core::SeedableRng as _;
|
||||
|
||||
/// PPK encryption configuration builder.
|
||||
///
|
||||
/// Could be constructed via [`PpkEncryptionConfig::builder()`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PpkEncryptionConfigBuilder {
|
||||
inner: PpkEncryptionConfig,
|
||||
}
|
||||
|
||||
impl PpkEncryptionConfigBuilder {
|
||||
pub fn argon2_flavour(mut self, argon2_flavour: Argon2FlavourValue) -> Self {
|
||||
self.inner.argon2_flavour = argon2_flavour;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn argon2_memory(mut self, argon2_memory: u32) -> Self {
|
||||
self.inner.argon2_memory = argon2_memory;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn argon2_passes(mut self, argon2_passes: u32) -> Self {
|
||||
self.inner.argon2_passes = argon2_passes;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn argon2_parallelism(mut self, argon2_parallelism: u32) -> Self {
|
||||
self.inner.argon2_parallelism = argon2_parallelism;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn argon2_salt_size(mut self, argon2_salt_size: u32) -> Self {
|
||||
self.inner.argon2_salt_size = argon2_salt_size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> PpkEncryptionConfig {
|
||||
self.inner
|
||||
}
|
||||
}
|
||||
|
||||
/// PPK encryption configuration.
|
||||
///
|
||||
/// Could be either constructed via [`Default::default()`] or [`PpkEncryptionConfig::builder()`]
|
||||
///
|
||||
/// Defaults are the same as in PuTTY.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PpkEncryptionConfig {
|
||||
argon2_flavour: Argon2FlavourValue,
|
||||
argon2_memory: u32,
|
||||
argon2_passes: u32,
|
||||
argon2_parallelism: u32,
|
||||
argon2_salt_size: u32,
|
||||
}
|
||||
|
||||
impl Default for PpkEncryptionConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
argon2_flavour: Argon2FlavourValue::Argon2id,
|
||||
argon2_memory: 8192,
|
||||
argon2_passes: 34,
|
||||
argon2_parallelism: 1,
|
||||
argon2_salt_size: 16,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PpkEncryptionConfig {
|
||||
pub fn builder() -> PpkEncryptionConfigBuilder {
|
||||
PpkEncryptionConfigBuilder {
|
||||
inner: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum PpkEncryptionKind {
|
||||
Aes256CbcV2,
|
||||
Aes256CbcV3(Argon2Params),
|
||||
}
|
||||
|
||||
impl From<Option<&PpkEncryptionKind>> for PpkEncryptionValue {
|
||||
fn from(params: Option<&PpkEncryptionKind>) -> Self {
|
||||
match params {
|
||||
None => PpkEncryptionValue::None,
|
||||
Some(PpkEncryptionKind::Aes256CbcV2) => PpkEncryptionValue::Aes256Cbc,
|
||||
Some(PpkEncryptionKind::Aes256CbcV3(_)) => PpkEncryptionValue::Aes256Cbc,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ppk {
|
||||
/// Returns true if the key is encrypted
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
self.encryption.is_some()
|
||||
}
|
||||
|
||||
/// Argon2 KDF parameters if encryption is used (V3 only)
|
||||
pub fn argon2_params(&self) -> Option<&Argon2Params> {
|
||||
match &self.encryption {
|
||||
Some(PpkEncryptionKind::Aes256CbcV3(params)) => Some(params),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns PPK key encrypted with the specified passphrase and config.
|
||||
pub fn encrypt(&self, passphrase: &str, config: PpkEncryptionConfig) -> Result<Self, PuttyError> {
|
||||
self.encrypt_with_rng(passphrase, config, StdRng::try_from_rng(&mut SysRng)?)
|
||||
}
|
||||
|
||||
/// Returns PPK key encrypted with the specified passphrase, config and RNG.
|
||||
pub fn encrypt_with_rng(
|
||||
&self,
|
||||
passphrase: &str,
|
||||
config: PpkEncryptionConfig,
|
||||
mut rng: impl rand_core::Rng,
|
||||
) -> Result<Ppk, PuttyError> {
|
||||
if self.encryption.is_some() {
|
||||
return Err(PuttyError::AlreadyEncrypted);
|
||||
}
|
||||
|
||||
let ppk = match self.version {
|
||||
PpkVersionKey::V2 => {
|
||||
let key_material = kdf::derive_key_material_v2(passphrase)?;
|
||||
|
||||
let mut private_key = ppk_aes::make_padding(self.private_key.clone(), rng);
|
||||
let mac = self.calculate_mac_v2(passphrase, &private_key, PpkEncryptionValue::Aes256Cbc)?;
|
||||
ppk_aes::encrypt(&mut private_key, key_material.key(), KeyMaterialV2::iv())?;
|
||||
|
||||
Ppk {
|
||||
version: self.version,
|
||||
algorithm: self.algorithm,
|
||||
encryption: Some(PpkEncryptionKind::Aes256CbcV2),
|
||||
comment: self.comment.clone(),
|
||||
public_key: self.public_key.clone(),
|
||||
private_key,
|
||||
mac,
|
||||
}
|
||||
}
|
||||
PpkVersionKey::V3 => {
|
||||
let mut argon2_salt = vec![0u8; config.argon2_salt_size as usize];
|
||||
rng.fill_bytes(&mut argon2_salt);
|
||||
let argon2_params = Argon2Params {
|
||||
flavor: config.argon2_flavour,
|
||||
memory: config.argon2_memory,
|
||||
passes: config.argon2_passes,
|
||||
parallelism: config.argon2_parallelism,
|
||||
salt: argon2_salt,
|
||||
};
|
||||
|
||||
let key_material = kdf::derive_key_material_v3(&argon2_params, passphrase)?;
|
||||
|
||||
let mut private_key = ppk_aes::make_padding(self.private_key.clone(), rng);
|
||||
let mac =
|
||||
self.calculate_mac_v3(key_material.hmac_key(), &private_key, PpkEncryptionValue::Aes256Cbc)?;
|
||||
ppk_aes::encrypt(&mut private_key, key_material.key(), key_material.iv())?;
|
||||
|
||||
Ppk {
|
||||
version: self.version,
|
||||
algorithm: self.algorithm,
|
||||
encryption: Some(PpkEncryptionKind::Aes256CbcV3(argon2_params)),
|
||||
comment: self.comment.clone(),
|
||||
public_key: self.public_key.clone(),
|
||||
private_key,
|
||||
mac,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(ppk)
|
||||
}
|
||||
|
||||
/// Returns PPK key decrypted with the specified passphrase
|
||||
pub fn decrypt(&self, passphrase: &str) -> Result<Self, PuttyError> {
|
||||
let encrytion = if let Some(encryption) = &self.encryption {
|
||||
encryption
|
||||
} else {
|
||||
return Err(PuttyError::AlreadyDecrypted);
|
||||
};
|
||||
|
||||
let (mut private_key, mac) = match encrytion {
|
||||
PpkEncryptionKind::Aes256CbcV2 => {
|
||||
let key_material = kdf::derive_key_material_v2(passphrase)?;
|
||||
let mut private_key = self.private_key.clone();
|
||||
ppk_aes::decrypt(&mut private_key, key_material.key(), KeyMaterialV2::iv())?;
|
||||
let mac = self.calculate_mac_v2(passphrase, &private_key, PpkEncryptionValue::Aes256Cbc)?;
|
||||
(private_key, mac)
|
||||
}
|
||||
PpkEncryptionKind::Aes256CbcV3(argon2_params) => {
|
||||
let key_material = kdf::derive_key_material_v3(argon2_params, passphrase)?;
|
||||
let mut private_key = self.private_key.clone();
|
||||
ppk_aes::decrypt(&mut private_key, key_material.key(), key_material.iv())?;
|
||||
let mac =
|
||||
self.calculate_mac_v3(key_material.hmac_key(), &private_key, PpkEncryptionValue::Aes256Cbc)?;
|
||||
(private_key, mac)
|
||||
}
|
||||
};
|
||||
|
||||
// Verify MAC
|
||||
if mac.as_slice() != self.mac.as_slice() {
|
||||
return Err(PuttyError::MacValidation);
|
||||
}
|
||||
|
||||
// Truncate private key padding if any
|
||||
let truncated_size = {
|
||||
let mut mpint_cursor = private_key.as_slice();
|
||||
for _ in 0..self.algorithm.key_mpint_values_count() {
|
||||
// NOTE: Bytes and mpint stored the same way, therefore to avoid BigUint
|
||||
// construction we can just read the bytes (we discard them either way)
|
||||
let _value = mpint_cursor.read_ssh_bytes()?;
|
||||
}
|
||||
|
||||
private_key.len().wrapping_sub(mpint_cursor.len())
|
||||
};
|
||||
|
||||
private_key.truncate(truncated_size);
|
||||
|
||||
let mac = self.calculate_unencrypted_mac(private_key.as_slice())?;
|
||||
|
||||
let ppk = Ppk {
|
||||
version: self.version,
|
||||
algorithm: self.algorithm,
|
||||
encryption: None,
|
||||
comment: self.comment.clone(),
|
||||
public_key: self.public_key.clone(),
|
||||
private_key,
|
||||
mac,
|
||||
};
|
||||
|
||||
Ok(ppk)
|
||||
}
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
//! Key derivation utilities for PPK files.
|
||||
|
||||
use crate::putty::key_value::Argon2FlavourValue;
|
||||
use crate::putty::ppk::{PuttyError, aes as ppk_aes};
|
||||
|
||||
use digest::Digest;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
const SHA256_DIGEST_SIZE: usize = 32;
|
||||
const SHA1_DIGEST_SIZE: usize = 20;
|
||||
|
||||
pub const MAC_SIZE_V3: usize = SHA256_DIGEST_SIZE;
|
||||
|
||||
/// Argon2 key derivation function parameters.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Argon2Params {
|
||||
pub flavor: Argon2FlavourValue,
|
||||
pub memory: u32,
|
||||
pub passes: u32,
|
||||
pub parallelism: u32,
|
||||
pub salt: Vec<u8>,
|
||||
}
|
||||
|
||||
const V3_KEY_MATERIAL_SIZE: usize = ppk_aes::KEY_SIZE // AES key
|
||||
+ ppk_aes::BLOCK_SIZE // AES IV
|
||||
+ MAC_SIZE_V3; // HMAC key
|
||||
|
||||
pub(crate) struct KeyMaterialV3 {
|
||||
key: Zeroizing<[u8; ppk_aes::KEY_SIZE]>,
|
||||
iv: Zeroizing<[u8; ppk_aes::BLOCK_SIZE]>,
|
||||
hmac_key: Zeroizing<[u8; MAC_SIZE_V3]>,
|
||||
}
|
||||
|
||||
impl KeyMaterialV3 {
|
||||
pub fn key(&self) -> &[u8] {
|
||||
self.key.as_ref()
|
||||
}
|
||||
|
||||
pub fn iv(&self) -> &[u8] {
|
||||
self.iv.as_ref()
|
||||
}
|
||||
|
||||
pub fn hmac_key(&self) -> &[u8] {
|
||||
self.hmac_key.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct KeyMaterialV2 {
|
||||
key: Zeroizing<[u8; ppk_aes::KEY_SIZE]>,
|
||||
}
|
||||
|
||||
impl KeyMaterialV2 {
|
||||
pub fn key(&self) -> &[u8] {
|
||||
self.key.as_ref()
|
||||
}
|
||||
|
||||
pub fn iv() -> &'static [u8; ppk_aes::BLOCK_SIZE] {
|
||||
&[0u8; ppk_aes::BLOCK_SIZE]
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn derive_key_material_v2(passphrase: &str) -> Result<KeyMaterialV2, PuttyError> {
|
||||
let tagged_hash = |tag: u32| -> [u8; SHA1_DIGEST_SIZE] {
|
||||
let mut digest = sha1::Sha1::new();
|
||||
digest.update(tag.to_be_bytes());
|
||||
digest.update(passphrase.as_bytes());
|
||||
digest.finalize().into()
|
||||
};
|
||||
|
||||
let hash1 = tagged_hash(0);
|
||||
let hash2 = tagged_hash(1);
|
||||
|
||||
let mut key = [0u8; ppk_aes::KEY_SIZE];
|
||||
key[..SHA1_DIGEST_SIZE].copy_from_slice(&hash1[..]);
|
||||
key[SHA1_DIGEST_SIZE..].copy_from_slice(&hash2[..ppk_aes::KEY_SIZE - SHA1_DIGEST_SIZE]);
|
||||
|
||||
Ok(KeyMaterialV2 { key: key.into() })
|
||||
}
|
||||
|
||||
pub(crate) fn derive_key_material_v3(
|
||||
argon2_params: &Argon2Params,
|
||||
passphrase: &str,
|
||||
) -> Result<KeyMaterialV3, PuttyError> {
|
||||
let mut key_material = [0u8; V3_KEY_MATERIAL_SIZE];
|
||||
|
||||
let kdf = argon2::Argon2::new(
|
||||
argon2_params.flavor.into(),
|
||||
argon2::Version::V0x13,
|
||||
argon2::Params::new(
|
||||
argon2_params.memory,
|
||||
argon2_params.passes,
|
||||
argon2_params.parallelism,
|
||||
Some(V3_KEY_MATERIAL_SIZE),
|
||||
)
|
||||
.map_err(|_| PuttyError::Argon2)?,
|
||||
);
|
||||
|
||||
kdf.hash_password_into(passphrase.as_bytes(), &argon2_params.salt, &mut key_material)
|
||||
.map_err(|_| PuttyError::Argon2)?;
|
||||
|
||||
let key_material = key_material.as_ref();
|
||||
|
||||
const IV_OFFSET: usize = ppk_aes::KEY_SIZE;
|
||||
const HMAC_KEY_OFFSET: usize = IV_OFFSET + ppk_aes::BLOCK_SIZE;
|
||||
|
||||
let mut key = [0u8; ppk_aes::KEY_SIZE];
|
||||
key.copy_from_slice(&key_material[..ppk_aes::KEY_SIZE]);
|
||||
|
||||
let mut iv = [0u8; ppk_aes::BLOCK_SIZE];
|
||||
iv.copy_from_slice(&key_material[IV_OFFSET..IV_OFFSET + ppk_aes::BLOCK_SIZE]);
|
||||
|
||||
let mut hmac_key = [0u8; MAC_SIZE_V3];
|
||||
hmac_key.copy_from_slice(&key_material[HMAC_KEY_OFFSET..]);
|
||||
|
||||
Ok(KeyMaterialV3 {
|
||||
key: key.into(),
|
||||
iv: iv.into(),
|
||||
hmac_key: hmac_key.into(),
|
||||
})
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
//! PPK MAC calculation functions
|
||||
|
||||
use crate::putty::key_value::{PpkEncryptionValue, PpkLiteral, PpkVersionKey};
|
||||
use crate::putty::{Ppk, PuttyError};
|
||||
|
||||
use digest::Digest;
|
||||
use hmac::{KeyInit, Mac};
|
||||
|
||||
impl Ppk {
|
||||
pub(super) fn calculate_mac_v3(
|
||||
&self,
|
||||
mac_key: &[u8],
|
||||
private_key_data: &[u8],
|
||||
encryption: PpkEncryptionValue,
|
||||
) -> Result<Vec<u8>, PuttyError> {
|
||||
let mut hmac = hmac::Hmac::<sha2::Sha256>::new_from_slice(mac_key).map_err(|_| PuttyError::MacValidation)?;
|
||||
|
||||
let mut hash_bytes = |data: &[u8]| -> Result<(), PuttyError> {
|
||||
hmac.update(
|
||||
&u32::try_from(data.len())
|
||||
.map_err(|_| PuttyError::MacValidation)?
|
||||
.to_be_bytes(),
|
||||
);
|
||||
hmac.update(data);
|
||||
Ok(())
|
||||
};
|
||||
|
||||
hash_bytes(self.algorithm.as_static_str().as_bytes())?;
|
||||
hash_bytes(encryption.as_static_str().as_bytes())?;
|
||||
hash_bytes(self.comment.as_bytes())?;
|
||||
hash_bytes(&self.public_key)?;
|
||||
hash_bytes(private_key_data)?;
|
||||
|
||||
let mac = hmac.finalize().into_bytes().to_vec();
|
||||
Ok(mac)
|
||||
}
|
||||
|
||||
pub(super) fn calculate_mac_v2(
|
||||
&self,
|
||||
passphrase: &str,
|
||||
private_key_data: &[u8],
|
||||
encryption: PpkEncryptionValue,
|
||||
) -> Result<Vec<u8>, PuttyError> {
|
||||
let mac_key = {
|
||||
let mut digest = sha1::Sha1::new();
|
||||
digest.update(b"putty-private-key-file-mac-key");
|
||||
digest.update(passphrase.as_bytes());
|
||||
digest.finalize()
|
||||
};
|
||||
|
||||
let mut hmac = hmac::Hmac::<sha1::Sha1>::new_from_slice(&mac_key).map_err(|_| PuttyError::MacValidation)?;
|
||||
|
||||
let mut hash_bytes = |data: &[u8]| -> Result<(), PuttyError> {
|
||||
hmac.update(
|
||||
&u32::try_from(data.len())
|
||||
.map_err(|_| PuttyError::MacValidation)?
|
||||
.to_be_bytes(),
|
||||
);
|
||||
hmac.update(data);
|
||||
Ok(())
|
||||
};
|
||||
|
||||
hash_bytes(self.algorithm.as_static_str().as_bytes())?;
|
||||
hash_bytes(encryption.as_static_str().as_bytes())?;
|
||||
hash_bytes(self.comment.as_bytes())?;
|
||||
hash_bytes(&self.public_key)?;
|
||||
hash_bytes(private_key_data)?;
|
||||
|
||||
let mac = hmac.finalize().into_bytes().to_vec();
|
||||
Ok(mac)
|
||||
}
|
||||
|
||||
pub(super) fn calculate_unencrypted_mac(&self, private_key_data: &[u8]) -> Result<Vec<u8>, PuttyError> {
|
||||
match self.version {
|
||||
PpkVersionKey::V2 => self.calculate_mac_v2("", private_key_data, PpkEncryptionValue::None),
|
||||
PpkVersionKey::V3 => self.calculate_mac_v3(&[], private_key_data, PpkEncryptionValue::None),
|
||||
}
|
||||
}
|
||||
}
|
||||
+258
@@ -0,0 +1,258 @@
|
||||
mod aes;
|
||||
mod encoding;
|
||||
mod encryption;
|
||||
mod kdf;
|
||||
mod mac;
|
||||
|
||||
use crate::key::{EcCurve, PrivateKey, PublicKey};
|
||||
use crate::putty::PuttyError;
|
||||
use crate::putty::key_value::{PpkKeyAlgorithmValue, PpkVersionKey};
|
||||
use crate::putty::private_key::{PuttyBasePrivateKey, PuttyPrivateKey};
|
||||
use crate::putty::public_key::{PuttyBasePublicKey, PuttyPublicKey};
|
||||
use crate::ssh::SshPrivateKey;
|
||||
|
||||
use self::encryption::PpkEncryptionKind;
|
||||
|
||||
pub use encryption::{PpkEncryptionConfig, PpkEncryptionConfigBuilder};
|
||||
pub use kdf::Argon2Params;
|
||||
|
||||
/// PuTTY Private Key (PPK) format.
|
||||
///
|
||||
/// ### Functionality
|
||||
/// - Generation of new keys.
|
||||
/// - Conversion to/from OpenSSH format.
|
||||
/// - Encoding/decoding to/from string.
|
||||
/// - Version upgrade/downgrade.
|
||||
///
|
||||
/// ### Usage notes
|
||||
/// - Ppk structure is immutable. All operations that modify the key return a new instance.
|
||||
/// - When input file is encrypted, all operations with the private key will be unavailable until
|
||||
/// ppk is decrypted via [`Ppk::decrypt`].
|
||||
/// - Newly generated keys are always unencrypted. They should be encrypted via [`Ppk::encrypt`]
|
||||
/// when required
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Ppk {
|
||||
version: PpkVersionKey,
|
||||
algorithm: PpkKeyAlgorithmValue,
|
||||
encryption: Option<PpkEncryptionKind>,
|
||||
comment: String,
|
||||
public_key: Vec<u8>,
|
||||
private_key: Vec<u8>,
|
||||
mac: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Ppk {
|
||||
pub fn generate_rsa(bits: usize, comment: Option<&str>) -> Result<Self, PuttyError> {
|
||||
let ssh_key = SshPrivateKey::generate_rsa(bits, None, comment.map(From::from))?;
|
||||
Self::from_openssh_private_key(&ssh_key)
|
||||
}
|
||||
|
||||
pub fn generate_ec(curve: EcCurve, comment: Option<&str>) -> Result<Self, PuttyError> {
|
||||
let ssh_key = SshPrivateKey::generate_ec(curve, None, comment.map(From::from))?;
|
||||
Self::from_openssh_private_key(&ssh_key)
|
||||
}
|
||||
|
||||
pub fn generate_ed25519(comment: Option<&str>) -> Result<Self, PuttyError> {
|
||||
let ssh_key = SshPrivateKey::generate_ed25519(None, comment.map(From::from))?;
|
||||
Self::from_openssh_private_key(&ssh_key)
|
||||
}
|
||||
|
||||
/// Converts the OpenSSH private key to a PPK key.
|
||||
pub fn from_openssh_private_key(key: &SshPrivateKey) -> Result<Self, PuttyError> {
|
||||
let PuttyPrivateKey { base, comment }: PuttyPrivateKey = PuttyPrivateKey::from_openssh(key)?;
|
||||
|
||||
let mut ppk = Ppk {
|
||||
version: PpkVersionKey::V3,
|
||||
algorithm: base.algorithm,
|
||||
encryption: None,
|
||||
comment,
|
||||
public_key: base.public_key.data,
|
||||
private_key: base.data,
|
||||
mac: vec![],
|
||||
};
|
||||
|
||||
ppk.mac = ppk.calculate_unencrypted_mac(ppk.private_key.as_slice())?;
|
||||
|
||||
Ok(ppk)
|
||||
}
|
||||
|
||||
/// Converts the PPK key to an OpenSSH private key (with or without encryption).
|
||||
pub fn to_openssh_private_key(&self, passphrase: Option<&str>) -> Result<SshPrivateKey, PuttyError> {
|
||||
if self.is_encrypted() {
|
||||
return Err(PuttyError::Encrypted);
|
||||
}
|
||||
|
||||
let base = PuttyBasePrivateKey {
|
||||
algorithm: self.algorithm,
|
||||
public_key: PuttyBasePublicKey {
|
||||
data: self.public_key.clone(),
|
||||
},
|
||||
data: self.private_key.clone(),
|
||||
};
|
||||
|
||||
let key = PuttyPrivateKey {
|
||||
base,
|
||||
comment: self.comment.clone(),
|
||||
};
|
||||
|
||||
key.to_openssh(passphrase)
|
||||
}
|
||||
|
||||
/// Returns PPK public key.
|
||||
pub fn public_key(&self) -> Result<PublicKey, PuttyError> {
|
||||
PuttyBasePublicKey {
|
||||
data: self.public_key.clone(),
|
||||
}
|
||||
.to_inner_key()
|
||||
}
|
||||
|
||||
/// Returns PPK private key.
|
||||
pub fn private_key(&self) -> Result<PrivateKey, PuttyError> {
|
||||
if self.is_encrypted() {
|
||||
return Err(PuttyError::Encrypted);
|
||||
}
|
||||
|
||||
PuttyBasePrivateKey {
|
||||
algorithm: self.algorithm,
|
||||
public_key: PuttyBasePublicKey {
|
||||
data: self.public_key.clone(),
|
||||
},
|
||||
data: self.private_key.clone(),
|
||||
}
|
||||
.to_inner_key()
|
||||
}
|
||||
|
||||
/// Returns extracted public key in PuTTY format.
|
||||
pub fn extract_putty_public_key(&self) -> Result<PuttyPublicKey, PuttyError> {
|
||||
Ok(PuttyPublicKey {
|
||||
base: PuttyBasePublicKey {
|
||||
data: self.public_key.clone(),
|
||||
},
|
||||
comment: self.comment.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns a new PPK key instance with a different comment.
|
||||
pub fn with_comment(&self, comment: &str) -> Result<Self, PuttyError> {
|
||||
if self.is_encrypted() {
|
||||
// We need to decrypt the key to change the comment (MAC should be recalculated).
|
||||
return Err(PuttyError::Encrypted);
|
||||
}
|
||||
|
||||
let mut ppk = Self {
|
||||
comment: comment.to_string(),
|
||||
..self.clone()
|
||||
};
|
||||
|
||||
ppk.mac = ppk.calculate_unencrypted_mac(ppk.private_key.as_slice())?;
|
||||
|
||||
Ok(ppk)
|
||||
}
|
||||
|
||||
/// Returns the version of the PPK file format.
|
||||
pub fn version(&self) -> PpkVersionKey {
|
||||
self.version
|
||||
}
|
||||
|
||||
/// Returns the key algorithm.
|
||||
pub fn algorithm(&self) -> PpkKeyAlgorithmValue {
|
||||
self.algorithm
|
||||
}
|
||||
|
||||
/// Returns key comment.
|
||||
pub fn comment(&self) -> &str {
|
||||
&self.comment
|
||||
}
|
||||
|
||||
/// Returns new PPK kew with the specified format version.
|
||||
///
|
||||
/// NOTE: `PpkVersionKey::V2` is considered insecure and should not be used for new keys in
|
||||
/// normal circumstances.
|
||||
pub fn to_version(&self, version: PpkVersionKey) -> Result<Ppk, PuttyError> {
|
||||
if self.is_encrypted() {
|
||||
return Err(PuttyError::Encrypted);
|
||||
}
|
||||
|
||||
let ppk = Ppk {
|
||||
version,
|
||||
algorithm: self.algorithm,
|
||||
encryption: None,
|
||||
comment: self.comment.clone(),
|
||||
public_key: self.public_key.clone(),
|
||||
private_key: self.private_key.clone(),
|
||||
mac: self.calculate_unencrypted_mac(self.private_key.as_slice())?,
|
||||
};
|
||||
|
||||
Ok(ppk)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rstest::rstest;
|
||||
|
||||
use picky_test_data::{
|
||||
PUTTY_KEY_ED25519, PUTTY_KEY_ED25519_ENCRYPTED, PUTTY_KEY_ED25519_V2, PUTTY_KEY_ED25519_V2_ENCRYPTED,
|
||||
SSH_PRIVATE_KEY_EC_P256, SSH_PRIVATE_KEY_EC_P384, SSH_PRIVATE_KEY_EC_P521, SSH_PRIVATE_KEY_ED25519,
|
||||
SSH_PRIVATE_KEY_RSA,
|
||||
};
|
||||
|
||||
#[rstest]
|
||||
#[case(PUTTY_KEY_ED25519)]
|
||||
#[case(PUTTY_KEY_ED25519_ENCRYPTED)]
|
||||
#[case(PUTTY_KEY_ED25519_V2)]
|
||||
#[case(PUTTY_KEY_ED25519_V2_ENCRYPTED)]
|
||||
fn ppk_encode_decode_roundtrip(#[case] input: &str) {
|
||||
let key: Ppk = input.parse().unwrap();
|
||||
let encoded = key.to_string().unwrap();
|
||||
|
||||
assert_eq!(encoded, input);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(PUTTY_KEY_ED25519_ENCRYPTED, PUTTY_KEY_ED25519)]
|
||||
#[case(PUTTY_KEY_ED25519_V2_ENCRYPTED, PUTTY_KEY_ED25519_V2)]
|
||||
fn decrypt_produces_same_key_as_puttygen(#[case] encrypted: &str, #[case] decrypted: &str) {
|
||||
let mut key: Ppk = encrypted.parse().unwrap();
|
||||
assert!(key.is_encrypted());
|
||||
key = key.decrypt("test").unwrap();
|
||||
|
||||
let encoded = key.to_string().unwrap();
|
||||
assert_eq!(encoded, decrypted);
|
||||
assert!(!key.is_encrypted());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(PUTTY_KEY_ED25519, PpkVersionKey::V3, "eddsa-key-20240414")]
|
||||
#[case(PUTTY_KEY_ED25519_V2, PpkVersionKey::V2, "ed25519-key-20240418")]
|
||||
fn encrypt_decrypt_roundtrip(#[case] input: &str, #[case] version: PpkVersionKey, #[case] comment: &str) {
|
||||
let mut key: Ppk = input.parse().unwrap();
|
||||
key = key.encrypt("test", Default::default()).unwrap();
|
||||
assert!(key.is_encrypted());
|
||||
key = key.decrypt("test").unwrap();
|
||||
assert_eq!(key.to_string().unwrap(), input);
|
||||
assert!(!key.is_encrypted());
|
||||
assert_eq!(key.version(), version);
|
||||
assert_eq!(key.algorithm(), PpkKeyAlgorithmValue::Ed25519);
|
||||
assert_eq!(key.comment(), comment);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(SSH_PRIVATE_KEY_RSA)]
|
||||
#[case(SSH_PRIVATE_KEY_EC_P256)]
|
||||
#[case(SSH_PRIVATE_KEY_EC_P384)]
|
||||
#[case(SSH_PRIVATE_KEY_EC_P521)]
|
||||
#[case(SSH_PRIVATE_KEY_ED25519)]
|
||||
fn test_openssh_roundtrip(#[case] input: &str) {
|
||||
let ssh_key = SshPrivateKey::from_pem_str(input, None).unwrap();
|
||||
let key = Ppk::from_openssh_private_key(&ssh_key).unwrap();
|
||||
let mut ssh_key2 = key.to_openssh_private_key(None).unwrap();
|
||||
|
||||
// Check is re-generated when new ssh is created from scratch
|
||||
ssh_key2.check = ssh_key.check;
|
||||
|
||||
let ssh_key_str = ssh_key2.to_string().unwrap();
|
||||
assert_eq!(ssh_key_str, input);
|
||||
}
|
||||
}
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
use crate::key::ec::{EcdsaKeypair, EcdsaPublicKey, NamedEcCurve};
|
||||
use crate::key::ed::{EdKeypair, EdPublicKey, NamedEdAlgorithm};
|
||||
use crate::key::{EcCurve, EdAlgorithm, PrivateKey};
|
||||
use crate::putty::PuttyError;
|
||||
use crate::putty::key_value::PpkKeyAlgorithmValue;
|
||||
use crate::putty::public_key::PuttyBasePublicKey;
|
||||
use crate::ssh::SshPrivateKey;
|
||||
use crate::ssh::decode::SshReadExt;
|
||||
use crate::ssh::encode::SshWriteExt;
|
||||
use crate::ssh::private_key::SshBasePrivateKey;
|
||||
use crate::ssh::public_key::SshBasePublicKey;
|
||||
use crypto_bigint::BoxedUint;
|
||||
use rsa::traits::{PrivateKeyParts, PublicKeyParts};
|
||||
use rsa::{RsaPrivateKey, RsaPublicKey};
|
||||
|
||||
/// PuTTY private key wrapper
|
||||
pub(crate) struct PuttyPrivateKey {
|
||||
pub(crate) base: PuttyBasePrivateKey,
|
||||
pub(crate) comment: String,
|
||||
}
|
||||
|
||||
impl PuttyPrivateKey {
|
||||
pub fn from_openssh(key: &SshPrivateKey) -> Result<Self, PuttyError> {
|
||||
let base = PuttyBasePrivateKey::from_openssh(&key.base_key)?;
|
||||
let comment = key.comment.clone();
|
||||
|
||||
Ok(Self { base, comment })
|
||||
}
|
||||
|
||||
/// Converts the key to an OpenSSH key (with or without encryption)
|
||||
pub fn to_openssh(&self, passphrase: Option<&str>) -> Result<SshPrivateKey, PuttyError> {
|
||||
let base = self.base.to_openssh()?;
|
||||
let comment = if self.comment.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(self.comment.clone())
|
||||
};
|
||||
|
||||
let key = match base {
|
||||
SshBasePrivateKey::Rsa(key) => key,
|
||||
SshBasePrivateKey::Ec(key) => key,
|
||||
SshBasePrivateKey::Ed(key) => key,
|
||||
SshBasePrivateKey::SkEcdsaSha2NistP256 { .. } | SshBasePrivateKey::SkEd25519 { .. } => {
|
||||
return Err(PuttyError::NotSupported { feature: "SK keys" });
|
||||
}
|
||||
};
|
||||
|
||||
Ok(SshPrivateKey::h_picky_private_key_to_ssh_private_key(
|
||||
key,
|
||||
passphrase.map(From::from),
|
||||
comment,
|
||||
)?)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct PuttyBasePrivateKey {
|
||||
pub(crate) algorithm: PpkKeyAlgorithmValue,
|
||||
pub(crate) public_key: PuttyBasePublicKey,
|
||||
pub(crate) data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl PuttyBasePrivateKey {
|
||||
pub fn from_openssh(key: &SshBasePrivateKey) -> Result<Self, PuttyError> {
|
||||
let mut data = Vec::new();
|
||||
let cursor = &mut data;
|
||||
|
||||
match key {
|
||||
SshBasePrivateKey::SkEcdsaSha2NistP256 { .. } | SshBasePrivateKey::SkEd25519 { .. } => {
|
||||
// Putty does not support SK keys
|
||||
Err(PuttyError::NotSupported { feature: "SK keys" })
|
||||
}
|
||||
SshBasePrivateKey::Rsa(key) => {
|
||||
let mut rsa_key = RsaPrivateKey::try_from(key)?;
|
||||
|
||||
cursor.write_ssh_mpint(rsa_key.d())?;
|
||||
if rsa_key.primes().len() != 2 {
|
||||
return Err(PuttyError::RsaInvalidPrimesCount {
|
||||
count: rsa_key.primes().len(),
|
||||
});
|
||||
}
|
||||
cursor.write_ssh_mpint(&rsa_key.primes()[0])?;
|
||||
cursor.write_ssh_mpint(&rsa_key.primes()[1])?;
|
||||
|
||||
rsa_key.precompute().map_err(|_| PuttyError::RsaPrecompute)?;
|
||||
let qinv = rsa_key
|
||||
.qinv()
|
||||
.expect("BUG: should be precomuted above")
|
||||
.retrieve()
|
||||
.to_be_bytes_trimmed_vartime();
|
||||
cursor.write_ssh_bytes(&qinv)?;
|
||||
|
||||
let ssh_public_key = SshBasePublicKey::Rsa(key.to_public_key()?);
|
||||
let public_key = PuttyBasePublicKey::from_openssh(&ssh_public_key)?;
|
||||
|
||||
Ok(Self {
|
||||
algorithm: PpkKeyAlgorithmValue::Rsa,
|
||||
data,
|
||||
public_key,
|
||||
})
|
||||
}
|
||||
SshBasePrivateKey::Ec(key) => {
|
||||
let ec_key = EcdsaKeypair::try_from(key)?;
|
||||
|
||||
let secret = BoxedUint::from_be_slice_vartime(ec_key.secret());
|
||||
cursor.write_ssh_mpint(&secret)?;
|
||||
|
||||
let algorithm = match ec_key.curve() {
|
||||
NamedEcCurve::Known(EcCurve::NistP256) => PpkKeyAlgorithmValue::EcdsaSha2Nistp256,
|
||||
NamedEcCurve::Known(EcCurve::NistP384) => PpkKeyAlgorithmValue::EcdsaSha2Nistp384,
|
||||
NamedEcCurve::Known(EcCurve::NistP521) => PpkKeyAlgorithmValue::EcdsaSha2Nistp521,
|
||||
_ => {
|
||||
return Err(PuttyError::NotSupported {
|
||||
feature: "unknown EC curve",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let ssh_public_key = SshBasePublicKey::Ec(key.to_public_key()?);
|
||||
let public_key = PuttyBasePublicKey::from_openssh(&ssh_public_key)?;
|
||||
|
||||
Ok(Self {
|
||||
algorithm,
|
||||
data,
|
||||
public_key,
|
||||
})
|
||||
}
|
||||
SshBasePrivateKey::Ed(key) => {
|
||||
let ed_key = EdKeypair::try_from(key)?;
|
||||
|
||||
cursor.write_ssh_mpint(&BoxedUint::from_be_slice_vartime(ed_key.secret()))?;
|
||||
|
||||
let algorithm = match ed_key.algorithm() {
|
||||
NamedEdAlgorithm::Known(EdAlgorithm::Ed25519) => PpkKeyAlgorithmValue::Ed25519,
|
||||
NamedEdAlgorithm::Known(EdAlgorithm::X25519) => {
|
||||
return Err(PuttyError::NotSupported { feature: "X25519 keys" });
|
||||
}
|
||||
_ => {
|
||||
return Err(PuttyError::NotSupported {
|
||||
feature: "unknown EdDSA algorithm",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let ssh_public_key = SshBasePublicKey::Ed(key.to_public_key()?);
|
||||
let public_key = PuttyBasePublicKey::from_openssh(&ssh_public_key)?;
|
||||
|
||||
Ok(Self {
|
||||
algorithm,
|
||||
data,
|
||||
public_key,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_openssh(&self) -> Result<SshBasePrivateKey, PuttyError> {
|
||||
let ssh_public_key = self.public_key.to_openssh()?;
|
||||
let mut data = self.data.as_slice();
|
||||
|
||||
match self.algorithm {
|
||||
PpkKeyAlgorithmValue::Rsa => {
|
||||
let public = match &ssh_public_key {
|
||||
SshBasePublicKey::Rsa(rsa) => RsaPublicKey::try_from(rsa)?,
|
||||
_ => return Err(PuttyError::PublicAndPrivateKeyMismatch),
|
||||
};
|
||||
|
||||
let d = data.read_ssh_mpint()?;
|
||||
let p1 = data.read_ssh_mpint()?;
|
||||
let p2 = data.read_ssh_mpint()?;
|
||||
let _qinv = data.read_ssh_mpint()?;
|
||||
|
||||
let private_key = PrivateKey::from_rsa_components(public.n(), public.e(), &d, &[p1, p2])?;
|
||||
|
||||
Ok(SshBasePrivateKey::Rsa(private_key))
|
||||
}
|
||||
PpkKeyAlgorithmValue::EcdsaSha2Nistp256
|
||||
| PpkKeyAlgorithmValue::EcdsaSha2Nistp384
|
||||
| PpkKeyAlgorithmValue::EcdsaSha2Nistp521 => {
|
||||
let public = match &ssh_public_key {
|
||||
SshBasePublicKey::Ec(rsa) => EcdsaPublicKey::try_from(rsa)?,
|
||||
_ => return Err(PuttyError::PublicAndPrivateKeyMismatch),
|
||||
};
|
||||
|
||||
let secret = data.read_ssh_mpint()?;
|
||||
|
||||
let curve = match self.algorithm {
|
||||
PpkKeyAlgorithmValue::EcdsaSha2Nistp256 => NamedEcCurve::Known(EcCurve::NistP256),
|
||||
PpkKeyAlgorithmValue::EcdsaSha2Nistp384 => NamedEcCurve::Known(EcCurve::NistP384),
|
||||
PpkKeyAlgorithmValue::EcdsaSha2Nistp521 => NamedEcCurve::Known(EcCurve::NistP521),
|
||||
_ => unreachable!("BUG: algorithm is checked above"),
|
||||
};
|
||||
|
||||
let private_key = PrivateKey::from_ec_encoded_components(
|
||||
curve.into(),
|
||||
&secret.to_be_bytes_trimmed_vartime(),
|
||||
Some(public.encoded_point()),
|
||||
);
|
||||
|
||||
Ok(SshBasePrivateKey::Ec(private_key))
|
||||
}
|
||||
PpkKeyAlgorithmValue::Ed25519 => {
|
||||
let public = match &ssh_public_key {
|
||||
SshBasePublicKey::Ed(rsa) => EdPublicKey::try_from(rsa)?,
|
||||
_ => return Err(PuttyError::PublicAndPrivateKeyMismatch),
|
||||
};
|
||||
|
||||
let secret = data.read_ssh_mpint()?;
|
||||
|
||||
let private_key = PrivateKey::from_ed_encoded_components(
|
||||
NamedEdAlgorithm::Known(EdAlgorithm::Ed25519).into(),
|
||||
&secret.to_be_bytes_trimmed_vartime(),
|
||||
Some(public.data()),
|
||||
);
|
||||
|
||||
Ok(SshBasePrivateKey::Ed(private_key))
|
||||
}
|
||||
_ => Err(PuttyError::NotSupported {
|
||||
feature: "unsupported key algorithm",
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_inner_key(&self) -> Result<PrivateKey, PuttyError> {
|
||||
let inner = match self.to_openssh()? {
|
||||
SshBasePrivateKey::Rsa(key) => key,
|
||||
SshBasePrivateKey::Ec(key) => key,
|
||||
SshBasePrivateKey::Ed(key) => key,
|
||||
SshBasePrivateKey::SkEcdsaSha2NistP256 { .. } | SshBasePrivateKey::SkEd25519 { .. } => {
|
||||
return Err(PuttyError::NotSupported { feature: "SK keys" });
|
||||
}
|
||||
};
|
||||
|
||||
Ok(inner)
|
||||
}
|
||||
}
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
use crate::key::PublicKey;
|
||||
use crate::putty::PuttyError;
|
||||
use crate::ssh::SshPublicKey;
|
||||
use crate::ssh::decode::SshComplexTypeDecode;
|
||||
use crate::ssh::encode::SshComplexTypeEncode;
|
||||
use crate::ssh::public_key::SshBasePublicKey;
|
||||
use std::str::FromStr;
|
||||
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_ENGINE;
|
||||
|
||||
const PUTTY_PUBKEY_HEADER: &str = "---- BEGIN SSH2 PUBLIC KEY ----";
|
||||
const PUTTY_PUBKEY_FOOTER: &str = "---- END SSH2 PUBLIC KEY ----";
|
||||
|
||||
/// PuTTY public key format.
|
||||
///
|
||||
/// ### Functionality:
|
||||
/// - Conversion to/from OpenSSH format.
|
||||
/// - Encoding/decoding to/from string.
|
||||
/// - Could be extracted from [`crate::putty::Ppk`] private keys.
|
||||
///
|
||||
/// ### Notes
|
||||
/// - Although top-level containeris similar to PEM, it is not compatible with it because of
|
||||
/// additional comment field after the header.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PuttyPublicKey {
|
||||
pub(crate) base: PuttyBasePublicKey,
|
||||
pub(crate) comment: String,
|
||||
}
|
||||
|
||||
impl PuttyPublicKey {
|
||||
/// Converts an OpenSSH public key to a PuTTY public key.
|
||||
pub fn from_openssh(key: &SshPublicKey) -> Result<Self, PuttyError> {
|
||||
let base = PuttyBasePublicKey::from_openssh(&key.inner_key)?;
|
||||
|
||||
Ok(Self {
|
||||
base,
|
||||
comment: key.comment.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Converts the key to an OpenSSH public key.
|
||||
pub fn to_openssh(&self) -> Result<SshPublicKey, PuttyError> {
|
||||
let base = self.base.to_openssh()?;
|
||||
Ok(SshPublicKey {
|
||||
inner_key: base,
|
||||
comment: self.comment.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns key comment.
|
||||
pub fn comment(&self) -> &str {
|
||||
&self.comment
|
||||
}
|
||||
|
||||
/// Returns a new public key instance with a different comment.
|
||||
pub fn with_comment(&self, comment: &str) -> Self {
|
||||
Self {
|
||||
comment: comment.to_string(),
|
||||
..self.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses and returns the inner key as standard picky key type.
|
||||
pub fn to_inner_key(&self) -> Result<PublicKey, PuttyError> {
|
||||
self.base.to_inner_key()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PuttyPublicKey {
|
||||
// False positive, clippy does not take into account that [`String::replace`] requires both
|
||||
// arguments to be the same type, e.g. we can't use `&str` as a replacement for `char`.
|
||||
#[allow(clippy::single_char_pattern)]
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
const MAX_CHARS_PER_LINE: usize = 64;
|
||||
const LINE_END: &str = "\r\n";
|
||||
|
||||
let encoded_key = BASE64_ENGINE.encode(&self.base.data);
|
||||
let escaped_comment = self.comment.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||
|
||||
let mut output = String::new();
|
||||
output.push_str(PUTTY_PUBKEY_HEADER);
|
||||
output.push_str(LINE_END);
|
||||
output.push_str("Comment: \"");
|
||||
output.push_str(&escaped_comment);
|
||||
output.push('"');
|
||||
output.push_str(LINE_END);
|
||||
|
||||
let mut value_remaining = encoded_key.as_str();
|
||||
|
||||
while !value_remaining.is_empty() {
|
||||
let line_len = value_remaining.len().min(MAX_CHARS_PER_LINE);
|
||||
let (line, remaining) = value_remaining.split_at(line_len);
|
||||
value_remaining = remaining;
|
||||
|
||||
output.push_str(line);
|
||||
output.push_str(LINE_END);
|
||||
}
|
||||
|
||||
output.push_str(PUTTY_PUBKEY_FOOTER);
|
||||
output.push_str(LINE_END);
|
||||
|
||||
f.write_str(&output)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for PuttyPublicKey {
|
||||
type Err = PuttyError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut lines = s.lines();
|
||||
let header = lines.next().ok_or(PuttyError::EndOfInput)?;
|
||||
if header != PUTTY_PUBKEY_HEADER {
|
||||
return Err(PuttyError::InvalidPublicKeyContainer);
|
||||
}
|
||||
|
||||
let comment_line = lines.next().ok_or(PuttyError::EndOfInput)?;
|
||||
if !comment_line.starts_with("Comment: ") {
|
||||
return Err(PuttyError::InvalidPublicKeyComment);
|
||||
}
|
||||
|
||||
let unescaped_comment = comment_line
|
||||
.split_once('"')
|
||||
.and_then(|(_, remainder)| remainder.rsplit_once('"'))
|
||||
.map(|(comment, _)| comment)
|
||||
.ok_or(PuttyError::InvalidPublicKeyComment)?;
|
||||
|
||||
let comment = unescaped_comment.replace("\\\"", "\"").replace("\\\\", "\\");
|
||||
|
||||
let mut encoded_key = String::new();
|
||||
for line in lines {
|
||||
if line == PUTTY_PUBKEY_FOOTER {
|
||||
let decoded = BASE64_ENGINE
|
||||
.decode(encoded_key)
|
||||
.map_err(|_| PuttyError::InvalidPublicKeyData)?;
|
||||
|
||||
return Ok(PuttyPublicKey {
|
||||
base: PuttyBasePublicKey { data: decoded },
|
||||
comment,
|
||||
});
|
||||
}
|
||||
|
||||
encoded_key.push_str(line);
|
||||
}
|
||||
|
||||
Err(PuttyError::EndOfInput)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct PuttyBasePublicKey {
|
||||
pub(crate) data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl PuttyBasePublicKey {
|
||||
pub fn from_openssh(key: &SshBasePublicKey) -> Result<Self, PuttyError> {
|
||||
match key {
|
||||
SshBasePublicKey::SkEcdsaSha2NistP256 { .. } | SshBasePublicKey::SkEd25519 { .. } => {
|
||||
// Putty does not support SK keys
|
||||
return Err(PuttyError::NotSupported { feature: "SK keys" });
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
let mut data = Vec::new();
|
||||
SshBasePublicKey::encode(key, &mut data)?;
|
||||
|
||||
Ok(Self { data })
|
||||
}
|
||||
|
||||
pub fn to_openssh(&self) -> Result<SshBasePublicKey, PuttyError> {
|
||||
let key = SshBasePublicKey::decode(self.data.as_slice())?;
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
pub fn to_inner_key(&self) -> Result<PublicKey, PuttyError> {
|
||||
let inner = match self.to_openssh()? {
|
||||
SshBasePublicKey::Rsa(key) => key,
|
||||
SshBasePublicKey::Ec(key) => key,
|
||||
SshBasePublicKey::Ed(key) => key,
|
||||
SshBasePublicKey::SkEcdsaSha2NistP256 { .. } | SshBasePublicKey::SkEd25519 { .. } => {
|
||||
return Err(PuttyError::NotSupported { feature: "SK keys" });
|
||||
}
|
||||
};
|
||||
|
||||
Ok(inner)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use picky_test_data::{
|
||||
PUTTY_KEY_ED25519_PUBLIC, PUTTY_KEY_RSA_PUBLIC_EMPTY_COMMENT, PUTTY_KEY_RSA_PUBLIC_ESCAPED_COMMENT,
|
||||
SSH_PUBLIC_KEY_EC_P256, SSH_PUBLIC_KEY_EC_P384, SSH_PUBLIC_KEY_EC_P521, SSH_PUBLIC_KEY_ED25519,
|
||||
SSH_PUBLIC_KEY_RSA,
|
||||
};
|
||||
use rstest::rstest;
|
||||
|
||||
#[rstest]
|
||||
#[case(PUTTY_KEY_ED25519_PUBLIC)]
|
||||
#[case(PUTTY_KEY_RSA_PUBLIC_EMPTY_COMMENT)]
|
||||
#[case(PUTTY_KEY_RSA_PUBLIC_ESCAPED_COMMENT)]
|
||||
fn public_key_rountrip(#[case] input: &str) {
|
||||
let key: PuttyPublicKey = input.parse().unwrap();
|
||||
let output = key.to_string();
|
||||
assert_eq!(input, output);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(SSH_PUBLIC_KEY_RSA)]
|
||||
#[case(SSH_PUBLIC_KEY_EC_P256)]
|
||||
#[case(SSH_PUBLIC_KEY_EC_P384)]
|
||||
#[case(SSH_PUBLIC_KEY_EC_P521)]
|
||||
#[case(SSH_PUBLIC_KEY_ED25519)]
|
||||
fn ssh_key_roundtrip(#[case] input: &str) {
|
||||
let ssh_key: SshPublicKey = input.parse().unwrap();
|
||||
let key = PuttyPublicKey::from_openssh(&ssh_key).unwrap();
|
||||
let ssh_key2 = key.to_openssh().unwrap();
|
||||
|
||||
let ssh_key_str = ssh_key2.to_string().unwrap();
|
||||
assert_eq!(ssh_key_str, input);
|
||||
}
|
||||
}
|
||||
Vendored
+633
@@ -0,0 +1,633 @@
|
||||
//! Signature algorithms supported by picky
|
||||
|
||||
use crate::hash::HashAlgorithm;
|
||||
use crate::key::ec::{EcComponent, EcCurve, NamedEcCurve};
|
||||
use crate::key::{KeyError, PrivateKey, PublicKey};
|
||||
|
||||
use picky_asn1_x509::{AlgorithmIdentifier, oids};
|
||||
use rsa::signature::Signer;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum SignatureError {
|
||||
/// Key error
|
||||
#[error("Key error: {source}")]
|
||||
Key { source: KeyError },
|
||||
|
||||
/// RSA error
|
||||
#[error("RSA error: {context}")]
|
||||
Rsa { context: String },
|
||||
|
||||
/// EC error
|
||||
#[error("EC error: {context}")]
|
||||
Ec { context: String },
|
||||
|
||||
/// ED error
|
||||
#[error("ED error: {context}")]
|
||||
Ed { context: String },
|
||||
|
||||
/// invalid signature
|
||||
#[error("invalid signature")]
|
||||
BadSignature,
|
||||
|
||||
/// unsupported algorithm
|
||||
#[error("unsupported algorithm: {algorithm}")]
|
||||
UnsupportedAlgorithm { algorithm: String },
|
||||
}
|
||||
|
||||
impl From<rsa::errors::Error> for SignatureError {
|
||||
fn from(e: rsa::errors::Error) -> Self {
|
||||
SignatureError::Rsa { context: e.to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<rsa::signature::Error> for SignatureError {
|
||||
fn from(e: rsa::signature::Error) -> Self {
|
||||
SignatureError::Rsa { context: e.to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KeyError> for SignatureError {
|
||||
fn from(e: KeyError) -> Self {
|
||||
SignatureError::Key { source: e }
|
||||
}
|
||||
}
|
||||
|
||||
/// Supported signature algorithms
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[non_exhaustive]
|
||||
pub enum SignatureAlgorithm {
|
||||
RsaPkcs1v15(HashAlgorithm),
|
||||
Ecdsa(HashAlgorithm),
|
||||
Ed25519,
|
||||
}
|
||||
|
||||
impl TryFrom<&'_ AlgorithmIdentifier> for SignatureAlgorithm {
|
||||
type Error = SignatureError;
|
||||
|
||||
fn try_from(v: &AlgorithmIdentifier) -> Result<Self, Self::Error> {
|
||||
let oid_string: String = v.oid().into();
|
||||
match oid_string.as_str() {
|
||||
oids::MD5_WITH_RSA_ENCRYPTHION => Ok(Self::RsaPkcs1v15(HashAlgorithm::MD5)),
|
||||
oids::SHA1_WITH_RSA_ENCRYPTION => Ok(Self::RsaPkcs1v15(HashAlgorithm::SHA1)),
|
||||
oids::SHA224_WITH_RSA_ENCRYPTION => Ok(Self::RsaPkcs1v15(HashAlgorithm::SHA2_224)),
|
||||
oids::SHA256_WITH_RSA_ENCRYPTION => Ok(Self::RsaPkcs1v15(HashAlgorithm::SHA2_256)),
|
||||
oids::SHA384_WITH_RSA_ENCRYPTION => Ok(Self::RsaPkcs1v15(HashAlgorithm::SHA2_384)),
|
||||
oids::SHA512_WITH_RSA_ENCRYPTION => Ok(Self::RsaPkcs1v15(HashAlgorithm::SHA2_512)),
|
||||
oids::ID_RSASSA_PKCS1_V1_5_WITH_SHA3_384 => Ok(Self::RsaPkcs1v15(HashAlgorithm::SHA3_384)),
|
||||
oids::ID_RSASSA_PKCS1_V1_5_WITH_SHA3_512 => Ok(Self::RsaPkcs1v15(HashAlgorithm::SHA3_512)),
|
||||
oids::ECDSA_WITH_SHA256 => Ok(Self::Ecdsa(HashAlgorithm::SHA2_256)),
|
||||
oids::ECDSA_WITH_SHA384 => Ok(Self::Ecdsa(HashAlgorithm::SHA2_384)),
|
||||
oids::ED25519 => Ok(Self::Ed25519),
|
||||
_ => Err(SignatureError::UnsupportedAlgorithm { algorithm: oid_string }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<SignatureAlgorithm> for AlgorithmIdentifier {
|
||||
type Error = SignatureError;
|
||||
|
||||
fn try_from(ty: SignatureAlgorithm) -> Result<Self, Self::Error> {
|
||||
match ty {
|
||||
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::MD5) => {
|
||||
Ok(AlgorithmIdentifier::new_md5_with_rsa_encryption())
|
||||
}
|
||||
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA1) => {
|
||||
Ok(AlgorithmIdentifier::new_sha1_with_rsa_encryption())
|
||||
}
|
||||
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_224) => {
|
||||
Ok(AlgorithmIdentifier::new_sha224_with_rsa_encryption())
|
||||
}
|
||||
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256) => {
|
||||
Ok(AlgorithmIdentifier::new_sha256_with_rsa_encryption())
|
||||
}
|
||||
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_384) => {
|
||||
Ok(AlgorithmIdentifier::new_sha384_with_rsa_encryption())
|
||||
}
|
||||
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_512) => {
|
||||
Ok(AlgorithmIdentifier::new_sha512_with_rsa_encryption())
|
||||
}
|
||||
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA3_384) => {
|
||||
Ok(AlgorithmIdentifier::new_sha3_384_with_rsa_encryption())
|
||||
}
|
||||
SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA3_512) => {
|
||||
Ok(AlgorithmIdentifier::new_sha3_512_with_rsa_encryption())
|
||||
}
|
||||
SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_256) => Ok(AlgorithmIdentifier::new_ecdsa_with_sha256()),
|
||||
SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_384) => Ok(AlgorithmIdentifier::new_ecdsa_with_sha384()),
|
||||
SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_512) => Ok(AlgorithmIdentifier::new_ecdsa_with_sha512()),
|
||||
SignatureAlgorithm::Ecdsa(hash) => {
|
||||
let msg = format!("ECDSA doesn't support {hash:?} hashing algorithm");
|
||||
Err(SignatureError::Ec { context: msg })
|
||||
}
|
||||
SignatureAlgorithm::Ed25519 => Ok(AlgorithmIdentifier::new_ed25519()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SignatureAlgorithm {
|
||||
pub fn from_algorithm_identifier(algorithm_identifier: &AlgorithmIdentifier) -> Result<Self, SignatureError> {
|
||||
Self::try_from(algorithm_identifier)
|
||||
}
|
||||
|
||||
pub fn sign(self, msg: &[u8], private_key: &PrivateKey) -> Result<Vec<u8>, SignatureError> {
|
||||
match self {
|
||||
SignatureAlgorithm::RsaPkcs1v15(picky_hash_algo) => {
|
||||
use rsa::signature::SignatureEncoding as _;
|
||||
use rsa::{RsaPrivateKey, pkcs1v15};
|
||||
|
||||
let rsa_private_key = RsaPrivateKey::try_from(private_key)?;
|
||||
|
||||
let signature = match picky_hash_algo {
|
||||
HashAlgorithm::MD5 => pkcs1v15::SigningKey::<md5::Md5>::new(rsa_private_key).try_sign(msg)?,
|
||||
HashAlgorithm::SHA1 => pkcs1v15::SigningKey::<sha1::Sha1>::new(rsa_private_key).try_sign(msg)?,
|
||||
HashAlgorithm::SHA2_224 => {
|
||||
pkcs1v15::SigningKey::<sha2::Sha224>::new(rsa_private_key).try_sign(msg)?
|
||||
}
|
||||
HashAlgorithm::SHA2_256 => {
|
||||
pkcs1v15::SigningKey::<sha2::Sha256>::new(rsa_private_key).try_sign(msg)?
|
||||
}
|
||||
HashAlgorithm::SHA2_384 => {
|
||||
pkcs1v15::SigningKey::<sha2::Sha384>::new(rsa_private_key).try_sign(msg)?
|
||||
}
|
||||
HashAlgorithm::SHA2_512 => {
|
||||
pkcs1v15::SigningKey::<sha2::Sha512>::new(rsa_private_key).try_sign(msg)?
|
||||
}
|
||||
HashAlgorithm::SHA3_384 => {
|
||||
pkcs1v15::SigningKey::<sha3::Sha3_384>::new(rsa_private_key).try_sign(msg)?
|
||||
}
|
||||
HashAlgorithm::SHA3_512 => {
|
||||
pkcs1v15::SigningKey::<sha3::Sha3_512>::new(rsa_private_key).try_sign(msg)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(signature.to_vec())
|
||||
}
|
||||
SignatureAlgorithm::Ecdsa(picky_hash_algo) => {
|
||||
use crate::key::ec::EcdsaKeypair;
|
||||
use p256::ecdsa::signature::Signer;
|
||||
|
||||
let ec_keypair = EcdsaKeypair::try_from(private_key)?;
|
||||
|
||||
match ec_keypair.curve() {
|
||||
NamedEcCurve::Known(EcCurve::NistP256) => match picky_hash_algo {
|
||||
HashAlgorithm::SHA2_256 => {
|
||||
let secret_validated =
|
||||
EcCurve::NistP256.validate_component(EcComponent::Secret(ec_keypair.secret()))?;
|
||||
|
||||
let key_bytes =
|
||||
p256::elliptic_curve::array::Array::try_from(secret_validated).map_err(|_| {
|
||||
SignatureError::Ec {
|
||||
context: format!(
|
||||
"validated secret is not the right size(expected: {}, actual: {})",
|
||||
EcCurve::NistP256.field_bytes_size(),
|
||||
secret_validated.len(),
|
||||
),
|
||||
}
|
||||
})?;
|
||||
let key =
|
||||
p256::ecdsa::SigningKey::from_bytes(&key_bytes).map_err(|e| SignatureError::Ec {
|
||||
context: format!("Cannot decode p256 EC keypair: {e}"),
|
||||
})?;
|
||||
let sig: p256::ecdsa::Signature = key.try_sign(msg).map_err(|e| SignatureError::Ec {
|
||||
context: format!("Cannot produce p256 signature: {e}"),
|
||||
})?;
|
||||
|
||||
Ok(sig.to_der().as_bytes().to_vec())
|
||||
}
|
||||
_ => Err(SignatureError::UnsupportedAlgorithm {
|
||||
algorithm: format!(
|
||||
"ECDSA P-256 curve with {picky_hash_algo:?} hash algorithm is not supported"
|
||||
),
|
||||
}),
|
||||
},
|
||||
NamedEcCurve::Known(EcCurve::NistP384) => match picky_hash_algo {
|
||||
HashAlgorithm::SHA2_384 => {
|
||||
let secret_validated =
|
||||
EcCurve::NistP384.validate_component(EcComponent::Secret(ec_keypair.secret()))?;
|
||||
|
||||
let key_bytes =
|
||||
p384::elliptic_curve::array::Array::try_from(secret_validated).map_err(|_| {
|
||||
SignatureError::Ec {
|
||||
context: format!(
|
||||
"validated secret is not the right size(expected: {}, actual: {})",
|
||||
EcCurve::NistP384.field_bytes_size(),
|
||||
secret_validated.len(),
|
||||
),
|
||||
}
|
||||
})?;
|
||||
|
||||
let key =
|
||||
p384::ecdsa::SigningKey::from_bytes(&key_bytes).map_err(|e| SignatureError::Ec {
|
||||
context: format!("Cannot decode p384 EC keypair: {e}"),
|
||||
})?;
|
||||
let sig: p384::ecdsa::Signature = key.try_sign(msg).map_err(|e| SignatureError::Ec {
|
||||
context: format!("Cannot produce p384 signature: {e}"),
|
||||
})?;
|
||||
Ok(sig.to_der().as_bytes().to_vec())
|
||||
}
|
||||
_ => Err(SignatureError::UnsupportedAlgorithm {
|
||||
algorithm: format!(
|
||||
"ECDSA P-384 curve with {picky_hash_algo:?} hash algorithm is not supported"
|
||||
),
|
||||
}),
|
||||
},
|
||||
NamedEcCurve::Known(EcCurve::NistP521) => match picky_hash_algo {
|
||||
HashAlgorithm::SHA2_512 => {
|
||||
let secret_validated =
|
||||
EcCurve::NistP521.validate_component(EcComponent::Secret(ec_keypair.secret()))?;
|
||||
|
||||
let key_bytes =
|
||||
p521::elliptic_curve::array::Array::try_from(secret_validated).map_err(|_| {
|
||||
SignatureError::Ec {
|
||||
context: format!(
|
||||
"validated secret is not the right size(expected: {}, actual: {})",
|
||||
EcCurve::NistP521.field_bytes_size(),
|
||||
secret_validated.len(),
|
||||
),
|
||||
}
|
||||
})?;
|
||||
|
||||
let key =
|
||||
p521::ecdsa::SigningKey::from_bytes(&key_bytes).map_err(|e| SignatureError::Ec {
|
||||
context: format!("Cannot decode p521 EC keypair: {e}"),
|
||||
})?;
|
||||
let sig: p521::ecdsa::Signature = key.try_sign(msg).map_err(|e| SignatureError::Ec {
|
||||
context: format!("Cannot produce p521 signature: {e}"),
|
||||
})?;
|
||||
Ok(sig.to_der().as_bytes().to_vec())
|
||||
}
|
||||
_ => Err(SignatureError::UnsupportedAlgorithm {
|
||||
algorithm: format!(
|
||||
"ECDSA P-521 curve with {picky_hash_algo:?} hash algorithm is not supported"
|
||||
),
|
||||
}),
|
||||
},
|
||||
NamedEcCurve::Unsupported(oid) => Err(KeyError::unsupported_curve(oid, "signing").into()),
|
||||
}
|
||||
}
|
||||
SignatureAlgorithm::Ed25519 => {
|
||||
use crate::key::ed::{EdAlgorithm, EdKeypair, EdPublicKey, NamedEdAlgorithm};
|
||||
|
||||
let keypair = EdKeypair::try_from(private_key)?;
|
||||
|
||||
let public_key = EdPublicKey::try_from(&keypair)?;
|
||||
|
||||
match keypair.algorithm() {
|
||||
NamedEdAlgorithm::Known(EdAlgorithm::Ed25519) => {
|
||||
let public_key = public_key.data().try_into().map_err(|e| SignatureError::Ed {
|
||||
context: format!("invalid key size: {e}"),
|
||||
})?;
|
||||
let verifying_key = ed25519_dalek::VerifyingKey::from_bytes(public_key).map_err(
|
||||
|e: ed25519_dalek::ed25519::Error| SignatureError::Ed {
|
||||
context: format!("Cannot decode ed25519 public key: {e}"),
|
||||
},
|
||||
)?;
|
||||
|
||||
let secret_key: ed25519_dalek::SecretKey =
|
||||
keypair.secret().try_into().map_err(|e| SignatureError::Ed {
|
||||
context: format!("invalid secret key size: {e}"),
|
||||
})?;
|
||||
|
||||
let esk = ed25519_dalek::hazmat::ExpandedSecretKey::from(&secret_key);
|
||||
|
||||
let signature = ed25519_dalek::hazmat::raw_sign::<sha2::Sha512>(&esk, msg, &verifying_key);
|
||||
|
||||
Ok(signature.to_vec())
|
||||
}
|
||||
NamedEdAlgorithm::Known(EdAlgorithm::X25519) => Err(SignatureError::Ed {
|
||||
context: "X25519 algorithm is not designed for signing".to_string(),
|
||||
}),
|
||||
NamedEdAlgorithm::Unsupported(oid) => {
|
||||
Err(KeyError::unsupported_ed_algorithm(oid, "signing").into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify(self, public_key: &PublicKey, msg: &[u8], signature: &[u8]) -> Result<(), SignatureError> {
|
||||
match self {
|
||||
SignatureAlgorithm::RsaPkcs1v15(picky_hash_algo) => {
|
||||
use rsa::signature::Verifier as _;
|
||||
use rsa::{RsaPublicKey, pkcs1v15};
|
||||
|
||||
let rsa_public_key = RsaPublicKey::try_from(public_key)?;
|
||||
let signature = pkcs1v15::Signature::try_from(signature)?;
|
||||
|
||||
match picky_hash_algo {
|
||||
HashAlgorithm::MD5 => {
|
||||
pkcs1v15::VerifyingKey::<md5::Md5>::new(rsa_public_key).verify(msg, &signature)
|
||||
}
|
||||
HashAlgorithm::SHA1 => {
|
||||
pkcs1v15::VerifyingKey::<sha1::Sha1>::new(rsa_public_key).verify(msg, &signature)
|
||||
}
|
||||
HashAlgorithm::SHA2_224 => {
|
||||
pkcs1v15::VerifyingKey::<sha2::Sha224>::new(rsa_public_key).verify(msg, &signature)
|
||||
}
|
||||
HashAlgorithm::SHA2_256 => {
|
||||
pkcs1v15::VerifyingKey::<sha2::Sha256>::new(rsa_public_key).verify(msg, &signature)
|
||||
}
|
||||
HashAlgorithm::SHA2_384 => {
|
||||
pkcs1v15::VerifyingKey::<sha2::Sha384>::new(rsa_public_key).verify(msg, &signature)
|
||||
}
|
||||
HashAlgorithm::SHA2_512 => {
|
||||
pkcs1v15::VerifyingKey::<sha2::Sha512>::new(rsa_public_key).verify(msg, &signature)
|
||||
}
|
||||
HashAlgorithm::SHA3_384 => {
|
||||
pkcs1v15::VerifyingKey::<sha3::Sha3_384>::new(rsa_public_key).verify(msg, &signature)
|
||||
}
|
||||
HashAlgorithm::SHA3_512 => {
|
||||
pkcs1v15::VerifyingKey::<sha3::Sha3_512>::new(rsa_public_key).verify(msg, &signature)
|
||||
}
|
||||
}
|
||||
.map_err(|_| SignatureError::BadSignature)?;
|
||||
}
|
||||
SignatureAlgorithm::Ecdsa(picky_hash_algo) => {
|
||||
let ec_pub_key = crate::key::ec::EcdsaPublicKey::try_from(public_key)?;
|
||||
|
||||
let curve = match ec_pub_key.curve() {
|
||||
NamedEcCurve::Known(curve) => curve,
|
||||
NamedEcCurve::Unsupported(oid) => return Err(KeyError::unsupported_curve(oid, "verifying").into()),
|
||||
};
|
||||
|
||||
match picky_hash_algo {
|
||||
HashAlgorithm::SHA2_256 => {
|
||||
use p256::ecdsa::signature::Verifier;
|
||||
|
||||
match curve {
|
||||
EcCurve::NistP256 => {}
|
||||
curve => {
|
||||
return Err(SignatureError::UnsupportedAlgorithm {
|
||||
algorithm: format!("SHA256 hash algorithm can't be used with `{curve}` curve"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let encoded_point = p256::Sec1Point::from_bytes(ec_pub_key.encoded_point()).map_err(|e| {
|
||||
SignatureError::Ec {
|
||||
context: format!("Cannot parse p256 public key from der bytes: {e}"),
|
||||
}
|
||||
})?;
|
||||
|
||||
let vkey = p256::ecdsa::VerifyingKey::from_sec1_point(&encoded_point).map_err(|e| {
|
||||
SignatureError::Ec {
|
||||
context: format!("Cannot parse p256 encoded point: {e}"),
|
||||
}
|
||||
})?;
|
||||
|
||||
let signature =
|
||||
p256::ecdsa::Signature::from_der(signature).map_err(|e| SignatureError::Ec {
|
||||
context: format!("Cannot parse p256 signature: {e}"),
|
||||
})?;
|
||||
vkey.verify(msg, &signature).map_err(|_| SignatureError::BadSignature)?
|
||||
}
|
||||
HashAlgorithm::SHA2_384 => {
|
||||
use p384::ecdsa::signature::Verifier;
|
||||
|
||||
match curve {
|
||||
EcCurve::NistP384 => {}
|
||||
curve => {
|
||||
return Err(SignatureError::UnsupportedAlgorithm {
|
||||
algorithm: format!("SHA384 hash algorithm can't be used with `{curve}` curve"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let encoded_point = p384::Sec1Point::from_bytes(ec_pub_key.encoded_point()).map_err(|e| {
|
||||
SignatureError::Ec {
|
||||
context: format!("Cannot parse p384 public key from der bytes: {e}"),
|
||||
}
|
||||
})?;
|
||||
|
||||
let vkey = p384::ecdsa::VerifyingKey::from_sec1_point(&encoded_point).map_err(|e| {
|
||||
SignatureError::Ec {
|
||||
context: format!("Cannot parse p384 encoded point: {e}"),
|
||||
}
|
||||
})?;
|
||||
|
||||
let signature =
|
||||
p384::ecdsa::Signature::from_der(signature).map_err(|e| SignatureError::Ec {
|
||||
context: format!("Cannot parse p384 signature: {e}"),
|
||||
})?;
|
||||
vkey.verify(msg, &signature).map_err(|_| SignatureError::BadSignature)?
|
||||
}
|
||||
HashAlgorithm::SHA2_512 => {
|
||||
use p521::ecdsa::signature::Verifier;
|
||||
|
||||
match curve {
|
||||
EcCurve::NistP521 => {}
|
||||
curve => {
|
||||
return Err(SignatureError::UnsupportedAlgorithm {
|
||||
algorithm: format!("SHA512 hash algorithm can't be used with `{curve}` curve"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let encoded_point = p521::Sec1Point::from_bytes(ec_pub_key.encoded_point()).map_err(|e| {
|
||||
SignatureError::Ec {
|
||||
context: format!("Cannot parse p521 public key from der bytes: {e}"),
|
||||
}
|
||||
})?;
|
||||
|
||||
let vkey = p521::ecdsa::VerifyingKey::from_sec1_point(&encoded_point).map_err(|e| {
|
||||
SignatureError::Ec {
|
||||
context: format!("Cannot parse p521 encoded point: {e}"),
|
||||
}
|
||||
})?;
|
||||
|
||||
let signature =
|
||||
p521::ecdsa::Signature::from_der(signature).map_err(|e| SignatureError::Ec {
|
||||
context: format!("Cannot parse p521 signature: {e}"),
|
||||
})?;
|
||||
vkey.verify(msg, &signature).map_err(|_| SignatureError::BadSignature)?
|
||||
}
|
||||
_ => {
|
||||
return Err(SignatureError::UnsupportedAlgorithm {
|
||||
algorithm: format!("ECDSA with {picky_hash_algo:?} hash algorithm is not supported"),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
SignatureAlgorithm::Ed25519 => {
|
||||
use crate::key::ed::{EdAlgorithm, EdPublicKey, NamedEdAlgorithm};
|
||||
use ed25519_dalek::Verifier;
|
||||
|
||||
let public_key = EdPublicKey::try_from(public_key)?;
|
||||
|
||||
match public_key.algorithm() {
|
||||
NamedEdAlgorithm::Known(EdAlgorithm::Ed25519) => {
|
||||
let public_key = public_key.data().try_into().map_err(|e| SignatureError::Ed {
|
||||
context: format!("invalid key size: {e}"),
|
||||
})?;
|
||||
let verifying_key = ed25519_dalek::VerifyingKey::from_bytes(public_key).map_err(
|
||||
|e: ed25519_dalek::ed25519::Error| SignatureError::Ed {
|
||||
context: format!("Cannot decode ed25519 public key: {e}"),
|
||||
},
|
||||
)?;
|
||||
|
||||
let signature = signature.try_into().map_err(|e| SignatureError::Ed {
|
||||
context: format!("invalid signature size: {e}"),
|
||||
})?;
|
||||
let signature = ed25519_dalek::Signature::from_bytes(signature);
|
||||
|
||||
verifying_key
|
||||
.verify(msg, &signature)
|
||||
.map_err(|_| SignatureError::BadSignature)?;
|
||||
}
|
||||
NamedEdAlgorithm::Known(EdAlgorithm::X25519) => {
|
||||
return Err(SignatureError::Ed {
|
||||
context: "X25519 algorithm is not designed for signing".to_string(),
|
||||
});
|
||||
}
|
||||
NamedEdAlgorithm::Unsupported(oid) => {
|
||||
return Err(KeyError::unsupported_ed_algorithm(oid, "verifying").into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn hash_algorithm(&self) -> HashAlgorithm {
|
||||
match &self {
|
||||
SignatureAlgorithm::RsaPkcs1v15(hash_algo) => *hash_algo,
|
||||
SignatureAlgorithm::Ecdsa(hash_algo) => *hash_algo,
|
||||
SignatureAlgorithm::Ed25519 => HashAlgorithm::SHA2_512,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod ec_tests {
|
||||
use super::*;
|
||||
use rstest::*;
|
||||
|
||||
const EC_PRIVATE_KEY_NIST256_PEM: &str = r#"-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEICHio5XUa+RbeFfGtGHfbPWehTFJJtCB4/izKHJ9Vm+goAoGCCqGSM49
|
||||
AwEHoUQDQgAEh7ZqcI6f0tgqq7nqdcxWM6P4GGCfkWc4q11uXFjtXOKHKCV3LzMY
|
||||
g8/V1PD/YOh0HodRJAjkjXub8AmYxiTcXw==
|
||||
-----END EC PRIVATE KEY-----"#;
|
||||
|
||||
const EC_PRIVATE_KEY_NIST384_PEM: &str = r#"-----BEGIN EC PRIVATE KEY-----
|
||||
MIGkAgEBBDDT8VOfdzHbIRaWOO1F0vgotY2qM2FfYS3zpdKE7Vqbh26hFsUw+iaG
|
||||
GmGnT+29kg+gBwYFK4EEACKhZANiAAQFvVVUKRdN3/bqaEpDA1aHu8FEd3ujuyS0
|
||||
AadG6QAiZxH37BGumBcyTTeGHyArqb+GTpsHTUXASbP+P+p5JgkfF9wBMF1SVTvu
|
||||
ACZOYcqzGbsAXXdMYqewckhc42ye0u0=
|
||||
-----END EC PRIVATE KEY-----"#;
|
||||
|
||||
#[rstest]
|
||||
#[case(HashAlgorithm::MD5, false)]
|
||||
#[case(HashAlgorithm::SHA1, false)]
|
||||
#[case(HashAlgorithm::SHA2_224, false)]
|
||||
#[case(HashAlgorithm::SHA2_256, true)]
|
||||
#[case(HashAlgorithm::SHA2_384, true)]
|
||||
#[case(HashAlgorithm::SHA2_512, true)]
|
||||
#[case(HashAlgorithm::SHA3_384, false)]
|
||||
#[case(HashAlgorithm::SHA3_512, false)]
|
||||
fn algorithm_identifier_conversions(#[case] hash: HashAlgorithm, #[case] success: bool) {
|
||||
let signature_algorithm = SignatureAlgorithm::Ecdsa(hash);
|
||||
let algorithm_identifier = AlgorithmIdentifier::try_from(signature_algorithm);
|
||||
if success {
|
||||
assert!(algorithm_identifier.is_ok());
|
||||
} else {
|
||||
assert!(matches!(algorithm_identifier, Err(SignatureError::Ec { context: _ })));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_bad_signature() {
|
||||
let private_key_signature = PrivateKey::from_pem_str(EC_PRIVATE_KEY_NIST256_PEM).unwrap();
|
||||
let signature_algorithm = SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_256);
|
||||
|
||||
let msg = b"hello world";
|
||||
let signature = signature_algorithm.sign(msg, &private_key_signature).unwrap();
|
||||
|
||||
let another_ec_private_key_nist256_pem = r#"-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIBVYtZ17YMj89Kuu47TOxJlLVlk7MDUuAlFrVXxexgkSoAoGCCqGSM49
|
||||
AwEHoUQDQgAE/irzdOJk28zjVv3sov15/NLIOxoIwL9kM2p/RfQAslATwHpD/T79
|
||||
csaQwO9jFvbQFIpCvcMRjaunLfhIWiYDdg==
|
||||
-----END EC PRIVATE KEY-----"#;
|
||||
|
||||
let another_private_key = PrivateKey::from_pem_str(another_ec_private_key_nist256_pem).unwrap();
|
||||
let wrong_public_key = PublicKey::try_from(another_private_key).unwrap();
|
||||
assert!(matches!(
|
||||
signature_algorithm.verify(&wrong_public_key, msg, &signature),
|
||||
Err(SignatureError::BadSignature)
|
||||
));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(picky_test_data::EC_NIST256_PK_1, HashAlgorithm::SHA2_256, true)]
|
||||
#[case(picky_test_data::EC_NIST384_PK_1, HashAlgorithm::SHA2_384, true)]
|
||||
#[case(picky_test_data::EC_NIST521_PK_1, HashAlgorithm::SHA2_512, true)]
|
||||
fn sign_and_verify(#[case] key_pem: &str, #[case] hash: HashAlgorithm, #[case] sign_successful: bool) {
|
||||
let private_key = PrivateKey::from_pem_str(key_pem).unwrap();
|
||||
|
||||
let signature_algorithm = SignatureAlgorithm::Ecdsa(hash);
|
||||
|
||||
let msg = b"hello world";
|
||||
let signature = signature_algorithm.sign(msg, &private_key);
|
||||
assert_eq!(signature.is_ok(), sign_successful);
|
||||
|
||||
if !sign_successful {
|
||||
return;
|
||||
}
|
||||
|
||||
let public_key = PublicKey::try_from(private_key).unwrap();
|
||||
signature_algorithm
|
||||
.verify(&public_key, msg, &signature.unwrap())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(EC_PRIVATE_KEY_NIST256_PEM, &ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING, HashAlgorithm::SHA2_256)]
|
||||
#[case(EC_PRIVATE_KEY_NIST384_PEM, &ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING, HashAlgorithm::SHA2_384)]
|
||||
fn sign_and_verify_compatibility_with_ring(
|
||||
#[case] key_pem: &str,
|
||||
#[case] algorithm: &'static ring::signature::EcdsaSigningAlgorithm,
|
||||
#[case] hash: HashAlgorithm,
|
||||
) {
|
||||
// sign using ring
|
||||
let private_key = PrivateKey::from_pem_str(key_pem).unwrap();
|
||||
let public_key = PublicKey::try_from(private_key).unwrap();
|
||||
let msg = b"hello world";
|
||||
|
||||
let (privk, pubk) = match hash {
|
||||
HashAlgorithm::SHA2_256 => {
|
||||
use p256::Sec1Point;
|
||||
let k = p256::SecretKey::from_sec1_pem(key_pem).unwrap();
|
||||
(
|
||||
k.to_bytes().as_slice().to_vec(),
|
||||
Into::<Sec1Point>::into(k.public_key()).as_bytes().to_vec(),
|
||||
)
|
||||
}
|
||||
HashAlgorithm::SHA2_384 => {
|
||||
use p384::Sec1Point;
|
||||
let k = p384::SecretKey::from_sec1_pem(key_pem).unwrap();
|
||||
(
|
||||
k.to_bytes().as_slice().to_vec(),
|
||||
Into::<Sec1Point>::into(k.public_key()).as_bytes().to_vec(),
|
||||
)
|
||||
}
|
||||
_ => panic!("no this condition"),
|
||||
};
|
||||
|
||||
let rng = ring::rand::SystemRandom::new();
|
||||
|
||||
let keypair =
|
||||
ring::signature::EcdsaKeyPair::from_private_key_and_public_key(algorithm, &privk, &pubk, &rng).unwrap();
|
||||
|
||||
let rng = ring::rand::SystemRandom::new();
|
||||
let signature = keypair.sign(&rng, msg).unwrap();
|
||||
let sig = signature.as_ref().to_vec();
|
||||
|
||||
// verify using rust-crypto
|
||||
let signature_algorithm = SignatureAlgorithm::Ecdsa(hash);
|
||||
|
||||
signature_algorithm.verify(&public_key, msg, &sig).unwrap();
|
||||
}
|
||||
}
|
||||
+1070
File diff suppressed because it is too large
Load Diff
Vendored
+595
@@ -0,0 +1,595 @@
|
||||
use crate::key::ec::{EcCurve, NamedEcCurve};
|
||||
use crate::key::ed::NamedEdAlgorithm;
|
||||
use crate::key::{EdAlgorithm, PrivateKey, PublicKey};
|
||||
use crate::ssh::certificate::{
|
||||
SshCertKeyType, SshCertType, SshCertTypeError, SshCertificate, SshCertificateError, SshCriticalOption,
|
||||
SshCriticalOptionError, SshCriticalOptionType, SshExtension, SshExtensionError, SshExtensionType, SshSignature,
|
||||
SshSignatureError, SshSignatureFormat, Timestamp,
|
||||
};
|
||||
use crate::ssh::private_key::{KdfOption, SshBasePrivateKey, SshPrivateKeyError};
|
||||
use crate::ssh::public_key::{SshBasePublicKey, SshPublicKey, SshPublicKeyError};
|
||||
use crate::ssh::{Base64Reader, SSH_COMBO_ED25519_KEY_LENGTH, key_type, read_until_linebreak, read_until_whitespace};
|
||||
|
||||
use super::certificate::SshSignatureBlob;
|
||||
use base64::engine::general_purpose;
|
||||
use byteorder::{BigEndian, ReadBytesExt};
|
||||
use crypto_bigint::BoxedUint;
|
||||
use picky_asn1_x509::oid::ObjectIdentifier;
|
||||
use picky_asn1_x509::oids;
|
||||
use std::io::{self, Cursor, Read};
|
||||
|
||||
pub trait SshReadExt {
|
||||
type Error;
|
||||
|
||||
fn read_ssh_string(&mut self) -> Result<String, Self::Error>;
|
||||
fn read_ssh_bytes(&mut self) -> Result<Vec<u8>, Self::Error>;
|
||||
fn read_ssh_mpint(&mut self) -> Result<BoxedUint, Self::Error>;
|
||||
}
|
||||
|
||||
impl<T> SshReadExt for T
|
||||
where
|
||||
T: Read,
|
||||
{
|
||||
type Error = io::Error;
|
||||
|
||||
fn read_ssh_string(&mut self) -> Result<String, Self::Error> {
|
||||
let size = self.read_u32::<BigEndian>()? as usize;
|
||||
let mut buffer = vec![0; size];
|
||||
self.read_exact(&mut buffer)?;
|
||||
|
||||
Ok(String::from_utf8_lossy(&buffer).into_owned())
|
||||
}
|
||||
|
||||
fn read_ssh_bytes(&mut self) -> Result<Vec<u8>, Self::Error> {
|
||||
let size = self.read_u32::<BigEndian>()? as usize;
|
||||
let mut buffer = vec![0; size];
|
||||
self.read_exact(&mut buffer)?;
|
||||
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
fn read_ssh_mpint(&mut self) -> Result<BoxedUint, Self::Error> {
|
||||
let size = self.read_u32::<BigEndian>()? as usize;
|
||||
let mut buffer = vec![0; size];
|
||||
self.read_exact(&mut buffer)?;
|
||||
|
||||
if buffer[0] == 0 {
|
||||
buffer.remove(0);
|
||||
}
|
||||
|
||||
Ok(BoxedUint::from_be_slice_vartime(&buffer))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait SshComplexTypeDecode: Sized {
|
||||
type Error;
|
||||
|
||||
fn decode(stream: impl Read) -> Result<Self, Self::Error>;
|
||||
}
|
||||
|
||||
impl SshComplexTypeDecode for SshCertType {
|
||||
type Error = SshCertTypeError;
|
||||
|
||||
fn decode(mut stream: impl Read) -> Result<Self, Self::Error> {
|
||||
SshCertType::try_from(stream.read_u32::<BigEndian>()?)
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeDecode for SshCriticalOption {
|
||||
type Error = SshCriticalOptionError;
|
||||
|
||||
fn decode(mut stream: impl Read) -> Result<Self, Self::Error> {
|
||||
let option_type: String = stream.read_ssh_string()?;
|
||||
let data: String = stream.read_ssh_string()?;
|
||||
Ok(SshCriticalOption {
|
||||
option_type: SshCriticalOptionType::try_from(option_type)?,
|
||||
data,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> SshComplexTypeDecode for Vec<T>
|
||||
where
|
||||
T: SshComplexTypeDecode,
|
||||
T::Error: From<std::io::Error>,
|
||||
{
|
||||
type Error = T::Error;
|
||||
|
||||
fn decode(mut stream: impl Read) -> Result<Self, Self::Error> {
|
||||
let data = stream.read_ssh_bytes()?;
|
||||
let len = data.len() as u64;
|
||||
let mut cursor = Cursor::new(data);
|
||||
let mut res = Vec::new();
|
||||
while cursor.position() < len {
|
||||
let elem: Result<T, Self::Error> = SshComplexTypeDecode::decode(&mut cursor);
|
||||
res.push(elem?);
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeDecode for SshExtension {
|
||||
type Error = SshExtensionError;
|
||||
|
||||
fn decode(mut stream: impl Read) -> Result<Self, Self::Error> {
|
||||
let extension_type = stream.read_ssh_string()?;
|
||||
let data = stream.read_ssh_string()?;
|
||||
Ok(SshExtension {
|
||||
extension_type: SshExtensionType::try_from(extension_type)?,
|
||||
data,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeDecode for Vec<String> {
|
||||
type Error = io::Error;
|
||||
|
||||
fn decode(mut stream: impl Read) -> Result<Self, Self::Error> {
|
||||
let data = stream.read_ssh_bytes()?;
|
||||
let len = data.len();
|
||||
let mut cursor = Cursor::new(data);
|
||||
let mut res = Vec::new();
|
||||
while cursor.position() < len as u64 {
|
||||
res.push(cursor.read_ssh_string()?);
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeDecode for SshSignature {
|
||||
type Error = SshSignatureError;
|
||||
|
||||
fn decode(mut stream: impl Read) -> Result<Self, Self::Error> {
|
||||
let _overall_size = stream.read_u32::<BigEndian>()?;
|
||||
|
||||
let format = SshSignatureFormat::new(stream.read_ssh_string()?.as_str())?;
|
||||
let data = stream.read_ssh_bytes()?;
|
||||
|
||||
match format {
|
||||
SshSignatureFormat::SkEd25519 | SshSignatureFormat::SkEcdsaSha2NistP256 => {
|
||||
let flags = stream.read_u8()?;
|
||||
let counter = stream.read_u32::<BigEndian>()?;
|
||||
|
||||
Ok(SshSignature {
|
||||
format,
|
||||
blob: SshSignatureBlob::Sk { data, flags, counter },
|
||||
})
|
||||
}
|
||||
_ => Ok(SshSignature {
|
||||
format,
|
||||
blob: SshSignatureBlob::Standard(data),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeDecode for KdfOption {
|
||||
type Error = io::Error;
|
||||
|
||||
fn decode(mut stream: impl Read) -> Result<Self, Self::Error> {
|
||||
let data = stream.read_ssh_bytes()?;
|
||||
if data.is_empty() {
|
||||
return Ok(KdfOption::default());
|
||||
}
|
||||
let mut data = data.as_slice();
|
||||
let salt = data.read_ssh_bytes()?;
|
||||
let rounds = data.read_u32::<BigEndian>()?;
|
||||
Ok(KdfOption { salt, rounds })
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeDecode for Timestamp {
|
||||
type Error = io::Error;
|
||||
|
||||
fn decode(mut stream: impl Read) -> Result<Self, Self::Error> {
|
||||
let timestamp = stream.read_u64::<BigEndian>()?;
|
||||
let time = Timestamp::from(timestamp);
|
||||
Ok(time)
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeDecode for SshBasePublicKey {
|
||||
type Error = SshPublicKeyError;
|
||||
|
||||
fn decode(mut stream: impl Read) -> Result<Self, Self::Error> {
|
||||
let key_type = stream.read_ssh_string()?;
|
||||
match key_type.as_str() {
|
||||
key_type::RSA => {
|
||||
let e = stream.read_ssh_mpint()?;
|
||||
let n = stream.read_ssh_mpint()?;
|
||||
Ok(SshBasePublicKey::Rsa(PublicKey::from_rsa_components(&n, &e)))
|
||||
}
|
||||
key_type::ECDSA_SHA2_NIST_P256 | key_type::ECDSA_SHA2_NIST_P384 | key_type::ECDSA_SHA2_NIST_P521 => {
|
||||
let (curve, point) = decode_ec_public_key_body_impl(key_type.as_str(), &mut stream)?;
|
||||
Ok(SshBasePublicKey::Ec(PublicKey::from_ec_encoded_components(
|
||||
&curve.into(),
|
||||
&point,
|
||||
)))
|
||||
}
|
||||
key_type::ED25519 => {
|
||||
let (algorithm, public_key) = decode_ed25519_public_key_body_impl(key_type.as_str(), &mut stream)?;
|
||||
|
||||
Ok(SshBasePublicKey::Ed(PublicKey::from_ed_encoded_components(
|
||||
&algorithm.into(),
|
||||
&public_key,
|
||||
)))
|
||||
}
|
||||
key_type::SK_ECDSA_SHA2_NIST_P256 => {
|
||||
let (curve, point) = decode_ec_public_key_body_impl(key_type.as_str(), &mut stream)?;
|
||||
let base_key = PublicKey::from_ec_encoded_components(&curve.into(), &point);
|
||||
let application = stream.read_ssh_string()?;
|
||||
|
||||
Ok(SshBasePublicKey::SkEcdsaSha2NistP256 { base_key, application })
|
||||
}
|
||||
key_type::SK_ED25519 => {
|
||||
let (algorithm, public_key) = decode_ed25519_public_key_body_impl(key_type.as_str(), &mut stream)?;
|
||||
let base_key = PublicKey::from_ed_encoded_components(&algorithm.into(), &public_key);
|
||||
let application = stream.read_ssh_string()?;
|
||||
|
||||
Ok(SshBasePublicKey::SkEd25519 { base_key, application })
|
||||
}
|
||||
_ => Err(SshPublicKeyError::UnknownKeyType),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_ed25519_public_key_body_impl(
|
||||
key_type: &str,
|
||||
stream: &mut impl Read,
|
||||
) -> Result<(NamedEdAlgorithm, Vec<u8>), SshPublicKeyError> {
|
||||
let algorithm = match key_type {
|
||||
key_type::ED25519 => NamedEdAlgorithm::Known(EdAlgorithm::Ed25519),
|
||||
key_type::SK_ED25519 => NamedEdAlgorithm::Known(EdAlgorithm::Ed25519),
|
||||
_ => {
|
||||
return Err(SshPublicKeyError::UnknownKeyType);
|
||||
}
|
||||
};
|
||||
let public_key = stream.read_ssh_bytes()?;
|
||||
Ok((algorithm, public_key))
|
||||
}
|
||||
|
||||
fn decode_ec_public_key_body_impl(
|
||||
key_type: &str,
|
||||
stream: &mut impl Read,
|
||||
) -> Result<(NamedEcCurve, Vec<u8>), SshPublicKeyError> {
|
||||
let curve = match key_type {
|
||||
key_type::ECDSA_SHA2_NIST_P256 => NamedEcCurve::Known(EcCurve::NistP256),
|
||||
key_type::ECDSA_SHA2_NIST_P384 => NamedEcCurve::Known(EcCurve::NistP384),
|
||||
key_type::ECDSA_SHA2_NIST_P521 => NamedEcCurve::Unsupported(oids::secp521r1()),
|
||||
key_type::SK_ECDSA_SHA2_NIST_P256 => NamedEcCurve::Known(EcCurve::NistP256),
|
||||
_ => {
|
||||
return Err(SshPublicKeyError::UnknownKeyType);
|
||||
}
|
||||
};
|
||||
|
||||
// Duplicated information about key type
|
||||
let _identifier = stream.read_ssh_string()?;
|
||||
|
||||
// Public key encoded from an elliptic curve point into an
|
||||
// octet string as per [RFC](https://datatracker.ietf.org/doc/html/rfc5656#section-3.1).
|
||||
let point_data = stream.read_ssh_bytes()?;
|
||||
|
||||
Ok((curve, point_data))
|
||||
}
|
||||
|
||||
impl SshComplexTypeDecode for SshPublicKey {
|
||||
type Error = SshPublicKeyError;
|
||||
|
||||
fn decode(mut stream: impl Read) -> Result<Self, Self::Error> {
|
||||
let mut buffer = Vec::with_capacity(1024);
|
||||
|
||||
read_until_whitespace(&mut stream, &mut buffer)?;
|
||||
|
||||
let header = String::from_utf8_lossy(&buffer).into_owned();
|
||||
buffer.clear();
|
||||
|
||||
let inner_key = match header.as_str() {
|
||||
key_type::RSA
|
||||
| key_type::ECDSA_SHA2_NIST_P256
|
||||
| key_type::ECDSA_SHA2_NIST_P384
|
||||
| key_type::ECDSA_SHA2_NIST_P521
|
||||
| key_type::ED25519
|
||||
| key_type::SK_ECDSA_SHA2_NIST_P256
|
||||
| key_type::SK_ED25519 => {
|
||||
read_until_whitespace(&mut stream, &mut buffer)?;
|
||||
let mut slice = buffer.as_slice();
|
||||
let decoder = Base64Reader::new(&mut slice, &general_purpose::STANDARD);
|
||||
SshComplexTypeDecode::decode(decoder)?
|
||||
}
|
||||
_ => return Err(SshPublicKeyError::UnknownKeyType),
|
||||
};
|
||||
|
||||
buffer.clear();
|
||||
read_until_linebreak(&mut stream, &mut buffer)?;
|
||||
let comment = core::str::from_utf8(&buffer)?.trim_end().to_owned();
|
||||
|
||||
Ok(SshPublicKey { inner_key, comment })
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeDecode for SshBasePrivateKey {
|
||||
type Error = SshPrivateKeyError;
|
||||
|
||||
fn decode(mut stream: impl Read) -> Result<Self, Self::Error> {
|
||||
let key_type = stream.read_ssh_string()?;
|
||||
match key_type.as_str() {
|
||||
key_type::RSA => {
|
||||
let n_constant = stream.read_ssh_mpint()?;
|
||||
let e_constant = stream.read_ssh_mpint()?;
|
||||
let d_constant = stream.read_ssh_mpint()?;
|
||||
let _iqmp = stream.read_ssh_mpint()?;
|
||||
let p_constant = stream.read_ssh_mpint()?;
|
||||
let q_constant = stream.read_ssh_mpint()?;
|
||||
|
||||
Ok(SshBasePrivateKey::Rsa(PrivateKey::from_rsa_components(
|
||||
&n_constant,
|
||||
&e_constant,
|
||||
&d_constant,
|
||||
&[p_constant, q_constant],
|
||||
)?))
|
||||
}
|
||||
key_type::ECDSA_SHA2_NIST_P256 | key_type::ECDSA_SHA2_NIST_P384 | key_type::ECDSA_SHA2_NIST_P521 => {
|
||||
let (curve, point) = decode_ec_public_key_body_impl(key_type.as_str(), &mut stream)?;
|
||||
|
||||
let private_key_secret = stream.read_ssh_mpint()?.to_be_bytes_trimmed_vartime();
|
||||
|
||||
Ok(SshBasePrivateKey::Ec(PrivateKey::from_ec_encoded_components(
|
||||
curve.into(),
|
||||
&private_key_secret,
|
||||
Some(point.as_slice()),
|
||||
)))
|
||||
}
|
||||
key_type::ED25519 => {
|
||||
let (algorithm, public_key) = decode_ed25519_public_key_body_impl(key_type.as_str(), &mut stream)?;
|
||||
|
||||
let private_key_secret = stream.read_ssh_mpint()?.to_be_bytes_trimmed_vartime();
|
||||
|
||||
// OpenSSH is really strange in regards to private ed25519 keys. It stores them as
|
||||
// 64 byte-array, but actually only first 32 bytes are the private key, and the rest
|
||||
// is public key copy
|
||||
if private_key_secret.len() != SSH_COMBO_ED25519_KEY_LENGTH {
|
||||
return Err(SshPrivateKeyError::InvalidKeyFormat);
|
||||
}
|
||||
|
||||
let private_key_secret = &private_key_secret[..ed25519_dalek::SECRET_KEY_LENGTH];
|
||||
|
||||
Ok(SshBasePrivateKey::Ed(PrivateKey::from_ed_encoded_components(
|
||||
algorithm.into(),
|
||||
private_key_secret,
|
||||
Some(&public_key),
|
||||
)))
|
||||
}
|
||||
key_type::SK_ECDSA_SHA2_NIST_P256 => {
|
||||
let (_curve, point) = decode_ec_public_key_body_impl(key_type.as_str(), &mut stream)?;
|
||||
|
||||
let application = stream.read_ssh_string()?;
|
||||
let flags = stream.read_u8()?;
|
||||
let handle = stream.read_ssh_bytes()?;
|
||||
let _reserved = stream.read_ssh_bytes()?;
|
||||
|
||||
Ok(SshBasePrivateKey::SkEcdsaSha2NistP256 {
|
||||
public_key: PublicKey::from_ec_encoded_components(
|
||||
&ObjectIdentifier::from(NamedEcCurve::Known(EcCurve::NistP256)),
|
||||
&point,
|
||||
),
|
||||
application,
|
||||
flags,
|
||||
handle,
|
||||
})
|
||||
}
|
||||
key_type::SK_ED25519 => {
|
||||
let (_algorithm, public_key) = decode_ed25519_public_key_body_impl(key_type.as_str(), &mut stream)?;
|
||||
|
||||
let application = stream.read_ssh_string()?;
|
||||
let flags = stream.read_u8()?;
|
||||
let handle = stream.read_ssh_bytes()?;
|
||||
let _reserved = stream.read_ssh_bytes()?;
|
||||
|
||||
Ok(SshBasePrivateKey::SkEd25519 {
|
||||
public_key: PublicKey::from_ed_encoded_components(
|
||||
&ObjectIdentifier::from(EdAlgorithm::Ed25519),
|
||||
&public_key,
|
||||
),
|
||||
application,
|
||||
flags,
|
||||
handle,
|
||||
})
|
||||
}
|
||||
key_type => Err(SshPrivateKeyError::UnsupportedKeyType(key_type.to_owned())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeDecode for SshCertificate {
|
||||
type Error = SshCertificateError;
|
||||
|
||||
fn decode(mut stream: impl Read) -> Result<Self, Self::Error> {
|
||||
let mut cert_type = Vec::new();
|
||||
read_until_whitespace(&mut stream, &mut cert_type)?;
|
||||
|
||||
let _ = SshCertKeyType::try_from(String::from_utf8(cert_type)?)?;
|
||||
|
||||
let mut cert_data = Vec::new();
|
||||
read_until_whitespace(&mut stream, &mut cert_data)?;
|
||||
|
||||
let mut cert_data = cert_data.as_slice();
|
||||
let mut cert_data = Base64Reader::new(&mut cert_data, &general_purpose::STANDARD);
|
||||
|
||||
let cert_key_type = cert_data.read_ssh_string()?;
|
||||
let cert_key_type = SshCertKeyType::try_from(cert_key_type)?;
|
||||
|
||||
let nonce = cert_data.read_ssh_bytes()?;
|
||||
|
||||
let inner_public_key = match &cert_key_type {
|
||||
SshCertKeyType::SshRsaV01 | SshCertKeyType::RsaSha2_256V01 | SshCertKeyType::RsaSha2_512v01 => {
|
||||
let e = cert_data.read_ssh_mpint()?;
|
||||
let n = cert_data.read_ssh_mpint()?;
|
||||
SshBasePublicKey::Rsa(PublicKey::from_rsa_components(&n, &e))
|
||||
}
|
||||
SshCertKeyType::EcdsaSha2Nistp256V01
|
||||
| SshCertKeyType::EcdsaSha2Nistp384V01
|
||||
| SshCertKeyType::EcdsaSha2Nistp521V01 => {
|
||||
let curve = match cert_key_type {
|
||||
SshCertKeyType::EcdsaSha2Nistp256V01 => NamedEcCurve::Known(EcCurve::NistP256),
|
||||
SshCertKeyType::EcdsaSha2Nistp384V01 => NamedEcCurve::Known(EcCurve::NistP384),
|
||||
SshCertKeyType::EcdsaSha2Nistp521V01 => NamedEcCurve::Known(EcCurve::NistP521),
|
||||
_ => unreachable!("Already validated in match above"),
|
||||
};
|
||||
|
||||
let _curve_identifier = cert_data.read_ssh_string()?;
|
||||
|
||||
let public_key_data = cert_data.read_ssh_bytes()?;
|
||||
SshBasePublicKey::Ec(PublicKey::from_ec_encoded_components(&curve.into(), &public_key_data))
|
||||
}
|
||||
SshCertKeyType::SshEd25519V01 => {
|
||||
let algorithm = NamedEdAlgorithm::Known(EdAlgorithm::Ed25519).into();
|
||||
|
||||
let public_key_data = cert_data.read_ssh_bytes()?;
|
||||
SshBasePublicKey::Ed(PublicKey::from_ed_encoded_components(&algorithm, &public_key_data))
|
||||
}
|
||||
SshCertKeyType::SshDssV01 => {
|
||||
return Err(SshCertificateError::UnsupportedCertificateType(
|
||||
cert_key_type.as_str().to_owned(),
|
||||
));
|
||||
}
|
||||
SshCertKeyType::SkSshSha2Nistp256V01 => {
|
||||
let _curve_identifier = cert_data.read_ssh_string()?;
|
||||
let public_key_data = cert_data.read_ssh_bytes()?;
|
||||
let application = cert_data.read_ssh_string()?;
|
||||
|
||||
SshBasePublicKey::SkEcdsaSha2NistP256 {
|
||||
base_key: PublicKey::from_ec_encoded_components(
|
||||
&NamedEcCurve::Known(EcCurve::NistP256).into(),
|
||||
&public_key_data,
|
||||
),
|
||||
application,
|
||||
}
|
||||
}
|
||||
SshCertKeyType::SkSshEd25519V01 => {
|
||||
let public_key_data = cert_data.read_ssh_bytes()?;
|
||||
let application = cert_data.read_ssh_string()?;
|
||||
|
||||
SshBasePublicKey::SkEd25519 {
|
||||
base_key: PublicKey::from_ed_encoded_components(
|
||||
&NamedEdAlgorithm::Known(EdAlgorithm::Ed25519).into(),
|
||||
&public_key_data,
|
||||
),
|
||||
application,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let serial = cert_data.read_u64::<BigEndian>()?;
|
||||
let cert_type: SshCertType = SshComplexTypeDecode::decode(&mut cert_data)?;
|
||||
|
||||
let key_id = cert_data.read_ssh_string()?;
|
||||
|
||||
let valid_principals: Vec<String> = SshComplexTypeDecode::decode(&mut cert_data)?;
|
||||
|
||||
let valid_after: Timestamp = SshComplexTypeDecode::decode(&mut cert_data)?;
|
||||
let valid_before: Timestamp = SshComplexTypeDecode::decode(&mut cert_data)?;
|
||||
|
||||
let critical_options: Vec<SshCriticalOption> = SshComplexTypeDecode::decode(&mut cert_data)?;
|
||||
|
||||
let extensions: Vec<SshExtension> = SshComplexTypeDecode::decode(&mut cert_data)?;
|
||||
|
||||
let _ = cert_data.read_ssh_bytes()?; // reserved
|
||||
|
||||
// here is public key
|
||||
let signature_key = cert_data.read_ssh_bytes()?;
|
||||
let signature_public_key: SshBasePublicKey = SshComplexTypeDecode::decode(signature_key.as_slice())?;
|
||||
|
||||
let signature = SshSignature::decode(cert_data)?;
|
||||
|
||||
let mut comment = Vec::new();
|
||||
read_until_linebreak(&mut stream, &mut comment)?;
|
||||
let comment = core::str::from_utf8(&comment)?.trim_end().to_owned();
|
||||
|
||||
Ok(SshCertificate {
|
||||
cert_key_type,
|
||||
public_key: SshPublicKey {
|
||||
inner_key: inner_public_key,
|
||||
comment: String::new(),
|
||||
},
|
||||
nonce,
|
||||
serial,
|
||||
cert_type,
|
||||
key_id,
|
||||
valid_principals,
|
||||
valid_after,
|
||||
valid_before,
|
||||
critical_options,
|
||||
extensions,
|
||||
signature_key: SshPublicKey {
|
||||
inner_key: signature_public_key,
|
||||
comment: String::new(),
|
||||
},
|
||||
signature,
|
||||
comment,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::SshReadExt;
|
||||
use std::io::Cursor;
|
||||
|
||||
#[test]
|
||||
fn ssh_string_decode() {
|
||||
let mut cursor = Cursor::new([0, 0, 0, 5, 112, 105, 99, 107, 121].to_vec());
|
||||
|
||||
let ssh_string = cursor.read_ssh_string().unwrap();
|
||||
|
||||
assert_eq!(5, ssh_string.len());
|
||||
assert_eq!("picky".to_owned(), ssh_string);
|
||||
assert_eq!(9, cursor.position());
|
||||
|
||||
let mut cursor = Cursor::new([0, 0, 0, 0].to_vec());
|
||||
|
||||
let ssh_string = cursor.read_ssh_string().unwrap();
|
||||
|
||||
assert_eq!(0, ssh_string.len());
|
||||
assert_eq!("".to_owned(), ssh_string);
|
||||
assert_eq!(4, cursor.position());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn byte_array_decode() {
|
||||
let mut cursor = Cursor::new([0, 0, 0, 5, 1, 2, 3, 4, 5].to_vec());
|
||||
|
||||
let byte_array = cursor.read_ssh_bytes().unwrap();
|
||||
|
||||
assert_eq!(5, byte_array.len());
|
||||
assert_eq!([1, 2, 3, 4, 5].to_vec(), byte_array);
|
||||
assert_eq!(9, cursor.position());
|
||||
|
||||
let mut cursor = Cursor::new([0, 0, 0, 0].to_vec());
|
||||
|
||||
let byte_array = cursor.read_ssh_bytes().unwrap();
|
||||
|
||||
assert_eq!(0, byte_array.len());
|
||||
assert_eq!(Vec::<u8>::new(), byte_array);
|
||||
assert_eq!(4, cursor.position());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mpint_decoding() {
|
||||
let mut cursor = Cursor::new(vec![
|
||||
0x00, 0x00, 0x00, 0x08, 0x09, 0xa3, 0x78, 0xf9, 0xb2, 0xe3, 0x32, 0xa7,
|
||||
]);
|
||||
let mpint = cursor.read_ssh_mpint().unwrap();
|
||||
assert_eq!(
|
||||
mpint.to_be_bytes_trimmed_vartime().as_ref(),
|
||||
&[0x09, 0xa3, 0x78, 0xf9, 0xb2, 0xe3, 0x32, 0xa7]
|
||||
);
|
||||
|
||||
let mut cursor = Cursor::new(vec![0x00, 0x00, 0x00, 0x02, 0x00, 0x80]);
|
||||
let mpint = cursor.read_ssh_mpint().unwrap();
|
||||
assert_eq!(mpint.to_be_bytes_trimmed_vartime().as_ref(), [0x80]);
|
||||
|
||||
let mut cursor = Cursor::new(vec![0x00, 0x00, 0x00, 0x02, 0xed, 0xcc]);
|
||||
let mpint = cursor.read_ssh_mpint().unwrap();
|
||||
assert_eq!(mpint.to_be_bytes_trimmed_vartime().as_ref(), &[0xed, 0xcc]);
|
||||
}
|
||||
}
|
||||
Vendored
+574
@@ -0,0 +1,574 @@
|
||||
use crate::key::ec::{EcdsaKeypair, EcdsaPublicKey};
|
||||
use crate::key::ed::{EdKeypair, EdPublicKey};
|
||||
use crate::ssh::certificate::{
|
||||
SshCertType, SshCertTypeError, SshCertificate, SshCertificateError, SshCriticalOption, SshCriticalOptionError,
|
||||
SshExtension, SshExtensionError, SshSignature, SshSignatureError, Timestamp,
|
||||
};
|
||||
use crate::ssh::private_key::{
|
||||
AES256_CTR, AUTH_MAGIC, Aes256Ctr, BCRYPT, KdfOption, NONE, SshBasePrivateKey, SshPrivateKey, SshPrivateKeyError,
|
||||
};
|
||||
use crate::ssh::public_key::{SshBasePublicKey, SshPublicKey, SshPublicKeyError};
|
||||
use crate::ssh::{Base64Writer, EcCurveSshExt as _, EdAlgorithmSshExt as _, SSH_COMBO_ED25519_KEY_LENGTH, key_type};
|
||||
|
||||
use super::certificate::SshSignatureBlob;
|
||||
use super::key_identifier;
|
||||
use aes::cipher::{KeyIvInit, StreamCipher};
|
||||
use base64::engine::general_purpose;
|
||||
use byteorder::{BigEndian, WriteBytesExt};
|
||||
use crypto_bigint::NonZero;
|
||||
use rsa::traits::{PrivateKeyParts as _, PublicKeyParts as _};
|
||||
use rsa::{BoxedUint, RsaPrivateKey, RsaPublicKey};
|
||||
use std::io::{self, Write};
|
||||
|
||||
pub trait SshWriteExt {
|
||||
type Error;
|
||||
|
||||
fn write_ssh_string(&mut self, data: &str) -> Result<(), Self::Error>;
|
||||
fn write_ssh_bytes(&mut self, data: &[u8]) -> Result<(), Self::Error>;
|
||||
fn write_ssh_mpint(&mut self, data: &BoxedUint) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
impl<T> SshWriteExt for T
|
||||
where
|
||||
T: Write,
|
||||
{
|
||||
type Error = io::Error;
|
||||
|
||||
fn write_ssh_string(&mut self, data: &str) -> Result<(), Self::Error> {
|
||||
self.write_u32::<BigEndian>(data.len() as u32)?;
|
||||
self.write_all(data.as_bytes())
|
||||
}
|
||||
|
||||
fn write_ssh_bytes(&mut self, data: &[u8]) -> Result<(), Self::Error> {
|
||||
self.write_u32::<BigEndian>(data.len() as u32)?;
|
||||
self.write_all(data)
|
||||
}
|
||||
|
||||
fn write_ssh_mpint(&mut self, data: &BoxedUint) -> Result<(), Self::Error> {
|
||||
let data = data.to_be_bytes_trimmed_vartime();
|
||||
let size = data.len() as u32;
|
||||
// If the most significant bit would be set for
|
||||
// a positive number, the number MUST be preceded by a zero byte.
|
||||
if size > 0 && data[0] & 0b10000000 != 0 {
|
||||
self.write_u32::<BigEndian>(size + 1)?;
|
||||
self.write_u8(0)?;
|
||||
} else {
|
||||
self.write_u32::<BigEndian>(size)?;
|
||||
}
|
||||
self.write_all(&data)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait SshComplexTypeEncode {
|
||||
type Error;
|
||||
|
||||
fn encode(&self, stream: impl Write) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
impl SshComplexTypeEncode for SshCertType {
|
||||
type Error = SshCertTypeError;
|
||||
|
||||
fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> {
|
||||
stream.write_u32::<BigEndian>((*self).into())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeEncode for SshCriticalOption {
|
||||
type Error = SshCriticalOptionError;
|
||||
|
||||
fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> {
|
||||
stream.write_ssh_string(self.option_type.as_str())?;
|
||||
stream.write_ssh_string(self.data.as_str())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> SshComplexTypeEncode for Vec<T>
|
||||
where
|
||||
T: SshComplexTypeEncode,
|
||||
T::Error: From<std::io::Error>,
|
||||
{
|
||||
type Error = T::Error;
|
||||
|
||||
fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> {
|
||||
let mut data = Vec::new();
|
||||
for elem in self.iter() {
|
||||
elem.encode(&mut data)?;
|
||||
}
|
||||
stream.write_ssh_bytes(&data)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeEncode for SshExtension {
|
||||
type Error = SshExtensionError;
|
||||
|
||||
fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> {
|
||||
stream.write_ssh_string(self.extension_type.as_str())?;
|
||||
stream.write_ssh_string(self.data.as_str())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeEncode for Vec<String> {
|
||||
type Error = io::Error;
|
||||
|
||||
fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> {
|
||||
let mut data = Vec::new();
|
||||
for s in self.iter() {
|
||||
data.write_ssh_string(s)?;
|
||||
}
|
||||
stream.write_ssh_bytes(&data)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeEncode for SshSignature {
|
||||
type Error = SshSignatureError;
|
||||
|
||||
fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> {
|
||||
let overall_size = self.format.as_str().len() + self.blob.size() + 8;
|
||||
stream.write_u32::<BigEndian>(overall_size as u32)?;
|
||||
stream.write_ssh_string(self.format.as_str())?;
|
||||
|
||||
match &self.blob {
|
||||
SshSignatureBlob::Standard(data) => {
|
||||
stream.write_ssh_bytes(data)?;
|
||||
}
|
||||
SshSignatureBlob::Sk { data, flags, counter } => {
|
||||
stream.write_ssh_bytes(data)?;
|
||||
stream.write_u8(*flags)?;
|
||||
stream.write_u32::<BigEndian>(*counter)?;
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeEncode for KdfOption {
|
||||
type Error = io::Error;
|
||||
|
||||
fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> {
|
||||
if self.salt.is_empty() {
|
||||
stream.write_u32::<BigEndian>(0)?;
|
||||
return Ok(());
|
||||
}
|
||||
let mut data = Vec::new();
|
||||
data.write_ssh_bytes(&self.salt)?;
|
||||
data.write_u32::<BigEndian>(self.rounds)?;
|
||||
stream.write_ssh_bytes(&data)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeEncode for Timestamp {
|
||||
type Error = io::Error;
|
||||
|
||||
fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> {
|
||||
stream.write_u64::<BigEndian>(self.0)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeEncode for SshBasePublicKey {
|
||||
type Error = SshPublicKeyError;
|
||||
|
||||
fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> {
|
||||
match self {
|
||||
SshBasePublicKey::Rsa(rsa) => {
|
||||
let rsa = RsaPublicKey::try_from(rsa)?;
|
||||
stream.write_ssh_string(key_type::RSA)?;
|
||||
stream.write_ssh_mpint(rsa.e())?;
|
||||
stream.write_ssh_mpint(rsa.n())?;
|
||||
Ok(())
|
||||
}
|
||||
SshBasePublicKey::Ec(ec) => {
|
||||
let key = EcdsaPublicKey::try_from(ec)?;
|
||||
encode_ecdsa_public_key_body(&mut stream, &key)?;
|
||||
Ok(())
|
||||
}
|
||||
SshBasePublicKey::Ed(ed) => {
|
||||
let key = EdPublicKey::try_from(ed)?;
|
||||
encode_ed_public_key_body(&mut stream, &key)
|
||||
}
|
||||
SshBasePublicKey::SkEcdsaSha2NistP256 { base_key, application } => {
|
||||
let key = EcdsaPublicKey::try_from(base_key)?;
|
||||
|
||||
stream.write_ssh_string(key_type::SK_ECDSA_SHA2_NIST_P256)?;
|
||||
stream.write_ssh_string(key_identifier::ECDSA_SHA2_NIST_P256)?;
|
||||
stream.write_ssh_bytes(key.encoded_point())?;
|
||||
|
||||
stream.write_ssh_string(application.as_str())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
SshBasePublicKey::SkEd25519 { base_key, application } => {
|
||||
let key = EdPublicKey::try_from(base_key)?;
|
||||
|
||||
stream.write_ssh_string(key_type::SK_ED25519)?;
|
||||
stream.write_ssh_bytes(key.data())?;
|
||||
|
||||
stream.write_ssh_string(application.as_str())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeEncode for SshPublicKey {
|
||||
type Error = SshPublicKeyError;
|
||||
|
||||
fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> {
|
||||
// Write key type
|
||||
match &self.inner_key {
|
||||
SshBasePublicKey::Rsa(_) => {
|
||||
stream.write_all(key_type::RSA.as_bytes())?;
|
||||
}
|
||||
SshBasePublicKey::Ec(key) => {
|
||||
let key = EcdsaPublicKey::try_from(key)?;
|
||||
stream.write_all(key.curve().to_ecdsa_ssh_key_type()?.as_bytes())?;
|
||||
}
|
||||
SshBasePublicKey::Ed(key) => {
|
||||
let key = EdPublicKey::try_from(key)?;
|
||||
stream.write_all(key.algorithm().to_ed_ssh_key_type()?.as_bytes())?;
|
||||
}
|
||||
SshBasePublicKey::SkEcdsaSha2NistP256 { .. } => {
|
||||
stream.write_all(key_type::SK_ECDSA_SHA2_NIST_P256.as_bytes())?;
|
||||
}
|
||||
SshBasePublicKey::SkEd25519 { .. } => {
|
||||
stream.write_all(key_type::SK_ED25519.as_bytes())?;
|
||||
}
|
||||
};
|
||||
|
||||
stream.write_u8(b' ')?;
|
||||
|
||||
{
|
||||
let mut base64_write = Base64Writer::new(&mut stream, &general_purpose::STANDARD);
|
||||
self.inner_key.encode(&mut base64_write)?;
|
||||
base64_write.finish()?;
|
||||
}
|
||||
|
||||
stream.write_u8(b' ')?;
|
||||
stream.write_all(self.comment.as_bytes())?;
|
||||
stream.write_all("\r\n".as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeEncode for SshBasePrivateKey {
|
||||
type Error = SshPrivateKeyError;
|
||||
|
||||
fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> {
|
||||
match self {
|
||||
SshBasePrivateKey::Rsa(rsa) => {
|
||||
let rsa = RsaPrivateKey::try_from(rsa)?;
|
||||
stream.write_ssh_string(key_type::RSA)?;
|
||||
stream.write_ssh_mpint(rsa.n())?;
|
||||
stream.write_ssh_mpint(rsa.e())?;
|
||||
stream.write_ssh_mpint(rsa.d())?;
|
||||
|
||||
let prime = NonZero::new(rsa.primes()[0].clone())
|
||||
.into_option()
|
||||
.ok_or(SshPrivateKeyError::RsaPrimeIsZero)?;
|
||||
let iqmp = rsa.primes()[1]
|
||||
.invert_mod(&prime)
|
||||
.into_option()
|
||||
.ok_or(SshPrivateKeyError::RsaSecondPrimeInvertModFirstPrimeFailed)?;
|
||||
stream.write_ssh_mpint(&iqmp)?;
|
||||
|
||||
for prime in rsa.primes().iter() {
|
||||
stream.write_ssh_mpint(prime)?;
|
||||
}
|
||||
}
|
||||
SshBasePrivateKey::Ec(key) => {
|
||||
let keypair = EcdsaKeypair::try_from(key)?;
|
||||
|
||||
let public_key = EcdsaPublicKey::try_from(&keypair)?;
|
||||
|
||||
// Encode the public key part
|
||||
encode_ecdsa_public_key_body(&mut stream, &public_key)?;
|
||||
|
||||
// Ecnode encoded secret
|
||||
let secret = BoxedUint::from_be_slice_vartime(keypair.secret());
|
||||
stream.write_ssh_mpint(&secret)?;
|
||||
}
|
||||
SshBasePrivateKey::Ed(key) => {
|
||||
let keypair = EdKeypair::try_from(key)?;
|
||||
let public_key = EdPublicKey::try_from(&keypair)?;
|
||||
encode_ed_public_key_body(&mut stream, &public_key)?;
|
||||
|
||||
// SSH Ed25519 key private kye field contains secret in first 32 bytes and the
|
||||
// public key copy in the last 32 bytes.
|
||||
let mut secret = Vec::with_capacity(SSH_COMBO_ED25519_KEY_LENGTH);
|
||||
secret.extend_from_slice(keypair.secret());
|
||||
secret.extend_from_slice(public_key.data());
|
||||
|
||||
stream.write_ssh_bytes(&secret)?;
|
||||
}
|
||||
SshBasePrivateKey::SkEcdsaSha2NistP256 {
|
||||
public_key,
|
||||
application,
|
||||
flags,
|
||||
handle,
|
||||
} => {
|
||||
let ec_key = EcdsaPublicKey::try_from(public_key)?;
|
||||
|
||||
// Encode the public key part
|
||||
stream.write_ssh_string(key_type::SK_ECDSA_SHA2_NIST_P256)?;
|
||||
stream.write_ssh_string(key_identifier::ECDSA_SHA2_NIST_P256)?;
|
||||
stream.write_ssh_bytes(ec_key.encoded_point())?;
|
||||
|
||||
stream.write_ssh_string(application.as_str())?;
|
||||
stream.write_u8(*flags)?;
|
||||
stream.write_ssh_bytes(handle)?;
|
||||
// Reserved
|
||||
stream.write_ssh_bytes(&[])?;
|
||||
}
|
||||
SshBasePrivateKey::SkEd25519 {
|
||||
public_key,
|
||||
application,
|
||||
flags,
|
||||
handle,
|
||||
} => {
|
||||
let ed_key = EdPublicKey::try_from(public_key)?;
|
||||
|
||||
stream.write_ssh_string(key_type::SK_ED25519)?;
|
||||
stream.write_ssh_bytes(ed_key.data())?;
|
||||
|
||||
stream.write_ssh_string(application.as_str())?;
|
||||
stream.write_u8(*flags)?;
|
||||
stream.write_ssh_bytes(handle)?;
|
||||
// Reserved
|
||||
stream.write_ssh_bytes(&[])?;
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_ed_public_key_body(mut stream: impl Write, key: &EdPublicKey<'_>) -> Result<(), SshPublicKeyError> {
|
||||
stream.write_ssh_string(key.algorithm().to_ed_ssh_key_type()?)?;
|
||||
stream.write_ssh_bytes(key.data())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn encode_ecdsa_public_key_body(mut stream: impl Write, key: &EcdsaPublicKey<'_>) -> Result<(), SshPublicKeyError> {
|
||||
stream.write_ssh_string(key.curve().to_ecdsa_ssh_key_type()?)?;
|
||||
stream.write_ssh_string(key.curve().to_ecdsa_ssh_key_identifier()?)?;
|
||||
|
||||
// So called "Q" value from RFC5656. In fact - standard SEC1 encoded public key representation
|
||||
stream.write_ssh_bytes(key.encoded_point())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl SshComplexTypeEncode for SshPrivateKey {
|
||||
type Error = SshPrivateKeyError;
|
||||
|
||||
fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> {
|
||||
const AES256_CTR_BLOCK_SIZE: usize = 16;
|
||||
const UNENCRYPTED_PADDING_SIZE: usize = 8;
|
||||
|
||||
stream.write_all(AUTH_MAGIC.as_bytes())?;
|
||||
stream.write_u8(b'\0')?;
|
||||
|
||||
if self.passphrase.is_some() {
|
||||
stream.write_ssh_string(AES256_CTR)?;
|
||||
stream.write_ssh_string(BCRYPT)?;
|
||||
|
||||
let salt = &self.kdf.option.salt;
|
||||
let rounds = self.kdf.option.rounds;
|
||||
|
||||
let mut kdf_options = Vec::new();
|
||||
kdf_options.write_ssh_bytes(salt)?;
|
||||
kdf_options.write_u32::<BigEndian>(rounds)?;
|
||||
|
||||
stream.write_ssh_bytes(&kdf_options)?;
|
||||
} else {
|
||||
stream.write_ssh_string(NONE)?;
|
||||
stream.write_ssh_string(NONE)?;
|
||||
stream.write_ssh_string("")?;
|
||||
}
|
||||
|
||||
stream.write_u32::<BigEndian>(1)?; // keys amount
|
||||
|
||||
let mut public_key = Vec::new();
|
||||
self.public_key().inner_key.encode(&mut public_key)?;
|
||||
stream.write_ssh_bytes(&public_key)?;
|
||||
|
||||
public_key.clear();
|
||||
let mut private_key = public_key;
|
||||
|
||||
private_key.write_u32::<BigEndian>(self.check)?;
|
||||
private_key.write_u32::<BigEndian>(self.check)?;
|
||||
self.base_key.encode(&mut private_key)?;
|
||||
|
||||
private_key.write_ssh_string(&self.comment)?;
|
||||
|
||||
let padding_size = if self.passphrase.is_some() {
|
||||
AES256_CTR_BLOCK_SIZE
|
||||
} else {
|
||||
UNENCRYPTED_PADDING_SIZE
|
||||
};
|
||||
|
||||
// add padding
|
||||
for i in 1..=(padding_size - (private_key.len() % padding_size)) {
|
||||
private_key.push(i as u8);
|
||||
}
|
||||
|
||||
if let Some(passphrase) = &self.passphrase {
|
||||
// encrypt private_key
|
||||
let n = 48;
|
||||
let mut hash = [0; 48];
|
||||
|
||||
let salt = &self.kdf.option.salt;
|
||||
let rounds = self.kdf.option.rounds;
|
||||
|
||||
bcrypt_pbkdf::bcrypt_pbkdf(passphrase, salt, rounds, &mut hash)?;
|
||||
|
||||
let (key, iv) = hash.split_at(n - 16);
|
||||
let mut cipher = Aes256Ctr::new_from_slices(key, iv).unwrap();
|
||||
|
||||
let private_key_len = private_key.len();
|
||||
private_key.resize(private_key_len + 32, 0u8);
|
||||
cipher.apply_keystream(&mut private_key);
|
||||
private_key.truncate(private_key_len);
|
||||
}
|
||||
|
||||
stream.write_ssh_bytes(&private_key)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SshComplexTypeEncode for SshCertificate {
|
||||
type Error = SshCertificateError;
|
||||
|
||||
fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> {
|
||||
stream.write_all(self.cert_key_type.as_str().as_bytes())?;
|
||||
stream.write_u8(b' ')?;
|
||||
|
||||
let mut cert_data = Base64Writer::new(stream, &general_purpose::STANDARD);
|
||||
|
||||
cert_data.write_ssh_string(self.cert_key_type.as_str())?;
|
||||
cert_data.write_ssh_bytes(&self.nonce)?;
|
||||
match &self.public_key.inner_key {
|
||||
SshBasePublicKey::Rsa(rsa) => {
|
||||
let rsa = RsaPublicKey::try_from(rsa)?;
|
||||
cert_data.write_ssh_mpint(rsa.e())?;
|
||||
cert_data.write_ssh_mpint(rsa.n())?;
|
||||
}
|
||||
SshBasePublicKey::Ec(ec) => {
|
||||
let ec = EcdsaPublicKey::try_from(ec)?;
|
||||
cert_data.write_ssh_string(ec.curve().to_ecdsa_ssh_key_identifier()?)?;
|
||||
cert_data.write_ssh_bytes(ec.encoded_point())?;
|
||||
}
|
||||
SshBasePublicKey::Ed(ed) => {
|
||||
let ed = EdPublicKey::try_from(ed)?;
|
||||
cert_data.write_ssh_bytes(ed.data())?;
|
||||
}
|
||||
SshBasePublicKey::SkEcdsaSha2NistP256 { base_key, application } => {
|
||||
let ec = EcdsaPublicKey::try_from(base_key)?;
|
||||
cert_data.write_ssh_string(key_identifier::ECDSA_SHA2_NIST_P256)?;
|
||||
cert_data.write_ssh_bytes(ec.encoded_point())?;
|
||||
cert_data.write_ssh_string(application.as_str())?;
|
||||
}
|
||||
SshBasePublicKey::SkEd25519 { base_key, application } => {
|
||||
let ed = EdPublicKey::try_from(base_key)?;
|
||||
cert_data.write_ssh_bytes(ed.data())?;
|
||||
cert_data.write_ssh_string(application.as_str())?;
|
||||
}
|
||||
};
|
||||
|
||||
cert_data.write_u64::<BigEndian>(self.serial)?;
|
||||
|
||||
self.cert_type.encode(&mut cert_data)?;
|
||||
|
||||
cert_data.write_ssh_string(self.key_id.as_str())?;
|
||||
|
||||
self.valid_principals.encode(&mut cert_data)?;
|
||||
self.valid_after.encode(&mut cert_data)?;
|
||||
self.valid_before.encode(&mut cert_data)?;
|
||||
self.critical_options.encode(&mut cert_data)?;
|
||||
self.extensions.encode(&mut cert_data)?;
|
||||
|
||||
cert_data.write_ssh_bytes(&[])?; // reserved
|
||||
|
||||
let mut rsa_key = Vec::new();
|
||||
self.signature_key.inner_key.encode(&mut rsa_key)?;
|
||||
|
||||
cert_data.write_ssh_bytes(&rsa_key)?;
|
||||
self.signature.encode(&mut cert_data)?;
|
||||
|
||||
// stream.write_all(cert_data.finish()?.as_slice())?;
|
||||
let mut stream = cert_data.finish().unwrap();
|
||||
stream.write_u8(b' ')?;
|
||||
|
||||
stream.write_all(self.comment.as_bytes())?;
|
||||
stream.write_all("\r\n".as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::SshWriteExt;
|
||||
use rsa::BoxedUint;
|
||||
|
||||
#[test]
|
||||
fn ssh_string_encode() {
|
||||
let mut res = Vec::new();
|
||||
let ssh_string = "picky";
|
||||
|
||||
res.write_ssh_string(ssh_string).unwrap();
|
||||
|
||||
assert_eq!(vec![0, 0, 0, 5, 112, 105, 99, 107, 121], res);
|
||||
|
||||
res.clear();
|
||||
let ssh_string = "";
|
||||
|
||||
res.write_ssh_string(ssh_string).unwrap();
|
||||
|
||||
assert_eq!(vec![0, 0, 0, 0], res);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn byte_array_encode() {
|
||||
let mut res = Vec::new();
|
||||
let byte_array = [1, 2, 3, 4, 5, 6];
|
||||
|
||||
res.write_ssh_bytes(&byte_array).unwrap();
|
||||
|
||||
assert_eq!(vec![0, 0, 0, 6, 1, 2, 3, 4, 5, 6], res);
|
||||
|
||||
res.clear();
|
||||
let byte_array = [];
|
||||
|
||||
res.write_ssh_bytes(&byte_array).unwrap();
|
||||
|
||||
assert_eq!(vec![0, 0, 0, 0], res);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mpint_encoding() {
|
||||
let mpint = BoxedUint::from_be_slice_vartime(&[0x09, 0xa3, 0x78, 0xf9, 0xb2, 0xe3, 0x32, 0xa7]);
|
||||
let mut res = Vec::new();
|
||||
res.write_ssh_mpint(&mpint).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![0x00, 0x00, 0x00, 0x08, 0x09, 0xa3, 0x78, 0xf9, 0xb2, 0xe3, 0x32, 0xa7],
|
||||
);
|
||||
|
||||
let mpint = BoxedUint::from_be_slice_vartime(&[0x80]);
|
||||
let mut res = Vec::new();
|
||||
res.write_ssh_mpint(&mpint).unwrap();
|
||||
|
||||
assert_eq!(res, vec![0x00, 0x00, 0x00, 0x02, 0x00, 0x80]);
|
||||
}
|
||||
}
|
||||
Vendored
+115
@@ -0,0 +1,115 @@
|
||||
pub mod certificate;
|
||||
pub mod decode;
|
||||
pub mod encode;
|
||||
pub mod private_key;
|
||||
pub mod public_key;
|
||||
|
||||
use crate::key::ec::NamedEcCurve;
|
||||
use crate::key::ed::NamedEdAlgorithm;
|
||||
use crate::key::{EcCurve, EdAlgorithm, KeyError};
|
||||
|
||||
use byteorder::ReadBytesExt;
|
||||
use std::io::{self, Read};
|
||||
|
||||
pub use certificate::{SshCertKeyType, SshCertType, SshCertificate, SshCertificateBuilder};
|
||||
pub use private_key::SshPrivateKey;
|
||||
pub use public_key::SshPublicKey;
|
||||
|
||||
pub(crate) type Base64Writer<'a, T, E> = base64::write::EncoderWriter<'a, T, E>;
|
||||
pub(crate) type Base64Reader<'a, T, E> = base64::read::DecoderReader<'a, T, E>;
|
||||
|
||||
const SSH_COMBO_ED25519_KEY_LENGTH: usize = ed25519_dalek::SECRET_KEY_LENGTH + ed25519_dalek::PUBLIC_KEY_LENGTH;
|
||||
|
||||
mod key_type {
|
||||
pub const RSA: &str = "ssh-rsa";
|
||||
pub const ECDSA_SHA2_NIST_P256: &str = "ecdsa-sha2-nistp256";
|
||||
pub const ECDSA_SHA2_NIST_P384: &str = "ecdsa-sha2-nistp384";
|
||||
pub const ECDSA_SHA2_NIST_P521: &str = "ecdsa-sha2-nistp521";
|
||||
pub const ED25519: &str = "ssh-ed25519";
|
||||
pub const SK_ECDSA_SHA2_NIST_P256: &str = "sk-ecdsa-sha2-nistp256@openssh.com";
|
||||
pub const SK_ED25519: &str = "sk-ssh-ed25519@openssh.com";
|
||||
}
|
||||
|
||||
mod key_identifier {
|
||||
pub const ECDSA_SHA2_NIST_P256: &str = "nistp256";
|
||||
pub const ECDSA_SHA2_NIST_P384: &str = "nistp384";
|
||||
pub const ECDSA_SHA2_NIST_P521: &str = "nistp521";
|
||||
}
|
||||
|
||||
trait EcCurveSshExt {
|
||||
fn to_ecdsa_ssh_key_type(&self) -> Result<&'static str, KeyError>;
|
||||
fn to_ecdsa_ssh_key_identifier(&self) -> Result<&'static str, KeyError>;
|
||||
}
|
||||
|
||||
impl EcCurveSshExt for NamedEcCurve {
|
||||
fn to_ecdsa_ssh_key_type(&self) -> Result<&'static str, KeyError> {
|
||||
match self {
|
||||
NamedEcCurve::Known(EcCurve::NistP256) => Ok(key_type::ECDSA_SHA2_NIST_P256),
|
||||
NamedEcCurve::Known(EcCurve::NistP384) => Ok(key_type::ECDSA_SHA2_NIST_P384),
|
||||
NamedEcCurve::Known(EcCurve::NistP521) => Ok(key_type::ECDSA_SHA2_NIST_P521),
|
||||
NamedEcCurve::Unsupported(oid) => Err(KeyError::unsupported_curve(oid, "ssh key type serialization")),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_ecdsa_ssh_key_identifier(&self) -> Result<&'static str, KeyError> {
|
||||
match self {
|
||||
NamedEcCurve::Known(EcCurve::NistP256) => Ok(key_identifier::ECDSA_SHA2_NIST_P256),
|
||||
NamedEcCurve::Known(EcCurve::NistP384) => Ok(key_identifier::ECDSA_SHA2_NIST_P384),
|
||||
NamedEcCurve::Known(EcCurve::NistP521) => Ok(key_identifier::ECDSA_SHA2_NIST_P521),
|
||||
NamedEcCurve::Unsupported(oid) => Err(KeyError::unsupported_curve(oid, "ssh key identifier serialization")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait EdAlgorithmSshExt {
|
||||
fn to_ed_ssh_key_type(&self) -> Result<&'static str, KeyError>;
|
||||
}
|
||||
|
||||
impl EdAlgorithmSshExt for NamedEdAlgorithm {
|
||||
fn to_ed_ssh_key_type(&self) -> Result<&'static str, KeyError> {
|
||||
match self {
|
||||
NamedEdAlgorithm::Known(EdAlgorithm::Ed25519) => Ok(key_type::ED25519),
|
||||
NamedEdAlgorithm::Known(EdAlgorithm::X25519) => Err(KeyError::UnsupportedAlgorithm {
|
||||
algorithm: "X25519 can't be use for SSH EdDSA keys",
|
||||
}),
|
||||
NamedEdAlgorithm::Unsupported(oid) => {
|
||||
Err(KeyError::unsupported_ed_algorithm(oid, "ssh key type serialization"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_until_whitespace(stream: &mut dyn Read, buffer: &mut Vec<u8>) -> io::Result<()> {
|
||||
loop {
|
||||
match stream.read_u8() {
|
||||
Ok(symbol) => {
|
||||
if symbol as char == ' ' {
|
||||
break;
|
||||
} else {
|
||||
buffer.push(symbol);
|
||||
}
|
||||
}
|
||||
Err(ref e) if e.kind() == io::ErrorKind::UnexpectedEof => {
|
||||
break;
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_until_linebreak(stream: &mut dyn Read, buffer: &mut Vec<u8>) -> io::Result<()> {
|
||||
loop {
|
||||
match stream.read_u8() {
|
||||
Ok(b'\r') | Ok(b'\n') => break,
|
||||
Ok(c) => buffer.push(c),
|
||||
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {
|
||||
break;
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+780
@@ -0,0 +1,780 @@
|
||||
use crate::key::{EcCurve, EdAlgorithm, KeyError, PrivateKey, PrivateKeyKind, PublicKey};
|
||||
use crate::pem::{Pem, PemError, parse_pem};
|
||||
use crate::ssh::decode::{SshComplexTypeDecode, SshReadExt};
|
||||
use crate::ssh::encode::SshComplexTypeEncode;
|
||||
use crate::ssh::public_key::{SshBasePublicKey, SshPublicKey, SshPublicKeyError};
|
||||
|
||||
use aes::cipher::block_padding::NoPadding;
|
||||
use aes::cipher::{KeyIvInit, StreamCipher};
|
||||
use byteorder::{BigEndian, ReadBytesExt};
|
||||
use cbc::cipher::BlockModeDecrypt;
|
||||
use rand::RngExt;
|
||||
use std::io::{Cursor, Read};
|
||||
use std::string;
|
||||
use thiserror::Error;
|
||||
|
||||
pub type Aes128CbcDec = cbc::Decryptor<aes::Aes128>;
|
||||
pub type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;
|
||||
pub type Aes128Ctr = ctr::Ctr32BE<aes::Aes128>;
|
||||
pub type Aes256Ctr = ctr::Ctr32BE<aes::Aes256>;
|
||||
|
||||
const SSH_PRIVATE_KEY_LABEL: &str = "OPENSSH PRIVATE KEY";
|
||||
pub(crate) const AUTH_MAGIC: &str = "openssh-key-v1";
|
||||
|
||||
const AES128_CTR: &str = "aes128-ctr";
|
||||
pub(crate) const AES256_CTR: &str = "aes256-ctr";
|
||||
|
||||
const AES128_CBC: &str = "aes128-cbc";
|
||||
const AES256_CBC: &str = "aes256-cbc";
|
||||
|
||||
pub(crate) const BCRYPT: &str = "bcrypt";
|
||||
pub(crate) const NONE: &str = "none";
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SshPrivateKeyError {
|
||||
#[error(transparent)]
|
||||
FromUtf8Error(#[from] string::FromUtf8Error),
|
||||
#[error(transparent)]
|
||||
Base64DecodeError(#[from] base64::DecodeError),
|
||||
#[error(transparent)]
|
||||
IoError(#[from] std::io::Error),
|
||||
#[error("Unsupported key type: {0}")]
|
||||
UnsupportedKeyType(String),
|
||||
#[error("Unsupported cipher: {0}")]
|
||||
UnsupportedCipher(String),
|
||||
#[error("Unsupported kdf: {0}")]
|
||||
UnsupportedKdf(String),
|
||||
#[error("Invalid auth magic header")]
|
||||
InvalidAuthMagicHeader,
|
||||
#[error("Invalid keys amount. Expected 1 but got {0}")]
|
||||
InvalidKeysAmount(u32),
|
||||
#[error("Check numbers are not equal: {0} {1}. Wrong passphrase or key is corrupted")]
|
||||
InvalidCheckNumbers(u32, u32),
|
||||
#[error("Invalid public key: {0:?}")]
|
||||
InvalidPublicKey(#[from] SshPublicKeyError),
|
||||
#[error("Invalid key format")]
|
||||
InvalidKeyFormat,
|
||||
#[error("Can not decrypt private key: {0}")]
|
||||
DecryptionError(String),
|
||||
#[error("Can not hash the passphrase: {0:?}")]
|
||||
HashingError(#[from] bcrypt_pbkdf::Error),
|
||||
#[error("Passphrase required for encrypted private key")]
|
||||
MissingPassphrase,
|
||||
#[error(transparent)]
|
||||
KeyError(#[from] KeyError),
|
||||
#[error(transparent)]
|
||||
PemError(#[from] PemError),
|
||||
#[error("RSA prime is zero")]
|
||||
RsaPrimeIsZero,
|
||||
#[error("RSA second prime invert mod first prime failed")]
|
||||
RsaSecondPrimeInvertModFirstPrimeFailed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Default)]
|
||||
pub struct KdfOption {
|
||||
pub salt: Vec<u8>,
|
||||
pub rounds: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub struct Kdf {
|
||||
pub name: String,
|
||||
pub option: KdfOption,
|
||||
}
|
||||
|
||||
impl Default for Kdf {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: NONE.to_owned(),
|
||||
option: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SshBasePrivateKey {
|
||||
Rsa(PrivateKey),
|
||||
Ec(PrivateKey),
|
||||
Ed(PrivateKey),
|
||||
/// U2F ecdsa SSH key. Note that this key does not contain the private key data, only handle
|
||||
/// is stored which could be used to sign data via hardware U2F key.
|
||||
SkEcdsaSha2NistP256 {
|
||||
public_key: PublicKey,
|
||||
application: String,
|
||||
flags: u8,
|
||||
handle: Vec<u8>,
|
||||
},
|
||||
/// U2F ed25519 SSH key. Note that this key does not contain the private key data, only handle
|
||||
/// is stored which could be used to sign data via hardware U2F key.
|
||||
SkEd25519 {
|
||||
public_key: PublicKey,
|
||||
application: String,
|
||||
flags: u8,
|
||||
handle: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
impl SshBasePrivateKey {
|
||||
pub fn base_public_key(&self) -> Result<SshBasePublicKey, SshPrivateKeyError> {
|
||||
let key = match self {
|
||||
SshBasePrivateKey::Rsa(rsa) => SshBasePublicKey::Rsa(rsa.to_public_key()?),
|
||||
SshBasePrivateKey::Ec(ec) => SshBasePublicKey::Ec(ec.to_public_key()?),
|
||||
SshBasePrivateKey::Ed(ed) => SshBasePublicKey::Ed(ed.to_public_key()?),
|
||||
SshBasePrivateKey::SkEcdsaSha2NistP256 {
|
||||
public_key,
|
||||
application,
|
||||
..
|
||||
} => SshBasePublicKey::SkEcdsaSha2NistP256 {
|
||||
base_key: public_key.clone(),
|
||||
application: application.clone(),
|
||||
},
|
||||
SshBasePrivateKey::SkEd25519 {
|
||||
public_key,
|
||||
application,
|
||||
..
|
||||
} => SshBasePublicKey::SkEd25519 {
|
||||
base_key: public_key.clone(),
|
||||
application: application.clone(),
|
||||
},
|
||||
};
|
||||
|
||||
Ok(key)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SshPrivateKey {
|
||||
pub cipher_name: String,
|
||||
pub kdf: Kdf,
|
||||
pub base_key: SshBasePrivateKey,
|
||||
pub public_key: SshPublicKey,
|
||||
pub check: u32,
|
||||
pub comment: String,
|
||||
pub passphrase: Option<String>,
|
||||
}
|
||||
|
||||
impl SshPrivateKey {
|
||||
pub fn generate_rsa(
|
||||
bits: usize,
|
||||
passphrase: Option<String>,
|
||||
comment: Option<String>,
|
||||
) -> Result<Self, SshPrivateKeyError> {
|
||||
SshPrivateKey::h_picky_private_key_to_ssh_private_key(PrivateKey::generate_rsa(bits)?, passphrase, comment)
|
||||
}
|
||||
|
||||
pub fn generate_ec(
|
||||
curve: EcCurve,
|
||||
passphrase: Option<String>,
|
||||
comment: Option<String>,
|
||||
) -> Result<Self, SshPrivateKeyError> {
|
||||
SshPrivateKey::h_picky_private_key_to_ssh_private_key(PrivateKey::generate_ec(curve)?, passphrase, comment)
|
||||
}
|
||||
|
||||
pub fn generate_ed25519(passphrase: Option<String>, comment: Option<String>) -> Result<Self, SshPrivateKeyError> {
|
||||
SshPrivateKey::h_picky_private_key_to_ssh_private_key(
|
||||
PrivateKey::generate_ed(EdAlgorithm::Ed25519, true)?,
|
||||
passphrase,
|
||||
comment,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn from_pem(pem: &Pem, passphrase: Option<String>) -> Result<Self, SshPrivateKeyError> {
|
||||
SshPrivateKey::decode(&mut pem.data(), passphrase)
|
||||
}
|
||||
|
||||
pub fn from_pem_str(pem: &str, passphrase: Option<String>) -> Result<Self, SshPrivateKeyError> {
|
||||
let pem = parse_pem(pem)?;
|
||||
SshPrivateKey::decode(&mut pem.data(), passphrase)
|
||||
}
|
||||
|
||||
pub fn to_pem(&self) -> Result<Pem<'static>, SshPrivateKeyError> {
|
||||
let mut buffer = Vec::with_capacity(2048);
|
||||
self.encode(&mut buffer)?;
|
||||
Ok(Pem::new(SSH_PRIVATE_KEY_LABEL, buffer))
|
||||
}
|
||||
|
||||
pub fn to_string(&self) -> Result<String, SshPrivateKeyError> {
|
||||
let mut buffer = Vec::with_capacity(2048);
|
||||
self.encode(&mut buffer)?;
|
||||
let mut result = Pem::new(SSH_PRIVATE_KEY_LABEL, buffer).to_string();
|
||||
// ssh private key must contain \x0A (\n) character at the end
|
||||
result.push('\x0A');
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn public_key(&self) -> &SshPublicKey {
|
||||
&self.public_key
|
||||
}
|
||||
|
||||
pub fn base_key(&self) -> &SshBasePrivateKey {
|
||||
&self.base_key
|
||||
}
|
||||
|
||||
pub fn inner_key(&self) -> Option<&PrivateKey> {
|
||||
match self.base_key() {
|
||||
SshBasePrivateKey::Rsa(key) => Some(key),
|
||||
SshBasePrivateKey::Ec(key) => Some(key),
|
||||
SshBasePrivateKey::Ed(key) => Some(key),
|
||||
SshBasePrivateKey::SkEcdsaSha2NistP256 { .. } | SshBasePrivateKey::SkEd25519 { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn h_picky_private_key_to_ssh_private_key(
|
||||
private_key: PrivateKey,
|
||||
passphrase: Option<String>,
|
||||
comment: Option<String>,
|
||||
) -> Result<SshPrivateKey, SshPrivateKeyError> {
|
||||
let (kdf, cipher_name) = match &passphrase {
|
||||
Some(_) => {
|
||||
let mut salt: Vec<u8> = Vec::new();
|
||||
let rounds = 16;
|
||||
let mut rnd = rand::rng();
|
||||
for _ in 0..rounds {
|
||||
salt.push(rnd.random::<u8>());
|
||||
}
|
||||
|
||||
let kdf = Kdf {
|
||||
name: BCRYPT.to_owned(),
|
||||
option: KdfOption { salt, rounds },
|
||||
};
|
||||
|
||||
(kdf, String::new())
|
||||
}
|
||||
None => (Kdf::default(), NONE.to_owned()),
|
||||
};
|
||||
|
||||
let public_key = private_key.to_public_key()?;
|
||||
|
||||
let (public, private) = match private_key.as_kind() {
|
||||
PrivateKeyKind::Rsa => {
|
||||
let private = SshBasePrivateKey::Rsa(private_key);
|
||||
let public = SshBasePublicKey::Rsa(public_key);
|
||||
(public, private)
|
||||
}
|
||||
PrivateKeyKind::Ec { .. } => {
|
||||
let private = SshBasePrivateKey::Ec(private_key);
|
||||
let public = SshBasePublicKey::Ec(public_key);
|
||||
(public, private)
|
||||
}
|
||||
PrivateKeyKind::Ed { .. } => {
|
||||
let private = SshBasePrivateKey::Ed(private_key);
|
||||
let public = SshBasePublicKey::Ed(public_key);
|
||||
(public, private)
|
||||
}
|
||||
};
|
||||
|
||||
let public_key = SshPublicKey {
|
||||
inner_key: public,
|
||||
comment: String::new(),
|
||||
};
|
||||
|
||||
Ok(SshPrivateKey {
|
||||
cipher_name,
|
||||
kdf,
|
||||
base_key: private,
|
||||
public_key,
|
||||
check: 0,
|
||||
comment: comment.unwrap_or_default(),
|
||||
passphrase,
|
||||
})
|
||||
}
|
||||
|
||||
fn decode(mut stream: impl Read, passphrase: Option<String>) -> Result<Self, SshPrivateKeyError>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let mut auth_magic = [0; AUTH_MAGIC.len()];
|
||||
stream.read_exact(&mut auth_magic)?;
|
||||
if auth_magic != AUTH_MAGIC.as_bytes() {
|
||||
return Err(SshPrivateKeyError::InvalidAuthMagicHeader);
|
||||
}
|
||||
stream.read_u8()?; // skip 1 byte (null-byte)
|
||||
|
||||
let cipher_name = stream.read_ssh_string()?;
|
||||
let kdf_name = stream.read_ssh_string()?;
|
||||
let kdf_option: KdfOption = SshComplexTypeDecode::decode(&mut stream)?;
|
||||
let keys_amount = stream.read_u32::<BigEndian>()?;
|
||||
|
||||
if keys_amount != 1 {
|
||||
return Err(SshPrivateKeyError::InvalidKeysAmount(keys_amount));
|
||||
}
|
||||
|
||||
// read public key
|
||||
let _ = stream.read_ssh_bytes()?;
|
||||
|
||||
// read private key
|
||||
let private_key = stream.read_ssh_bytes()?;
|
||||
|
||||
let data = decrypt(&cipher_name, &kdf_name, &kdf_option, passphrase.as_deref(), private_key)?;
|
||||
|
||||
let mut cursor = Cursor::new(data);
|
||||
|
||||
let check0 = cursor.read_u32::<BigEndian>()?;
|
||||
let check1 = cursor.read_u32::<BigEndian>()?;
|
||||
if check0 != check1 {
|
||||
return Err(SshPrivateKeyError::InvalidCheckNumbers(check0, check1));
|
||||
}
|
||||
|
||||
let base_key: SshBasePrivateKey = SshComplexTypeDecode::decode(&mut cursor)?;
|
||||
let base_public_key = base_key.base_public_key()?;
|
||||
|
||||
let comment = cursor.read_ssh_string()?.trim_end().to_owned();
|
||||
|
||||
Ok(SshPrivateKey {
|
||||
base_key,
|
||||
public_key: SshPublicKey {
|
||||
inner_key: base_public_key,
|
||||
comment: String::new(),
|
||||
},
|
||||
passphrase,
|
||||
kdf: Kdf {
|
||||
name: kdf_name,
|
||||
option: kdf_option,
|
||||
},
|
||||
cipher_name,
|
||||
check: check0,
|
||||
comment,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<PrivateKey> for SshPrivateKey {
|
||||
type Error = SshPrivateKeyError;
|
||||
|
||||
fn try_from(private_key: PrivateKey) -> Result<Self, Self::Error> {
|
||||
SshPrivateKey::h_picky_private_key_to_ssh_private_key(private_key, None, None)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn decrypt(
|
||||
cipher_name: &str,
|
||||
kdf_name: &str,
|
||||
kdf_options: &KdfOption,
|
||||
passphrase: Option<&str>,
|
||||
mut data: Vec<u8>,
|
||||
) -> Result<Vec<u8>, SshPrivateKeyError> {
|
||||
if kdf_name == NONE {
|
||||
Ok(data)
|
||||
} else {
|
||||
let n = match cipher_name {
|
||||
AES128_CBC | AES128_CTR => 32,
|
||||
AES256_CBC | AES256_CTR => 48,
|
||||
name => return Err(SshPrivateKeyError::UnsupportedCipher(name.to_owned())),
|
||||
};
|
||||
|
||||
let mut key = [0; 48];
|
||||
match kdf_name {
|
||||
BCRYPT => {
|
||||
let salt = &kdf_options.salt;
|
||||
let rounds = kdf_options.rounds;
|
||||
let passphrase = passphrase.ok_or(SshPrivateKeyError::MissingPassphrase)?;
|
||||
|
||||
bcrypt_pbkdf::bcrypt_pbkdf(passphrase, salt, rounds, &mut key[..n])?;
|
||||
}
|
||||
name => return Err(SshPrivateKeyError::UnsupportedKdf(name.to_owned())),
|
||||
};
|
||||
|
||||
let (key, iv) = key.split_at(n - 16);
|
||||
|
||||
let start_len = data.len();
|
||||
data.resize(data.len() + 32, 0u8);
|
||||
match cipher_name {
|
||||
AES128_CBC => {
|
||||
let cipher = Aes128CbcDec::new_from_slices(key, iv).unwrap();
|
||||
let n = cipher
|
||||
.decrypt_padded_inout::<NoPadding>(data.as_mut_slice().into())
|
||||
.map_err(|e| SshPrivateKeyError::DecryptionError(e.to_string()))?
|
||||
.len();
|
||||
data.truncate(n);
|
||||
Ok(data)
|
||||
}
|
||||
AES256_CBC => {
|
||||
let cipher = Aes256CbcDec::new_from_slices(key, iv).unwrap();
|
||||
let n = cipher
|
||||
.decrypt_padded_inout::<NoPadding>(data.as_mut_slice().into())
|
||||
.map_err(|e| SshPrivateKeyError::DecryptionError(e.to_string()))?
|
||||
.len();
|
||||
data.truncate(n);
|
||||
Ok(data)
|
||||
}
|
||||
AES128_CTR => {
|
||||
let mut cipher = Aes128Ctr::new_from_slices(key, iv).unwrap();
|
||||
cipher.apply_keystream(&mut data);
|
||||
data.truncate(start_len);
|
||||
Ok(data)
|
||||
}
|
||||
AES256_CTR => {
|
||||
let mut cipher = Aes256Ctr::new_from_slices(key, iv).unwrap();
|
||||
cipher.apply_keystream(&mut data);
|
||||
data.truncate(start_len);
|
||||
Ok(data)
|
||||
}
|
||||
name => Err(SshPrivateKeyError::UnsupportedCipher(name.to_owned())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
use crate::key::ec::EcdsaKeypair;
|
||||
use crate::key::ed::EdKeypair;
|
||||
use crate::ssh::private_key::SshPrivateKey;
|
||||
use rsa::RsaPrivateKey;
|
||||
use rstest::rstest;
|
||||
|
||||
#[test]
|
||||
fn decode_without_passphrase_2048() {
|
||||
// ssh-keygen -t rsa -b 2048 -C "test2@picky.com" (without the passphrase)
|
||||
let ssh_private_key_pem = "-----BEGIN OPENSSH PRIVATE KEY-----\n\
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdz\n\
|
||||
c2gtcnNhAAAAAwEAAQAAAQEAyPYbdoNqjj4EhuYblWIxVKLsmsOff+kLkKlFRsIJ\n\
|
||||
yE5YUWzPm5LyUH3LoqnL/rw/f/Og37oJO/bEn4P2lSvlf6ZagAGaLo8/8ACw4xKY\n\
|
||||
UsQFHAEfreIthd/T2u9TEnN+yPS99M99bXG2tV+6He4c61TJfYrq5DsgQuMXCFmt\n\
|
||||
R/IdJg8qF8lj06qEzjQ1HvXQdXruhm4sQn1HMb3VbdKQFSU3TpmzVysEaOVl3zK7\n\
|
||||
KirBU9gHIOFZuE3y0oUklFuK6jOhjgQnxeo58Rb00g3p7R+YcpI1i95TAoIQ/tYS\n\
|
||||
cjnZzByQv+ak1BjgfOjMbEeEQl6kvi2axqTEnFcg0IHu6wAAA8iqDGUDqgxlAwAA\n\
|
||||
AAdzc2gtcnNhAAABAQDI9ht2g2qOPgSG5huVYjFUouyaw59/6QuQqUVGwgnITlhR\n\
|
||||
bM+bkvJQfcuiqcv+vD9/86Dfugk79sSfg/aVK+V/plqAAZoujz/wALDjEphSxAUc\n\
|
||||
AR+t4i2F39Pa71MSc37I9L30z31tcba1X7od7hzrVMl9iurkOyBC4xcIWa1H8h0m\n\
|
||||
DyoXyWPTqoTONDUe9dB1eu6GbixCfUcxvdVt0pAVJTdOmbNXKwRo5WXfMrsqKsFT\n\
|
||||
2Acg4Vm4TfLShSSUW4rqM6GOBCfF6jnxFvTSDentH5hykjWL3lMCghD+1hJyOdnM\n\
|
||||
HJC/5qTUGOB86MxsR4RCXqS+LZrGpMScVyDQge7rAAAAAwEAAQAAAQATZEw6H2xE\n\
|
||||
1Y8yRTocLCF+fUo/lOjrOt22096veUHgZk73bHyMEp33Tmw8Ag6BQkEOY7/+VsFV\n\
|
||||
W/aVPfKpalb2/mJ1P7JVE9Wjny1ye/Te57NmhGU+LjkeVf7nfXiSqzpswdEisnL0\n\
|
||||
AKkUz2vyP2vi+YeH6cPIyjvOuIMcdyrVakejnGbss19ZoXw660X/7TRqG/41KhTm\n\
|
||||
lkN610JBKI2Rozecx9l3LZ3CTRpOOJ2sfssegvL+qxvvH1YVkRat4dwNZxsi+cho\n\
|
||||
zqWOciXrbzifBghBp0Upe5fgR2JRpyB6sMVXIHKkeP9YBQUARm1ECdbdJmPSiNYP\n\
|
||||
gMKpTaEObMahAAAAgCtugmDSAwIPibrD9MAbJB6KbN15heA6vTtCLOvFe1Hikw94\n\
|
||||
DYAJz+vlKadbOZW5SfGAOuIe7IynafthWm4RcbXEXxhnVtqHxzMHOZo/Mnoh+bUO\n\
|
||||
esDSoERyNHokpNK6m1NKbmQeFj4n7rkcrR8hrwX8+Ng8CsBEglDi+ULtVivbAAAA\n\
|
||||
gQD1vEPRUu9aD7CjkYgDyD2vNRRevARf01ImgT1tpiEA+GLHJ0xMetd7OH0wutAZ\n\
|
||||
uH26V19Kt4sWpsTwfdl2fIw7XHPc+G1OSqiOk6AS9qT/sy/VL1Wn7CqyAN2jikzn\n\
|
||||
quE6MbebTUJQSNHK9vQhn+u4hUDdEoMOLTYdWxxcjdJirQAAAIEA0VsOxBRDSTLc\n\
|
||||
Ar0Y97oCmb/6tU9XGAZwL2E14GVK85PnJNwHrx4aqb0qATE4iPLfE7ms+eBtT8Uj\n\
|
||||
HF0fxM3KDQiFSrvtgM4JjGTDS4dTYIBD/eQ0/aTaRgLOQqplyBgYVr3x7ATfcIP5\n\
|
||||
961TfdiJ/QESutdb1KQquFXIMRII4vcAAAAPdGVzdDJAcGlja3kuY29tAQIDBA==\n\
|
||||
-----END OPENSSH PRIVATE KEY-----";
|
||||
|
||||
let private_key: SshPrivateKey = SshPrivateKey::from_pem_str(ssh_private_key_pem, None).unwrap();
|
||||
|
||||
let kdf = Kdf::default();
|
||||
|
||||
assert_eq!("test2@picky.com".to_owned(), private_key.comment);
|
||||
assert_eq!(kdf, private_key.kdf);
|
||||
assert_eq!("none", private_key.cipher_name);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_without_passphrase_4096() {
|
||||
// ssh-keygen -t rsa -b 4096 -C "test@picky.com" (without the passphrase)
|
||||
let ssh_private_key_pem = "-----BEGIN OPENSSH PRIVATE KEY-----b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAACFwAAAAdzc2gtcnNhAAAAAwEAAQAAAgEA21AiuHR9Z+HThQb/7I3zJmuKuanu0mePY9hjgxiq/A7nmTFmC03JOtblDDJVQU918l+pnul+FrAaIo80Fr4MKSwhk6pYUE57ZuRaYVxx5CsRb4zIT8wpxzUvi9Hm83sHHnLGOa7YMPugYRcHWRoRQX4n9f+rPau8u/vBnt4VCBKi3YjAw88XOusyGltuo2cTuATB7iqe15Z9iXg47ER789LwTQHXTn5L7afoDO9jh+LZvcEv1fG1TmevKFNKLPA7ohBp8AOUZ4zo2hXR1rdZg/Afp0SDcSPM2MkHKqd7eKeedj9Ba4b44IsYuu0cmsdA1DbszdjKUNDkVIEZH8v8VryJlLHj/wX6rzYlpBQFhzQw0rHOdFpq/oNCYnBtoKMBy2D8SkYyyGzqviYMR6xOE3WgNjSaHlKaSYFlOMrhpeX8dRvgXHa9AvpbDI9eB6fmhmoxDi0OzKtx81hKMfRtSoDeK9uujKH3fE+L64xeiWvRPqadKV4BL9nL7WCSz9Knax1mn295VrD+ISVp7/zWlz+mQMYhHh7IoK2PfJJoGWx5v+gJogSe2ykP0vz3pWI95ky9GmJBhe/albQM0pe8iPclch7Je3beY3ZqeviKH7hLTX5wHH6Gki7tDo6LafVQTL4peqI0nGyTSwS/LRjePrqyHLDVL1YwDp8HN56LYSsAAAdIA4ihRQOIoUUAAAAHc3NoLXJzYQAAAgEA21AiuHR9Z+HThQb/7I3zJmuKuanu0mePY9hjgxiq/A7nmTFmC03JOtblDDJVQU918l+pnul+FrAaIo80Fr4MKSwhk6pYUE57ZuRaYVxx5CsRb4zIT8wpxzUvi9Hm83sHHnLGOa7YMPugYRcHWRoRQX4n9f+rPau8u/vBnt4VCBKi3YjAw88XOusyGltuo2cTuATB7iqe15Z9iXg47ER789LwTQHXTn5L7afoDO9jh+LZvcEv1fG1TmevKFNKLPA7ohBp8AOUZ4zo2hXR1rdZg/Afp0SDcSPM2MkHKqd7eKeedj9Ba4b44IsYuu0cmsdA1DbszdjKUNDkVIEZH8v8VryJlLHj/wX6rzYlpBQFhzQw0rHOdFpq/oNCYnBtoKMBy2D8SkYyyGzqviYMR6xOE3WgNjSaHlKaSYFlOMrhpeX8dRvgXHa9AvpbDI9eB6fmhmoxDi0OzKtx81hKMfRtSoDeK9uujKH3fE+L64xeiWvRPqadKV4BL9nL7WCSz9Knax1mn295VrD+ISVp7/zWlz+mQMYhHh7IoK2PfJJoGWx5v+gJogSe2ykP0vz3pWI95ky9GmJBhe/albQM0pe8iPclch7Je3beY3ZqeviKH7hLTX5wHH6Gki7tDo6LafVQTL4peqI0nGyTSwS/LRjePrqyHLDVL1YwDp8HN56LYSsAAAADAQABAAACAC7OXIqnefhIzx7uDoLLDODfRN05Mlo/de/mR967zgo7mBwu2cuBz3e6U2oV9/IXZmHTHt1mkd1/uiQ0Efbkmq3S2FuumGiTR2z/QXbUBw6eTntTPZEiTqxQYpRhuPuv/yX1cu7urP9PRLxT8OKIWLR0m0y6Qy7HT2GDaqBgX3a4m3/SZumjch7GAYx0hRlkr2Wvxj/xYrM6UBKd0PBD8XxpQZX91ZjQBZ50HmdcVA61UKlZ6L6tdneEU3K0y/jpUKDXBfUOnoa3IR8iVwWPXhB1mBvX2IG2FUsTJG9rDUQD6iLsfybWyJkLtrx2TIuQCPsBuep44Tz8SC7s2pLZs0HeihnrM5YmqprMggvZ1TkVFoR3bq/42XO6ULy5k8QPuP6t91UN5iVljgr8H/6Jo9MuCeRA45ZPZN94Cn1mKJWYamrqRuCqDR5za3A0oHPKYUAfzzD90BLL6Yaib75VpiEDTkOiBuW3MJUcJsqZipDDl/6eas2Qyloplw60dx42FzcRIDXkXzRNn8hBSy7xmQ5MOKGBszCeV/eTBtRITQN38yDVMerb8xDlwOsTtjo3PHCg4HEqqSzjv/B0op9aP7RJ8zp9xLOGlxRZ9YhAlHctUOO6ATsv4uCFwCniZbVOdcUEYwNebYQ0x3IRGUF6RpqjOudUwgLlo0Lq1KV05fM5AAABAC7fkAB4l5YMAseu+lcj+CwHySzcI+baRFCrMIKldNjEPvvZcCSOU/n5pgp2bw0ulw8c4mFQv0GsG//qQCBX1IrIWO0/nRBjEUTPIe2BUswoxm3+F7pirphdIpABKMzV7ZvENn53p2ByrW9+uiwwXLo/z4tH18JW41Jyp5mXH2+1iWIYzq5d4gVgMKLGnqWG3DisViHBGg/ExxQCayeXAhlcXVaWZiaVYsgyreaQg58S2RRUIveWP+ZAeb8+ZJ72ZjIYLc0GIbP673GpcNWkRlCykTJXF9x+Ts0trffqvSxF+2YJnaacLSWJmWFU1BsxUO2pIM4SI8VeHYBdEoAVqcQAAAEBAPUodhyNIr8dtcJona8Lkn+3BxLdvYAV1bnlWnUcG9m0RQ2L95kH6folOG00aWhRgJHFDoXcCaHND8Mg3PkAXYUKCucipiIITyd8YeYnF0ckau5GmUEzwc6s4HcGyFilX1yBoyLE7hFMzOJ4+Rcq+zpD2TfaWcuoo+njDWEHeTbzvGIDQoBYsPnGOtw57q9IA5oWYAG3LtwygazmNF2xeEnMEtYPyPu7+W0teO0QIJiHWEuK/yLPOb+RHBfA6YJ1f9Jcgc614DxyW6qnB5YuzQBovLzgp/7j9J4Z9F8n8f9PAwYScf7IG8icVVhl5NwNgfNOpcjdg6+YB8Z0AXa4dYcAAAEBAOUDEl6yS1nwZ0QsJwfHE232dpsOqxxqfV4ei4R8/obq+b5YPHiUgbt2PlHyHtgfQr639BwMmIaAMSR9CLti44Mw6Z3k2DEz3Ef4+XilPeScNiZmWfYanWmVwFEtb2c+YT3QweUH3DUAViHL+UdU7xp+zhkrd04daVPpYc9NNN9b9Gwmj6Pm0RP05UJxsG1ipvN1rGpaCsJiLfS9IoSsKh0Vzdzdty1YvFhEErTl0WBVGGK6xaA5lfMtaclWi2mGGNXfWflyQzkz87eYlPe2RhM7jW1Lo9h1BBYE6R+jKt3q0mHwRehj+updAAXJx0RWF7EDQVJtlTfSrUCm+SSFoD0AAAAOdGVzdEBwaWNreS5jb20BAgMEBQ==-----END OPENSSH PRIVATE KEY-----";
|
||||
|
||||
let private_key: SshPrivateKey = SshPrivateKey::from_pem_str(ssh_private_key_pem, None).unwrap();
|
||||
let kdf = Kdf::default();
|
||||
assert_eq!("test@picky.com".to_owned(), private_key.comment);
|
||||
assert_eq!(kdf, private_key.kdf);
|
||||
assert_eq!("none", private_key.cipher_name);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_with_passphrase_2048() {
|
||||
// ssh-keygen -t rsa -b 2048 -C "test_with_pass2@picky.com"
|
||||
let passphrase = Some("123123".to_string());
|
||||
let ssh_private_key_pem = "-----BEGIN OPENSSH PRIVATE KEY-----\n\
|
||||
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABBI\n\
|
||||
MsVovOqXSrZa+iEvQwXzAAAAEAAAAAEAAAEXAAAAB3NzaC1yc2EAAAADAQABAAAB\n\
|
||||
AQCkR5WaC3NTPZdj9X/bX88YYbR2k5r3aE+I/ezxzbG6xIJi+So9AohypAhReyW9\n\
|
||||
7XSGut5n6a9O+n/c9nCiXFVoyXbMSdM90Av5bu799+V4w3kBlRzN5D3A6uIZRjgl\n\
|
||||
wc3Xso9kthneNByB7OjZuSDdmuWE3YOgmW0TirP3dztbtVScLPZUSsEveIMt90aw\n\
|
||||
uOFWaEUshqb7l713bdEB0Tb77Z1wZpt6UmIgpraV58kN/ahepbY8lirMS4ym75wt\n\
|
||||
Be6PgyKGKIR3aNQdbfHYMHCgxNQMAFUt2yD9f+JE5HWG7kyKDcLTHCY60dtKTNfp\n\
|
||||
cByi4Bwm3209V4rGYSKAzFXvAAAD0CdYEpFE8Dda2GBNy1l5vDNdbyJvx7SSP49l\n\
|
||||
4OmHsgRE2WneNC9CfO2IxPRNXsPEmXimeubqm6alsmJ1Ch+KsdjvyU7WIEnjuonC\n\
|
||||
lLWx6rhsuppJqZSICbikMUXjhlHpLirGnL0WoaBnLYEVYgu8cMbIgE9BNho+bS+1\n\
|
||||
qvyIrIdIblJwwc66CJKUYPz1yRA84WIMZOWlsYfeHnCvTGjiYUG2YFayVAuXvAz/\n\
|
||||
ND3bQYUlO34XOOsJvZxfQNEg1/tzhB7RvcGOG1InoQxT6dZtTp85CkTU/QQ6w2eY\n\
|
||||
j4qDDmsFm/eSDgEFfOJDLrfHsB4+G2aBZLmgk2bn7vo3JBkcPAETX6kKd7bkyEfh\n\
|
||||
LVph9i48vbmNJ8mXWiXMoRXqRgkKqBAMVnuXtbKVDVzzlZXIFu1cbuKyt0zUg7jB\n\
|
||||
IeIdG+5U0L6qygTjOKU6aP+dK1wRc0XyC8jxTJupt2eTEKBLzy4TwlLH5QhEcj1c\n\
|
||||
coV97PyslJ/NnQx8IKflHxxxQF4CbYgyXt9fWZpBfaD9TVWgsFoKrlZ9HOb6s5WJ\n\
|
||||
MwijgNLfllKNkJB/KpQUIwMAqEjkfk4HyKeC9sfCHkjkXoZO28GypRR8Bd5M+/Qf\n\
|
||||
otFvcdRHqbvv+mj1y6nBIv0hv5eqJEil5s/dwGI7cexMGBjPVOPK63kbh6JlMcrb\n\
|
||||
58jKid1VTzUbxxKm6YfL2aQpGp/veGPZRkm+x3DHoANYLYJ64WRQgBOGcf4QSqiT\n\
|
||||
xP9Y5ZxfQuheDzOkiQCt3ToTWwguXtVLm3AAUKxUhHVgMy2PQNFXcNsPWGCzhOW1\n\
|
||||
FzC82iZhuQi7SlTX7iA40np23nMkHu37hkHpfpipySxEIIjv1T0UglqN25hPlHDI\n\
|
||||
jrTRwcBVxikVhP0IFbDUtlmqSP5MkDEE2ZKTeD0ivd8c2WLO5RUoEICaTVHOx+Mx\n\
|
||||
OJ9L07ZhA2NMKiMMqhe0bXwZoFFHMUxXh8+iTTy89oE1PQ7xz/d6hJUtbqJ/N2xp\n\
|
||||
cWMNtnvbjWpxzwhjPGiqKx8GCtpGoAjpUeNqWL9V0a20rJBYqzJGLYfKDd+PW2XT\n\
|
||||
tOHbQwl0DFNq41jP4nYnaFo2YCjWb3mleRUWkU5SoUHq+vUvs4dxqKjlzvKnK5pc\n\
|
||||
yH9bnpKPaBI28QHtye7o25AfkOj7eHVSe5CV4u8okVaBEq1OFhBeWm+jx1fBrk82\n\
|
||||
hEGamuq1GZsZre2y9jauusOFcMXrV5oxJjBLLbGCi0i5ES0O+kBOlB/kY3hdkReC\n\
|
||||
HCJlMN7v92mkSsadahzwx3fTQWCwgVDg6LLN+xCPGFTMts4XDwg=\n\
|
||||
-----END OPENSSH PRIVATE KEY-----";
|
||||
|
||||
let private_key: SshPrivateKey = SshPrivateKey::from_pem_str(ssh_private_key_pem, passphrase).unwrap();
|
||||
|
||||
assert_eq!("test_with_pass2@picky.com".to_owned(), private_key.comment);
|
||||
assert_eq!(
|
||||
Kdf {
|
||||
name: "bcrypt".to_owned(),
|
||||
option: KdfOption {
|
||||
salt: vec![72, 50, 197, 104, 188, 234, 151, 74, 182, 90, 250, 33, 47, 67, 5, 243],
|
||||
rounds: 16,
|
||||
}
|
||||
},
|
||||
private_key.kdf
|
||||
);
|
||||
assert_eq!("aes256-ctr", private_key.cipher_name);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_without_passphrase_2048() {
|
||||
// ssh-keygen -t rsa -b 2048 -C "test2@picky.com" (without the passphrase)
|
||||
let ssh_private_key_pem = "-----BEGIN OPENSSH PRIVATE KEY-----\n\
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdz\n\
|
||||
c2gtcnNhAAAAAwEAAQAAAQEAyPYbdoNqjj4EhuYblWIxVKLsmsOff+kLkKlFRsIJ\n\
|
||||
yE5YUWzPm5LyUH3LoqnL/rw/f/Og37oJO/bEn4P2lSvlf6ZagAGaLo8/8ACw4xKY\n\
|
||||
UsQFHAEfreIthd/T2u9TEnN+yPS99M99bXG2tV+6He4c61TJfYrq5DsgQuMXCFmt\n\
|
||||
R/IdJg8qF8lj06qEzjQ1HvXQdXruhm4sQn1HMb3VbdKQFSU3TpmzVysEaOVl3zK7\n\
|
||||
KirBU9gHIOFZuE3y0oUklFuK6jOhjgQnxeo58Rb00g3p7R+YcpI1i95TAoIQ/tYS\n\
|
||||
cjnZzByQv+ak1BjgfOjMbEeEQl6kvi2axqTEnFcg0IHu6wAAA8iqDGUDqgxlAwAA\n\
|
||||
AAdzc2gtcnNhAAABAQDI9ht2g2qOPgSG5huVYjFUouyaw59/6QuQqUVGwgnITlhR\n\
|
||||
bM+bkvJQfcuiqcv+vD9/86Dfugk79sSfg/aVK+V/plqAAZoujz/wALDjEphSxAUc\n\
|
||||
AR+t4i2F39Pa71MSc37I9L30z31tcba1X7od7hzrVMl9iurkOyBC4xcIWa1H8h0m\n\
|
||||
DyoXyWPTqoTONDUe9dB1eu6GbixCfUcxvdVt0pAVJTdOmbNXKwRo5WXfMrsqKsFT\n\
|
||||
2Acg4Vm4TfLShSSUW4rqM6GOBCfF6jnxFvTSDentH5hykjWL3lMCghD+1hJyOdnM\n\
|
||||
HJC/5qTUGOB86MxsR4RCXqS+LZrGpMScVyDQge7rAAAAAwEAAQAAAQATZEw6H2xE\n\
|
||||
1Y8yRTocLCF+fUo/lOjrOt22096veUHgZk73bHyMEp33Tmw8Ag6BQkEOY7/+VsFV\n\
|
||||
W/aVPfKpalb2/mJ1P7JVE9Wjny1ye/Te57NmhGU+LjkeVf7nfXiSqzpswdEisnL0\n\
|
||||
AKkUz2vyP2vi+YeH6cPIyjvOuIMcdyrVakejnGbss19ZoXw660X/7TRqG/41KhTm\n\
|
||||
lkN610JBKI2Rozecx9l3LZ3CTRpOOJ2sfssegvL+qxvvH1YVkRat4dwNZxsi+cho\n\
|
||||
zqWOciXrbzifBghBp0Upe5fgR2JRpyB6sMVXIHKkeP9YBQUARm1ECdbdJmPSiNYP\n\
|
||||
gMKpTaEObMahAAAAgCtugmDSAwIPibrD9MAbJB6KbN15heA6vTtCLOvFe1Hikw94\n\
|
||||
DYAJz+vlKadbOZW5SfGAOuIe7IynafthWm4RcbXEXxhnVtqHxzMHOZo/Mnoh+bUO\n\
|
||||
esDSoERyNHokpNK6m1NKbmQeFj4n7rkcrR8hrwX8+Ng8CsBEglDi+ULtVivbAAAA\n\
|
||||
gQD1vEPRUu9aD7CjkYgDyD2vNRRevARf01ImgT1tpiEA+GLHJ0xMetd7OH0wutAZ\n\
|
||||
uH26V19Kt4sWpsTwfdl2fIw7XHPc+G1OSqiOk6AS9qT/sy/VL1Wn7CqyAN2jikzn\n\
|
||||
quE6MbebTUJQSNHK9vQhn+u4hUDdEoMOLTYdWxxcjdJirQAAAIEA0VsOxBRDSTLc\n\
|
||||
Ar0Y97oCmb/6tU9XGAZwL2E14GVK85PnJNwHrx4aqb0qATE4iPLfE7ms+eBtT8Uj\n\
|
||||
HF0fxM3KDQiFSrvtgM4JjGTDS4dTYIBD/eQ0/aTaRgLOQqplyBgYVr3x7ATfcIP5\n\
|
||||
961TfdiJ/QESutdb1KQquFXIMRII4vcAAAAPdGVzdDJAcGlja3kuY29tAQIDBA==\n\
|
||||
-----END OPENSSH PRIVATE KEY-----\x0A";
|
||||
|
||||
let private_key = SshPrivateKey::from_pem_str(ssh_private_key_pem, None).unwrap();
|
||||
let ssh_private_key_after = private_key.to_string().unwrap();
|
||||
|
||||
pretty_assertions::assert_eq!(ssh_private_key_pem, ssh_private_key_after.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_with_passphrase_2048() {
|
||||
// ssh-keygen -t rsa -b 2048 -C "test_with_pass2@picky.com"
|
||||
let passphrase = Some("123123".to_string());
|
||||
let ssh_private_key_pem = "-----BEGIN OPENSSH PRIVATE KEY-----\n\
|
||||
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABBI\n\
|
||||
MsVovOqXSrZa+iEvQwXzAAAAEAAAAAEAAAEXAAAAB3NzaC1yc2EAAAADAQABAAAB\n\
|
||||
AQCkR5WaC3NTPZdj9X/bX88YYbR2k5r3aE+I/ezxzbG6xIJi+So9AohypAhReyW9\n\
|
||||
7XSGut5n6a9O+n/c9nCiXFVoyXbMSdM90Av5bu799+V4w3kBlRzN5D3A6uIZRjgl\n\
|
||||
wc3Xso9kthneNByB7OjZuSDdmuWE3YOgmW0TirP3dztbtVScLPZUSsEveIMt90aw\n\
|
||||
uOFWaEUshqb7l713bdEB0Tb77Z1wZpt6UmIgpraV58kN/ahepbY8lirMS4ym75wt\n\
|
||||
Be6PgyKGKIR3aNQdbfHYMHCgxNQMAFUt2yD9f+JE5HWG7kyKDcLTHCY60dtKTNfp\n\
|
||||
cByi4Bwm3209V4rGYSKAzFXvAAAD0CdYEpFE8Dda2GBNy1l5vDNdbyJvx7SSP49l\n\
|
||||
4OmHsgRE2WneNC9CfO2IxPRNXsPEmXimeubqm6alsmJ1Ch+KsdjvyU7WIEnjuonC\n\
|
||||
lLWx6rhsuppJqZSICbikMUXjhlHpLirGnL0WoaBnLYEVYgu8cMbIgE9BNho+bS+1\n\
|
||||
qvyIrIdIblJwwc66CJKUYPz1yRA84WIMZOWlsYfeHnCvTGjiYUG2YFayVAuXvAz/\n\
|
||||
ND3bQYUlO34XOOsJvZxfQNEg1/tzhB7RvcGOG1InoQxT6dZtTp85CkTU/QQ6w2eY\n\
|
||||
j4qDDmsFm/eSDgEFfOJDLrfHsB4+G2aBZLmgk2bn7vo3JBkcPAETX6kKd7bkyEfh\n\
|
||||
LVph9i48vbmNJ8mXWiXMoRXqRgkKqBAMVnuXtbKVDVzzlZXIFu1cbuKyt0zUg7jB\n\
|
||||
IeIdG+5U0L6qygTjOKU6aP+dK1wRc0XyC8jxTJupt2eTEKBLzy4TwlLH5QhEcj1c\n\
|
||||
coV97PyslJ/NnQx8IKflHxxxQF4CbYgyXt9fWZpBfaD9TVWgsFoKrlZ9HOb6s5WJ\n\
|
||||
MwijgNLfllKNkJB/KpQUIwMAqEjkfk4HyKeC9sfCHkjkXoZO28GypRR8Bd5M+/Qf\n\
|
||||
otFvcdRHqbvv+mj1y6nBIv0hv5eqJEil5s/dwGI7cexMGBjPVOPK63kbh6JlMcrb\n\
|
||||
58jKid1VTzUbxxKm6YfL2aQpGp/veGPZRkm+x3DHoANYLYJ64WRQgBOGcf4QSqiT\n\
|
||||
xP9Y5ZxfQuheDzOkiQCt3ToTWwguXtVLm3AAUKxUhHVgMy2PQNFXcNsPWGCzhOW1\n\
|
||||
FzC82iZhuQi7SlTX7iA40np23nMkHu37hkHpfpipySxEIIjv1T0UglqN25hPlHDI\n\
|
||||
jrTRwcBVxikVhP0IFbDUtlmqSP5MkDEE2ZKTeD0ivd8c2WLO5RUoEICaTVHOx+Mx\n\
|
||||
OJ9L07ZhA2NMKiMMqhe0bXwZoFFHMUxXh8+iTTy89oE1PQ7xz/d6hJUtbqJ/N2xp\n\
|
||||
cWMNtnvbjWpxzwhjPGiqKx8GCtpGoAjpUeNqWL9V0a20rJBYqzJGLYfKDd+PW2XT\n\
|
||||
tOHbQwl0DFNq41jP4nYnaFo2YCjWb3mleRUWkU5SoUHq+vUvs4dxqKjlzvKnK5pc\n\
|
||||
yH9bnpKPaBI28QHtye7o25AfkOj7eHVSe5CV4u8okVaBEq1OFhBeWm+jx1fBrk82\n\
|
||||
hEGamuq1GZsZre2y9jauusOFcMXrV5oxJjBLLbGCi0i5ES0O+kBOlB/kY3hdkReC\n\
|
||||
HCJlMN7v92mkSsadahzwx3fTQWCwgVDg6LLN+xCPGFTMts4XDwg=\n\
|
||||
-----END OPENSSH PRIVATE KEY-----\x0A";
|
||||
|
||||
let private_key = SshPrivateKey::from_pem_str(ssh_private_key_pem, passphrase).unwrap();
|
||||
let ssh_private_key_after = private_key.to_string().unwrap();
|
||||
|
||||
pretty_assertions::assert_eq!(ssh_private_key_pem, ssh_private_key_after.as_str());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(picky_test_data::SSH_PRIVATE_KEY_EC_P256)]
|
||||
#[case(picky_test_data::SSH_PRIVATE_KEY_EC_P384)]
|
||||
#[case(picky_test_data::SSH_PRIVATE_KEY_EC_P521)]
|
||||
fn ecdsa_keys_unencrypted(#[case] pem: &str) {
|
||||
let key = SshPrivateKey::from_pem_str(pem, None).unwrap();
|
||||
let encoded = key.to_string().unwrap();
|
||||
pretty_assertions::assert_eq!(encoded.as_str(), pem);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ecdsa_keys_encrypted() {
|
||||
let passphrase = Some("test".to_string());
|
||||
let key = SshPrivateKey::from_pem_str(picky_test_data::SSH_PRIVATE_KEY_EC_P256_ENCRYPTED, passphrase).unwrap();
|
||||
let encoded = key.to_string().unwrap();
|
||||
pretty_assertions::assert_eq!(encoded.as_str(), picky_test_data::SSH_PRIVATE_KEY_EC_P256_ENCRYPTED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rsa_rounttrip() {
|
||||
let private_key = SshPrivateKey::from_pem_str(picky_test_data::SSH_PRIVATE_KEY_RSA, None).unwrap();
|
||||
let encoded = private_key.to_string().unwrap();
|
||||
assert_eq!(picky_test_data::SSH_PRIVATE_KEY_RSA, encoded.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ed25519_roundtrip() {
|
||||
let private_key = SshPrivateKey::from_pem_str(picky_test_data::SSH_PRIVATE_KEY_ED25519, None).unwrap();
|
||||
let encoded = private_key.to_string().unwrap();
|
||||
assert_eq!(picky_test_data::SSH_PRIVATE_KEY_ED25519, encoded.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ed25519_roundtrip_encrypted() {
|
||||
let passphrase = Some("test".to_string());
|
||||
let private_key =
|
||||
SshPrivateKey::from_pem_str(picky_test_data::SSH_PRIVATE_KEY_ED25519_ENCRYPTED, passphrase).unwrap();
|
||||
let encoded = private_key.to_string().unwrap();
|
||||
assert_eq!(picky_test_data::SSH_PRIVATE_KEY_ED25519_ENCRYPTED, encoded.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sk_ed25519_roundtrip() {
|
||||
let private_key = SshPrivateKey::from_pem_str(picky_test_data::SSH_PRIVATE_KEY_SK_ED25519, None).unwrap();
|
||||
let encoded = private_key.to_string().unwrap();
|
||||
assert_eq!(picky_test_data::SSH_PRIVATE_KEY_SK_ED25519, encoded.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sk_ed25519_roundtrip_encrypted() {
|
||||
let private_key = SshPrivateKey::from_pem_str(
|
||||
picky_test_data::SSH_PRIVATE_KEY_SK_ED25519_ENCRYPTED,
|
||||
Some("test".to_string()),
|
||||
)
|
||||
.unwrap();
|
||||
let encoded = private_key.to_string().unwrap();
|
||||
assert_eq!(picky_test_data::SSH_PRIVATE_KEY_SK_ED25519_ENCRYPTED, encoded.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sk_ecdsa_roundtrip() {
|
||||
let private_key = SshPrivateKey::from_pem_str(picky_test_data::SSH_PRIVATE_KEY_SK_ECDSA, None).unwrap();
|
||||
let encoded = private_key.to_string().unwrap();
|
||||
assert_eq!(picky_test_data::SSH_PRIVATE_KEY_SK_ECDSA, encoded.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sk_ecdsa_roundtrip_encrypted() {
|
||||
let private_key = SshPrivateKey::from_pem_str(
|
||||
picky_test_data::SSH_PRIVATE_KEY_SK_ECDSA_ENCRYPTED,
|
||||
Some("test".to_string()),
|
||||
)
|
||||
.unwrap();
|
||||
let encoded = private_key.to_string().unwrap();
|
||||
assert_eq!(picky_test_data::SSH_PRIVATE_KEY_SK_ECDSA_ENCRYPTED, encoded.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rsa_private_key_generation() {
|
||||
let private_key = SshPrivateKey::generate_rsa(2048, Option::Some("123".to_string()), None).unwrap();
|
||||
let data = private_key.to_pem().unwrap();
|
||||
let parsed = SshPrivateKey::from_pem(&data, Option::Some("123".to_string())).unwrap();
|
||||
|
||||
match parsed.base_key() {
|
||||
SshBasePrivateKey::Rsa(key) => {
|
||||
let _rsa: RsaPrivateKey = key.try_into().unwrap();
|
||||
}
|
||||
_ => panic!("Invalid key type"),
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(EcCurve::NistP256)]
|
||||
#[case(EcCurve::NistP384)]
|
||||
#[case(EcCurve::NistP521)]
|
||||
fn test_ec_private_key_generation(#[case] curve: EcCurve) {
|
||||
let private_key = SshPrivateKey::generate_ec(curve, Option::Some("123".to_string()), None).unwrap();
|
||||
let data = private_key.to_pem().unwrap();
|
||||
let parsed = SshPrivateKey::from_pem(&data, Option::Some("123".to_string())).unwrap();
|
||||
|
||||
match parsed.base_key() {
|
||||
SshBasePrivateKey::Ec(ec) => {
|
||||
let _ec: EcdsaKeypair = ec.try_into().unwrap();
|
||||
}
|
||||
_ => panic!("Invalid key type"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ed_private_key_generation() {
|
||||
let private_key = SshPrivateKey::generate_ed25519(Option::Some("123".to_string()), None).unwrap();
|
||||
let data = private_key.to_pem().unwrap();
|
||||
let parsed = SshPrivateKey::from_pem(&data, Option::Some("123".to_string())).unwrap();
|
||||
|
||||
match parsed.base_key() {
|
||||
SshBasePrivateKey::Ed(ed) => {
|
||||
let _ed: EdKeypair = ed.try_into().unwrap();
|
||||
}
|
||||
_ => panic!("Invalid key type"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kdf_option_decode() {
|
||||
let mut cursor = Cursor::new(vec![
|
||||
0, 0, 0, 24, 0, 0, 0, 16, 72, 50, 197, 104, 188, 234, 151, 74, 182, 90, 250, 33, 47, 67, 5, 243, 0, 0, 0,
|
||||
16,
|
||||
]);
|
||||
let kdf_option: KdfOption = SshComplexTypeDecode::decode(&mut cursor).unwrap();
|
||||
let KdfOption { salt, rounds } = kdf_option;
|
||||
|
||||
assert_eq!(
|
||||
vec![72, 50, 197, 104, 188, 234, 151, 74, 182, 90, 250, 33, 47, 67, 5, 243],
|
||||
salt
|
||||
);
|
||||
assert_eq!(16, rounds);
|
||||
|
||||
let mut cursor = Cursor::new(vec![0, 0, 0, 0]);
|
||||
let kdf_option: KdfOption = SshComplexTypeDecode::decode(&mut cursor).unwrap();
|
||||
let KdfOption { salt, rounds } = kdf_option;
|
||||
|
||||
assert!(salt.is_empty());
|
||||
assert_eq!(0, rounds);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kdf_option_encode() {
|
||||
let mut res: Vec<u8> = Vec::new();
|
||||
let kdf_option = KdfOption {
|
||||
salt: vec![72, 50, 197, 104, 188, 234, 151, 74, 182, 90, 250, 33, 47, 67, 5, 243],
|
||||
rounds: 16,
|
||||
};
|
||||
|
||||
kdf_option.encode(&mut res).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
vec![
|
||||
0, 0, 0, 24, 0, 0, 0, 16, 72, 50, 197, 104, 188, 234, 151, 74, 182, 90, 250, 33, 47, 67, 5, 243, 0, 0,
|
||||
0, 16
|
||||
],
|
||||
res
|
||||
);
|
||||
|
||||
res.clear();
|
||||
let kdf_option = KdfOption::default();
|
||||
kdf_option.encode(&mut res).unwrap();
|
||||
|
||||
assert_eq!(vec![0, 0, 0, 0], res);
|
||||
}
|
||||
}
|
||||
+309
@@ -0,0 +1,309 @@
|
||||
use crate::key::{KeyError, PublicKey};
|
||||
use crate::ssh::decode::SshComplexTypeDecode;
|
||||
use crate::ssh::encode::SshComplexTypeEncode;
|
||||
|
||||
use std::io;
|
||||
use std::str::FromStr;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SshPublicKeyError {
|
||||
#[error(transparent)]
|
||||
IoError(#[from] io::Error),
|
||||
#[error("invalid UTF-8")]
|
||||
InvalidUtf8,
|
||||
#[error(transparent)]
|
||||
RsaError(#[from] rsa::errors::Error),
|
||||
#[error(transparent)]
|
||||
Base64DecodeError(#[from] base64::DecodeError),
|
||||
#[error("Unknown key type. We only support RSA")]
|
||||
UnknownKeyType,
|
||||
#[error(transparent)]
|
||||
KeyError(#[from] KeyError),
|
||||
}
|
||||
|
||||
impl From<core::str::Utf8Error> for SshPublicKeyError {
|
||||
fn from(_: core::str::Utf8Error) -> Self {
|
||||
Self::InvalidUtf8
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::string::FromUtf8Error> for SshPublicKeyError {
|
||||
fn from(_: std::string::FromUtf8Error) -> Self {
|
||||
Self::InvalidUtf8
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SshBasePublicKey {
|
||||
Rsa(PublicKey),
|
||||
Ec(PublicKey),
|
||||
Ed(PublicKey),
|
||||
/// U2F ecdsa SSH key
|
||||
SkEcdsaSha2NistP256 {
|
||||
base_key: PublicKey,
|
||||
application: String,
|
||||
},
|
||||
/// U2F ed25519 SSH key
|
||||
SkEd25519 {
|
||||
base_key: PublicKey,
|
||||
application: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SshPublicKey {
|
||||
pub inner_key: SshBasePublicKey,
|
||||
pub comment: String,
|
||||
}
|
||||
|
||||
impl SshPublicKey {
|
||||
pub fn to_string(&self) -> Result<String, SshPublicKeyError> {
|
||||
let mut buffer = Vec::with_capacity(1024);
|
||||
self.encode(&mut buffer)?;
|
||||
Ok(String::from_utf8(buffer)?)
|
||||
}
|
||||
|
||||
pub fn inner_key(&self) -> &PublicKey {
|
||||
match &self.inner_key {
|
||||
SshBasePublicKey::Rsa(key) => key,
|
||||
SshBasePublicKey::Ec(key) => key,
|
||||
SshBasePublicKey::Ed(key) => key,
|
||||
SshBasePublicKey::SkEcdsaSha2NistP256 { base_key, .. } => base_key,
|
||||
SshBasePublicKey::SkEd25519 { base_key, .. } => base_key,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fingerprint_md5(&self) -> Result<[u8; 16], SshPublicKeyError> {
|
||||
use md5::{Digest, Md5};
|
||||
|
||||
let mut encoded = Vec::new();
|
||||
self.inner_key.encode(&mut encoded)?;
|
||||
|
||||
let mut hasher = Md5::new();
|
||||
hasher.update(&encoded);
|
||||
let fingerprint = hasher.finalize();
|
||||
|
||||
Ok(fingerprint.into())
|
||||
}
|
||||
|
||||
pub fn fingerprint_sha1(&self) -> Result<[u8; 20], SshPublicKeyError> {
|
||||
use sha1::{Digest, Sha1};
|
||||
|
||||
let mut encoded = Vec::new();
|
||||
self.inner_key.encode(&mut encoded)?;
|
||||
|
||||
let mut hasher = Sha1::new();
|
||||
hasher.update(&encoded);
|
||||
let fingerprint = hasher.finalize();
|
||||
|
||||
Ok(fingerprint.into())
|
||||
}
|
||||
|
||||
pub fn fingerprint_sha256(&self) -> Result<[u8; 32], SshPublicKeyError> {
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
let mut encoded = Vec::new();
|
||||
self.inner_key.encode(&mut encoded)?;
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&encoded);
|
||||
let fingerprint = hasher.finalize();
|
||||
|
||||
Ok(fingerprint.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for SshPublicKey {
|
||||
type Err = SshPublicKeyError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
SshComplexTypeDecode::decode(s.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD_NO_PAD;
|
||||
use crypto_bigint::BoxedUint;
|
||||
use rstest::rstest;
|
||||
|
||||
#[test]
|
||||
fn decode_ssh_rsa_4096_public_key() {
|
||||
// ssh-keygen -t rsa -b 4096 -C "test@picky.com"
|
||||
let ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDbUCK4dH1n4dOFBv/sjfMma4q5qe7SZ49j2GODGKr8DueZMWYLTck61uUMMlVBT3XyX6me6X4WsBoijzQWvgwpLCGTqlhQTntm5FphXHHkKxFvjMhPzCnHNS+L0ebzewcecsY5rtgw+6BhFwdZGhFBfif1/6s9q7y7+8Ge3hUIEqLdiMDDzxc66zIaW26jZxO4BMHuKp7Xln2JeDjsRHvz0vBNAddOfkvtp+gM72OH4tm9wS/V8bVOZ68oU0os8DuiEGnwA5RnjOjaFdHWt1mD8B+nRINxI8zYyQcqp3t4p552P0Frhvjgixi67Ryax0DUNuzN2MpQ0ORUgRkfy/xWvImUseP/BfqvNiWkFAWHNDDSsc50Wmr+g0JicG2gowHLYPxKRjLIbOq+JgxHrE4TdaA2NJoeUppJgWU4yuGl5fx1G+Bcdr0C+lsMj14Hp+aGajEOLQ7Mq3HzWEox9G1KgN4r266Mofd8T4vrjF6Ja9E+pp0pXgEv2cvtYJLP0qdrHWafb3lWsP4hJWnv/NaXP6ZAxiEeHsigrY98kmgZbHm/6AmiBJ7bKQ/S/PelYj3mTL0aYkGF79qVtAzSl7yI9yVyHsl7dt5jdmp6+IofuEtNfnAcfoaSLu0Ojotp9VBMvil6ojScbJNLBL8tGN4+urIcsNUvVjAOnwc3nothKw== test@picky.com\r\n";
|
||||
|
||||
let public_key = SshPublicKey::from_str(ssh_public_key).unwrap();
|
||||
|
||||
assert_eq!("test@picky.com".to_owned(), public_key.comment);
|
||||
assert_eq!(
|
||||
SshBasePublicKey::Rsa(PublicKey::from_rsa_components(
|
||||
&BoxedUint::from_be_slice_vartime(&[
|
||||
219, 80, 34, 184, 116, 125, 103, 225, 211, 133, 6, 255, 236, 141, 243, 38, 107, 138, 185, 169, 238,
|
||||
210, 103, 143, 99, 216, 99, 131, 24, 170, 252, 14, 231, 153, 49, 102, 11, 77, 201, 58, 214, 229,
|
||||
12, 50, 85, 65, 79, 117, 242, 95, 169, 158, 233, 126, 22, 176, 26, 34, 143, 52, 22, 190, 12, 41,
|
||||
44, 33, 147, 170, 88, 80, 78, 123, 102, 228, 90, 97, 92, 113, 228, 43, 17, 111, 140, 200, 79, 204,
|
||||
41, 199, 53, 47, 139, 209, 230, 243, 123, 7, 30, 114, 198, 57, 174, 216, 48, 251, 160, 97, 23, 7,
|
||||
89, 26, 17, 65, 126, 39, 245, 255, 171, 61, 171, 188, 187, 251, 193, 158, 222, 21, 8, 18, 162, 221,
|
||||
136, 192, 195, 207, 23, 58, 235, 50, 26, 91, 110, 163, 103, 19, 184, 4, 193, 238, 42, 158, 215,
|
||||
150, 125, 137, 120, 56, 236, 68, 123, 243, 210, 240, 77, 1, 215, 78, 126, 75, 237, 167, 232, 12,
|
||||
239, 99, 135, 226, 217, 189, 193, 47, 213, 241, 181, 78, 103, 175, 40, 83, 74, 44, 240, 59, 162,
|
||||
16, 105, 240, 3, 148, 103, 140, 232, 218, 21, 209, 214, 183, 89, 131, 240, 31, 167, 68, 131, 113,
|
||||
35, 204, 216, 201, 7, 42, 167, 123, 120, 167, 158, 118, 63, 65, 107, 134, 248, 224, 139, 24, 186,
|
||||
237, 28, 154, 199, 64, 212, 54, 236, 205, 216, 202, 80, 208, 228, 84, 129, 25, 31, 203, 252, 86,
|
||||
188, 137, 148, 177, 227, 255, 5, 250, 175, 54, 37, 164, 20, 5, 135, 52, 48, 210, 177, 206, 116, 90,
|
||||
106, 254, 131, 66, 98, 112, 109, 160, 163, 1, 203, 96, 252, 74, 70, 50, 200, 108, 234, 190, 38, 12,
|
||||
71, 172, 78, 19, 117, 160, 54, 52, 154, 30, 82, 154, 73, 129, 101, 56, 202, 225, 165, 229, 252,
|
||||
117, 27, 224, 92, 118, 189, 2, 250, 91, 12, 143, 94, 7, 167, 230, 134, 106, 49, 14, 45, 14, 204,
|
||||
171, 113, 243, 88, 74, 49, 244, 109, 74, 128, 222, 43, 219, 174, 140, 161, 247, 124, 79, 139, 235,
|
||||
140, 94, 137, 107, 209, 62, 166, 157, 41, 94, 1, 47, 217, 203, 237, 96, 146, 207, 210, 167, 107,
|
||||
29, 102, 159, 111, 121, 86, 176, 254, 33, 37, 105, 239, 252, 214, 151, 63, 166, 64, 198, 33, 30,
|
||||
30, 200, 160, 173, 143, 124, 146, 104, 25, 108, 121, 191, 232, 9, 162, 4, 158, 219, 41, 15, 210,
|
||||
252, 247, 165, 98, 61, 230, 76, 189, 26, 98, 65, 133, 239, 218, 149, 180, 12, 210, 151, 188, 136,
|
||||
247, 37, 114, 30, 201, 123, 118, 222, 99, 118, 106, 122, 248, 138, 31, 184, 75, 77, 126, 112, 28,
|
||||
126, 134, 146, 46, 237, 14, 142, 139, 105, 245, 80, 76, 190, 41, 122, 162, 52, 156, 108, 147, 75,
|
||||
4, 191, 45, 24, 222, 62, 186, 178, 28, 176, 213, 47, 86, 48, 14, 159, 7, 55, 158, 139, 97, 43
|
||||
]),
|
||||
&BoxedUint::from_be_slice_vartime(&[1, 0, 1])
|
||||
)),
|
||||
public_key.inner_key
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_ssh_rsa_2048_public_key() {
|
||||
// ssh-keygen -t rsa -b 2048 -C "test2@picky.com"
|
||||
let ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDI9ht2g2qOPgSG5huVYjFUouyaw59/6QuQqUVGwgnITlhRbM+bkvJQfcuiqcv+vD9/86Dfugk79sSfg/aVK+V/plqAAZoujz/wALDjEphSxAUcAR+t4i2F39Pa71MSc37I9L30z31tcba1X7od7hzrVMl9iurkOyBC4xcIWa1H8h0mDyoXyWPTqoTONDUe9dB1eu6GbixCfUcxvdVt0pAVJTdOmbNXKwRo5WXfMrsqKsFT2Acg4Vm4TfLShSSUW4rqM6GOBCfF6jnxFvTSDentH5hykjWL3lMCghD+1hJyOdnMHJC/5qTUGOB86MxsR4RCXqS+LZrGpMScVyDQge7r test2@picky.com\r\n";
|
||||
|
||||
let public_key: SshPublicKey = SshPublicKey::from_str(ssh_public_key).unwrap();
|
||||
|
||||
assert_eq!("test2@picky.com".to_owned(), public_key.comment);
|
||||
assert_eq!(
|
||||
SshBasePublicKey::Rsa(PublicKey::from_rsa_components(
|
||||
&BoxedUint::from_be_slice_vartime(&[
|
||||
200, 246, 27, 118, 131, 106, 142, 62, 4, 134, 230, 27, 149, 98, 49, 84, 162, 236, 154, 195, 159,
|
||||
127, 233, 11, 144, 169, 69, 70, 194, 9, 200, 78, 88, 81, 108, 207, 155, 146, 242, 80, 125, 203,
|
||||
162, 169, 203, 254, 188, 63, 127, 243, 160, 223, 186, 9, 59, 246, 196, 159, 131, 246, 149, 43, 229,
|
||||
127, 166, 90, 128, 1, 154, 46, 143, 63, 240, 0, 176, 227, 18, 152, 82, 196, 5, 28, 1, 31, 173, 226,
|
||||
45, 133, 223, 211, 218, 239, 83, 18, 115, 126, 200, 244, 189, 244, 207, 125, 109, 113, 182, 181,
|
||||
95, 186, 29, 238, 28, 235, 84, 201, 125, 138, 234, 228, 59, 32, 66, 227, 23, 8, 89, 173, 71, 242,
|
||||
29, 38, 15, 42, 23, 201, 99, 211, 170, 132, 206, 52, 53, 30, 245, 208, 117, 122, 238, 134, 110, 44,
|
||||
66, 125, 71, 49, 189, 213, 109, 210, 144, 21, 37, 55, 78, 153, 179, 87, 43, 4, 104, 229, 101, 223,
|
||||
50, 187, 42, 42, 193, 83, 216, 7, 32, 225, 89, 184, 77, 242, 210, 133, 36, 148, 91, 138, 234, 51,
|
||||
161, 142, 4, 39, 197, 234, 57, 241, 22, 244, 210, 13, 233, 237, 31, 152, 114, 146, 53, 139, 222,
|
||||
83, 2, 130, 16, 254, 214, 18, 114, 57, 217, 204, 28, 144, 191, 230, 164, 212, 24, 224, 124, 232,
|
||||
204, 108, 71, 132, 66, 94, 164, 190, 45, 154, 198, 164, 196, 156, 87, 32, 208, 129, 238, 235
|
||||
]),
|
||||
&BoxedUint::from_be_slice_vartime(&[1, 0, 1])
|
||||
)),
|
||||
public_key.inner_key
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_ssh_rsa_4096_public_key() {
|
||||
// ssh-keygen -t rsa -b 4096 -C "test@picky.com"
|
||||
let ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDbUCK4dH1n4dOFBv/sjfMma4q5qe7SZ49j2GODGKr8DueZMWYLTck61uUMMlVBT3XyX6me6X4WsBoijzQWvgwpLCGTqlhQTntm5FphXHHkKxFvjMhPzCnHNS+L0ebzewcecsY5rtgw+6BhFwdZGhFBfif1/6s9q7y7+8Ge3hUIEqLdiMDDzxc66zIaW26jZxO4BMHuKp7Xln2JeDjsRHvz0vBNAddOfkvtp+gM72OH4tm9wS/V8bVOZ68oU0os8DuiEGnwA5RnjOjaFdHWt1mD8B+nRINxI8zYyQcqp3t4p552P0Frhvjgixi67Ryax0DUNuzN2MpQ0ORUgRkfy/xWvImUseP/BfqvNiWkFAWHNDDSsc50Wmr+g0JicG2gowHLYPxKRjLIbOq+JgxHrE4TdaA2NJoeUppJgWU4yuGl5fx1G+Bcdr0C+lsMj14Hp+aGajEOLQ7Mq3HzWEox9G1KgN4r266Mofd8T4vrjF6Ja9E+pp0pXgEv2cvtYJLP0qdrHWafb3lWsP4hJWnv/NaXP6ZAxiEeHsigrY98kmgZbHm/6AmiBJ7bKQ/S/PelYj3mTL0aYkGF79qVtAzSl7yI9yVyHsl7dt5jdmp6+IofuEtNfnAcfoaSLu0Ojotp9VBMvil6ojScbJNLBL8tGN4+urIcsNUvVjAOnwc3nothKw== test@picky.com\r\n";
|
||||
let public_key = SshPublicKey::from_str(ssh_public_key).unwrap();
|
||||
|
||||
let ssh_public_key_after = public_key.to_string().unwrap();
|
||||
|
||||
assert_eq!(ssh_public_key, ssh_public_key_after.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_ssh_rsa_2048_public_key() {
|
||||
// ssh-keygen -t rsa -b 4096 -C "test@picky.com"
|
||||
let ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDI9ht2g2qOPgSG5huVYjFUouyaw59/6QuQqUVGwgnITlhRbM+bkvJQfcuiqcv+vD9/86Dfugk79sSfg/aVK+V/plqAAZoujz/wALDjEphSxAUcAR+t4i2F39Pa71MSc37I9L30z31tcba1X7od7hzrVMl9iurkOyBC4xcIWa1H8h0mDyoXyWPTqoTONDUe9dB1eu6GbixCfUcxvdVt0pAVJTdOmbNXKwRo5WXfMrsqKsFT2Acg4Vm4TfLShSSUW4rqM6GOBCfF6jnxFvTSDentH5hykjWL3lMCghD+1hJyOdnMHJC/5qTUGOB86MxsR4RCXqS+LZrGpMScVyDQge7r test2@picky.com\r\n";
|
||||
let public_key = SshPublicKey::from_str(ssh_public_key).unwrap();
|
||||
|
||||
let ssh_public_key_after = public_key.to_string().unwrap();
|
||||
|
||||
assert_eq!(ssh_public_key, ssh_public_key_after.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rsa_roundtrip() {
|
||||
let public_key = SshPublicKey::from_str(picky_test_data::SSH_PUBLIC_KEY_RSA).unwrap();
|
||||
let ssh_public_key_after = public_key.to_string().unwrap();
|
||||
assert_eq!(picky_test_data::SSH_PUBLIC_KEY_RSA, ssh_public_key_after.as_str());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(picky_test_data::SSH_PUBLIC_KEY_EC_P256)]
|
||||
#[case(picky_test_data::SSH_PUBLIC_KEY_EC_P384)]
|
||||
#[case(picky_test_data::SSH_PUBLIC_KEY_EC_P521)]
|
||||
fn ecdsa_roundtrip(#[case] key_str: &str) {
|
||||
let public_key = SshPublicKey::from_str(key_str).unwrap();
|
||||
let ssh_public_key_after = public_key.to_string().unwrap();
|
||||
assert_eq!(key_str, ssh_public_key_after.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ed25519_roundtrip() {
|
||||
let public_key = SshPublicKey::from_str(picky_test_data::SSH_PUBLIC_KEY_ED25519).unwrap();
|
||||
let ssh_public_key_after = public_key.to_string().unwrap();
|
||||
assert_eq!(picky_test_data::SSH_PUBLIC_KEY_ED25519, ssh_public_key_after.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sk_ed25519_roundtrip() {
|
||||
let public_key: SshPublicKey = SshPublicKey::from_str(picky_test_data::SSH_PUBLIC_KEY_SK_ED25519).unwrap();
|
||||
let ssh_public_key_after = public_key.to_string().unwrap();
|
||||
assert_eq!(
|
||||
picky_test_data::SSH_PUBLIC_KEY_SK_ED25519,
|
||||
ssh_public_key_after.as_str()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sk_ecdsa_roundtrip() {
|
||||
let public_key = SshPublicKey::from_str(picky_test_data::SSH_PUBLIC_KEY_SK_ECDSA).unwrap();
|
||||
let ssh_public_key_after = public_key.to_string().unwrap();
|
||||
assert_eq!(picky_test_data::SSH_PUBLIC_KEY_SK_ECDSA, ssh_public_key_after.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fingerprint_md5_ssh_rsa_2048_public_key() {
|
||||
let ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDI9ht2g2qOPgSG5huVYjFUouyaw59/6QuQqUVGwgnITlhRbM+bkvJQfcuiqcv+vD9/86Dfugk79sSfg/aVK+V/plqAAZoujz/wALDjEphSxAUcAR+t4i2F39Pa71MSc37I9L30z31tcba1X7od7hzrVMl9iurkOyBC4xcIWa1H8h0mDyoXyWPTqoTONDUe9dB1eu6GbixCfUcxvdVt0pAVJTdOmbNXKwRo5WXfMrsqKsFT2Acg4Vm4TfLShSSUW4rqM6GOBCfF6jnxFvTSDentH5hykjWL3lMCghD+1hJyOdnMHJC/5qTUGOB86MxsR4RCXqS+LZrGpMScVyDQge7r test2@picky.com\r\n";
|
||||
|
||||
let public_key: SshPublicKey = SshPublicKey::from_str(ssh_public_key).unwrap();
|
||||
let md5 = hex::encode(public_key.fingerprint_md5().unwrap());
|
||||
|
||||
assert_eq!(md5, "7b6b9cc2e44452aec58c3a0a31d6258d");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fingerprint_sha1_ssh_rsa_2048_public_key() {
|
||||
let ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDI9ht2g2qOPgSG5huVYjFUouyaw59/6QuQqUVGwgnITlhRbM+bkvJQfcuiqcv+vD9/86Dfugk79sSfg/aVK+V/plqAAZoujz/wALDjEphSxAUcAR+t4i2F39Pa71MSc37I9L30z31tcba1X7od7hzrVMl9iurkOyBC4xcIWa1H8h0mDyoXyWPTqoTONDUe9dB1eu6GbixCfUcxvdVt0pAVJTdOmbNXKwRo5WXfMrsqKsFT2Acg4Vm4TfLShSSUW4rqM6GOBCfF6jnxFvTSDentH5hykjWL3lMCghD+1hJyOdnMHJC/5qTUGOB86MxsR4RCXqS+LZrGpMScVyDQge7r test2@picky.com\r\n";
|
||||
|
||||
let public_key: SshPublicKey = SshPublicKey::from_str(ssh_public_key).unwrap();
|
||||
let sha1 = STANDARD_NO_PAD.encode(public_key.fingerprint_sha1().unwrap());
|
||||
|
||||
assert_eq!(sha1, "ezHoULh4V/R9NybfxCW2pL9ADcU");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fingerprint_sha256_ssh_rsa_2048_public_key() {
|
||||
let ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDI9ht2g2qOPgSG5huVYjFUouyaw59/6QuQqUVGwgnITlhRbM+bkvJQfcuiqcv+vD9/86Dfugk79sSfg/aVK+V/plqAAZoujz/wALDjEphSxAUcAR+t4i2F39Pa71MSc37I9L30z31tcba1X7od7hzrVMl9iurkOyBC4xcIWa1H8h0mDyoXyWPTqoTONDUe9dB1eu6GbixCfUcxvdVt0pAVJTdOmbNXKwRo5WXfMrsqKsFT2Acg4Vm4TfLShSSUW4rqM6GOBCfF6jnxFvTSDentH5hykjWL3lMCghD+1hJyOdnMHJC/5qTUGOB86MxsR4RCXqS+LZrGpMScVyDQge7r test2@picky.com\r\n";
|
||||
|
||||
let public_key: SshPublicKey = SshPublicKey::from_str(ssh_public_key).unwrap();
|
||||
|
||||
let sha256 = STANDARD_NO_PAD.encode(public_key.fingerprint_sha256().unwrap());
|
||||
|
||||
assert_eq!(sha256, "cTXkM4frGl07u46Bhzy+YMOS01lX51oE2j6STi7g568");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_ssh_rsa_2024_public_key_with_multiwords_comment() {
|
||||
// ssh-keygen -t rsa -b 2048 -C "test using several words"
|
||||
let ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+76yL1+ocu4iuVam4VO5YlODohmHbIuhjyQgiMGS8ZtdFKcltzAH0ot4zJDH/Z3ja6xO2IOc/4+UMABgPOgFwmzyl414/Zo42CYqk9OB5GJylFYI99HrATqH03Wz2qJ3dzP6QJVf8g05hY27RKaU5H+0fo471SACeHet9uqstRecsUcauPS91xwpPhrcpRXjGH1yLBdWTpDq5R6c1Wgh9SVuzY/ITMB3pq8rzwal8e2rR4T+wHc48l61LGwmuOTkhAo5/0sn72CzKWQZVd0CarfCr3biCW7cUai0FvH79aAfIBV/FIMXgtgqdpY/Qg7v+JWIyJk/OB8Be1ix8YVRV test using several words\r\n";
|
||||
|
||||
let public_key = SshPublicKey::from_str(ssh_public_key).unwrap();
|
||||
|
||||
assert_eq!(public_key.comment, "test using several words");
|
||||
}
|
||||
}
|
||||
Vendored
+131
@@ -0,0 +1,131 @@
|
||||
// TODO: support `SshTime` without `chrono` nor `time`
|
||||
#[cfg(not(any(feature = "chrono_conversion", feature = "time_conversion")))]
|
||||
compile_error!(
|
||||
"Either feature \"chrono_conversion\" or \"time_conversion\" must be enabled when the feature \"ssh\" is set."
|
||||
);
|
||||
|
||||
pub use time_impl::SshTime;
|
||||
|
||||
#[cfg(feature = "time_conversion")]
|
||||
mod time_impl {
|
||||
use time::OffsetDateTime;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub struct SshTime(pub(crate) OffsetDateTime);
|
||||
|
||||
impl SshTime {
|
||||
pub fn now() -> Self {
|
||||
Self(OffsetDateTime::now_utc())
|
||||
}
|
||||
|
||||
pub fn from_timestamp(timestamp: u64) -> Self {
|
||||
Self(OffsetDateTime::from_unix_timestamp(timestamp as i64).unwrap())
|
||||
}
|
||||
|
||||
pub fn timestamp(&self) -> u64 {
|
||||
self.0.unix_timestamp() as u64
|
||||
}
|
||||
|
||||
pub fn month(&self) -> u8 {
|
||||
u8::from(self.0.month())
|
||||
}
|
||||
|
||||
pub fn day(&self) -> u8 {
|
||||
self.0.day()
|
||||
}
|
||||
|
||||
pub fn hour(&self) -> u8 {
|
||||
self.0.hour()
|
||||
}
|
||||
|
||||
pub fn minute(&self) -> u8 {
|
||||
self.0.minute()
|
||||
}
|
||||
|
||||
pub fn second(&self) -> u8 {
|
||||
self.0.second()
|
||||
}
|
||||
|
||||
pub fn year(&self) -> u16 {
|
||||
self.0.year().try_into().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SshTime> for OffsetDateTime {
|
||||
fn from(time: SshTime) -> Self {
|
||||
time.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OffsetDateTime> for SshTime {
|
||||
fn from(time: OffsetDateTime) -> Self {
|
||||
Self::from_timestamp(time.unix_timestamp() as u64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SshTime> for u64 {
|
||||
fn from(time: SshTime) -> u64 {
|
||||
time.0.unix_timestamp() as u64
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "chrono_conversion", not(feature = "time_conversion")))]
|
||||
mod time_impl {
|
||||
use chrono::{DateTime, Utc};
|
||||
pub use chrono::{Datelike, Timelike};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub struct SshTime(pub(crate) DateTime<Utc>);
|
||||
|
||||
impl SshTime {
|
||||
pub fn now() -> Self {
|
||||
SshTime(DateTime::<Utc>::from(SystemTime::now()))
|
||||
}
|
||||
|
||||
pub fn from_timestamp(timestamp: u64) -> Self {
|
||||
Self(DateTime::<Utc>::from(UNIX_EPOCH + Duration::from_secs(timestamp)))
|
||||
}
|
||||
|
||||
pub fn timestamp(&self) -> u64 {
|
||||
self.0.timestamp() as u64
|
||||
}
|
||||
|
||||
pub fn month(&self) -> u8 {
|
||||
self.0.month().try_into().unwrap()
|
||||
}
|
||||
|
||||
pub fn day(&self) -> u8 {
|
||||
self.0.day().try_into().unwrap()
|
||||
}
|
||||
|
||||
pub fn hour(&self) -> u8 {
|
||||
self.0.hour().try_into().unwrap()
|
||||
}
|
||||
|
||||
pub fn minute(&self) -> u8 {
|
||||
self.0.minute().try_into().unwrap()
|
||||
}
|
||||
|
||||
pub fn second(&self) -> u8 {
|
||||
self.0.second().try_into().unwrap()
|
||||
}
|
||||
|
||||
pub fn year(&self) -> u16 {
|
||||
self.0.year().try_into().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DateTime<Utc>> for SshTime {
|
||||
fn from(date: DateTime<Utc>) -> Self {
|
||||
Self(date)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SshTime> for DateTime<Utc> {
|
||||
fn from(time: SshTime) -> Self {
|
||||
time.0
|
||||
}
|
||||
}
|
||||
}
|
||||
+1677
File diff suppressed because it is too large
Load Diff
Vendored
+182
@@ -0,0 +1,182 @@
|
||||
use super::utils::{from_der, from_pem, from_pem_str, to_der, to_pem};
|
||||
use crate::key::{KeyError, PrivateKey, PublicKey};
|
||||
use crate::pem::{Pem, PemError};
|
||||
use crate::signature::{SignatureAlgorithm, SignatureError};
|
||||
use crate::x509::certificate::CertError;
|
||||
use crate::x509::name::DirectoryName;
|
||||
use picky_asn1::bit_string::BitString;
|
||||
use picky_asn1_der::Asn1DerError;
|
||||
use picky_asn1_x509::{CertificationRequest, CertificationRequestInfo};
|
||||
use thiserror::Error;
|
||||
|
||||
pub use picky_asn1_x509::Attribute;
|
||||
|
||||
const ELEMENT_NAME: &str = "certification request";
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum CsrError {
|
||||
/// ASN1 serialization error
|
||||
#[error("(ASN1) couldn't serialize {element}: {source}")]
|
||||
Asn1Serialization {
|
||||
element: &'static str,
|
||||
source: Asn1DerError,
|
||||
},
|
||||
|
||||
/// ASN1 deserialization error
|
||||
#[error("(ASN1) couldn't deserialize {}: {}", element, source)]
|
||||
Asn1Deserialization {
|
||||
element: &'static str,
|
||||
source: Asn1DerError,
|
||||
},
|
||||
|
||||
/// signature error
|
||||
#[error("signature error: {}", source)]
|
||||
Signature { source: SignatureError },
|
||||
|
||||
/// invalid PEM label error
|
||||
#[error("invalid PEM label: {}", label)]
|
||||
InvalidPemLabel { label: String },
|
||||
|
||||
/// invalid PEM provided
|
||||
#[error("invalid PEM provided: {source}")]
|
||||
Pem { source: PemError },
|
||||
|
||||
#[error("failed to get public key from private key: {source}")]
|
||||
PrivateKeyToPublicKey { source: KeyError },
|
||||
}
|
||||
|
||||
impl From<CertError> for CsrError {
|
||||
fn from(e: CertError) -> Self {
|
||||
match e {
|
||||
CertError::Asn1Deserialization { element, source } => CsrError::Asn1Deserialization { element, source },
|
||||
CertError::Asn1Serialization { element, source } => CsrError::Asn1Serialization { element, source },
|
||||
CertError::Pem { source } => CsrError::Pem { source },
|
||||
CertError::InvalidPemLabel { label } => CsrError::InvalidPemLabel { label },
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const CSR_PEM_LABEL: &str = "CERTIFICATE REQUEST";
|
||||
|
||||
/// Certificate Signing Request
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Csr(pub(crate) CertificationRequest);
|
||||
|
||||
impl From<CertificationRequest> for Csr {
|
||||
fn from(certification_request: CertificationRequest) -> Self {
|
||||
Self(certification_request)
|
||||
}
|
||||
}
|
||||
|
||||
impl Csr {
|
||||
pub fn from_der<T: ?Sized + AsRef<[u8]>>(der: &T) -> Result<Self, CsrError> {
|
||||
Ok(from_der(der, ELEMENT_NAME).map(Self)?)
|
||||
}
|
||||
|
||||
pub fn from_pem_str(pem_str: &str) -> Result<Self, CsrError> {
|
||||
Ok(from_pem_str(pem_str, &[CSR_PEM_LABEL], ELEMENT_NAME).map(Self)?)
|
||||
}
|
||||
|
||||
pub fn from_pem(pem: &Pem) -> Result<Self, CsrError> {
|
||||
Ok(from_pem(pem, &[CSR_PEM_LABEL], ELEMENT_NAME).map(Self)?)
|
||||
}
|
||||
|
||||
pub fn to_der(&self) -> Result<Vec<u8>, CsrError> {
|
||||
Ok(to_der(&self.0, ELEMENT_NAME)?)
|
||||
}
|
||||
|
||||
pub fn to_pem(&self) -> Result<Pem<'static>, CsrError> {
|
||||
Ok(to_pem(&self.0, CSR_PEM_LABEL, ELEMENT_NAME)?)
|
||||
}
|
||||
|
||||
pub fn generate(
|
||||
subject: DirectoryName,
|
||||
private_key: &PrivateKey,
|
||||
signature_hash_type: SignatureAlgorithm,
|
||||
) -> Result<Self, CsrError> {
|
||||
let public_key = private_key
|
||||
.to_public_key()
|
||||
.map_err(|source| CsrError::PrivateKeyToPublicKey { source })?;
|
||||
|
||||
let cri = CertificationRequestInfo::new(subject.into(), public_key.into());
|
||||
h_generate_from_cri(cri, private_key, signature_hash_type)
|
||||
}
|
||||
|
||||
pub fn generate_with_attributes(
|
||||
subject: DirectoryName,
|
||||
private_key: &PrivateKey,
|
||||
signature_hash_type: SignatureAlgorithm,
|
||||
attributes: Vec<Attribute>,
|
||||
) -> Result<Self, CsrError> {
|
||||
let public_key = private_key
|
||||
.to_public_key()
|
||||
.map_err(|source| CsrError::PrivateKeyToPublicKey { source })?;
|
||||
|
||||
let mut cri = CertificationRequestInfo::new(subject.into(), public_key.into());
|
||||
for attr in attributes {
|
||||
cri.add_attribute(attr);
|
||||
}
|
||||
h_generate_from_cri(cri, private_key, signature_hash_type)
|
||||
}
|
||||
|
||||
pub fn subject_name(&self) -> DirectoryName {
|
||||
self.0.certification_request_info.subject.clone().into()
|
||||
}
|
||||
|
||||
pub fn public_key(&self) -> &PublicKey {
|
||||
(&self.0.certification_request_info.subject_public_key_info).into()
|
||||
}
|
||||
|
||||
pub fn into_subject_infos(self) -> (DirectoryName, PublicKey) {
|
||||
(
|
||||
self.0.certification_request_info.subject.into(),
|
||||
self.0.certification_request_info.subject_public_key_info.into(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn verify(&self) -> Result<(), CsrError> {
|
||||
let hash_type = SignatureAlgorithm::from_algorithm_identifier(&self.0.signature_algorithm)
|
||||
.map_err(|e| CsrError::Signature { source: e })?;
|
||||
|
||||
let public_key = &self.0.certification_request_info.subject_public_key_info;
|
||||
|
||||
let msg =
|
||||
picky_asn1_der::to_vec(&self.0.certification_request_info).map_err(|e| CsrError::Asn1Serialization {
|
||||
source: e,
|
||||
element: "certification request info",
|
||||
})?;
|
||||
|
||||
hash_type
|
||||
.verify(&public_key.clone().into(), &msg, self.0.signature.0.payload_view())
|
||||
.map_err(|e| CsrError::Signature { source: e })?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn h_generate_from_cri(
|
||||
cri: CertificationRequestInfo,
|
||||
private_key: &PrivateKey,
|
||||
signature_hash_type: SignatureAlgorithm,
|
||||
) -> Result<Csr, CsrError> {
|
||||
let cri_der = picky_asn1_der::to_vec(&cri).map_err(|e| CsrError::Asn1Serialization {
|
||||
source: e,
|
||||
element: "certification request cri",
|
||||
})?;
|
||||
let signature = BitString::with_bytes(
|
||||
signature_hash_type
|
||||
.sign(&cri_der, private_key)
|
||||
.map_err(|e| CsrError::Signature { source: e })?,
|
||||
);
|
||||
|
||||
let signature_algorithm = signature_hash_type
|
||||
.try_into()
|
||||
.map_err(|e| CsrError::Signature { source: e })?;
|
||||
|
||||
Ok(Csr(CertificationRequest {
|
||||
certification_request_info: cri,
|
||||
signature_algorithm,
|
||||
signature: signature.into(),
|
||||
}))
|
||||
}
|
||||
Vendored
+182
@@ -0,0 +1,182 @@
|
||||
use picky_asn1::date::{Date, GeneralizedTime, UTCTime, UTCTimeRepr};
|
||||
use picky_asn1_x509::validity::Time;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct UtcDate(GeneralizedTime);
|
||||
|
||||
impl UtcDate {
|
||||
#[inline]
|
||||
pub fn new(year: u16, month: u8, day: u8, hour: u8, minute: u8, second: u8) -> Option<Self> {
|
||||
Some(Self(GeneralizedTime::new(year, month, day, hour, minute, second)?))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn ymd(year: u16, month: u8, day: u8) -> Option<Self> {
|
||||
Some(Self(GeneralizedTime::new(year, month, day, 0, 0, 0)?))
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "time_conversion", feature = "chrono_conversion"))]
|
||||
#[inline]
|
||||
pub fn now() -> Self {
|
||||
#[cfg(feature = "time_conversion")]
|
||||
{
|
||||
Self(time::OffsetDateTime::now_utc().into())
|
||||
}
|
||||
#[cfg(all(feature = "chrono_conversion", not(feature = "time_conversion")))]
|
||||
{
|
||||
Self(chrono::offset::Utc::now().into())
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn year(&self) -> u16 {
|
||||
self.0.year()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn month(&self) -> u8 {
|
||||
self.0.month()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn day(&self) -> u8 {
|
||||
self.0.day()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn hour(&self) -> u8 {
|
||||
self.0.hour()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn minute(&self) -> u8 {
|
||||
self.0.minute()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn second(&self) -> u8 {
|
||||
self.0.second()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UtcDate> for UTCTime {
|
||||
fn from(date: UtcDate) -> Self {
|
||||
unsafe {
|
||||
UTCTime::new_unchecked(
|
||||
date.0.year(),
|
||||
date.0.month(),
|
||||
date.0.day(),
|
||||
date.0.hour(),
|
||||
date.0.minute(),
|
||||
date.0.second(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UTCTime> for UtcDate {
|
||||
fn from(date: Date<UTCTimeRepr>) -> Self {
|
||||
Self(unsafe {
|
||||
GeneralizedTime::new_unchecked(
|
||||
date.year(),
|
||||
date.month(),
|
||||
date.day(),
|
||||
date.hour(),
|
||||
date.minute(),
|
||||
date.second(),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UtcDate> for GeneralizedTime {
|
||||
fn from(date: UtcDate) -> GeneralizedTime {
|
||||
date.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GeneralizedTime> for UtcDate {
|
||||
fn from(date: GeneralizedTime) -> Self {
|
||||
Self(date)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UtcDate> for Time {
|
||||
fn from(date: UtcDate) -> Self {
|
||||
// Time is used to encode validity period.
|
||||
// As per RFC 5280,
|
||||
// > CAs conforming to this profile MUST always encode certificate
|
||||
// > validity dates through the year 2049 as UTCTime; certificate validity
|
||||
// > dates in 2050 or later MUST be encoded as GeneralizedTime.
|
||||
// > Conforming applications MUST be able to process validity dates that
|
||||
// > are encoded in either UTCTime or GeneralizedTime.
|
||||
if date.year() >= 2050 {
|
||||
Self::Generalized(Into::<GeneralizedTime>::into(date).into())
|
||||
} else {
|
||||
Self::Utc(Into::<UTCTime>::into(date).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Time> for UtcDate {
|
||||
fn from(time: Time) -> Self {
|
||||
match time {
|
||||
Time::Utc(utc) => utc.0.into(),
|
||||
Time::Generalized(gen_time) => gen_time.0.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for UtcDate {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
|
||||
self.year(),
|
||||
self.month(),
|
||||
self.day(),
|
||||
self.hour(),
|
||||
self.minute(),
|
||||
self.second()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "time_conversion")]
|
||||
mod time_convert {
|
||||
use super::*;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
impl From<OffsetDateTime> for UtcDate {
|
||||
fn from(dt: OffsetDateTime) -> Self {
|
||||
Self(dt.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<UtcDate> for OffsetDateTime {
|
||||
type Error = time::error::ComponentRange;
|
||||
|
||||
fn try_from(date: UtcDate) -> Result<Self, Self::Error> {
|
||||
date.0.try_into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "chrono_conversion")]
|
||||
mod chrono_convert {
|
||||
use super::*;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
impl From<DateTime<Utc>> for UtcDate {
|
||||
fn from(dt: DateTime<Utc>) -> Self {
|
||||
Self(dt.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UtcDate> for DateTime<Utc> {
|
||||
fn from(date: UtcDate) -> Self {
|
||||
date.0.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
use crate::hash::HashAlgorithm;
|
||||
use crate::key::{KeyError, PublicKey};
|
||||
use picky_asn1::wrapper::BitStringAsn1Container;
|
||||
use picky_asn1_der::Asn1DerError;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum KeyIdGenError {
|
||||
/// asn1 serialization error
|
||||
#[error("(asn1) couldn't serialize {element}: {source}")]
|
||||
Asn1Serialization {
|
||||
element: &'static str,
|
||||
source: Asn1DerError,
|
||||
},
|
||||
|
||||
/// invalid key
|
||||
#[error("invalid key: {source}")]
|
||||
InvalidKey { source: KeyError },
|
||||
}
|
||||
|
||||
/// Describes which method to use to generate key identifiers.
|
||||
///
|
||||
/// See [RFC5280 #4](https://tools.ietf.org/html/rfc5280#section-4.2.1.2) and
|
||||
/// [RFC7093 #2](https://tools.ietf.org/html/rfc7093#section-2).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum KeyIdGenMethod {
|
||||
/// Hash the leftmost 160-bits of the
|
||||
/// SHA-256 hash of the value of the BIT STRING subjectPublicKey
|
||||
/// (excluding the tag, length, and number of unused bits).
|
||||
SPKValueHashedLeftmost160(HashAlgorithm),
|
||||
/// Hash the DER encoding of the SubjectPublicKeyInfo value.
|
||||
SPKFullDER(HashAlgorithm),
|
||||
}
|
||||
|
||||
impl KeyIdGenMethod {
|
||||
pub fn generate_from(self, public_key: &PublicKey) -> Result<Vec<u8>, KeyIdGenError> {
|
||||
use picky_asn1_x509::PublicKey as InnerPublicKey;
|
||||
|
||||
match self {
|
||||
KeyIdGenMethod::SPKValueHashedLeftmost160(hash_algo) => match &public_key.as_inner().subject_public_key {
|
||||
InnerPublicKey::Rsa(BitStringAsn1Container(rsa_pk)) => {
|
||||
let der = picky_asn1_der::to_vec(rsa_pk).map_err(|e| KeyIdGenError::Asn1Serialization {
|
||||
source: e,
|
||||
element: "RSA private key",
|
||||
})?;
|
||||
Ok(hash_algo.digest(&der)[..20].to_vec())
|
||||
}
|
||||
InnerPublicKey::Ec(bitstring) => {
|
||||
let der = bitstring.0.payload_view();
|
||||
Ok(hash_algo.digest(der)[..20].to_vec())
|
||||
}
|
||||
InnerPublicKey::Ed(bitstring) => {
|
||||
let der = bitstring.0.payload_view();
|
||||
Ok(hash_algo.digest(der)[..20].to_vec())
|
||||
}
|
||||
InnerPublicKey::Mldsa(bitstring) => {
|
||||
let der = bitstring.0.payload_view();
|
||||
Ok(hash_algo.digest(der)[..20].to_vec())
|
||||
}
|
||||
},
|
||||
KeyIdGenMethod::SPKFullDER(hash_algo) => {
|
||||
let der = public_key
|
||||
.to_der()
|
||||
.map_err(|e| KeyIdGenError::InvalidKey { source: e })?;
|
||||
Ok(hash_algo.digest(&der))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+151
@@ -0,0 +1,151 @@
|
||||
//! X.509 certificates implementation based on [RFC5280](https://tools.ietf.org/html/rfc5280)
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! use picky::key::PrivateKey;
|
||||
//! use picky::pem::{parse_pem, Pem};
|
||||
//! use picky::{signature::SignatureAlgorithm, hash::HashAlgorithm};
|
||||
//! use picky::x509::key_id_gen_method::{KeyIdGenError, KeyIdGenMethod};
|
||||
//! use picky::x509::{certificate::CertType, csr::Csr};
|
||||
//! use picky::x509::{name::DirectoryName, date::UtcDate};
|
||||
//! use picky::x509::certificate::CertificateBuilder;
|
||||
//!
|
||||
//!# use std::error::Error;
|
||||
//!# fn main() -> Result<(), Box<dyn Error>> {
|
||||
//!# let root_key_pem_str =
|
||||
//!# "-----BEGIN PRIVATE KEY-----\n\
|
||||
//!# MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCzl5R1QzQGznTe\n\
|
||||
//!# Jw1STzQz1ZOrbG58ZWdJJDOrjBB60PO+MgOIxDzn41p4W/OeqmzhxUPyzs9d3iAx\n\
|
||||
//!# RcF8gWkS8QvQXtHQtFlux61oE8GHvK9pqrEERQKjjTN/yculekauopAMQOcGDS1L\n\
|
||||
//!# t55z5oByunc/ZQYKHIZ82zfuw/VyKOdJ31/fXn2Djhnfq19YE8pgBDGxEPqCRv7S\n\
|
||||
//!# t/ThptUD+Xr3jwR2ycemq/OVLmsTETh8fYeGrcDIx2EsjL18ptPOg6rZly/UNN+w\n\
|
||||
//!# Sy8L5HlJqzDj03ytYTkgpT442TK3eGpxwX5uaHF7GuL6l9xOIDH+FuPKMt1huFWC\n\
|
||||
//!# Y+hHC8yXAgMBAAECggEATpCbM4lIs0mSOrUe0MkxbxOZ3D49P1IlEwwxRnSpqPDM\n\
|
||||
//!# EIJUpXojotYE9+5R7zFjHIltpkTu1oOb1bJMaKk8TDsDDg7M/t5G2X2k0IkD+tuC\n\
|
||||
//!# aVlwwwtlkiLtSpP+8M9DKJaZKaMi7bTKAfu757wIqSgFjsglEjtUYj6Mg3laYKvk\n\
|
||||
//!# EoR6NsOojR1tOhqGfPFJTH21ZBWDmFHhsyRHXyZOQ9I2u8pv5l33yasgmuZLZ2g3\n\
|
||||
//!# Hok6j0uU/fPpjSRTu6khVZV8+stjnFaHezTWGsz/CLqzC1EWJ1icFTb7UwafyWts\n\
|
||||
//!# EHgdMjqpu2qfqB+miFElhld4fdZIoBWTQwCdN7MnUQKBgQDnBIK6RUJYUjcX9Nei\n\
|
||||
//!# Qvf7Kl3sEY+QTQdI4na3puXjOk6eFqTUAbdsKVwU/yJyR3L+bQNTUmy3GdPlZkYt\n\
|
||||
//!# mx18fGpq9t8whDIFZG9jI5YX4/vEMOHa9zY9FI3QLpIgbifIBEIR76UVe/xakT+I\n\
|
||||
//!# bx8C1p/9msDuqVBJm7pZLIX3DQKBgQDHA2ftRkQVL0lOmvwFl56n2gbnnk0KAZ/8\n\
|
||||
//!# LyuN9FvadqzbpXQqfEUS5f1cfM4JSAXj2e9POBw2FWD1uFROIGPrPEAfSqInI6G9\n\
|
||||
//!# i/MSeBwlcvTXmWMphVGCnJwmCPlznT0Vizr5wIBImgUOpXZpxAqUQUvxOl4R4VYI\n\
|
||||
//!# gLW8iFSpMwKBgCAzacFrC/9hnlpRf4kXipdQ5XyVSgyUIBLRtjiNI/gTVYgFof4H\n\
|
||||
//!# KzkBXttyYKvLN8UtDsybbZnsGLQeGGQc/fQvJ2o3dQ6/LnW/9SK9gBteZOaI5cJu\n\
|
||||
//!# uPm0lrvQ8f9hO1xO86KqY7ll6dv56QAsdQchQXXJD2F06kMIWOY7JYU1AoGAJSKg\n\
|
||||
//!# kOjsqVtSfYV0A4MgSsfnQ+8JBxX4iXEv2mQ/g4tjg/TisU7RAM7DsS9krtyupK60\n\
|
||||
//!# 9f5NXVYt6owDxzRKEMoEWRJvIYiHlLv5lnetINvLjjOECmpjJFEe3gYMriMoE84Q\n\
|
||||
//!# Kixeg62hxfLgHqpDIxjwF8pBZWq7yAhkYRK7YXsCgYAxqdZwpOYyntEBIQa3mOE8\n\
|
||||
//!# a2mthICUXKE30OkMJdrbP3qlFECWZcvKspPHzXWiy8cP9dS2D06ax/UBZmKGXUFv\n\
|
||||
//!# D48nECaQ2glgMPLyyd+tznXed9nZVYBlOFjUQ0fvfpsmhp975n1b/UxAg1jqvigK\n\
|
||||
//!# YHWKXfDNtZ9Ci6BMYVELsw==\n\
|
||||
//!# -----END PRIVATE KEY-----";
|
||||
//!# let intermediate_key_pem_str = root_key_pem_str;
|
||||
//!# let leaf_key_pem_str = root_key_pem_str;
|
||||
//!#
|
||||
//! // Generate a self-signed root certificate
|
||||
//!
|
||||
//! // Load private key
|
||||
//! let root_key = PrivateKey::from_pem_str(root_key_pem_str)?;
|
||||
//!
|
||||
//! let root = CertificateBuilder::new()
|
||||
//! .validity(UtcDate::ymd(2020, 9, 28).unwrap(), UtcDate::ymd(2023, 9, 28).unwrap())
|
||||
//! .self_signed(DirectoryName::new_common_name("My Root CA"), &root_key)
|
||||
//! .ca(true)
|
||||
//! .signature_hash_type(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_512))
|
||||
//! .key_id_gen_method(KeyIdGenMethod::SPKFullDER(HashAlgorithm::SHA2_384))
|
||||
//! .build()?;
|
||||
//!
|
||||
//! assert_eq!(root.ty(), CertType::Root);
|
||||
//!
|
||||
//! // Generate intermediate certificate signed by root CA
|
||||
//!
|
||||
//! let intermediate_key = PrivateKey::from_pem_str(intermediate_key_pem_str)?;
|
||||
//!
|
||||
//! let intermediate = CertificateBuilder::new()
|
||||
//! .validity(UtcDate::ymd(2020, 10, 15).unwrap(), UtcDate::ymd(2021, 10, 15).unwrap())
|
||||
//! .subject(DirectoryName::new_common_name("My Authority"), intermediate_key.to_public_key().unwrap())
|
||||
//! .issuer_cert(&root, &root_key)
|
||||
//! .signature_hash_type(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_224))
|
||||
//! .key_id_gen_method(KeyIdGenMethod::SPKValueHashedLeftmost160(HashAlgorithm::SHA1))
|
||||
//! .ca(true)
|
||||
//! .pathlen(0)
|
||||
//! .build()?;
|
||||
//!
|
||||
//! assert_eq!(intermediate.ty(), CertType::Intermediate);
|
||||
//!
|
||||
//! // Generate leaf certificate signed by intermediate authority
|
||||
//!
|
||||
//! let leaf_key = PrivateKey::from_pem_str(leaf_key_pem_str)?;
|
||||
//!
|
||||
//! // A CSR can be used
|
||||
//! let csr = Csr::generate(
|
||||
//! DirectoryName::new_common_name("My Leaf"),
|
||||
//! &leaf_key,
|
||||
//! SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA1),
|
||||
//! )?;
|
||||
//!
|
||||
//! let signed_leaf = CertificateBuilder::new()
|
||||
//! .validity(UtcDate::ymd(2020, 11, 1).unwrap(), UtcDate::ymd(2021, 1, 1).unwrap())
|
||||
//! .subject_from_csr(csr)
|
||||
//! .issuer_cert(&intermediate, &intermediate_key)
|
||||
//! .signature_hash_type(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_384))
|
||||
//! .key_id_gen_method(KeyIdGenMethod::SPKFullDER(HashAlgorithm::SHA2_512))
|
||||
//! .build()?;
|
||||
//!
|
||||
//! assert_eq!(signed_leaf.ty(), CertType::Leaf);
|
||||
//!
|
||||
//! // Check leaf using CA chain
|
||||
//!
|
||||
//! let chain = [intermediate, root];
|
||||
//!
|
||||
//! signed_leaf
|
||||
//! .verifier()
|
||||
//! .chain(chain.iter())
|
||||
//! .exact_date(&UtcDate::ymd(2020, 12, 20).unwrap())
|
||||
//! .verify()?;
|
||||
//!
|
||||
//! // If `not_after` date is behind…
|
||||
//!
|
||||
//! let err = signed_leaf
|
||||
//! .verifier()
|
||||
//! .chain(chain.iter())
|
||||
//! .exact_date(&UtcDate::ymd(2021, 1, 2).unwrap())
|
||||
//! .verify()
|
||||
//! .err()
|
||||
//! .unwrap();
|
||||
//!
|
||||
//! assert_eq!(
|
||||
//! err.to_string(),
|
||||
//! "invalid certificate \'CN=My Leaf\': \
|
||||
//! certificate expired (not after: 2021-01-01 00:00:00, now: 2021-01-02 00:00:00)"
|
||||
//! );
|
||||
//!#
|
||||
//!# Ok(())
|
||||
//!# }
|
||||
//! ```
|
||||
|
||||
#[cfg(feature = "pkcs7")]
|
||||
pub mod pkcs7;
|
||||
|
||||
#[cfg(feature = "wincert")]
|
||||
pub mod wincert;
|
||||
|
||||
pub mod certificate;
|
||||
pub mod csr;
|
||||
pub mod date;
|
||||
pub mod key_id_gen_method;
|
||||
pub mod name;
|
||||
|
||||
pub use certificate::Cert;
|
||||
pub use csr::Csr;
|
||||
pub use key_id_gen_method::KeyIdGenMethod;
|
||||
pub use picky_asn1_x509::{DirectoryString, Extension, Extensions};
|
||||
|
||||
pub mod extension {
|
||||
pub use picky_asn1_x509::extension::*;
|
||||
}
|
||||
|
||||
mod utils;
|
||||
Vendored
+313
@@ -0,0 +1,313 @@
|
||||
use crate::oid::ObjectIdentifier;
|
||||
use picky_asn1::restricted_string::{CharSetError, Ia5String};
|
||||
use picky_asn1::wrapper::{Asn1SequenceOf, Ia5StringAsn1};
|
||||
use picky_asn1_x509::{
|
||||
DirectoryString, GeneralName as SerdeGeneralName, GeneralNames as SerdeGeneralNames, Name, NamePrettyFormatter,
|
||||
OtherName,
|
||||
};
|
||||
use std::fmt;
|
||||
|
||||
// === DirectoryName ===
|
||||
|
||||
pub use picky_asn1_x509::NameAttr;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct DirectoryName(Name);
|
||||
|
||||
impl Default for DirectoryName {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl DirectoryName {
|
||||
pub fn new() -> Self {
|
||||
Self(Name::new())
|
||||
}
|
||||
|
||||
pub fn new_common_name<S: Into<DirectoryString>>(name: S) -> Self {
|
||||
Self(Name::new_common_name(name))
|
||||
}
|
||||
|
||||
/// Find the first common name contained in this `Name`
|
||||
pub fn find_common_name(&self) -> Option<&DirectoryString> {
|
||||
self.0.find_common_name()
|
||||
}
|
||||
|
||||
pub fn add_attr<S: Into<DirectoryString>>(&mut self, attr: NameAttr, value: S) {
|
||||
self.0.add_attr(attr, value)
|
||||
}
|
||||
|
||||
/// Add an emailAddress attribute.
|
||||
/// NOTE: this attribute does not conform with the RFC 5280, email should be placed in SAN instead
|
||||
pub fn add_email<S: Into<Ia5StringAsn1>>(&mut self, value: S) {
|
||||
self.0.add_email(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for DirectoryName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
NamePrettyFormatter(&self.0).fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Name> for DirectoryName {
|
||||
fn from(name: Name) -> Self {
|
||||
Self(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DirectoryName> for Name {
|
||||
fn from(name: DirectoryName) -> Self {
|
||||
name.0
|
||||
}
|
||||
}
|
||||
|
||||
// === GeneralNames === //
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum GeneralName {
|
||||
OtherName(OtherName),
|
||||
RFC822Name(Ia5String),
|
||||
DNSName(Ia5String),
|
||||
DirectoryName(DirectoryName),
|
||||
EDIPartyName {
|
||||
name_assigner: Option<DirectoryString>,
|
||||
party_name: DirectoryString,
|
||||
},
|
||||
URI(Ia5String),
|
||||
IpAddress(Vec<u8>),
|
||||
RegisteredId(ObjectIdentifier),
|
||||
}
|
||||
|
||||
impl GeneralName {
|
||||
pub fn new_rfc822_name<S: Into<String>>(name: S) -> Result<Self, CharSetError> {
|
||||
Ok(Self::RFC822Name(Ia5String::from_string(name.into())?))
|
||||
}
|
||||
|
||||
pub fn new_dns_name<S: Into<String>>(name: S) -> Result<Self, CharSetError> {
|
||||
Ok(Self::DNSName(Ia5String::from_string(name.into())?))
|
||||
}
|
||||
|
||||
pub fn new_directory_name<N: Into<DirectoryName>>(name: N) -> Self {
|
||||
Self::DirectoryName(name.into())
|
||||
}
|
||||
|
||||
pub fn new_edi_party_name<PN, NA>(party_name: PN, name_assigner: Option<NA>) -> Self
|
||||
where
|
||||
PN: Into<DirectoryString>,
|
||||
NA: Into<DirectoryString>,
|
||||
{
|
||||
Self::EDIPartyName {
|
||||
name_assigner: name_assigner.map(Into::into),
|
||||
party_name: party_name.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_uri<S: Into<String>>(uri: S) -> Result<Self, CharSetError> {
|
||||
Ok(Self::URI(Ia5String::from_string(uri.into())?))
|
||||
}
|
||||
|
||||
pub fn new_ip_address<ADDR: Into<Vec<u8>>>(ip_address: ADDR) -> Self {
|
||||
Self::IpAddress(ip_address.into())
|
||||
}
|
||||
|
||||
pub fn new_registered_id<OID: Into<ObjectIdentifier>>(oid: OID) -> Self {
|
||||
Self::RegisteredId(oid.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SerdeGeneralName> for GeneralName {
|
||||
fn from(gn: SerdeGeneralName) -> Self {
|
||||
match gn {
|
||||
SerdeGeneralName::OtherName(other_name) => Self::OtherName(other_name),
|
||||
SerdeGeneralName::Rfc822Name(name) => Self::RFC822Name(name.0),
|
||||
SerdeGeneralName::DnsName(name) => Self::DNSName(name.0),
|
||||
SerdeGeneralName::DirectoryName(name) => Self::DirectoryName(name.into()),
|
||||
SerdeGeneralName::EdiPartyName(edi_pn) => Self::EDIPartyName {
|
||||
name_assigner: edi_pn.name_assigner.0.map(|na| na.0),
|
||||
party_name: edi_pn.party_name.0,
|
||||
},
|
||||
SerdeGeneralName::Uri(uri) => Self::URI(uri.0),
|
||||
SerdeGeneralName::IpAddress(ip_addr) => Self::IpAddress(ip_addr.0),
|
||||
SerdeGeneralName::RegisteredId(id) => Self::RegisteredId(id.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GeneralName> for SerdeGeneralName {
|
||||
fn from(gn: GeneralName) -> Self {
|
||||
match gn {
|
||||
GeneralName::OtherName(other_name) => SerdeGeneralName::OtherName(other_name),
|
||||
GeneralName::RFC822Name(name) => SerdeGeneralName::Rfc822Name(name.into()),
|
||||
GeneralName::DNSName(name) => SerdeGeneralName::DnsName(name.into()),
|
||||
GeneralName::DirectoryName(name) => SerdeGeneralName::DirectoryName(name.into()),
|
||||
GeneralName::EDIPartyName {
|
||||
name_assigner,
|
||||
party_name,
|
||||
} => SerdeGeneralName::new_edi_party_name(party_name, name_assigner),
|
||||
GeneralName::URI(uri) => SerdeGeneralName::Uri(uri.into()),
|
||||
GeneralName::IpAddress(ip_addr) => SerdeGeneralName::IpAddress(ip_addr.into()),
|
||||
GeneralName::RegisteredId(id) => SerdeGeneralName::RegisteredId(id.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GeneralName> for SerdeGeneralNames {
|
||||
fn from(gn: GeneralName) -> Self {
|
||||
GeneralNames::new(gn).into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps x509 `GeneralNames` into an easy to use API.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use picky::x509::name::{GeneralNames, GeneralName, DirectoryName};
|
||||
///
|
||||
/// let common_name = GeneralName::new_directory_name(DirectoryName::new_common_name("MyName"));
|
||||
/// let dns_name = GeneralName::new_dns_name("localhost").expect("invalid name string");
|
||||
/// let names = GeneralNames::from(vec![common_name, dns_name]);
|
||||
/// ```
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct GeneralNames(SerdeGeneralNames);
|
||||
|
||||
impl GeneralNames {
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use picky::x509::name::{GeneralName, GeneralNames};
|
||||
///
|
||||
/// let dns_name = GeneralName::new_dns_name("localhost").expect("invalid name string");
|
||||
/// let names = GeneralNames::new(dns_name);
|
||||
/// ```
|
||||
pub fn new<Gn: Into<GeneralName>>(gn: Gn) -> Self {
|
||||
let gn = gn.into();
|
||||
Self(Asn1SequenceOf(vec![gn.into()]))
|
||||
}
|
||||
|
||||
pub fn new_directory_name<Dn: Into<DirectoryName>>(name: Dn) -> Self {
|
||||
let gn = GeneralName::new_directory_name(name);
|
||||
Self::new(gn)
|
||||
}
|
||||
|
||||
pub fn with_directory_name<Dn: Into<DirectoryName>>(mut self, name: Dn) -> Self {
|
||||
let gn = GeneralName::new_directory_name(name);
|
||||
(self.0).0.push(gn.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn find_directory_name(&self) -> Option<DirectoryName> {
|
||||
for name in &(self.0).0 {
|
||||
if let SerdeGeneralName::DirectoryName(name) = name {
|
||||
return Some(name.clone().into());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use picky::x509::name::GeneralNames;
|
||||
/// use picky_asn1::restricted_string::Ia5String;
|
||||
///
|
||||
/// let names = GeneralNames::new_dns_name(Ia5String::new("localhost").unwrap());
|
||||
/// ```
|
||||
pub fn new_dns_name<Ia5: Into<Ia5String>>(dns_name: Ia5) -> Self {
|
||||
let gn = GeneralName::DNSName(dns_name.into());
|
||||
Self::new(gn)
|
||||
}
|
||||
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use picky::x509::name::{GeneralNames, DirectoryName};
|
||||
/// use picky_asn1::restricted_string::Ia5String;
|
||||
///
|
||||
/// let names = GeneralNames::new_directory_name(DirectoryName::new_common_name("MyName"))
|
||||
/// .with_dns_name(Ia5String::new("localhost").unwrap());
|
||||
/// ```
|
||||
pub fn with_dns_name<Ia5: Into<Ia5String>>(mut self, dns_name: Ia5) -> Self {
|
||||
let gn = GeneralName::DNSName(dns_name.into());
|
||||
(self.0).0.push(gn.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn find_dns_name(&self) -> Option<&Ia5String> {
|
||||
for name in &(self.0).0 {
|
||||
if let SerdeGeneralName::DnsName(name) = name {
|
||||
return Some(&name.0);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn add_name<Gn: Into<GeneralName>>(&mut self, name: Gn) {
|
||||
let gn = name.into();
|
||||
(self.0).0.push(gn.into());
|
||||
}
|
||||
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use picky::x509::name::{GeneralNames, GeneralName, DirectoryName};
|
||||
///
|
||||
/// let common_name = GeneralName::new_directory_name(DirectoryName::new_common_name("MyName"));
|
||||
/// let dns_name = GeneralName::new_dns_name("localhost").expect("invalid name string");
|
||||
/// let names = GeneralNames::new(common_name).with_name(dns_name);
|
||||
/// ```
|
||||
pub fn with_name<GN: Into<GeneralName>>(mut self, name: GN) -> Self {
|
||||
let gn = name.into();
|
||||
(self.0).0.push(gn.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn into_general_names(self) -> Vec<GeneralName> {
|
||||
(self.0).0.into_iter().map(|gn| gn.into()).collect()
|
||||
}
|
||||
|
||||
pub fn to_general_names(&self) -> Vec<GeneralName> {
|
||||
(self.0).0.iter().map(|gn| gn.clone().into()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SerdeGeneralNames> for GeneralNames {
|
||||
fn from(gn: SerdeGeneralNames) -> Self {
|
||||
Self(gn)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GeneralNames> for SerdeGeneralNames {
|
||||
fn from(gn: GeneralNames) -> Self {
|
||||
gn.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<GeneralName>> for GeneralNames {
|
||||
fn from(names: Vec<GeneralName>) -> Self {
|
||||
let serde_names = names.into_iter().map(|n| n.into()).collect();
|
||||
Self(Asn1SequenceOf(serde_names))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn build_and_format_directory_name() {
|
||||
let mut my_name = DirectoryName::new_common_name("CommonName");
|
||||
my_name.add_attr(NameAttr::StateOrProvinceName, "SomeState");
|
||||
my_name.add_attr(NameAttr::CountryName, "SomeCountry");
|
||||
assert_eq!(my_name.to_string(), "CN=CommonName,ST=SomeState,C=SomeCountry");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_common_name() {
|
||||
let my_name = DirectoryName::new_common_name("CommonName");
|
||||
let cn = my_name.find_common_name().unwrap();
|
||||
assert_eq!(cn.to_utf8_lossy(), "CommonName");
|
||||
}
|
||||
}
|
||||
+2558
File diff suppressed because it is too large
Load Diff
+111
@@ -0,0 +1,111 @@
|
||||
use crate::x509::pkcs7::{Pkcs7, Pkcs7Error};
|
||||
use picky_asn1_x509::content_info::ContentValue;
|
||||
use picky_asn1_x509::pkcs7::ctl::CTLEntry;
|
||||
pub use picky_asn1_x509::pkcs7::ctl::CTLEntryAttributeValues;
|
||||
use std::io;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum CtlError {
|
||||
#[error("Failed to download CTL: {description}")]
|
||||
DownloadError { description: String },
|
||||
#[error("{description}")]
|
||||
ExtractingError { description: String },
|
||||
#[error("Failed to parse CertificateTrustList: {0}")]
|
||||
FailedToParseCtl(Pkcs7Error),
|
||||
#[error(transparent)]
|
||||
IoError(#[from] io::Error),
|
||||
#[error("For CTL we expect CertificateTrustList content in EncapsulatedContentInfo, but something else")]
|
||||
IncorrectContentValue,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CertificateTrustList {
|
||||
pkcs7: Pkcs7,
|
||||
}
|
||||
|
||||
impl CertificateTrustList {
|
||||
pub fn ctl_entries(&self) -> Result<&[CTLEntry], CtlError> {
|
||||
let content_value = self
|
||||
.pkcs7
|
||||
.0
|
||||
.signed_data
|
||||
.content_info
|
||||
.content
|
||||
.as_ref()
|
||||
.expect("CTL Content should be present in EncapsulatedContentInfo");
|
||||
|
||||
let ctl = match &content_value.0 {
|
||||
ContentValue::CertificateTrustList(ctl) => ctl,
|
||||
_ => return Err(CtlError::IncorrectContentValue),
|
||||
};
|
||||
|
||||
Ok(&ctl.crl_entries.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ctl_http_fetch")]
|
||||
pub mod http_fetch {
|
||||
use super::*;
|
||||
use std::io::{Cursor, Read};
|
||||
|
||||
pub trait CtlHttpFetch {
|
||||
fn fetch() -> Result<CertificateTrustList, CtlError>;
|
||||
}
|
||||
|
||||
impl CtlHttpFetch for CertificateTrustList {
|
||||
fn fetch() -> Result<CertificateTrustList, CtlError> {
|
||||
let ctl_url: &str =
|
||||
"http://www.download.windowsupdate.com/msdownload/update/v3/static/trustedr/en/authrootstl.cab";
|
||||
|
||||
let mut cab = reqwest::blocking::get(ctl_url).map_err(|err| CtlError::DownloadError {
|
||||
description: err.to_string(),
|
||||
})?;
|
||||
|
||||
if !cab.status().is_success() {
|
||||
return Err(CtlError::DownloadError {
|
||||
description: format!("Response status code is {}", cab.status()),
|
||||
});
|
||||
}
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
cab.copy_to(&mut buffer).map_err(|err| CtlError::ExtractingError {
|
||||
description: format!("Failed to copy Response body to Vec: {err}"),
|
||||
})?;
|
||||
|
||||
let mut cabinet = cab::Cabinet::new(Cursor::new(&mut buffer)).map_err(|err| CtlError::ExtractingError {
|
||||
description: format!("Failed to parse Cabinet file: {err}"),
|
||||
})?;
|
||||
|
||||
let mut authroot = cabinet
|
||||
.read_file("authroot.stl")
|
||||
.expect("authroot.stl should be present in authrootstl.cab");
|
||||
|
||||
let mut ctl_buffer = Vec::new();
|
||||
authroot.read_to_end(&mut ctl_buffer)?;
|
||||
|
||||
let pkcs7: Pkcs7 = Pkcs7::from_der(&ctl_buffer).map_err(CtlError::FailedToParseCtl)?;
|
||||
|
||||
Ok(CertificateTrustList { pkcs7 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::x509::pkcs7::Pkcs7;
|
||||
|
||||
#[test]
|
||||
fn parse_certificate_trust_list_in_der() {
|
||||
Pkcs7::from_der(picky_test_data::CERTIFICATE_TRUST_LIST).unwrap();
|
||||
}
|
||||
|
||||
#[cfg(feature = "ctl_http_fetch")]
|
||||
#[ignore = "temporarily disabled for CI issues,which is under investigation"]
|
||||
#[test]
|
||||
fn create_ctl() {
|
||||
use super::http_fetch::CtlHttpFetch;
|
||||
CertificateTrustList::fetch().unwrap();
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
use crate::AlgorithmIdentifier;
|
||||
use crate::pem::Pem;
|
||||
use crate::x509::certificate::{Cert, CertError};
|
||||
use crate::x509::utils::{from_der, from_pem, from_pem_str, to_der, to_pem};
|
||||
use picky_asn1_der::Asn1DerError;
|
||||
use picky_asn1_x509::content_info::EncapsulatedContentInfo;
|
||||
use picky_asn1_x509::pkcs7::Pkcs7Certificate;
|
||||
use picky_asn1_x509::signed_data::CertificateChoices;
|
||||
use picky_asn1_x509::signer_info::SignerInfo;
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod authenticode;
|
||||
#[cfg(feature = "ctl")]
|
||||
pub mod ctl;
|
||||
pub mod timestamp;
|
||||
|
||||
type Pkcs7Result<T> = Result<T, Pkcs7Error>;
|
||||
|
||||
const ELEMENT_NAME: &str = "pkcs7 certificate";
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Pkcs7Error {
|
||||
#[error(transparent)]
|
||||
Cert(#[from] CertError),
|
||||
#[error(transparent)]
|
||||
Asn1DerError(#[from] Asn1DerError),
|
||||
}
|
||||
|
||||
const PKCS7_PEM_LABEL: &str = "PKCS7";
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Pkcs7(Pkcs7Certificate);
|
||||
|
||||
impl Pkcs7 {
|
||||
pub fn from_der<V: ?Sized + AsRef<[u8]>>(data: &V) -> Pkcs7Result<Self> {
|
||||
Ok(from_der(data, ELEMENT_NAME).map(Self)?)
|
||||
}
|
||||
|
||||
pub fn from_pem(pem: &Pem) -> Pkcs7Result<Self> {
|
||||
Ok(from_pem(pem, &[PKCS7_PEM_LABEL], ELEMENT_NAME).map(Self)?)
|
||||
}
|
||||
|
||||
pub fn from_pem_str(pem_str: &str) -> Pkcs7Result<Self> {
|
||||
Ok(from_pem_str(pem_str, &[PKCS7_PEM_LABEL], ELEMENT_NAME).map(Self)?)
|
||||
}
|
||||
|
||||
pub fn to_der(&self) -> Pkcs7Result<Vec<u8>> {
|
||||
Ok(to_der(&self.0, ELEMENT_NAME)?)
|
||||
}
|
||||
|
||||
pub fn to_pem(&self) -> Pkcs7Result<Pem<'static>> {
|
||||
Ok(to_pem(&self.0, PKCS7_PEM_LABEL, ELEMENT_NAME)?)
|
||||
}
|
||||
|
||||
pub fn digest_algorithms(&self) -> &[AlgorithmIdentifier] {
|
||||
self.0.signed_data.digest_algorithms.0.0.as_slice()
|
||||
}
|
||||
|
||||
pub fn signer_infos(&self) -> &[SignerInfo] {
|
||||
&self.0.signed_data.signers_infos.0
|
||||
}
|
||||
|
||||
pub fn encapsulated_content_info(&self) -> &EncapsulatedContentInfo {
|
||||
&self.0.signed_data.0.content_info
|
||||
}
|
||||
|
||||
pub fn decode_certificates(&self) -> Vec<Cert> {
|
||||
self.0
|
||||
.signed_data
|
||||
.certificates
|
||||
.0
|
||||
.0
|
||||
.iter()
|
||||
.filter_map(|cert| match cert {
|
||||
CertificateChoices::Certificate(certificate) => Cert::from_der(&certificate.0).ok(),
|
||||
CertificateChoices::Other(_) => None,
|
||||
})
|
||||
.collect::<Vec<Cert>>()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Pkcs7Certificate> for Pkcs7 {
|
||||
fn from(pkcs7_certificate: Pkcs7Certificate) -> Self {
|
||||
Pkcs7(pkcs7_certificate)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Pkcs7> for Pkcs7Certificate {
|
||||
fn from(pkcs7: Pkcs7) -> Self {
|
||||
pkcs7.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::pem::parse_pem;
|
||||
|
||||
#[test]
|
||||
fn read_pem_and_parse_certificate() {
|
||||
let pem = parse_pem(picky_test_data::PKCS7.as_bytes()).unwrap();
|
||||
Pkcs7::from_pem(&pem).unwrap();
|
||||
}
|
||||
}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
use crate::hash::HashAlgorithm;
|
||||
use crate::x509::certificate::CertError;
|
||||
use crate::x509::pkcs7::{Pkcs7, Pkcs7Error};
|
||||
use crate::x509::utils::{from_der, to_der};
|
||||
use picky_asn1::wrapper::ExplicitContextTag0;
|
||||
|
||||
use picky_asn1_x509::oids;
|
||||
use picky_asn1_x509::pkcs7::content_info::{ContentValue, EncapsulatedContentInfo};
|
||||
use picky_asn1_x509::pkcs7::signed_data::SignedData;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum TimestampError {
|
||||
#[error(transparent)]
|
||||
Asn1DerError(#[from] CertError),
|
||||
#[error(transparent)]
|
||||
Pkcs7Error(#[from] Pkcs7Error),
|
||||
#[error("Timestamp token is empty")]
|
||||
TimestampTokenEmpty,
|
||||
#[cfg(feature = "http_timestamp")]
|
||||
#[error("Failed to decode base64 response")]
|
||||
Base64DecodeError,
|
||||
#[cfg(feature = "http_timestamp")]
|
||||
#[error("Remote Authenticode TSA server responded with `{0}` status code")]
|
||||
BadResponse(reqwest::StatusCode),
|
||||
#[cfg(feature = "http_timestamp")]
|
||||
#[error("Remote Authenticode TSA server response error: {0}")]
|
||||
RemoteServerResponseError(reqwest::Error),
|
||||
#[cfg(feature = "http_timestamp")]
|
||||
#[error("Badly formatted URL")]
|
||||
BadUrl,
|
||||
}
|
||||
|
||||
pub trait Timestamper: Sized {
|
||||
fn timestamp(&self, digest: Vec<u8>, hash_algo: HashAlgorithm) -> Result<Pkcs7, TimestampError>; // hash_algo is used in RFC3161
|
||||
fn modify_signed_data(&self, token: Pkcs7, signed_data: &mut SignedData);
|
||||
}
|
||||
|
||||
#[cfg(feature = "http_timestamp")]
|
||||
pub mod http_timestamp {
|
||||
use super::*;
|
||||
use base64::engine::{Engine as _, general_purpose};
|
||||
use picky_asn1_x509::pkcs7::signer_info::{UnsignedAttribute, UnsignedAttributeValue};
|
||||
use reqwest::blocking::Client;
|
||||
use reqwest::header::{CACHE_CONTROL, CONTENT_LENGTH, CONTENT_TYPE};
|
||||
use reqwest::{Method, StatusCode, Url};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct AuthenticodeTimestamper {
|
||||
url: Url,
|
||||
}
|
||||
|
||||
impl AuthenticodeTimestamper {
|
||||
pub fn new<U: AsRef<str>>(url: U) -> Result<AuthenticodeTimestamper, TimestampError> {
|
||||
let url = Url::parse(url.as_ref()).map_err(|_| TimestampError::BadUrl)?;
|
||||
Ok(Self { url })
|
||||
}
|
||||
}
|
||||
|
||||
impl Timestamper for AuthenticodeTimestamper {
|
||||
fn timestamp(&self, digest: Vec<u8>, _: HashAlgorithm) -> Result<Pkcs7, TimestampError> {
|
||||
let timestamp_request = TimestampRequest::new(digest);
|
||||
|
||||
let client = Client::new();
|
||||
let content = general_purpose::STANDARD.encode(timestamp_request.to_der()?);
|
||||
|
||||
let request = client
|
||||
.request(Method::POST, self.url.clone())
|
||||
.header(CACHE_CONTROL, "no-cache")
|
||||
.header(CONTENT_TYPE, "application/octet-stream")
|
||||
.header(CONTENT_LENGTH, content.len())
|
||||
.body(content)
|
||||
.build()
|
||||
.expect("RequestBuilder should not panic");
|
||||
|
||||
let response = client
|
||||
.execute(request)
|
||||
.map_err(TimestampError::RemoteServerResponseError)?;
|
||||
|
||||
if response.status() != StatusCode::OK {
|
||||
return Err(TimestampError::BadResponse(response.status()));
|
||||
}
|
||||
|
||||
let mut body = response
|
||||
.bytes()
|
||||
.map_err(TimestampError::RemoteServerResponseError)?
|
||||
.to_vec();
|
||||
|
||||
body.retain(|&x| x != b'\n' && x != b'\r' && x != b'\0'); // Removing CRLF entries
|
||||
|
||||
let der = general_purpose::STANDARD
|
||||
.decode(body)
|
||||
.map_err(|_| TimestampError::Base64DecodeError)?;
|
||||
let token = Pkcs7::from_der(&der).map_err(TimestampError::Pkcs7Error)?;
|
||||
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
fn modify_signed_data(&self, token: Pkcs7, signed_data: &mut SignedData) {
|
||||
let SignedData {
|
||||
certificates,
|
||||
signers_infos,
|
||||
..
|
||||
} = token.0.signed_data.0;
|
||||
|
||||
let singer_info = signers_infos
|
||||
.0
|
||||
.first()
|
||||
.expect("Exactly one SignedInfo should be present");
|
||||
|
||||
let unsigned_attribute = UnsignedAttribute {
|
||||
ty: oids::counter_sign().into(),
|
||||
value: UnsignedAttributeValue::CounterSign(vec![singer_info.clone()].into()),
|
||||
};
|
||||
|
||||
let signer_info = signed_data
|
||||
.signers_infos
|
||||
.0
|
||||
.0
|
||||
.first_mut()
|
||||
.expect("Exactly one SignedInfo should be present");
|
||||
|
||||
signer_info.unsigned_attrs.0.0.push(unsigned_attribute);
|
||||
|
||||
signed_data.certificates.0.0.extend(certificates.0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ELEMENT_NAME: &str = "TimestampRequest";
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct TimestampRequest(picky_asn1_x509::timestamp::TimestampRequest);
|
||||
|
||||
impl TimestampRequest {
|
||||
pub fn new(digest: Vec<u8>) -> TimestampRequest {
|
||||
Self(picky_asn1_x509::timestamp::TimestampRequest {
|
||||
countersignature_type: oids::timestamp_request().into(),
|
||||
content: EncapsulatedContentInfo {
|
||||
content_type: oids::content_info_type_data().into(),
|
||||
content: Some(ContentValue::Data(digest.into()).into()),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn content(&self) -> &EncapsulatedContentInfo {
|
||||
&self.0.content
|
||||
}
|
||||
|
||||
pub fn into_content(self) -> EncapsulatedContentInfo {
|
||||
self.0.content
|
||||
}
|
||||
|
||||
pub fn digest(&self) -> &[u8] {
|
||||
if let ExplicitContextTag0(ContentValue::Data(data)) = self.0.content.content.as_ref().unwrap() {
|
||||
&data.0
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_der<V: ?Sized + AsRef<[u8]>>(data: &V) -> Result<Self, CertError> {
|
||||
from_der(data, ELEMENT_NAME).map(Self)
|
||||
}
|
||||
|
||||
pub fn to_der(&self) -> Result<Vec<u8>, CertError> {
|
||||
to_der(&self.0, ELEMENT_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TimestampRequest> for picky_asn1_x509::timestamp::TimestampRequest {
|
||||
fn from(tr: TimestampRequest) -> picky_asn1_x509::timestamp::TimestampRequest {
|
||||
tr.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<picky_asn1_x509::timestamp::TimestampRequest> for TimestampRequest {
|
||||
fn from(tr: picky_asn1_x509::timestamp::TimestampRequest) -> TimestampRequest {
|
||||
Self(tr)
|
||||
}
|
||||
}
|
||||
Vendored
+42
@@ -0,0 +1,42 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::certificate::CertError;
|
||||
use crate::pem::{Pem, parse_pem};
|
||||
|
||||
pub(super) fn from_der<'a, T, V>(data: &'a V, element: &'static str) -> Result<T, CertError>
|
||||
where
|
||||
T: Deserialize<'a>,
|
||||
V: ?Sized + AsRef<[u8]>,
|
||||
{
|
||||
picky_asn1_der::from_bytes(data.as_ref()).map_err(|e| CertError::Asn1Deserialization { source: e, element })
|
||||
}
|
||||
|
||||
pub(super) fn from_pem<'a, T: Deserialize<'a>>(
|
||||
pem: &'a Pem,
|
||||
valid_pem_labels: &[&str],
|
||||
element: &'static str,
|
||||
) -> Result<T, CertError> {
|
||||
if valid_pem_labels.iter().any(|&label| pem.label() == label) {
|
||||
from_der(pem.data(), element)
|
||||
} else {
|
||||
Err(CertError::InvalidPemLabel {
|
||||
label: pem.label().to_owned(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn from_pem_str<T>(pem_str: &str, valid_pem_labels: &[&str], element: &'static str) -> Result<T, CertError>
|
||||
where
|
||||
for<'a> T: Deserialize<'a>,
|
||||
{
|
||||
let pem = parse_pem(pem_str)?;
|
||||
from_pem(&pem, valid_pem_labels, element)
|
||||
}
|
||||
|
||||
pub(super) fn to_der<T: Serialize>(val: &T, element: &'static str) -> Result<Vec<u8>, CertError> {
|
||||
picky_asn1_der::to_vec(val).map_err(|e| CertError::Asn1Serialization { source: e, element })
|
||||
}
|
||||
|
||||
pub(super) fn to_pem<T: Serialize>(val: &T, pem_label: &str, element: &'static str) -> Result<Pem<'static>, CertError> {
|
||||
Ok(Pem::new(pem_label, to_der(val, element)?))
|
||||
}
|
||||
+291
@@ -0,0 +1,291 @@
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use std::error;
|
||||
use std::io::{self, BufReader, BufWriter, Write};
|
||||
use thiserror::Error;
|
||||
|
||||
const MINIMUM_BYTES_TO_DECODE: usize = 4 /* WinCertificate::length */ + 2 /* WinCertificate::revision */ + 2 /* WinCertificate::certificate */;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum WinCertificateError {
|
||||
#[error("Revision value is wrong(expected any of {expected}, but {got} got)")]
|
||||
WrongRevisionValue { expected: String, got: u16 },
|
||||
#[error("Certificate type is wrong(expected any of {expected}, but {got} got)")]
|
||||
WrongCertificateType { expected: String, got: u16 },
|
||||
#[error("Length is wrong({minimum} at least, but {got} got)")]
|
||||
WrongLength { minimum: usize, got: usize },
|
||||
#[error("Certificate data is empty")]
|
||||
CertificateDataIsEmpty,
|
||||
#[error(transparent)]
|
||||
Io(#[from] io::Error),
|
||||
#[error(transparent)]
|
||||
Other(#[from] Box<dyn error::Error>),
|
||||
}
|
||||
|
||||
pub type WinCertificateResult<T> = Result<T, WinCertificateError>;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct WinCertificate {
|
||||
length: u32,
|
||||
revision: RevisionType,
|
||||
certificate_type: CertificateType,
|
||||
certificate: Vec<u8>,
|
||||
}
|
||||
|
||||
impl WinCertificate {
|
||||
pub fn decode<V: ?Sized + AsRef<[u8]>>(data: &V) -> WinCertificateResult<Self> {
|
||||
if data.as_ref().len() < MINIMUM_BYTES_TO_DECODE {
|
||||
return Err(WinCertificateError::WrongLength {
|
||||
minimum: MINIMUM_BYTES_TO_DECODE,
|
||||
got: data.as_ref().len(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut buffer = BufReader::with_capacity(data.as_ref().len(), data.as_ref());
|
||||
|
||||
let length = buffer.read_u32::<LittleEndian>()?;
|
||||
|
||||
if length == 0 {
|
||||
return Err(WinCertificateError::CertificateDataIsEmpty);
|
||||
}
|
||||
|
||||
let revision = RevisionType::try_from(buffer.read_u16::<LittleEndian>()?)?;
|
||||
|
||||
let certificate_type = CertificateType::try_from(buffer.read_u16::<LittleEndian>()?)?;
|
||||
|
||||
let certificate_length = length as usize - MINIMUM_BYTES_TO_DECODE;
|
||||
let mut certificate = Vec::with_capacity(certificate_length);
|
||||
|
||||
for _ in 0..certificate_length {
|
||||
certificate.push(buffer.read_u8()?);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
length,
|
||||
revision,
|
||||
certificate_type,
|
||||
certificate,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn encode(self) -> WinCertificateResult<Vec<u8>> {
|
||||
let Self {
|
||||
length,
|
||||
revision,
|
||||
certificate_type,
|
||||
certificate,
|
||||
} = self;
|
||||
|
||||
let padding = (8 - (certificate.len() % 8)) % 8;
|
||||
|
||||
let mut buffer = BufWriter::with_capacity(length as usize + padding, Vec::new());
|
||||
|
||||
buffer.write_u32::<LittleEndian>(length + padding as u32)?;
|
||||
buffer.write_u16::<LittleEndian>(revision as u16)?;
|
||||
buffer.write_u16::<LittleEndian>(certificate_type as u16)?;
|
||||
|
||||
buffer.write_all(&certificate)?;
|
||||
buffer.write_all(&vec![0; padding])?;
|
||||
|
||||
buffer
|
||||
.into_inner()
|
||||
.map_err(|err| WinCertificateError::Other(Box::new(err) as Box<dyn error::Error>))
|
||||
}
|
||||
|
||||
pub fn from_certificate<V: Into<Vec<u8>>>(certificate: V, certificate_type: CertificateType) -> Self {
|
||||
let certificate = certificate.into();
|
||||
Self {
|
||||
// According to Authenticode_PE.docx WinCertificate::length is set to the WinCertificate::certificate length,
|
||||
// but if the length does not include other WinCertificate's fields (WinCertificate::revision,
|
||||
// WinCertificate::certificate_type and WinCertificate::length itself) sizes the signature will invalid.
|
||||
// MSDN says the same:
|
||||
// dwLength - Specifies the length, in bytes, of the signature
|
||||
// (see https://docs.microsoft.com/en-us/windows/win32/api/wintrust/ns-wintrust-win_certificate).
|
||||
length: (MINIMUM_BYTES_TO_DECODE + certificate.len()) as u32,
|
||||
revision: RevisionType::WinCertificateRevision20,
|
||||
certificate_type,
|
||||
certificate,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_certificate(&self) -> &[u8] {
|
||||
self.certificate.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
#[repr(u16)]
|
||||
pub enum RevisionType {
|
||||
WinCertificateRevision10 = 0x0100,
|
||||
WinCertificateRevision20 = 0x0200,
|
||||
}
|
||||
|
||||
impl TryFrom<u16> for RevisionType {
|
||||
type Error = WinCertificateError;
|
||||
fn try_from(value: u16) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0x0100 => Ok(RevisionType::WinCertificateRevision10),
|
||||
0x0200 => Ok(RevisionType::WinCertificateRevision20),
|
||||
_ => Err(WinCertificateError::WrongRevisionValue {
|
||||
expected: format!("{:?}", [0x0100, 0x0200]),
|
||||
got: value,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
#[repr(u16)]
|
||||
pub enum CertificateType {
|
||||
WinCertTypeX509 = 0x0001,
|
||||
WinCertTypePkcsSignedData = 0x0002,
|
||||
WinCertTypeReserved1 = 0x0003,
|
||||
WinCertTypePkcs1Sign = 0x0009,
|
||||
}
|
||||
|
||||
impl TryFrom<u16> for CertificateType {
|
||||
type Error = WinCertificateError;
|
||||
|
||||
fn try_from(value: u16) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0x0001 => Ok(CertificateType::WinCertTypeX509),
|
||||
0x0002 => Ok(CertificateType::WinCertTypePkcsSignedData),
|
||||
0x0003 => Ok(CertificateType::WinCertTypeReserved1),
|
||||
0x0009 => Ok(CertificateType::WinCertTypePkcs1Sign),
|
||||
_ => Err(WinCertificateError::WrongCertificateType {
|
||||
expected: format!("{:?}", [0x0001, 0x0002, 0x0003, 0x0009]),
|
||||
got: value,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const WINCERT_WITH_INVALID_LENGTH: [u8; 8] = [
|
||||
0x00, 0x00, 0x00, 0x00, // -> WIN_CERTIFICATE::dwLength = 0x00 = 0
|
||||
0x00, 0x01, // -> WIN_CERTIFICATE::wRevision = 0x0100 = WIN_CERT_REVISION_1_0
|
||||
0x01,
|
||||
0x00, // -> WIN_CERTIFICATE::wCertificateType = 0x01 = WIN_CERTIFICATE::WIN_CERT_TYPE_X509
|
||||
// empty WIN_CERTIFICATE::bCertificate field
|
||||
];
|
||||
|
||||
const WINCERT_WITH_ONE_BYTE_CERTIFICATE: [u8; 16] = [
|
||||
0x10, 0x00, 0x00, 0x00, // -> WIN_CERTIFICATE::dwLength = 0x10 = 16
|
||||
0x00, 0x01, // -> WIN_CERTIFICATE::wRevision = 0x0100 = WIN_CERT_REVISION_1_0
|
||||
0x01, 0x00, // -> WIN_CERTIFICATE::wCertificateType = 0x01 = WIN_CERTIFICATE::WIN_CERT_TYPE_X509
|
||||
0x01, // -> WIN_CERTIFICATE::bCertificate = bCertificate[0] = 1
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // padding
|
||||
];
|
||||
|
||||
const WINCERT_WITH_TEN_BYTES_CERTIFICATE: [u8; 24] = [
|
||||
0x18, 0x00, 0x00, 0x00, // -> WIN_CERTIFICATE::dwLength = 0x18 = 24
|
||||
0x00, 0x02, // -> WIN_CERTIFICATE::wRevision = 0x0200 = WIN_CERT_REVISION_2_0
|
||||
0x09, 0x00, // -> WIN_CERTIFICATE::wCertificateType = 0x09 = WIN_CERTIFICATE::WinCertTypePkcs1Sign
|
||||
0x01, 0x20, 0x03, 0x40, 0x05, 0x60, 0x70, 0x08, // -> WIN_CERTIFICATE::bCertificate = bCertificate[10]
|
||||
0x90, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // padding
|
||||
];
|
||||
|
||||
const WINCERT_WITH_INVALID_REVISION: [u8; 9] = [
|
||||
0x09, 0x00, 0x00, 0x00, // -> WIN_CERTIFICATE::dwLength = 0x09 = 9
|
||||
0x00, 0x03, // -> WIN_CERTIFICATE::wRevision = 0x0300(not existing)
|
||||
0x01, 0x00, // -> WIN_CERTIFICATE::wCertificateType = 0x01 = WIN_CERTIFICATE::WIN_CERT_TYPE_X509
|
||||
0x01, // -> WIN_CERTIFICATE::bCertificate = bCertificate[0] = 1
|
||||
];
|
||||
|
||||
const WINCERT_WITH_X509_CERTIFICATE: [u8; 136] = [
|
||||
0x88, 0x00, 0x00, 0x00, // -> WIN_CERTIFICATE::dwLength = 0x80 = 136
|
||||
0x00, 0x02, // -> WIN_CERTIFICATE::wRevision = 0x0200 = WIN_CERT_REVISION_2_0
|
||||
0x01, 0x00, // -> WIN_CERTIFICATE::wCertificateType = 0x01 = WIN_CERTIFICATE::WIN_CERT_TYPE_X509
|
||||
// X509 certificate
|
||||
0x0B, 0x04, 0x55, 0x03, 0x06, 0x15, 0x30, 0x17, 0x31, 0x74, 0x69, 0x72, 0x6F, 0x69, 0x72, 0x70, 0x41, 0x08,
|
||||
0x0C, 0x0A, 0x04, 0x55, 0x03, 0x06, 0x0F, 0x30, 0x11, 0x31, 0x6F, 0x72, 0x70, 0x69, 0x6E, 0x44, 0x06, 0x0C,
|
||||
0x07, 0x04, 0x55, 0x03, 0x06, 0x0D, 0x30, 0x0F, 0x31, 0x6F, 0x72, 0x70, 0x69, 0x6E, 0x44, 0x06, 0x0C, 0x08,
|
||||
0x04, 0x55, 0x03, 0x06, 0x0D, 0x30, 0x0F, 0x31, 0x41, 0x55, 0x02, 0x13, 0x06, 0x04, 0x55, 0x03, 0x06, 0x09,
|
||||
0x30, 0x0B, 0x31, 0x97, 0x81, 0x30, 0x00, 0x05, 0x0B, 0x01, 0x01, 0x0D, 0xF7, 0x86, 0x48, 0x86, 0x2A, 0x09,
|
||||
0x06, 0x0D, 0x30, 0x92, 0x72, 0xDC, 0xE9, 0x6B, 0x13, 0x8F, 0x0D, 0x06, 0xFB, 0xC1, 0xD1, 0x69, 0x97, 0x79,
|
||||
0x4B, 0xA1, 0x69, 0x63, 0x19, 0x14, 0x02, 0x02, 0x01, 0x02, 0x03, 0xA0, 0xF9, 0x03, 0x82, 0x30, 0x11, 0x06,
|
||||
0x82, 0x30,
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn decode_with_invalid_length() {
|
||||
let decoded = WinCertificate::decode(WINCERT_WITH_INVALID_LENGTH.as_ref());
|
||||
assert!(decoded.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_wincert_with_one_byte_certificate() {
|
||||
let decoded = WinCertificate::decode(WINCERT_WITH_ONE_BYTE_CERTIFICATE.as_ref()).unwrap();
|
||||
|
||||
pretty_assertions::assert_eq!(decoded.length, 16);
|
||||
pretty_assertions::assert_eq!(decoded.revision, RevisionType::WinCertificateRevision10);
|
||||
pretty_assertions::assert_eq!(decoded.certificate_type, CertificateType::WinCertTypeX509);
|
||||
pretty_assertions::assert_eq!(decoded.certificate, vec![1, 0, 0, 0, 0, 0, 0, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_into_wincert_with_one_byte_certificate() {
|
||||
let wincert = WinCertificate {
|
||||
length: 9,
|
||||
revision: RevisionType::WinCertificateRevision10,
|
||||
certificate_type: CertificateType::WinCertTypeX509,
|
||||
certificate: vec![1],
|
||||
};
|
||||
|
||||
let encoded = wincert.encode().unwrap();
|
||||
assert_eq!(encoded, WINCERT_WITH_ONE_BYTE_CERTIFICATE.to_vec());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_wincert_with_ten_bytes_certificate() {
|
||||
let decoded = WinCertificate::decode(WINCERT_WITH_TEN_BYTES_CERTIFICATE.as_ref()).unwrap();
|
||||
|
||||
pretty_assertions::assert_eq!(decoded.length, 24);
|
||||
pretty_assertions::assert_eq!(decoded.revision, RevisionType::WinCertificateRevision20);
|
||||
pretty_assertions::assert_eq!(decoded.certificate_type, CertificateType::WinCertTypePkcs1Sign);
|
||||
pretty_assertions::assert_eq!(
|
||||
decoded.certificate,
|
||||
vec![1, 32, 3, 64, 5, 96, 112, 8, 144, 1, 0, 0, 0, 0, 0, 0]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_into_wincert_with_ten_bytes_certificate() {
|
||||
let wincert = WinCertificate {
|
||||
length: 18,
|
||||
revision: RevisionType::WinCertificateRevision20,
|
||||
certificate_type: CertificateType::WinCertTypePkcs1Sign,
|
||||
certificate: vec![1, 32, 3, 64, 5, 96, 112, 8, 144, 1],
|
||||
};
|
||||
|
||||
let encoded = wincert.encode().unwrap();
|
||||
assert_eq!(encoded, WINCERT_WITH_TEN_BYTES_CERTIFICATE.to_vec());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_wincert_with_invalid_revision() {
|
||||
let decoded = WinCertificate::decode(WINCERT_WITH_INVALID_REVISION.as_ref());
|
||||
assert!(decoded.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_wincert_with_x509_certificate() {
|
||||
WinCertificate::decode(WINCERT_WITH_X509_CERTIFICATE.as_ref()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_wincert_with_x509_certificate() {
|
||||
let wincert = WinCertificate {
|
||||
length: 136,
|
||||
revision: RevisionType::WinCertificateRevision20,
|
||||
certificate_type: CertificateType::WinCertTypeX509,
|
||||
certificate: WINCERT_WITH_X509_CERTIFICATE[8..].to_vec(),
|
||||
};
|
||||
|
||||
let encoded = wincert.encode().unwrap();
|
||||
pretty_assertions::assert_eq!(encoded, WINCERT_WITH_X509_CERTIFICATE.to_vec());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user