Skip to content

Commit

Permalink
Merge pull request #209 from kazk/pkcs8
Browse files Browse the repository at this point in the history
Add PKCS8 Support
  • Loading branch information
sfackler authored Mar 27, 2022
2 parents 5a695e2 + 658800e commit f641a64
Show file tree
Hide file tree
Showing 8 changed files with 508 additions and 39 deletions.
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ libc = "0.2"
tempfile = "3.1.0"

[target.'cfg(target_os = "windows")'.dependencies]
schannel = "0.1.16"
schannel = "0.1.17"

[target.'cfg(not(any(target_os = "windows", target_os = "macos", target_os = "ios")))'.dependencies]
log = "0.4.5"
Expand All @@ -36,4 +36,4 @@ openssl-src = { version = "300.0.3", optional = true }

[dev-dependencies]
tempfile = "3.0"
test-cert-gen = "0.1"
test-cert-gen = "0.7"
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ native-tls = "0.2"

An example client looks like:

```rust
```rust,ignore
extern crate native_tls;
use native_tls::TlsConnector;
Expand All @@ -46,7 +46,7 @@ fn main() {

To accept connections as a server from remote clients:

```rust,no_run
```rust,ignore
extern crate native_tls;
use native_tls::{Identity, TlsAcceptor, TlsStream};
Expand Down
45 changes: 45 additions & 0 deletions examples/simple-server-pkcs8.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
extern crate native_tls;

use native_tls::{Identity, TlsAcceptor, TlsStream};
use std::fs::File;
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::sync::Arc;
use std::thread;

fn main() {
let mut cert_file = File::open("test/cert.pem").unwrap();
let mut certs = vec![];
cert_file.read_to_end(&mut certs).unwrap();
let mut key_file = File::open("test/key.pem").unwrap();
let mut key = vec![];
key_file.read_to_end(&mut key).unwrap();
let pkcs8 = Identity::from_pkcs8(&certs, &key).unwrap();

let acceptor = TlsAcceptor::new(pkcs8).unwrap();
let acceptor = Arc::new(acceptor);

let listener = TcpListener::bind("0.0.0.0:8443").unwrap();

fn handle_client(mut stream: TlsStream<TcpStream>) {
let mut buf = [0; 1024];
let read = stream.read(&mut buf).unwrap();
let received = std::str::from_utf8(&buf[0..read]).unwrap();
stream
.write_all(format!("received '{}'", received).as_bytes())
.unwrap();
}

for stream in listener.incoming() {
match stream {
Ok(stream) => {
let acceptor = acceptor.clone();
thread::spawn(move || {
let stream = acceptor.accept(stream).unwrap();
handle_client(stream);
});
}
Err(_e) => { /* connection failed */ }
}
}
}
39 changes: 34 additions & 5 deletions src/imp/openssl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use self::openssl::error::ErrorStack;
use self::openssl::hash::MessageDigest;
use self::openssl::nid::Nid;
use self::openssl::pkcs12::Pkcs12;
use self::openssl::pkey::PKey;
use self::openssl::pkey::{PKey, Private};
use self::openssl::ssl::{
self, MidHandshakeSslStream, SslAcceptor, SslConnector, SslContextBuilder, SslMethod,
SslVerifyMode,
Expand All @@ -16,7 +16,6 @@ use std::fmt;
use std::io;
use std::sync::Once;

use self::openssl::pkey::Private;
use {Protocol, TlsAcceptorBuilder, TlsConnectorBuilder};

#[cfg(have_min_max_version)]
Expand Down Expand Up @@ -117,13 +116,17 @@ fn load_android_root_certs(connector: &mut SslContextBuilder) -> Result<(), Erro
pub enum Error {
Normal(ErrorStack),
Ssl(ssl::Error, X509VerifyResult),
EmptyChain,
NotPkcs8,
}

impl error::Error for Error {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match *self {
Error::Normal(ref e) => error::Error::source(e),
Error::Ssl(ref e, _) => error::Error::source(e),
Error::EmptyChain => None,
Error::NotPkcs8 => None,
}
}
}
Expand All @@ -134,6 +137,11 @@ impl fmt::Display for Error {
Error::Normal(ref e) => fmt::Display::fmt(e, fmt),
Error::Ssl(ref e, X509VerifyResult::OK) => fmt::Display::fmt(e, fmt),
Error::Ssl(ref e, v) => write!(fmt, "{} ({})", e, v),
Error::EmptyChain => write!(
fmt,
"at least one certificate must be provided to create an identity"
),
Error::NotPkcs8 => write!(fmt, "expected PKCS#8 PEM"),
}
}
}
Expand All @@ -158,9 +166,24 @@ impl Identity {
Ok(Identity {
pkey: parsed.pkey,
cert: parsed.cert,
chain: parsed.chain.into_iter().flatten().collect(),
// > The stack is the reverse of what you might expect due to the way
// > PKCS12_parse is implemented, so we need to load it backwards.
// > https://github.com/sfackler/rust-native-tls/commit/05fb5e583be589ab63d9f83d986d095639f8ec44
chain: parsed.chain.into_iter().flatten().rev().collect(),
})
}

