Skip to content

Commit

Permalink
Implement PKCS8 certificate support for all three backends.
Browse files Browse the repository at this point in the history
  • Loading branch information
Goirad authored and kazk committed Dec 8, 2021
1 parent 5a695e2 commit 73fcc44
Show file tree
Hide file tree
Showing 15 changed files with 552 additions and 8 deletions.
2 changes: 1 addition & 1 deletion 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 Down
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
43 changes: 43 additions & 0 deletions examples/simple-server-pkcs8.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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 */ }
}
}
}
26 changes: 22 additions & 4 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 @@ -161,6 +160,19 @@ impl Identity {
chain: parsed.chain.into_iter().flatten().collect(),
})
}

pub fn from_pkcs8(buf: &[u8], key: &[u8]) -> Result<Identity, Error> {
let pkey = PKey::private_key_from_pem(key)?;
let mut cert_chain = X509::stack_from_pem(buf)?.into_iter();
let cert = cert_chain.next();
let chain = cert_chain.collect();
Ok(Identity {
pkey,
// an identity must have at least one certificate, the leaf cert
cert: cert.expect("at least one certificate must be provided to create an identity"),
chain,
})
}
}

#[derive(Clone)]
Expand Down Expand Up @@ -258,7 +270,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 +357,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
116 changes: 115 additions & 1 deletion src/imp/schannel.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
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::schannel_cred::{Direction, Protocol, SchannelCred};
use self::schannel::crypt_prov::{AcquireOptions, ProviderType};
use self::schannel::tls_stream;
use std::error;
use std::fmt;
Expand Down Expand Up @@ -93,6 +94,38 @@ impl Identity {

Ok(Identity { cert: identity })
}

pub fn from_pkcs8(pem: &[u8], key: &[u8]) -> Result<Identity, Error> {
let mut store = Memory::new()?.into_store();
let mut cert_iter = pem::PemBlock::new(pem).into_iter();
let leaf = cert_iter.next().expect("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 mut options = AcquireOptions::new();
options.container("schannel");
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("schannel")
.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, schannel::cert_store::CertAdd::Always)?;
}
Ok(Identity{cert: context})
}
}

#[derive(Clone)]
Expand Down Expand Up @@ -384,3 +417,84 @@ 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]]);
}
}

29 changes: 29 additions & 0 deletions src/imp/security_framework.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ use self::security_framework::os::macos::certificate::{PropertyType, SecCertific
#[cfg(not(target_os = "ios"))]
use self::security_framework::os::macos::certificate_oids::CertificateOid;
#[cfg(not(target_os = "ios"))]
use self::security_framework::os::macos::identity::SecIdentityExt;
#[cfg(not(target_os = "ios"))]
use self::security_framework::os::macos::import_export::{
ImportOptions, Pkcs12ImportOptionsExt, SecItems,
};
Expand Down Expand Up @@ -82,6 +84,33 @@ pub struct Identity {
}

impl Identity {
pub fn from_pkcs8(pem: &[u8], key: &[u8]) -> Result<Identity, Error> {
let dir = TempDir::new().unwrap();
let keychain = keychain::CreateOptions::new()
.password("password")
.create(dir.path().join("identity.keychain"))?;

let mut items = SecItems::default();

ImportOptions::new()
.filename("key.pem")
.items(&mut items)
.keychain(&keychain)
.import(&key)?;

ImportOptions::new()
.filename("chain.pem")
.items(&mut items)
.keychain(&keychain)
.import(&pem)?;

let ident = SecIdentity::with_certificate(&[keychain], &items.certificates[0])?;
Ok(Identity {
identity: ident,
chain: items.certificates
})
}

pub fn from_pkcs12(buf: &[u8], pass: &str) -> Result<Identity, Error> {
let mut imports = Identity::import_options(buf, pass)?;
let import = imports.pop().unwrap();
Expand Down
13 changes: 13 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
//! * TLS/SSL client communication
//! * TLS/SSL server communication
//! * PKCS#12 encoded identities
//! * X.509/PKCS#8 encoded identities
//! * Secure-by-default for client and server
//! * Includes hostname verification for clients
//! * Supports asynchronous I/O for both the server and the client
Expand Down Expand Up @@ -177,6 +178,18 @@ impl Identity {
let identity = imp::Identity::from_pkcs12(der, password)?;
Ok(Identity(identity))
}

/// Parses a chain of PEM encoded X509 certificates, with the leaf certificate first.
/// `key` is a PEM encoded PKCS #8 formatted private key for the leaf certificate.
///
/// The certificate chain should contain any intermediate cerficates that should be sent to
/// clients to allow them to build a chain to a trusted root.
///
/// A certificate chain here means a series of PEM encoded certificates concatenated together.
pub fn from_pkcs8(pem: &[u8], key: &[u8]) -> Result<Identity> {
let identity = imp::Identity::from_pkcs8(pem, key)?;
Ok(Identity(identity))
}
}

/// An X509 certificate.
Expand Down
Loading

0 comments on commit 73fcc44

Please sign in to comment.