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:
Andy Dixon
2026-10-12 10:50:24 +01:00
parent 6a4d7381d9
commit 50c0104d57
227 changed files with 66687 additions and 20 deletions
+101
View File
@@ -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
View File
@@ -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"
);
}
}
}
File diff suppressed because it is too large Load Diff
+143
View File
@@ -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;
+1602
View File
File diff suppressed because it is too large Load Diff
+767
View File
@@ -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);
}
}
+668
View File
@@ -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");
}
}
+690
View File
@@ -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);
}
}
+14
View File
@@ -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;
+396
View File
@@ -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))
}
}
+240
View File
@@ -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());
}
}
+1299
View File
File diff suppressed because it is too large Load Diff
+31
View File
@@ -0,0 +1,31 @@
//! [![Crates.io](https://img.shields.io/crates/v/picky.svg)](https://crates.io/crates/picky)
//! [![docs.rs](https://docs.rs/picky/badge.svg)](https://docs.rs/picky)
//! ![Crates.io](https://img.shields.io/crates/l/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};
+292
View File
@@ -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
View File
@@ -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
View File
@@ -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 &params.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 &params.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(&params, password, &data).unwrap();
let decrypted = decrypt_pbes2(&params, password, &encrypted).unwrap();
assert_eq!(decrypted, data);
}
}
+196
View File
@@ -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
}
}
+614
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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,
}
}
}
+53
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
}
+18
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
}
+633
View File
@@ -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();
}
}
File diff suppressed because it is too large Load Diff
+595
View File
@@ -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]);
}
}
+574
View File
@@ -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]);
}
}
+115
View File
@@ -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
View File
@@ -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
View File
@@ -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");
}
}
+131
View File
@@ -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
}
}
}
File diff suppressed because it is too large Load Diff
+182
View File
@@ -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(),
}))
}
+182
View File
@@ -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
View File
@@ -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))
}
}
}
}
+151
View File
@@ -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;
+313
View File
@@ -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");
}
}
File diff suppressed because it is too large Load Diff
+111
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
}
+42
View File
@@ -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
View File
@@ -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());
}
}