pub fn from_pkcs8(buf: &[u8], key: &[u8]) -> Result<Identity, Error> {
if !key.starts_with(b"-----BEGIN PRIVATE KEY-----") {
return Err(Error::NotPkcs8);
}

let pkey = PKey::private_key_from_pem(key)?;
let mut cert_chain = X509::stack_from_pem(buf)?.into_iter();
let cert = cert_chain.next().ok_or(Error::EmptyChain)?;
let chain = cert_chain.collect();
Ok(Identity { pkey, cert, chain })
}
}

#[derive(Clone)]
Expand Down Expand Up @@ -258,7 +281,10 @@ impl TlsConnector {
if let Some(ref identity) = builder.identity {
connector.set_certificate(&identity.0.cert)?;
connector.set_private_key(&identity.0.pkey)?;
for cert in identity.0.chain.iter().rev() {
for cert in identity.0.chain.iter() {
// https://www.openssl.org/docs/manmaster/man3/SSL_CTX_add_extra_chain_cert.html
// specifies that "When sending a certificate chain, extra chain certificates are
// sent in order following the end entity certificate."
connector.add_extra_chain_cert(cert.to_owned())?;
}
}
Expand Down Expand Up @@ -342,7 +368,10 @@ impl TlsAcceptor {
let mut acceptor = SslAcceptor::mozilla_intermediate(SslMethod::tls())?;
acceptor.set_private_key(&builder.identity.0.pkey)?;
acceptor.set_certificate(&builder.identity.0.cert)?;
for cert in builder.identity.0.chain.iter().rev() {
for cert in builder.identity.0.chain.iter() {
// https://www.openssl.org/docs/manmaster/man3/SSL_CTX_add_extra_chain_cert.html
// specifies that "When sending a certificate chain, extra chain certificates are
// sent in order following the end entity certificate."
acceptor.add_extra_chain_cert(cert.to_owned())?;
}
supported_protocols(builder.min_protocol, builder.max_protocol, &mut acceptor)?;
Expand Down
178 changes: 177 additions & 1 deletion src/imp/schannel.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
extern crate schannel;

use self::schannel::cert_context::{CertContext, HashAlgorithm};
use self::schannel::cert_context::{CertContext, HashAlgorithm, KeySpec};
use self::schannel::cert_store::{CertAdd, CertStore, Memory, PfxImportOptions};
use self::schannel::crypt_prov::{AcquireOptions, ProviderType};
use self::schannel::schannel_cred::{Direction, Protocol, SchannelCred};
use self::schannel::tls_stream;
use std::error;
Expand Down Expand Up @@ -93,6 +94,59 @@ impl Identity {

Ok(Identity { cert: identity })
}

pub fn from_pkcs8(pem: &[u8], key: &[u8]) -> Result<Identity, Error> {
if !key.starts_with(b"-----BEGIN PRIVATE KEY-----") {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "not a PKCS#8 key").into());
}

let mut store = Memory::new()?.into_store();
let mut cert_iter = pem::PemBlock::new(pem).into_iter();
let leaf = cert_iter.next().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"at least one certificate must be provided to create an identity",
)
})?;
let cert = CertContext::from_pem(std::str::from_utf8(leaf).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
"leaf cert contains invalid utf8",
)
})?)?;

let name = gen_container_name();
let mut options = AcquireOptions::new();
options.container(&name);
let type_ = ProviderType::rsa_full();

let mut container = match options.acquire(type_) {
Ok(container) => container,
Err(_) => options.new_keyset(true).acquire(type_)?,
};
container.import().import_pkcs8_pem(&key)?;

cert.set_key_prov_info()
.container(&name)
.type_(type_)
.keep_open(true)
.key_spec(KeySpec::key_exchange())
.set()?;
let mut context = store.add_cert(&cert, CertAdd::Always)?;

for int_cert in cert_iter {
let certificate = Certificate::from_pem(int_cert)?;
context = store.add_cert(&certificate.0, CertAdd::Always)?;
}
Ok(Identity { cert: context })
}
}

// The name of the container must be unique to have multiple active keys.
fn gen_container_name() -> String {
use std::sync::atomic::{AtomicUsize, Ordering};
static COUNTER: AtomicUsize = AtomicUsize::new(0);
format!("native-tls-{}", COUNTER.fetch_add(1, Ordering::Relaxed))
}

#[derive(Clone)]
Expand Down Expand Up @@ -384,3 +438,125 @@ impl<S: io::Read + io::Write> io::Write for TlsStream<S> {
self.0.flush()
}
}

