use std::{
fmt,
fs::File,
io::Read,
path::{Path, PathBuf},
};
use lookup::lookup_v2::OptionalValuePath;
use openssl::{
pkcs12::{ParsedPkcs12_2, Pkcs12},
pkey::{PKey, Private},
ssl::{select_next_proto, AlpnError, ConnectConfiguration, SslContextBuilder, SslVerifyMode},
stack::Stack,
x509::{store::X509StoreBuilder, X509},
};
use snafu::ResultExt;
use vector_config::configurable_component;
use super::{
AddCertToStoreSnafu, AddExtraChainCertSnafu, CaStackPushSnafu, DerExportSnafu,
EncodeAlpnProtocolsSnafu, FileOpenFailedSnafu, FileReadFailedSnafu, MaybeTls, NewCaStackSnafu,
NewStoreBuilderSnafu, ParsePkcs12Snafu, Pkcs12Snafu, PrivateKeyParseSnafu, Result,
SetAlpnProtocolsSnafu, SetCertificateSnafu, SetPrivateKeySnafu, SetVerifyCertSnafu, TlsError,
TlsIdentitySnafu, X509ParseSnafu,
};
pub const PEM_START_MARKER: &str = "-----BEGIN ";
pub const TEST_PEM_CA_PATH: &str = "tests/data/ca/certs/ca.cert.pem";
pub const TEST_PEM_INTERMEDIATE_CA_PATH: &str =
"tests/data/ca/intermediate_server/certs/ca-chain.cert.pem";
pub const TEST_PEM_CRT_PATH: &str =
"tests/data/ca/intermediate_server/certs/localhost-chain.cert.pem";
pub const TEST_PEM_KEY_PATH: &str = "tests/data/ca/intermediate_server/private/localhost.key.pem";
pub const TEST_PEM_CLIENT_CRT_PATH: &str =
"tests/data/ca/intermediate_client/certs/localhost-chain.cert.pem";
pub const TEST_PEM_CLIENT_KEY_PATH: &str =
"tests/data/ca/intermediate_client/private/localhost.key.pem";
#[configurable_component]
#[configurable(metadata(docs::advanced))]
#[derive(Clone, Debug, Default)]
pub struct TlsEnableableConfig {
pub enabled: Option<bool>,
#[serde(flatten)]
pub options: TlsConfig,
}
impl TlsEnableableConfig {
pub fn enabled() -> Self {
Self {
enabled: Some(true),
..Self::default()
}
}
pub fn test_config() -> Self {
Self {
enabled: Some(true),
options: TlsConfig::test_config(),
}
}
}
#[configurable_component]
#[derive(Clone, Debug, Default)]
pub struct TlsSourceConfig {
pub client_metadata_key: Option<OptionalValuePath>,
#[serde(flatten)]
pub tls_config: TlsEnableableConfig,
}
#[configurable_component]
#[configurable(metadata(docs::advanced))]
#[derive(Clone, Debug, Default)]
#[serde(deny_unknown_fields)]
pub struct TlsConfig {
pub verify_certificate: Option<bool>,
pub verify_hostname: Option<bool>,
#[configurable(metadata(docs::examples = "h2"))]
pub alpn_protocols: Option<Vec<String>>,
#[serde(alias = "ca_path")]
#[configurable(metadata(docs::examples = "/path/to/certificate_authority.crt"))]
#[configurable(metadata(docs::human_name = "CA File Path"))]
pub ca_file: Option<PathBuf>,
#[serde(alias = "crt_path")]
#[configurable(metadata(docs::examples = "/path/to/host_certificate.crt"))]
#[configurable(metadata(docs::human_name = "Certificate File Path"))]
pub crt_file: Option<PathBuf>,
#[serde(alias = "key_path")]
#[configurable(metadata(docs::examples = "/path/to/host_certificate.key"))]
#[configurable(metadata(docs::human_name = "Key File Path"))]
pub key_file: Option<PathBuf>,
#[configurable(metadata(docs::examples = "${KEY_PASS_ENV_VAR}"))]
#[configurable(metadata(docs::examples = "PassWord1"))]
#[configurable(metadata(docs::human_name = "Key File Password"))]
pub key_pass: Option<String>,
#[serde(alias = "server_name")]
#[configurable(metadata(docs::examples = "www.example.com"))]
#[configurable(metadata(docs::human_name = "Server Name"))]
pub server_name: Option<String>,
}
impl TlsConfig {
pub fn test_config() -> Self {
Self {
ca_file: Some(TEST_PEM_CA_PATH.into()),
crt_file: Some(TEST_PEM_CRT_PATH.into()),
key_file: Some(TEST_PEM_KEY_PATH.into()),
..Self::default()
}
}
}
#[derive(Clone, Default)]
pub struct TlsSettings {
verify_certificate: bool,
pub(super) verify_hostname: bool,
authorities: Vec<X509>,
pub(super) identity: Option<IdentityStore>, alpn_protocols: Option<Vec<u8>>,
server_name: Option<String>,
}
#[derive(Clone)]
pub(super) struct IdentityStore(Vec<u8>, String);
impl TlsSettings {
pub fn from_options(options: &Option<TlsConfig>) -> Result<Self> {
Self::from_options_base(options, false)
}
pub(super) fn from_options_base(options: &Option<TlsConfig>, for_server: bool) -> Result<Self> {
let default = TlsConfig::default();
let options = options.as_ref().unwrap_or(&default);
if !for_server {
if options.verify_certificate == Some(false) {
warn!(
"The `verify_certificate` option is DISABLED, this may lead to security vulnerabilities."
);
}
if options.verify_hostname == Some(false) {
warn!("The `verify_hostname` option is DISABLED, this may lead to security vulnerabilities.");
}
}
Ok(Self {
verify_certificate: options.verify_certificate.unwrap_or(!for_server),
verify_hostname: options.verify_hostname.unwrap_or(!for_server),
authorities: options.load_authorities()?,
identity: options.load_identity()?,
alpn_protocols: options.parse_alpn_protocols()?,
server_name: options.server_name.clone(),
})
}
fn identity(&self) -> Option<ParsedPkcs12_2> {
self.identity.as_ref().map(|identity| {
Pkcs12::from_der(&identity.0)
.expect("Could not build PKCS#12 archive from parsed data")
.parse2(&identity.1)
.expect("Could not parse stored PKCS#12 archive")
})
}
pub fn identity_pem(&self) -> Option<(Vec<u8>, Vec<u8>)> {
self.identity().map(|identity| {
let mut cert = identity
.cert
.expect("Identity required")
.to_pem()
.expect("Invalid stored identity");
if let Some(chain) = identity.ca {
for authority in chain {
cert.extend(
authority
.to_pem()
.expect("Invalid stored identity chain certificate"),
);
}
}
let key = identity
.pkey
.expect("Private key required")
.private_key_to_pem_pkcs8()
.expect("Invalid stored private key");
(cert, key)
})
}
pub fn authorities_pem(&self) -> impl Iterator<Item = Vec<u8>> + '_ {
self.authorities.iter().map(|authority| {
authority
.to_pem()
.expect("Invalid stored authority certificate")
})
}
pub(super) fn apply_context(&self, context: &mut SslContextBuilder) -> Result<()> {
self.apply_context_base(context, false)
}
pub(super) fn apply_context_base(
&self,
context: &mut SslContextBuilder,
for_server: bool,
) -> Result<()> {
context.set_verify(if self.verify_certificate {
SslVerifyMode::PEER | SslVerifyMode::FAIL_IF_NO_PEER_CERT
} else {
SslVerifyMode::NONE
});
if let Some(identity) = self.identity() {
if let Some(cert) = &identity.cert {
context.set_certificate(cert).context(SetCertificateSnafu)?;
}
if let Some(pkey) = &identity.pkey {
context.set_private_key(pkey).context(SetPrivateKeySnafu)?;
}
if let Some(chain) = identity.ca {
for cert in chain {
context
.add_extra_chain_cert(cert)
.context(AddExtraChainCertSnafu)?;
}
}
}
if self.authorities.is_empty() {
debug!("Fetching system root certs.");
#[cfg(windows)]
load_windows_certs(context).unwrap();
#[cfg(target_os = "macos")]
load_mac_certs(context).unwrap();
} else {
let mut store = X509StoreBuilder::new().context(NewStoreBuilderSnafu)?;
for authority in &self.authorities {
store
.add_cert(authority.clone())
.context(AddCertToStoreSnafu)?;
}
context
.set_verify_cert_store(store.build())
.context(SetVerifyCertSnafu)?;
}
if let Some(alpn) = &self.alpn_protocols {
if for_server {
let server_proto = alpn.clone();
context.set_alpn_select_callback(move |_, client_proto| {
select_next_proto(server_proto.as_slice(), client_proto).ok_or(AlpnError::NOACK)
});
} else {
context
.set_alpn_protos(alpn.as_slice())
.context(SetAlpnProtocolsSnafu)?;
}
}
Ok(())
}
pub fn apply_connect_configuration(
&self,
connection: &mut ConnectConfiguration,
) -> std::result::Result<(), openssl::error::ErrorStack> {
connection.set_verify_hostname(self.verify_hostname);
if let Some(server_name) = &self.server_name {
connection.set_use_server_name_indication(false);
connection.set_hostname(server_name)?;
}
Ok(())
}
}
impl TlsConfig {
fn load_authorities(&self) -> Result<Vec<X509>> {
match &self.ca_file {
None => Ok(vec![]),
Some(filename) => {
let (data, filename) = open_read(filename, "certificate")?;
der_or_pem(
data,
|der| X509::from_der(&der).map(|x509| vec![x509]),
|pem| {
pem.match_indices(PEM_START_MARKER)
.map(|(start, _)| X509::from_pem(pem[start..].as_bytes()))
.collect()
},
)
.with_context(|_| X509ParseSnafu { filename })
}
}
}
fn load_identity(&self) -> Result<Option<IdentityStore>> {
match (&self.crt_file, &self.key_file) {
(None, Some(_)) => Err(TlsError::MissingCrtKeyFile),
(None, None) => Ok(None),
(Some(filename), _) => {
let (data, filename) = open_read(filename, "certificate")?;
der_or_pem(
data,
|der| self.parse_pkcs12_identity(der),
|pem| self.parse_pem_identity(&pem, &filename),
)
}
}
}
fn parse_alpn_protocols(&self) -> Result<Option<Vec<u8>>> {
match &self.alpn_protocols {
None => Ok(None),
Some(protocols) => {
let mut data: Vec<u8> = Vec::new();
for str in protocols {
data.push(str.len().try_into().context(EncodeAlpnProtocolsSnafu)?);
data.append(&mut str.clone().into_bytes());
}
Ok(Some(data))
}
}
}
fn parse_pem_identity(&self, pem: &str, crt_file: &Path) -> Result<Option<IdentityStore>> {
match &self.key_file {
None => Err(TlsError::MissingKey),
Some(key_file) => {
let name = crt_file.to_string_lossy().to_string();
let mut crt_stack = X509::stack_from_pem(pem.as_bytes())
.with_context(|_| X509ParseSnafu { filename: crt_file })?
.into_iter();
let crt = crt_stack.next().ok_or(TlsError::MissingCertificate)?;
let key = load_key(key_file, &self.key_pass)?;
let mut ca_stack = Stack::new().context(NewCaStackSnafu)?;
for intermediate in crt_stack {
ca_stack.push(intermediate).context(CaStackPushSnafu)?;
}
let pkcs12 = Pkcs12::builder()
.ca(ca_stack)
.name(&name)
.pkey(&key)
.cert(&crt)
.build2("")
.context(Pkcs12Snafu)?;
let identity = pkcs12.to_der().context(DerExportSnafu)?;
pkcs12.parse2("").context(TlsIdentitySnafu)?;
Ok(Some(IdentityStore(identity, String::new())))
}
}
}
fn parse_pkcs12_identity(&self, der: Vec<u8>) -> Result<Option<IdentityStore>> {
let pkcs12 = Pkcs12::from_der(&der).context(ParsePkcs12Snafu)?;
let key_pass = self.key_pass.as_deref().unwrap_or("");
pkcs12.parse2(key_pass).context(ParsePkcs12Snafu)?;
Ok(Some(IdentityStore(der, key_pass.to_string())))
}
}
#[cfg(windows)]
fn load_windows_certs(builder: &mut SslContextBuilder) -> Result<()> {
use super::SchannelSnafu;
let mut store = X509StoreBuilder::new().context(NewStoreBuilderSnafu)?;
let current_user_store =
schannel::cert_store::CertStore::open_current_user("ROOT").context(SchannelSnafu)?;
for cert in current_user_store.certs() {
let cert = cert.to_der().to_vec();
let cert = X509::from_der(&cert[..]).context(super::X509SystemParseSnafu)?;
store.add_cert(cert).context(AddCertToStoreSnafu)?;
}
builder
.set_verify_cert_store(store.build())
.context(SetVerifyCertSnafu)?;
Ok(())
}
#[cfg(target_os = "macos")]
fn load_mac_certs(builder: &mut SslContextBuilder) -> Result<()> {
use std::collections::HashMap;
use security_framework::trust_settings::{Domain, TrustSettings, TrustSettingsForCertificate};
use super::SecurityFrameworkSnafu;
let mut store = X509StoreBuilder::new().context(NewStoreBuilderSnafu)?;
let mut all_certs = HashMap::new();
for domain in &[Domain::User, Domain::Admin, Domain::System] {
let ts = TrustSettings::new(*domain);
for cert in ts.iter().context(SecurityFrameworkSnafu)? {
let trusted = ts
.tls_trust_settings_for_certificate(&cert)
.context(SecurityFrameworkSnafu)?
.unwrap_or(TrustSettingsForCertificate::TrustRoot);
all_certs.entry(cert.to_der()).or_insert(trusted);
}
}
for (cert, trusted) in all_certs {
if matches!(
trusted,
TrustSettingsForCertificate::TrustRoot | TrustSettingsForCertificate::TrustAsRoot
) {
let cert = X509::from_der(&cert[..]).context(super::X509SystemParseSnafu)?;
store.add_cert(cert).context(AddCertToStoreSnafu)?;
}
}
builder
.set_verify_cert_store(store.build())
.context(SetVerifyCertSnafu)?;
Ok(())
}
impl fmt::Debug for TlsSettings {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TlsSettings")
.field("verify_certificate", &self.verify_certificate)
.field("verify_hostname", &self.verify_hostname)
.finish_non_exhaustive()
}
}
pub type MaybeTlsSettings = MaybeTls<(), TlsSettings>;
impl MaybeTlsSettings {
pub fn enable_client() -> Result<Self> {
let tls = TlsSettings::from_options_base(&None, false)?;
Ok(Self::Tls(tls))
}
pub fn tls_client(config: &Option<TlsConfig>) -> Result<Self> {
Ok(Self::Tls(TlsSettings::from_options_base(config, false)?))
}
pub fn from_config(config: &Option<TlsEnableableConfig>, for_server: bool) -> Result<Self> {
match config {
None => Ok(Self::Raw(())), Some(config) => {
if config.enabled.unwrap_or(false) {
let tls =
TlsSettings::from_options_base(&Some(config.options.clone()), for_server)?;
match (for_server, &tls.identity) {
(true, None) => Err(TlsError::MissingRequiredIdentity),
_ => Ok(Self::Tls(tls)),
}
} else {
Ok(Self::Raw(())) }
}
}
}
pub const fn http_protocol_name(&self) -> &'static str {
match self {
MaybeTls::Raw(()) => "http",
MaybeTls::Tls(_) => "https",
}
}
}
impl From<TlsSettings> for MaybeTlsSettings {
fn from(tls: TlsSettings) -> Self {
Self::Tls(tls)
}
}
fn load_key(filename: &Path, pass_phrase: &Option<String>) -> Result<PKey<Private>> {
let (data, filename) = open_read(filename, "key")?;
match pass_phrase {
None => der_or_pem(
data,
|der| PKey::private_key_from_der(&der),
|pem| PKey::private_key_from_pem(pem.as_bytes()),
)
.with_context(|_| PrivateKeyParseSnafu { filename }),
Some(phrase) => der_or_pem(
data,
|der| PKey::private_key_from_pkcs8_passphrase(&der, phrase.as_bytes()),
|pem| PKey::private_key_from_pem_passphrase(pem.as_bytes(), phrase.as_bytes()),
)
.with_context(|_| PrivateKeyParseSnafu { filename }),
}
}
fn der_or_pem<T>(data: Vec<u8>, der_fn: impl Fn(Vec<u8>) -> T, pem_fn: impl Fn(String) -> T) -> T {
match String::from_utf8(data) {
Ok(text) => match text.find(PEM_START_MARKER) {
Some(_) => pem_fn(text),
None => der_fn(text.into_bytes()),
},
Err(err) => der_fn(err.into_bytes()),
}
}
fn open_read(filename: &Path, note: &'static str) -> Result<(Vec<u8>, PathBuf)> {
if let Some(filename) = filename.to_str() {
if filename.contains(PEM_START_MARKER) {
return Ok((Vec::from(filename), "inline text".into()));
}
}
let mut text = Vec::<u8>::new();
File::open(filename)
.with_context(|_| FileOpenFailedSnafu { note, filename })?
.read_to_end(&mut text)
.with_context(|_| FileReadFailedSnafu { note, filename })?;
Ok((text, filename.into()))
}
#[cfg(test)]
mod test {
use super::*;
const TEST_PKCS12_PATH: &str = "tests/data/ca/intermediate_client/private/localhost.p12";
const TEST_PEM_CRT_BYTES: &[u8] =
include_bytes!("../../../../tests/data/ca/intermediate_server/certs/localhost.cert.pem");
const TEST_PEM_KEY_BYTES: &[u8] =
include_bytes!("../../../../tests/data/ca/intermediate_server/private/localhost.key.pem");
#[test]
fn parse_alpn_protocols() {
let options = TlsConfig {
alpn_protocols: Some(vec![String::from("h2")]),
..Default::default()
};
let settings =
TlsSettings::from_options(&Some(options)).expect("Failed to parse alpn_protocols");
assert_eq!(settings.alpn_protocols, Some(vec![2, 104, 50]));
}
#[test]
fn from_options_pkcs12() {
let _provider = openssl::provider::Provider::try_load(None, "legacy", true).unwrap();
let options = TlsConfig {
crt_file: Some(TEST_PKCS12_PATH.into()),
key_pass: Some("NOPASS".into()),
..Default::default()
};
let settings =
TlsSettings::from_options(&Some(options)).expect("Failed to load PKCS#12 certificate");
assert!(settings.identity.is_some());
assert_eq!(settings.authorities.len(), 0);
}
#[test]
fn from_options_pem() {
let options = TlsConfig {
crt_file: Some(TEST_PEM_CRT_PATH.into()),
key_file: Some(TEST_PEM_KEY_PATH.into()),
..Default::default()
};
let settings =
TlsSettings::from_options(&Some(options)).expect("Failed to load PEM certificate");
assert!(settings.identity.is_some());
assert_eq!(settings.authorities.len(), 0);
}
#[test]
fn from_options_inline_pem() {
let crt = String::from_utf8(TEST_PEM_CRT_BYTES.to_vec()).unwrap();
let key = String::from_utf8(TEST_PEM_KEY_BYTES.to_vec()).unwrap();
let options = TlsConfig {
crt_file: Some(crt.into()),
key_file: Some(key.into()),
..Default::default()
};
let settings =
TlsSettings::from_options(&Some(options)).expect("Failed to load PEM certificate");
assert!(settings.identity.is_some());
assert_eq!(settings.authorities.len(), 0);
}
#[test]
fn from_options_ca() {
let options = TlsConfig {
ca_file: Some(TEST_PEM_CA_PATH.into()),
..Default::default()
};
let settings = TlsSettings::from_options(&Some(options))
.expect("Failed to load authority certificate");
assert!(settings.identity.is_none());
assert_eq!(settings.authorities.len(), 1);
}
#[test]
fn from_options_inline_ca() {
let ca = String::from_utf8(
include_bytes!("../../../../tests/data/ca/certs/ca.cert.pem").to_vec(),
)
.unwrap();
let options = TlsConfig {
ca_file: Some(ca.into()),
..Default::default()
};
let settings = TlsSettings::from_options(&Some(options))
.expect("Failed to load authority certificate");
assert!(settings.identity.is_none());
assert_eq!(settings.authorities.len(), 1);
}
#[test]
fn from_options_intermediate_ca() {
let options = TlsConfig {
ca_file: Some("tests/data/ca/intermediate_server/certs/ca-chain.cert.pem".into()),
..Default::default()
};
let settings = TlsSettings::from_options(&Some(options))
.expect("Failed to load authority certificate");
assert!(settings.identity.is_none());
assert_eq!(settings.authorities.len(), 2);
}
#[test]
fn from_options_multi_ca() {
let options = TlsConfig {
ca_file: Some("tests/data/Multi_CA.crt".into()),
..Default::default()
};
let settings = TlsSettings::from_options(&Some(options))
.expect("Failed to load authority certificate");
assert!(settings.identity.is_none());
assert_eq!(settings.authorities.len(), 2);
}
#[test]
fn from_options_none() {
let settings = TlsSettings::from_options(&None).expect("Failed to generate null settings");
assert!(settings.identity.is_none());
assert_eq!(settings.authorities.len(), 0);
}
#[test]
fn from_options_bad_certificate() {
let options = TlsConfig {
key_file: Some(TEST_PEM_KEY_PATH.into()),
..Default::default()
};
let error = TlsSettings::from_options(&Some(options))
.expect_err("from_options failed to check certificate");
assert!(matches!(error, TlsError::MissingCrtKeyFile));
let options = TlsConfig {
crt_file: Some(TEST_PEM_CRT_PATH.into()),
..Default::default()
};
let _error = TlsSettings::from_options(&Some(options))
.expect_err("from_options failed to check certificate");
}
#[test]
fn from_config_none() {
assert!(MaybeTlsSettings::from_config(&None, true).unwrap().is_raw());
assert!(MaybeTlsSettings::from_config(&None, false)
.unwrap()
.is_raw());
}
#[test]
fn from_config_not_enabled() {
assert!(settings_from_config(None, false, false, true).is_raw());
assert!(settings_from_config(None, false, false, false).is_raw());
assert!(settings_from_config(Some(false), false, false, true).is_raw());
assert!(settings_from_config(Some(false), false, false, false).is_raw());
}
#[test]
fn from_config_fails_without_certificate() {
let config = make_config(Some(true), false, false);
let error = MaybeTlsSettings::from_config(&Some(config), true)
.expect_err("from_config failed to check for a certificate");
assert!(matches!(error, TlsError::MissingRequiredIdentity));
}
#[test]
fn from_config_with_certificate() {
let config = settings_from_config(Some(true), true, true, true);
assert!(config.is_tls());
}
fn settings_from_config(
enabled: Option<bool>,
set_crt: bool,
set_key: bool,
for_server: bool,
) -> MaybeTlsSettings {
let config = make_config(enabled, set_crt, set_key);
MaybeTlsSettings::from_config(&Some(config), for_server)
.expect("Failed to generate settings from config")
}
fn make_config(enabled: Option<bool>, set_crt: bool, set_key: bool) -> TlsEnableableConfig {
TlsEnableableConfig {
enabled,
options: TlsConfig {
crt_file: set_crt.then(|| TEST_PEM_CRT_PATH.into()),
key_file: set_key.then(|| TEST_PEM_KEY_PATH.into()),
..Default::default()
},
}
}
}