mod pem {
/// Split data by PEM guard lines
pub struct PemBlock<'a> {
pem_block: &'a str,
cur_end: usize,
}

impl<'a> PemBlock<'a> {
pub fn new(data: &'a [u8]) -> PemBlock<'a> {
let s = ::std::str::from_utf8(data).unwrap();
PemBlock {
pem_block: s,
cur_end: s.find("-----BEGIN").unwrap_or(s.len()),
}
}
}

impl<'a> Iterator for PemBlock<'a> {
type Item = &'a [u8];
fn next(&mut self) -> Option<Self::Item> {
let last = self.pem_block.len();
if self.cur_end >= last {
return None;
}
let begin = self.cur_end;
let pos = self.pem_block[begin + 1..].find("-----BEGIN");
self.cur_end = match pos {
Some(end) => end + begin + 1,
None => last,
};
return Some(&self.pem_block[begin..self.cur_end].as_bytes());
}
}

#[test]
fn test_split() {
// Split three certs, CRLF line terminators.
assert_eq!(
PemBlock::new(
b"-----BEGIN FIRST-----\r\n-----END FIRST-----\r\n\
-----BEGIN SECOND-----\r\n-----END SECOND\r\n\
-----BEGIN THIRD-----\r\n-----END THIRD\r\n"
)
.collect::<Vec<&[u8]>>(),
vec![
b"-----BEGIN FIRST-----\r\n-----END FIRST-----\r\n" as &[u8],
b"-----BEGIN SECOND-----\r\n-----END SECOND\r\n",
b"-----BEGIN THIRD-----\r\n-----END THIRD\r\n"
]
);
// Split three certs, CRLF line terminators except at EOF.
assert_eq!(
PemBlock::new(
b"-----BEGIN FIRST-----\r\n-----END FIRST-----\r\n\
-----BEGIN SECOND-----\r\n-----END SECOND-----\r\n\
-----BEGIN THIRD-----\r\n-----END THIRD-----"
)
.collect::<Vec<&[u8]>>(),
vec![
b"-----BEGIN FIRST-----\r\n-----END FIRST-----\r\n" as &[u8],
b"-----BEGIN SECOND-----\r\n-----END SECOND-----\r\n",
b"-----BEGIN THIRD-----\r\n-----END THIRD-----"
]
);
// Split two certs, LF line terminators.
assert_eq!(
PemBlock::new(
b"-----BEGIN FIRST-----\n-----END FIRST-----\n\
-----BEGIN SECOND-----\n-----END SECOND\n"
)
.collect::<Vec<&[u8]>>(),
vec![
b"-----BEGIN FIRST-----\n-----END FIRST-----\n" as &[u8],
b"-----BEGIN SECOND-----\n-----END SECOND\n"
]
);
// Split two certs, CR line terminators.
assert_eq!(
PemBlock::new(
b"-----BEGIN FIRST-----\r-----END FIRST-----\r\
-----BEGIN SECOND-----\r-----END SECOND\r"
)
.collect::<Vec<&[u8]>>(),
vec![
b"-----BEGIN FIRST-----\r-----END FIRST-----\r" as &[u8],
b"-----BEGIN SECOND-----\r-----END SECOND\r"
]
);
// Split two certs, LF line terminators except at EOF.
assert_eq!(
PemBlock::new(
b"-----BEGIN FIRST-----\n-----END FIRST-----\n\
-----BEGIN SECOND-----\n-----END SECOND"
)
.collect::<Vec<&[u8]>>(),
vec![
b"-----BEGIN FIRST-----\n-----END FIRST-----\n" as &[u8],
b"-----BEGIN SECOND-----\n-----END SECOND"
]
);
// Split a single cert, LF line terminators.
assert_eq!(
PemBlock::new(b"-----BEGIN FIRST-----\n-----END FIRST-----\n").collect::<Vec<&[u8]>>(),
vec![b"-----BEGIN FIRST-----\n-----END FIRST-----\n" as &[u8]]
);
// Split a single cert, LF line terminators except at EOF.
assert_eq!(
PemBlock::new(b"-----BEGIN FIRST-----\n-----END FIRST-----").collect::<Vec<&[u8]>>(),
vec![b"-----BEGIN FIRST-----\n-----END FIRST-----" as &[u8]]
);
// (Don't) split garbage.
assert_eq!(
PemBlock::new(b"junk").collect::<Vec<&[u8]>>(),
Vec::<&[u8]>::new()
);
assert_eq!(
PemBlock::new(b"junk-----BEGIN garbage").collect::<Vec<&[u8]>>(),
vec![b"-----BEGIN garbage" as &[u8]]
);
}
}
Loading

0 comments on commit f641a64

Please sign in to comment.