From 34f833c495479f1666c7fc72a7084356678bbd78 Mon Sep 17 00:00:00 2001 From: mrohera Date: Wed, 31 Jul 2019 18:28:31 -0700 Subject: [PATCH] Use file URIs for X.509 identity certificates (#1487) - Modify the config.yaml to accept file URIs for X.509 identity certificates. - Support file paths and URIs for Edge gateway certificates for backwards compatibility - Support both URI and file paths for identity certificates and the Edge gateway certificates in the Windows installer script. --- edgelet/Cargo.lock | 1 + edgelet/contrib/config/linux/config.yaml | 74 +++-- .../contrib/config/linux/debian/config.yaml | 74 +++-- edgelet/contrib/config/windows/config.yaml | 56 +++- edgelet/edgelet-core/src/error.rs | 24 ++ edgelet/edgelet-core/src/settings.rs | 298 +++++++++++++++++- edgelet/edgelet-docker/Cargo.toml | 1 + edgelet/edgelet-docker/src/settings.rs | 254 ++++++++++++--- .../test/linux/bad_settings.dps.x509.1.yaml | 4 +- .../test/linux/bad_settings.dps.x509.2.yaml | 4 +- ...09.2.yaml => bad_settings.dps.x509.3.yaml} | 4 +- ...09.1.yaml => bad_settings.dps.x509.4.yaml} | 5 +- ...yaml => sample_settings.tg.filepaths.yaml} | 7 +- .../test/windows/bad_settings.dps.x509.1.yaml | 4 +- .../test/windows/bad_settings.dps.x509.2.yaml | 4 +- ...09.2.yaml => bad_settings.dps.x509.3.yaml} | 4 +- ...9.1.yaml => bad_settings.dps.x509.4..yaml} | 5 +- ...yaml => sample_settings.tg.filepaths.yaml} | 7 +- edgelet/iotedge/src/check/mod.rs | 170 +++++----- edgelet/iotedged/src/error.rs | 10 + edgelet/iotedged/src/lib.rs | 164 ++++++---- .../windows/setup/IotEdgeSecurityDaemon.ps1 | 67 ++-- .../details/IotedgedLinux.cs | 6 +- 23 files changed, 936 insertions(+), 311 deletions(-) rename edgelet/edgelet-docker/test/linux/{sample_settings.dps.x509.2.yaml => bad_settings.dps.x509.3.yaml} (87%) rename edgelet/edgelet-docker/test/linux/{sample_settings.dps.x509.1.yaml => bad_settings.dps.x509.4.yaml} (83%) rename edgelet/edgelet-docker/test/linux/{sample_settings.tg.yaml => sample_settings.tg.filepaths.yaml} (85%) rename edgelet/edgelet-docker/test/windows/{sample_settings.dps.x509.2.yaml => bad_settings.dps.x509.3.yaml} (87%) rename edgelet/edgelet-docker/test/windows/{sample_settings.dps.x509.1.yaml => bad_settings.dps.x509.4..yaml} (83%) rename edgelet/edgelet-docker/test/windows/{sample_settings.tg.yaml => sample_settings.tg.filepaths.yaml} (85%) diff --git a/edgelet/Cargo.lock b/edgelet/Cargo.lock index 8ebfbdc5a53..9b5e628a2a1 100755 --- a/edgelet/Cargo.lock +++ b/edgelet/Cargo.lock @@ -371,6 +371,7 @@ dependencies = [ "serde 1.0.92 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.92 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", + "tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", "tempfile 3.0.3 (registry+https://github.com/rust-lang/crates.io-index)", "time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", "tokio 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/edgelet/contrib/config/linux/config.yaml b/edgelet/contrib/config/linux/config.yaml index 056837ac0cc..b68a4d80b31 100644 --- a/edgelet/contrib/config/linux/config.yaml +++ b/edgelet/contrib/config/linux/config.yaml @@ -18,16 +18,35 @@ # Supported modes: # manual - using an iothub connection string # dps - using dps for provisioning -# external - the device has been provisioned externally. Uses an external provisioning endpoint to get device specific information. +# external - the device has been provisioned externally. +# Uses an external provisioning endpoint to get device specific information. # # DPS Settings -# scope_id - Required. Value of a specific DPS instance's ID scope -# registration_id - Required. Registration ID of a specific device in DPS -# symmetric_key - Optional. This entry should only be specified when -# provisioning devices configured for symmetric key -# attestation +# scope_id - Required. Value of a specific DPS instance's ID scope +# registration_id - Required for TPM and symmetric key provisioning flows. +# Optional for X.509 provisioning. Registration ID of a +# specific device in DPS. +# For more information regarding DPS registration ids +# please see https://docs.microsoft.com/en-us/azure/iot-dps/concepts-device#registration-id +# symmetric_key - Optional. This entry should only be specified when +# provisioning devices configured for symmetric key +# attestation. Device specific symmetric key. +# identity_cert - Optional. The Edge device identity X.509 certificate +# entry should only be specified when provisioning +# an Edge device configured for X.509 attestation. +# The value should be specified as a URI. +# Ex. when specifying a PEM encoded certificate file, the URI +# should be specified as file:///path/identity_certificate.pem +# identity_pk - Optional. The Edge device identity private key +# entry should only be specified when provisioning +# an Edge device configured for X.509 attestation. +# The value should be specified as a URI. +# Ex. when specifying a PEM encoded private key file, the URI +# should be specified as file:///path/identity_key.pem +# # External Settings -# endpoint - Required. Value of the endpoint used to retrieve device specific information such as its IoT hub connection information. +# endpoint - Required. Value of the endpoint used to retrieve device specific +# information such as its IoT hub connection information. ############################################################################### # Manual provisioning configuration @@ -62,8 +81,8 @@ provisioning: # attestation: # method: "x509" # registration_id: "" -# identity_pk: "" +# identity_cert: "" +# identity_pk: "" # External provisioning configuration # provisioning: @@ -81,17 +100,24 @@ provisioning: # production environments. # # Settings: -# device_ca_cert - path to the device ca certificate and its chain -# device_ca_pk - path to the device ca private key file -# trusted_ca_certs - path to a file containing all the trusted CA +# device_ca_cert - URI of the device ca certificate and its chain. +# Optionally can be specified as a file path. +# device_ca_pk - URI of the device ca private key file. +# Optionally can be specified as a file path. +# trusted_ca_certs - URI containing all the trusted CA # certificates required for Edge module communication +# Optionally can be specified as a file path. # +# Note: +# The values of all of these fields can be specified either as a +# "file" scheme URI such as "file:///path/cert_key.pem" or a +# file path such as "/path/cert_key.pem" ############################################################################### # certificates: -# device_ca_cert: "" -# device_ca_pk: "" -# trusted_ca_certs: "" +# device_ca_cert: "" +# device_ca_pk: "" +# trusted_ca_certs: "" ############################################################################### # Edge Agent module spec @@ -247,21 +273,21 @@ homedir: "/var/lib/iotedge" # # uri - configures the uri for the container runtime. # network - configures the network on which the containers will be created. -# -# Additional container network configuration such as enabling IPv6 networking -# and providing the IPAM settings can be achieved by specifying the relevant +# +# Additional container network configuration such as enabling IPv6 networking +# and providing the IPAM settings can be achieved by specifying the relevant # configuration in the network settings. -# +# # network: # name: "azure-iot-edge" # ipv6: true # ipam: # config: -# - +# - # gateway: '172.18.0.1' # subnet: '172.18.0.0/16' # ip_range: '172.18.0.0/16' -# - +# - # gateway: '2021:ffff:e0:3b1:1::1' # subnet: '2021:ffff:e0:3b1:1::/80' # ip_range: '2021:ffff:e0:3b1:1::/80' @@ -270,17 +296,17 @@ homedir: "/var/lib/iotedge" moby_runtime: uri: "unix:///var/run/docker.sock" # network: "azure-iot-edge" - # + # # network: # name: "azure-iot-edge" # ipv6: true # ipam: # config: - # - + # - # gateway: '172.18.0.1' # subnet: '172.18.0.0/16' # ip_range: '172.18.0.0/16' - # - + # - # gateway: '2021:ffff:e0:3b1:1::1' # subnet: '2021:ffff:e0:3b1:1::/80' # ip_range: '2021:ffff:e0:3b1:1::/80' diff --git a/edgelet/contrib/config/linux/debian/config.yaml b/edgelet/contrib/config/linux/debian/config.yaml index 89cdedd8701..27d24e3ba07 100644 --- a/edgelet/contrib/config/linux/debian/config.yaml +++ b/edgelet/contrib/config/linux/debian/config.yaml @@ -18,16 +18,35 @@ # Supported modes: # manual - using an iothub connection string # dps - using dps for provisioning -# external - the device has been provisioned externally. Uses an external provisioning endpoint to get device specific information. +# external - the device has been provisioned externally. +# Uses an external provisioning endpoint to get device specific information. # # DPS Settings -# scope_id - Required. Value of a specific DPS instance's ID scope -# registration_id - Required. Registration ID of a specific device in DPS -# symmetric_key - Optional. This entry should only be specified when -# provisioning devices configured for symmetric key -# attestation +# scope_id - Required. Value of a specific DPS instance's ID scope +# registration_id - Required for TPM and symmetric key provisioning flows. +# Optional for X.509 provisioning. Registration ID of a +# specific device in DPS. +# For more information regarding DPS registration ids +# please see https://docs.microsoft.com/en-us/azure/iot-dps/concepts-device#registration-id +# symmetric_key - Optional. This entry should only be specified when +# provisioning devices configured for symmetric key +# attestation. Device specific symmetric key. +# identity_cert - Optional. The Edge device identity X.509 certificate +# entry should only be specified when provisioning +# an Edge device configured for X.509 attestation. +# The value should be specified as a URI. +# Ex. when specifying a PEM encoded certificate file, the URI +# should be specified as file:///path/identity_certificate.pem +# identity_pk - Optional. The Edge device identity private key +# entry should only be specified when provisioning +# an Edge device configured for X.509 attestation. +# The value should be specified as a URI. +# Ex. when specifying a PEM encoded private key file, the URI +# should be specified as file:///path/identity_key.pem +# # External Settings -# endpoint - Required. Value of the endpoint used to retrieve device specific information such as its IoT hub connection information. +# endpoint - Required. Value of the endpoint used to retrieve device specific +# information such as its IoT hub connection information. ############################################################################### # Manual provisioning configuration @@ -62,8 +81,8 @@ provisioning: # attestation: # method: "x509" # registration_id: "" -# identity_pk: "" +# identity_cert: "" +# identity_pk: "" # External provisioning configuration # provisioning: @@ -81,17 +100,24 @@ provisioning: # production environments. # # Settings: -# device_ca_cert - path to the device ca certificate and its chain -# device_ca_pk - path to the device ca private key file -# trusted_ca_certs - path to a file containing all the trusted CA +# device_ca_cert - URI of the device ca certificate and its chain. +# Optionally can be specified as a file path. +# device_ca_pk - URI of the device ca private key file. +# Optionally can be specified as a file path. +# trusted_ca_certs - URI containing all the trusted CA # certificates required for Edge module communication +# Optionally can be specified as a file path. # +# Note: +# The values of all of these fields can be specified either as a +# "file" scheme URI such as "file:///path/cert_key.pem" or a +# file path such as "/path/cert_key.pem" ############################################################################### # certificates: -# device_ca_cert: "" -# device_ca_pk: "" -# trusted_ca_certs: "" +# device_ca_cert: "" +# device_ca_pk: "" +# trusted_ca_certs: "" ############################################################################### # Edge Agent module spec @@ -237,21 +263,21 @@ homedir: "/var/lib/iotedge" # # uri - configures the uri for the container runtime. # network - configures the network on which the containers will be created. -# -# Additional container network configuration such as enabling IPv6 networking -# and providing the IPAM settings can be achieved by specifying the relevant +# +# Additional container network configuration such as enabling IPv6 networking +# and providing the IPAM settings can be achieved by specifying the relevant # configuration in the network settings. -# +# # network: # name: "azure-iot-edge" # ipv6: true # ipam: # config: -# - +# - # gateway: '172.18.0.1' # subnet: '172.18.0.0/16' # ip_range: '172.18.0.0/16' -# - +# - # gateway: '2021:ffff:e0:3b1:1::1' # subnet: '2021:ffff:e0:3b1:1::/80' # ip_range: '2021:ffff:e0:3b1:1::/80' @@ -260,17 +286,17 @@ homedir: "/var/lib/iotedge" moby_runtime: uri: "unix:///var/run/docker.sock" # network: "azure-iot-edge" - # + # # network: # name: "azure-iot-edge" # ipv6: true # ipam: # config: - # - + # - # gateway: '172.18.0.1' # subnet: '172.18.0.0/16' # ip_range: '172.18.0.0/16' - # - + # - # gateway: '2021:ffff:e0:3b1:1::1' # subnet: '2021:ffff:e0:3b1:1::/80' # ip_range: '2021:ffff:e0:3b1:1::/80' diff --git a/edgelet/contrib/config/windows/config.yaml b/edgelet/contrib/config/windows/config.yaml index f601645b24a..0af61f077fc 100644 --- a/edgelet/contrib/config/windows/config.yaml +++ b/edgelet/contrib/config/windows/config.yaml @@ -18,16 +18,35 @@ # Supported modes: # manual - using an iothub connection string # dps - using dps for provisioning -# external - the device has been provisioned externally. Uses an external provisioning endpoint to get device specific information. +# external - the device has been provisioned externally. +# Uses an external provisioning endpoint to get device specific information. # # DPS Settings -# scope_id - Required. Value of a specific DPS instance's ID scope -# registration_id - Required. Registration ID of a specific device in DPS -# symmetric_key - Optional. This entry should only be specified when -# provisioning devices configured for symmetric key -# attestation +# scope_id - Required. Value of a specific DPS instance's ID scope +# registration_id - Required for TPM and symmetric key provisioning flows. +# Optional for X.509 provisioning. Registration ID of a +# specific device in DPS. +# For more information regarding DPS registration ids +# please see https://docs.microsoft.com/en-us/azure/iot-dps/concepts-device#registration-id +# symmetric_key - Optional. This entry should only be specified when +# provisioning devices configured for symmetric key +# attestation. Device specific symmetric key. +# identity_cert - Optional. The Edge device identity X.509 certificate +# entry should only be specified when provisioning +# an Edge device configured for X.509 attestation. +# The value should be specified as a URI. +# Ex. when specifying a PEM encoded certificate file, the URI +# should be specified as file:///C:/identity_certificate.pem +# identity_pk - Optional. The Edge device identity private key +# entry should only be specified when provisioning +# an Edge device configured for X.509 attestation. +# The value should be specified as a URI. +# Ex. when specifying a PEM encoded private key file, the URI +# should be specified as file:///C:/identity_key.pem +# # External Settings -# endpoint - Required. Value of the endpoint used to retrieve device specific information such as its IoT hub connection information. +# endpoint - Required. Value of the endpoint used to retrieve device specific +# information such as its IoT hub connection information. ############################################################################### # Manual provisioning configuration @@ -62,8 +81,8 @@ provisioning: # attestation: # method: "x509" # registration_id: "" -# identity_pk: "" +# identity_cert: "" +# identity_pk: "" # External provisioning configuration # provisioning: @@ -81,17 +100,24 @@ provisioning: # production environments. # # Settings: -# device_ca_cert - path to the device ca certificate and its chain -# device_ca_pk - path to the device ca private key file -# trusted_ca_certs - path to a file containing all the trusted CA +# device_ca_cert - URI of the device ca certificate and its chain. +# Optionally can be specified as a file path. +# device_ca_pk - URI of the device ca private key file. +# Optionally can be specified as a file path. +# trusted_ca_certs - URI containing all the trusted CA # certificates required for Edge module communication +# Optionally can be specified as a file path. # +# Note: +# The values of all of these fields can be specified either as a +# "file" scheme URI such as "file:///C:/cert_key.pem" or a +# file path such as "C:\\cert_key.pem" ############################################################################### # certificates: -# device_ca_cert: "" -# device_ca_pk: "" -# trusted_ca_certs: "" +# device_ca_cert: "" +# device_ca_pk: "" +# trusted_ca_certs: "" ############################################################################### # Edge Agent module spec diff --git a/edgelet/edgelet-core/src/error.rs b/edgelet/edgelet-core/src/error.rs index c279592ba33..28d1f72bbda 100644 --- a/edgelet/edgelet-core/src/error.rs +++ b/edgelet/edgelet-core/src/error.rs @@ -72,6 +72,18 @@ pub enum ErrorKind { #[fail(display = "Invalid module type {:?}", _0)] InvalidModuleType(String), + #[fail( + display = "Error parsing URI {} specified for '{}'. Please check the config.yaml file.", + _0, _1 + )] + InvalidSettingsUri(String, &'static str), + + #[fail( + display = "Invalid file URI {} path specified for '{}'. Please check the config.yaml file.", + _0, _1 + )] + InvalidSettingsUriFilePath(String, &'static str), + #[fail(display = "Invalid URL {:?}", _0)] InvalidUrl(String), @@ -92,6 +104,18 @@ pub enum ErrorKind { #[fail(display = "Signing error occurred. Invalid key length: {}", _0)] SignInvalidKeyLength(usize), + + #[fail( + display = "URI {} is unsupported for '{}'. Please check the config.yaml file.", + _0, _1 + )] + UnsupportedSettingsUri(String, &'static str), + + #[fail( + display = "File URI {} is unsupported for '{}'. Please check the config.yaml file.", + _0, _1 + )] + UnsupportedSettingsFileUri(String, &'static str), } impl Fail for Error { diff --git a/edgelet/edgelet-core/src/settings.rs b/edgelet/edgelet-core/src/settings.rs index 1a2d91fbc67..9106348e6aa 100644 --- a/edgelet/edgelet-core/src/settings.rs +++ b/edgelet/edgelet-core/src/settings.rs @@ -9,6 +9,7 @@ use url::Url; use url_serde; use crate::crypto::MemoryKey; +use crate::error::{Error, ErrorKind}; use crate::module::ModuleSpec; const DEVICEID_KEY: &str = "DeviceId"; @@ -146,17 +147,44 @@ impl SymmetricKeyAttestationInfo { pub struct X509AttestationInfo { #[serde(skip_serializing_if = "Option::is_none")] registration_id: Option, - identity_cert: PathBuf, - identity_pk: PathBuf, + #[serde(with = "url_serde")] + identity_cert: Url, + #[serde(with = "url_serde")] + identity_pk: Url, } impl X509AttestationInfo { - pub fn identity_cert(&self) -> &Path { - &self.identity_cert.as_path() + pub fn identity_cert(&self) -> Result { + get_path_from_uri( + &self.identity_cert, + "provisioning.attestation.identity_cert", + ) } - pub fn identity_pk(&self) -> &Path { - &self.identity_pk.as_path() + pub fn identity_pk(&self) -> Result { + get_path_from_uri(&self.identity_pk, "provisioning.attestation.identity_pk") + } + + pub fn identity_pk_uri(&self) -> Result<&Url, Error> { + if is_supported_uri(&self.identity_pk) { + Ok(&self.identity_pk) + } else { + Err(Error::from(ErrorKind::UnsupportedSettingsUri( + self.identity_pk.to_string(), + "provisioning.attestation.identity_pk", + ))) + } + } + + pub fn identity_cert_uri(&self) -> Result<&Url, Error> { + if is_supported_uri(&self.identity_cert) { + Ok(&self.identity_cert) + } else { + Err(Error::from(ErrorKind::UnsupportedSettingsUri( + self.identity_cert.to_string(), + "provisioning.attestation.identity_cert", + ))) + } } pub fn registration_id(&self) -> Option<&str> { @@ -250,7 +278,7 @@ impl External { #[serde(rename_all = "lowercase")] pub enum Provisioning { Manual(Manual), - Dps(Dps), + Dps(Box), External(External), } @@ -292,22 +320,100 @@ impl Listen { #[derive(Clone, Debug, serde_derive::Deserialize, serde_derive::Serialize)] pub struct Certificates { - device_ca_cert: PathBuf, - device_ca_pk: PathBuf, - trusted_ca_certs: PathBuf, + device_ca_cert: String, + device_ca_pk: String, + trusted_ca_certs: String, +} + +fn is_supported_uri(uri: &Url) -> bool { + if uri.scheme() == "file" && uri.port().is_none() && uri.query().is_none() { + if let Some(host) = uri.host_str() { + return host == "localhost"; + } + return true; + } + false +} + +fn get_path_from_uri(uri: &Url, setting_name: &'static str) -> Result { + if is_supported_uri(&uri) { + let path = uri + .to_file_path() + .map_err(|()| ErrorKind::InvalidSettingsUriFilePath(uri.to_string(), setting_name))?; + Ok(path) + } else { + Err(Error::from(ErrorKind::UnsupportedSettingsFileUri( + uri.to_string(), + setting_name, + ))) + } +} + +fn convert_to_path(maybe_path: &str, setting_name: &'static str) -> Result { + if let Ok(file_uri) = Url::from_file_path(maybe_path) { + // maybe_path was specified as a valid absolute path not a URI + get_path_from_uri(&file_uri, setting_name) + } else { + // maybe_path is a URI or a relative path + if let Ok(uri) = Url::parse(maybe_path) { + get_path_from_uri(&uri, setting_name) + } else { + Ok(PathBuf::from(maybe_path)) + } + } +} + +fn convert_to_uri(maybe_uri: &str, setting_name: &'static str) -> Result { + if let Ok(uri) = Url::parse(maybe_uri) { + // maybe_uri was specified as a URI + if is_supported_uri(&uri) { + Ok(uri) + } else { + Err(Error::from(ErrorKind::UnsupportedSettingsUri( + maybe_uri.to_owned(), + setting_name, + ))) + } + } else { + // maybe_uri was specified as a valid path not a URI + Url::from_file_path(maybe_uri) + .map(|uri| { + if is_supported_uri(&uri) { + Ok(uri) + } else { + Err(Error::from(ErrorKind::UnsupportedSettingsUri( + maybe_uri.to_owned(), + setting_name, + ))) + } + }) + .map_err(|()| ErrorKind::InvalidSettingsUri(maybe_uri.to_owned(), setting_name))? + } } impl Certificates { - pub fn device_ca_cert(&self) -> &Path { - &self.device_ca_cert + pub fn device_ca_cert(&self) -> Result { + convert_to_path(&self.device_ca_cert, "certificates.device_ca_cert") + } + + pub fn device_ca_pk(&self) -> Result { + convert_to_path(&self.device_ca_pk, "certificates.device_ca_pk") + } + + pub fn trusted_ca_certs(&self) -> Result { + convert_to_path(&self.trusted_ca_certs, "certificates.trusted_ca_certs") } - pub fn device_ca_pk(&self) -> &Path { - &self.device_ca_pk + pub fn device_ca_cert_uri(&self) -> Result { + convert_to_uri(&self.device_ca_cert, "certificates.device_ca_cert") } - pub fn trusted_ca_certs(&self) -> &Path { - &self.trusted_ca_certs + pub fn device_ca_pk_uri(&self) -> Result { + convert_to_uri(&self.device_ca_pk, "certificates.device_ca_pk") + } + + pub fn trusted_ca_certs_uri(&self) -> Result { + convert_to_uri(&self.trusted_ca_certs, "certificates.trusted_ca_certs") } } @@ -431,3 +537,163 @@ where &self.watchdog } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_convert_to_path() { + if cfg!(windows) { + assert_eq!( + r"..\sample.txt", + convert_to_path(r"..\sample.txt", "test") + .unwrap() + .to_str() + .unwrap() + ); + + let expected_path = r"C:\temp\sample.txt"; + assert_eq!( + expected_path, + convert_to_path(r"C:\temp\sample.txt", "test") + .unwrap() + .to_str() + .unwrap() + ); + assert_eq!( + expected_path, + convert_to_path("file:///C:/temp/sample.txt", "test") + .unwrap() + .to_str() + .unwrap() + ); + assert_eq!( + expected_path, + convert_to_path("file://localhost/C:/temp/sample.txt", "test") + .unwrap() + .to_str() + .unwrap() + ); + assert_eq!( + expected_path, + convert_to_path("file://localhost/C:/temp/../temp/sample.txt", "test") + .unwrap() + .to_str() + .unwrap() + ); + // oddly this works because the host is null since local drive is specified + assert_eq!( + expected_path, + convert_to_path("file://deadhost/C:/temp/sample.txt", "test") + .unwrap() + .to_str() + .unwrap() + ); + convert_to_path("file://deadhost/temp/sample.txt", "test") + .expect_err("Non localhost host specified"); + convert_to_path("https:///C:/temp/sample.txt", "test") + .expect_err("Non file scheme specified"); + } else { + assert_eq!( + "./sample.txt", + convert_to_path("./sample.txt", "test") + .unwrap() + .to_str() + .unwrap() + ); + + let expected_path = "/tmp/sample.txt"; + assert_eq!( + expected_path, + convert_to_path("/tmp/sample.txt", "test") + .unwrap() + .to_str() + .unwrap() + ); + assert_eq!( + expected_path, + convert_to_path("file:///tmp/sample.txt", "test") + .unwrap() + .to_str() + .unwrap() + ); + assert_eq!( + expected_path, + convert_to_path("file://localhost/tmp/sample.txt", "test") + .unwrap() + .to_str() + .unwrap() + ); + assert_eq!( + expected_path, + convert_to_path("file:///tmp/../tmp/sample.txt", "test") + .unwrap() + .to_str() + .unwrap() + ); + convert_to_path("file://deadhost/tmp/sample.txt", "test") + .expect_err("Non localhost host specified"); + convert_to_path("https://localhost/tmp/sample.txt", "test") + .expect_err("Non file scheme specified"); + } + } + + #[test] + fn test_convert_to_uri() { + if cfg!(windows) { + let expected_uri_str = "file:///C:/temp/sample.txt"; + let expected_uri = Url::parse(expected_uri_str).unwrap(); + + assert_eq!( + expected_uri, + convert_to_uri("file:///C:/temp/sample.txt", "test").unwrap() + ); + assert_eq!( + expected_uri, + convert_to_uri("file://localhost/C:/temp/sample.txt", "test").unwrap() + ); + assert_eq!( + expected_uri, + convert_to_uri("file://localhost/C:/temp/../temp/sample.txt", "test").unwrap() + ); + // oddly this works because the host is null since local drive is specified + assert_eq!( + expected_uri, + convert_to_uri("file://deadhost/C:/temp/sample.txt", "test").unwrap() + ); + convert_to_uri("file://deadhost/temp/sample.txt", "test") + .expect_err("Non localhost host specified"); + convert_to_uri("file://deadhost/temp/sample.txt", "test") + .expect_err("Non file scheme specified"); + convert_to_uri("../tmp/../tmp/sample.txt", "test") + .expect_err("Non absolute path specified"); + } else { + let expected_uri_str = "file:///tmp/sample.txt"; + let expected_uri = Url::parse(expected_uri_str).unwrap(); + + assert_eq!( + expected_uri, + convert_to_uri("file:///tmp/sample.txt", "test").unwrap() + ); + assert_eq!( + expected_uri, + convert_to_uri("file://localhost/tmp/sample.txt", "test").unwrap() + ); + assert_eq!( + expected_uri, + convert_to_uri("file:///tmp/../tmp/sample.txt", "test").unwrap() + ); + convert_to_uri("https://localhost/tmp/sample.txt", "test") + .expect_err("Non absolute path specified"); + assert_eq!( + expected_uri, + convert_to_uri("/tmp/sample.txt", "test").unwrap() + ); + convert_to_uri("../tmp/../tmp/sample.txt", "test") + .expect_err("Non absolute path specified"); + convert_to_uri("file://deadhost/tmp/sample.txt", "test") + .expect_err("Non localhost host specified"); + } + } +} diff --git a/edgelet/edgelet-docker/Cargo.toml b/edgelet/edgelet-docker/Cargo.toml index 1a9eeb5c259..559a419a205 100644 --- a/edgelet/edgelet-docker/Cargo.toml +++ b/edgelet/edgelet-docker/Cargo.toml @@ -33,6 +33,7 @@ config = { version = "0.9", default-features = false, features = ["json", "yaml" json-patch = "0.2.5" maplit = "1.0" time = "0.1" +tempdir = "0.3.7" typed-headers = "0.1" edgelet-test-utils = { path = "../edgelet-test-utils" } diff --git a/edgelet/edgelet-docker/src/settings.rs b/edgelet/edgelet-docker/src/settings.rs index 88350f69892..127a74facdd 100644 --- a/edgelet/edgelet-docker/src/settings.rs +++ b/edgelet/edgelet-docker/src/settings.rs @@ -235,8 +235,12 @@ mod tests { use super::*; use std::cmp::Ordering; + use std::fs::File; + use std::io::prelude::*; - use config::{File, FileFormat}; + use config::{File as ConfigFile, FileFormat}; + use serde_json::json; + use tempdir::TempDir; use edgelet_core::{ AttestationMethod, IpamConfig, DEFAULT_CONNECTION_STRING, DEFAULT_NETWORKID, @@ -247,8 +251,6 @@ mod tests { #[cfg(unix)] static BAD_SETTINGS: &str = "test/linux/bad_sample_settings.yaml"; #[cfg(unix)] - static GOOD_SETTINGS_TG: &str = "test/linux/sample_settings.tg.yaml"; - #[cfg(unix)] static GOOD_SETTINGS_DPS_SYM_KEY: &str = "test/linux/sample_settings.dps.sym.yaml"; #[cfg(unix)] static GOOD_SETTINGS_CASE_SENSITIVE: &str = "test/linux/case_sensitive.yaml"; @@ -263,14 +265,14 @@ mod tests { #[cfg(unix)] static BAD_SETTINGS_DPS_SYM_KEY: &str = "test/linux/bad_sample_settings.dps.sym.yaml"; #[cfg(unix)] - static X509_GOOD_SETTINGS1: &str = "test/linux/sample_settings.dps.x509.1.yaml"; - #[cfg(unix)] - static X509_GOOD_SETTINGS2: &str = "test/linux/sample_settings.dps.x509.2.yaml"; - #[cfg(unix)] static BAD_SETTINGS_DPS_X5091: &str = "test/linux/bad_settings.dps.x509.1.yaml"; #[cfg(unix)] static BAD_SETTINGS_DPS_X5092: &str = "test/linux/bad_settings.dps.x509.2.yaml"; #[cfg(unix)] + static BAD_SETTINGS_DPS_X5093: &str = "test/linux/bad_settings.dps.x509.3.yaml"; + #[cfg(unix)] + static BAD_SETTINGS_DPS_X5094: &str = "test/linux/bad_settings.dps.x509.4.yaml"; + #[cfg(unix)] static GOOD_SETTINGS_EXTERNAL: &str = "test/linux/sample_settings.external.yaml"; #[cfg(unix)] static GOOD_SETTINGS_NETWORK: &str = "test/linux/sample_settings.network.yaml"; @@ -280,8 +282,6 @@ mod tests { #[cfg(windows)] static BAD_SETTINGS: &str = "test/windows/bad_sample_settings.yaml"; #[cfg(windows)] - static GOOD_SETTINGS_TG: &str = "test/windows/sample_settings.tg.yaml"; - #[cfg(windows)] static GOOD_SETTINGS_DPS_SYM_KEY: &str = "test/windows/sample_settings.dps.sym.yaml"; #[cfg(windows)] static GOOD_SETTINGS_CASE_SENSITIVE: &str = "test/windows/case_sensitive.yaml"; @@ -296,14 +296,14 @@ mod tests { #[cfg(windows)] static BAD_SETTINGS_DPS_SYM_KEY: &str = "test/windows/bad_sample_settings.dps.sym.yaml"; #[cfg(windows)] - static X509_GOOD_SETTINGS1: &str = "test/windows/sample_settings.dps.x509.1.yaml"; - #[cfg(windows)] - static X509_GOOD_SETTINGS2: &str = "test/windows/sample_settings.dps.x509.2.yaml"; - #[cfg(windows)] static BAD_SETTINGS_DPS_X5091: &str = "test/windows/bad_settings.dps.x509.1.yaml"; #[cfg(windows)] static BAD_SETTINGS_DPS_X5092: &str = "test/windows/bad_settings.dps.x509.2.yaml"; #[cfg(windows)] + static BAD_SETTINGS_DPS_X5093: &str = "test/windows/bad_settings.dps.x509.3.yaml"; + #[cfg(windows)] + static BAD_SETTINGS_DPS_X5094: &str = "test/windows/bad_settings.dps.x509.4.yaml"; + #[cfg(windows)] static GOOD_SETTINGS_EXTERNAL: &str = "test/windows/sample_settings.external.yaml"; #[cfg(windows)] static GOOD_SETTINGS_NETWORK: &str = "test/windows/sample_settings.network.yaml"; @@ -319,7 +319,7 @@ mod tests { fn default_in_yaml_matches_constant() { let mut config = Config::default(); config - .merge(File::from_str(DEFAULTS, FileFormat::Yaml)) + .merge(ConfigFile::from_str(DEFAULTS, FileFormat::Yaml)) .unwrap(); let settings: Settings = config.try_into().unwrap(); @@ -407,6 +407,12 @@ mod tests { let settings = Settings::new(Some(Path::new(BAD_SETTINGS_DPS_X5092))); assert!(settings.is_err()); + + let settings = Settings::new(Some(Path::new(BAD_SETTINGS_DPS_X5093))); + assert!(settings.is_err()); + + let settings = Settings::new(Some(Path::new(BAD_SETTINGS_DPS_X5094))); + assert!(settings.is_err()); } #[test] @@ -423,21 +429,120 @@ mod tests { ); } + fn prepare_test_gateway_x509_certificate_settings_yaml( + settings_path: &Path, + ca_cert_path: &Path, + ca_key_path: &Path, + trust_bundle_path: &Path, + use_uri_format: bool, + ) -> String { + File::create(&ca_cert_path) + .expect("Test cert file could not be created") + .write_all(b"CN=Gateway CA") + .expect("Test cert file could not be written"); + + File::create(&ca_key_path) + .expect("Test cert private key file could not be created") + .write_all(b"Gateway Private Key") + .expect("Test cert private key file could not be written"); + + File::create(&trust_bundle_path) + .expect("Test trust bundle file could not be created") + .write_all(b"Trust me, I'm good for it.") + .expect("Test trust bundle file could not be written"); + + let (ca_cert_setting, ca_key_setting, trust_bundle_setting) = if use_uri_format { + ( + Url::from_file_path(ca_cert_path).unwrap().into_string(), + Url::from_file_path(ca_key_path).unwrap().into_string(), + Url::from_file_path(trust_bundle_path) + .unwrap() + .into_string(), + ) + } else { + ( + ca_cert_path.to_str().unwrap().to_owned(), + ca_key_path.to_str().unwrap().to_owned(), + trust_bundle_path.to_str().unwrap().to_owned(), + ) + }; + let settings_yaml = json!({ + "provisioning": { + "source": "manual", + "device_connection_string": "HostName=something.something.com;DeviceId=something;SharedAccessKey=QXp1cmUgSW9UIEVkZ2U=" + }, + "certificates": { + "device_ca_cert": ca_cert_setting, + "device_ca_pk": ca_key_setting, + "trusted_ca_certs": trust_bundle_setting, + }}).to_string(); + + File::create(&settings_path) + .expect("Test settings file could not be created") + .write_all(settings_yaml.as_bytes()) + .expect("Test settings file could not be written"); + + settings_yaml + } + #[test] - fn manual_file_gets_sample_tg_paths() { - let settings = Settings::new(Some(Path::new(GOOD_SETTINGS_TG))); + fn manual_file_gets_sample_tg_file_paths() { + let tmp_dir = TempDir::new("blah").unwrap(); + let ca_cert_path = tmp_dir.path().join("device_ca_cert.pem"); + let ca_key_path = tmp_dir.path().join("device_ca_pk.pem"); + let trust_bundle_path = tmp_dir.path().join("trusted_ca_certs.pem"); + let settings_path = tmp_dir.path().join("test_settings.yaml"); + prepare_test_gateway_x509_certificate_settings_yaml( + &settings_path, + &ca_cert_path, + &ca_key_path, + &trust_bundle_path, + false, + ); + let settings = Settings::new(Some(&settings_path)).expect("Settings create failed"); println!("{:?}", settings); - assert!(settings.is_ok()); - let s = settings.unwrap(); - let certificates = s.certificates(); + let certificates = settings.certificates(); certificates .map(|c| { - assert_eq!(c.device_ca_cert().to_str().unwrap(), "device_ca_cert.pem"); - assert_eq!(c.device_ca_pk().to_str().unwrap(), "device_ca_pk.pem"); - assert_eq!( - c.trusted_ca_certs().to_str().unwrap(), - "trusted_ca_certs.pem" - ); + let path = c.device_ca_cert().expect("Did not obtain device CA cert"); + assert_eq!(ca_cert_path, path); + let path = c + .device_ca_pk() + .expect("Did not obtain device CA private key"); + assert_eq!(ca_key_path, path); + let path = c.trusted_ca_certs().expect("Did not obtain trust bundle"); + assert_eq!(trust_bundle_path, path); + }) + .expect("certificates not configured"); + } + + #[test] + fn manual_file_gets_sample_tg_file_uris() { + let tmp_dir = TempDir::new("blah").unwrap(); + let ca_cert_path = tmp_dir.path().join("device_ca_cert.pem"); + let ca_key_path = tmp_dir.path().join("device_ca_pk.pem"); + let trust_bundle_path = tmp_dir.path().join("trusted_ca_certs.pem"); + let settings_path = tmp_dir.path().join("test_settings.yaml"); + prepare_test_gateway_x509_certificate_settings_yaml( + &settings_path, + &ca_cert_path, + &ca_key_path, + &trust_bundle_path, + true, + ); + let settings = Settings::new(Some(&settings_path)).unwrap(); + println!("{:?}", settings); + let certificates = settings.certificates(); + certificates + .map(|c| { + let path = c.device_ca_cert().expect("Did not obtain device CA cert"); + assert_eq!(ca_cert_path, path); + let path = c + .device_ca_pk() + .expect("Did not obtain device CA private key"); + assert_eq!(ca_key_path, path); + let path = c.trusted_ca_certs().expect("Did not obtain trust bundle"); + assert_eq!(trust_bundle_path, path); }) .expect("certificates not configured"); } @@ -508,13 +613,53 @@ mod tests { }; } + fn prepare_test_dps_x509_settings_yaml( + settings_path: &Path, + cert_path: &Path, + key_path: &Path, + ) -> String { + File::create(&cert_path) + .expect("Test cert file could not be created") + .write_all(b"CN=Mr. T") + .expect("Test cert file could not be written"); + + File::create(&key_path) + .expect("Test cert private key file could not be created") + .write_all(b"i pity the fool") + .expect("Test cert private key file could not be written"); + + let cert_uri = format!("file://{}", cert_path.to_str().unwrap()); + let pk_uri = format!("file://{}", key_path.to_str().unwrap()); + let settings_yaml = json!({ + "provisioning": { + "source": "dps", + "global_endpoint": "scheme://jibba-jabba.net", + "scope_id": "i got no time for the jibba-jabba", + "attestation": { + "method": "x509", + "identity_cert": cert_uri, + "identity_pk": pk_uri, + }, + }}) + .to_string(); + File::create(&settings_path) + .expect("Test settings file could not be created") + .write_all(settings_yaml.as_bytes()) + .expect("Test settings file could not be written"); + + settings_yaml + } + #[test] fn dps_prov_x509_default_settings() { - let settings = Settings::new(Some(Path::new(X509_GOOD_SETTINGS1))); + let tmp_dir = TempDir::new("blah").unwrap(); + let cert_path = tmp_dir.path().join("test_cert"); + let key_path = tmp_dir.path().join("test_key"); + let settings_path = tmp_dir.path().join("test_settings.yaml"); + prepare_test_dps_x509_settings_yaml(&settings_path, &cert_path, &key_path); + let settings = Settings::new(Some(&settings_path)).unwrap(); println!("{:?}", settings); - assert!(settings.is_ok()); - let s = settings.unwrap(); - match s.provisioning() { + match settings.provisioning() { Provisioning::Dps(ref dps) => { assert_eq!(dps.global_endpoint().scheme(), "scheme"); assert_eq!(dps.global_endpoint().host_str().unwrap(), "jibba-jabba.net"); @@ -522,8 +667,23 @@ mod tests { match dps.attestation() { AttestationMethod::X509(ref x509) => { assert!(x509.registration_id().is_none()); - assert_eq!(x509.identity_cert(), Path::new("some/path/mr.t.cer.pem")); - assert_eq!(x509.identity_pk(), Path::new("some/path/mr.t.pk.pem")); + assert_eq!( + &Url::parse(&format!("file://{}", cert_path.to_str().unwrap())) + .unwrap(), + x509.identity_cert_uri().unwrap(), + ); + assert_eq!( + &Url::parse(&format!("file://{}", key_path.to_str().unwrap())).unwrap(), + x509.identity_pk_uri().unwrap(), + ); + assert_eq!( + cert_path.to_str().unwrap(), + x509.identity_cert().unwrap().to_str().unwrap(), + ); + assert_eq!( + key_path.to_str().unwrap(), + x509.identity_pk().unwrap().to_str().unwrap(), + ); } _ => unreachable!(), } @@ -534,20 +694,38 @@ mod tests { #[test] fn dps_prov_x509_reg_id_and_default_settings() { - let settings = Settings::new(Some(Path::new(X509_GOOD_SETTINGS2))); + let tmp_dir = TempDir::new("blah").unwrap(); + let cert_path = tmp_dir.path().join("test_cert"); + let key_path = tmp_dir.path().join("test_key"); + let settings_path = tmp_dir.path().join("test_settings.yaml"); + prepare_test_dps_x509_settings_yaml(&settings_path, &cert_path, &key_path); + let settings = Settings::new(Some(&settings_path)).unwrap(); println!("{:?}", settings); - assert!(settings.is_ok()); - let s = settings.unwrap(); - match s.provisioning() { + match settings.provisioning() { Provisioning::Dps(ref dps) => { assert_eq!(dps.global_endpoint().scheme(), "scheme"); assert_eq!(dps.global_endpoint().host_str().unwrap(), "jibba-jabba.net"); assert_eq!(dps.scope_id(), "i got no time for the jibba-jabba"); match dps.attestation() { AttestationMethod::X509(ref x509) => { - assert_eq!(x509.registration_id().unwrap(), "register me fool"); - assert_eq!(x509.identity_cert(), Path::new("some/path/mr.t.cer.pem")); - assert_eq!(x509.identity_pk(), Path::new("some/path/mr.t.pk.pem")); + assert!(x509.registration_id().is_none()); + assert_eq!( + &Url::parse(&format!("file://{}", cert_path.to_str().unwrap())) + .unwrap(), + x509.identity_cert_uri().unwrap(), + ); + assert_eq!( + &Url::parse(&format!("file://{}", key_path.to_str().unwrap())).unwrap(), + x509.identity_pk_uri().unwrap(), + ); + assert_eq!( + cert_path.to_str().unwrap(), + x509.identity_cert().unwrap().to_str().unwrap(), + ); + assert_eq!( + key_path.to_str().unwrap(), + x509.identity_pk().unwrap().to_str().unwrap(), + ); } _ => unreachable!(), } diff --git a/edgelet/edgelet-docker/test/linux/bad_settings.dps.x509.1.yaml b/edgelet/edgelet-docker/test/linux/bad_settings.dps.x509.1.yaml index 373ee2ed303..02ca83511a0 100644 --- a/edgelet/edgelet-docker/test/linux/bad_settings.dps.x509.1.yaml +++ b/edgelet/edgelet-docker/test/linux/bad_settings.dps.x509.1.yaml @@ -1,4 +1,6 @@ # Configures the provisioning mode + +# x509 provisioning does not have a private key URI specified provisioning: source: "dps" global_endpoint: "scheme://jibba-jabba.net" @@ -6,7 +8,7 @@ provisioning: attestation: method: "x509" registration_id: "register me fool" - identity_cert: "some/path/mr.t.cer.pem" + identity_cert: "file:///some/path/mr.t.cer.pem" agent: name: "edgeAgent" diff --git a/edgelet/edgelet-docker/test/linux/bad_settings.dps.x509.2.yaml b/edgelet/edgelet-docker/test/linux/bad_settings.dps.x509.2.yaml index 2843fbed29f..c22544c5b15 100644 --- a/edgelet/edgelet-docker/test/linux/bad_settings.dps.x509.2.yaml +++ b/edgelet/edgelet-docker/test/linux/bad_settings.dps.x509.2.yaml @@ -1,4 +1,6 @@ # Configures the provisioning mode + +# x509 provisioning does not have a identity certificate URI specified provisioning: source: "dps" global_endpoint: "scheme://jibba-jabba.net" @@ -6,7 +8,7 @@ provisioning: attestation: method: "x509" registration_id: "register me fool" - identity_pk: "some/path/mr.t.pk.pem" + identity_pk: "file:///some/path/mr.t.pk.pem" agent: name: "edgeAgent" diff --git a/edgelet/edgelet-docker/test/linux/sample_settings.dps.x509.2.yaml b/edgelet/edgelet-docker/test/linux/bad_settings.dps.x509.3.yaml similarity index 87% rename from edgelet/edgelet-docker/test/linux/sample_settings.dps.x509.2.yaml rename to edgelet/edgelet-docker/test/linux/bad_settings.dps.x509.3.yaml index ebd1b8c8ecb..55ef5067bb1 100644 --- a/edgelet/edgelet-docker/test/linux/sample_settings.dps.x509.2.yaml +++ b/edgelet/edgelet-docker/test/linux/bad_settings.dps.x509.3.yaml @@ -1,4 +1,6 @@ # Configures the provisioning mode + +# x509 provisioning does not have a valid identity certificate URI specified provisioning: source: "dps" global_endpoint: "scheme://jibba-jabba.net" @@ -7,7 +9,7 @@ provisioning: method: "x509" registration_id: "register me fool" identity_cert: "some/path/mr.t.cer.pem" - identity_pk: "some/path/mr.t.pk.pem" + identity_pk: "file:///some/path/mr.t.pk.pem" agent: name: "edgeAgent" diff --git a/edgelet/edgelet-docker/test/linux/sample_settings.dps.x509.1.yaml b/edgelet/edgelet-docker/test/linux/bad_settings.dps.x509.4.yaml similarity index 83% rename from edgelet/edgelet-docker/test/linux/sample_settings.dps.x509.1.yaml rename to edgelet/edgelet-docker/test/linux/bad_settings.dps.x509.4.yaml index a0c8930ce96..bf2afa54ca4 100644 --- a/edgelet/edgelet-docker/test/linux/sample_settings.dps.x509.1.yaml +++ b/edgelet/edgelet-docker/test/linux/bad_settings.dps.x509.4.yaml @@ -1,11 +1,14 @@ # Configures the provisioning mode + +# x509 provisioning does not have a valid identity private key URI specified provisioning: source: "dps" global_endpoint: "scheme://jibba-jabba.net" scope_id: "i got no time for the jibba-jabba" attestation: method: "x509" - identity_cert: "some/path/mr.t.cer.pem" + registration_id: "register me fool" + identity_cert: "file:///some/path/mr.t.cer.pem" identity_pk: "some/path/mr.t.pk.pem" agent: diff --git a/edgelet/edgelet-docker/test/linux/sample_settings.tg.yaml b/edgelet/edgelet-docker/test/linux/sample_settings.tg.filepaths.yaml similarity index 85% rename from edgelet/edgelet-docker/test/linux/sample_settings.tg.yaml rename to edgelet/edgelet-docker/test/linux/sample_settings.tg.filepaths.yaml index bc48a498273..629033d503a 100644 --- a/edgelet/edgelet-docker/test/linux/sample_settings.tg.yaml +++ b/edgelet/edgelet-docker/test/linux/sample_settings.tg.filepaths.yaml @@ -1,13 +1,12 @@ - # Configures the provisioning mode provisioning: source: "manual" device_connection_string: "HostName=something.something.com;DeviceId=something;SharedAccessKey=QXp1cmUgSW9UIEVkZ2U=" certificates: - device_ca_cert: "device_ca_cert.pem" - device_ca_pk: "device_ca_pk.pem" - trusted_ca_certs: "trusted_ca_certs.pem" + device_ca_cert: "/tmp/device_ca_cert.pem" + device_ca_pk: "/tmp/device_ca_pk.pem" + trusted_ca_certs: "/tmp/trusted_ca_certs.pem" agent: name: "edgeAgent" diff --git a/edgelet/edgelet-docker/test/windows/bad_settings.dps.x509.1.yaml b/edgelet/edgelet-docker/test/windows/bad_settings.dps.x509.1.yaml index 373ee2ed303..02ca83511a0 100644 --- a/edgelet/edgelet-docker/test/windows/bad_settings.dps.x509.1.yaml +++ b/edgelet/edgelet-docker/test/windows/bad_settings.dps.x509.1.yaml @@ -1,4 +1,6 @@ # Configures the provisioning mode + +# x509 provisioning does not have a private key URI specified provisioning: source: "dps" global_endpoint: "scheme://jibba-jabba.net" @@ -6,7 +8,7 @@ provisioning: attestation: method: "x509" registration_id: "register me fool" - identity_cert: "some/path/mr.t.cer.pem" + identity_cert: "file:///some/path/mr.t.cer.pem" agent: name: "edgeAgent" diff --git a/edgelet/edgelet-docker/test/windows/bad_settings.dps.x509.2.yaml b/edgelet/edgelet-docker/test/windows/bad_settings.dps.x509.2.yaml index 2843fbed29f..c22544c5b15 100644 --- a/edgelet/edgelet-docker/test/windows/bad_settings.dps.x509.2.yaml +++ b/edgelet/edgelet-docker/test/windows/bad_settings.dps.x509.2.yaml @@ -1,4 +1,6 @@ # Configures the provisioning mode + +# x509 provisioning does not have a identity certificate URI specified provisioning: source: "dps" global_endpoint: "scheme://jibba-jabba.net" @@ -6,7 +8,7 @@ provisioning: attestation: method: "x509" registration_id: "register me fool" - identity_pk: "some/path/mr.t.pk.pem" + identity_pk: "file:///some/path/mr.t.pk.pem" agent: name: "edgeAgent" diff --git a/edgelet/edgelet-docker/test/windows/sample_settings.dps.x509.2.yaml b/edgelet/edgelet-docker/test/windows/bad_settings.dps.x509.3.yaml similarity index 87% rename from edgelet/edgelet-docker/test/windows/sample_settings.dps.x509.2.yaml rename to edgelet/edgelet-docker/test/windows/bad_settings.dps.x509.3.yaml index ebd1b8c8ecb..55ef5067bb1 100644 --- a/edgelet/edgelet-docker/test/windows/sample_settings.dps.x509.2.yaml +++ b/edgelet/edgelet-docker/test/windows/bad_settings.dps.x509.3.yaml @@ -1,4 +1,6 @@ # Configures the provisioning mode + +# x509 provisioning does not have a valid identity certificate URI specified provisioning: source: "dps" global_endpoint: "scheme://jibba-jabba.net" @@ -7,7 +9,7 @@ provisioning: method: "x509" registration_id: "register me fool" identity_cert: "some/path/mr.t.cer.pem" - identity_pk: "some/path/mr.t.pk.pem" + identity_pk: "file:///some/path/mr.t.pk.pem" agent: name: "edgeAgent" diff --git a/edgelet/edgelet-docker/test/windows/sample_settings.dps.x509.1.yaml b/edgelet/edgelet-docker/test/windows/bad_settings.dps.x509.4..yaml similarity index 83% rename from edgelet/edgelet-docker/test/windows/sample_settings.dps.x509.1.yaml rename to edgelet/edgelet-docker/test/windows/bad_settings.dps.x509.4..yaml index a0c8930ce96..bf2afa54ca4 100644 --- a/edgelet/edgelet-docker/test/windows/sample_settings.dps.x509.1.yaml +++ b/edgelet/edgelet-docker/test/windows/bad_settings.dps.x509.4..yaml @@ -1,11 +1,14 @@ # Configures the provisioning mode + +# x509 provisioning does not have a valid identity private key URI specified provisioning: source: "dps" global_endpoint: "scheme://jibba-jabba.net" scope_id: "i got no time for the jibba-jabba" attestation: method: "x509" - identity_cert: "some/path/mr.t.cer.pem" + registration_id: "register me fool" + identity_cert: "file:///some/path/mr.t.cer.pem" identity_pk: "some/path/mr.t.pk.pem" agent: diff --git a/edgelet/edgelet-docker/test/windows/sample_settings.tg.yaml b/edgelet/edgelet-docker/test/windows/sample_settings.tg.filepaths.yaml similarity index 85% rename from edgelet/edgelet-docker/test/windows/sample_settings.tg.yaml rename to edgelet/edgelet-docker/test/windows/sample_settings.tg.filepaths.yaml index 96d53be3b1b..0155e43d18d 100644 --- a/edgelet/edgelet-docker/test/windows/sample_settings.tg.yaml +++ b/edgelet/edgelet-docker/test/windows/sample_settings.tg.filepaths.yaml @@ -1,13 +1,12 @@ - # Configures the provisioning mode provisioning: source: "manual" device_connection_string: "HostName=something.something.com;DeviceId=something;SharedAccessKey=QXp1cmUgSW9UIEVkZ2U=" certificates: - device_ca_cert: "device_ca_cert.pem" - device_ca_pk: "device_ca_pk.pem" - trusted_ca_certs: "trusted_ca_certs.pem" + device_ca_cert: "C:\\Temp\\device_ca_cert.pem" + device_ca_pk: "C:\\Temp\\device_ca_pk.pem" + trusted_ca_certs: "C:\\Temp\\trusted_ca_certs.pem" agent: name: "edgeAgent" diff --git a/edgelet/iotedge/src/check/mod.rs b/edgelet/iotedge/src/check/mod.rs index bd7f4c8ed00..e5365c16599 100644 --- a/edgelet/iotedge/src/check/mod.rs +++ b/edgelet/iotedge/src/check/mod.rs @@ -19,7 +19,7 @@ use libc; use regex::Regex; use serde_json; -use edgelet_core::{self, MobyNetwork, Provisioning, RuntimeSettings, UrlExt}; +use edgelet_core::{self, AttestationMethod, MobyNetwork, Provisioning, RuntimeSettings, UrlExt}; use edgelet_docker::Settings; use edgelet_http::client::ClientImpl; use edgelet_http::MaybeProxyClient; @@ -70,6 +70,11 @@ static CHECKS: &[( ("container-local-time", "container time is close to host time", container_local_time), ("container-engine-dns", "DNS server", container_engine_dns), ("container-engine-ipv6", "IPv6 network configuration", container_engine_ipv6), + ( + "identity-certificate-expiry", + "production readiness: identity certificates expiry", + settings_identity_certificates_expiry, + ), ("certificates-quickstart", "production readiness: certificates", settings_certificates), ( "certificates-expiry", @@ -1278,32 +1283,35 @@ fn settings_certificates(check: &mut Check) -> Result Result { - fn parse_openssl_time( - time: &openssl::asn1::Asn1TimeRef, - ) -> chrono::ParseResult> { - // openssl::asn1::Asn1TimeRef does not expose any way to convert the ASN1_TIME to a Rust-friendly type - // - // Its Display impl uses ASN1_TIME_print, so we convert it into a String and parse it back - // into a chrono::DateTime - let time = time.to_string(); - let time = chrono::NaiveDateTime::parse_from_str(&time, "%b %e %H:%M:%S %Y GMT")?; - Ok(chrono::DateTime::::from_utc(time, chrono::Utc)) +fn settings_identity_certificates_expiry(check: &mut Check) -> Result { + let settings = if let Some(settings) = &check.settings { + settings + } else { + return Ok(CheckResult::Skipped); + }; + + if let Provisioning::Dps(dps) = settings.provisioning() { + if let AttestationMethod::X509(x509_info) = dps.attestation() { + let path = x509_info.identity_cert()?; + check_certificate_expiry(path) + } else { + Ok(CheckResult::Skipped) + } + } else { + Ok(CheckResult::Skipped) } +} +fn settings_certificates_expiry(check: &mut Check) -> Result { let settings = if let Some(settings) = &check.settings { settings } else { return Ok(CheckResult::Skipped); }; - let (device_ca_cert_path, device_ca_cert_path_source) = if let Some(certificates) = - settings.certificates() - { - ( - certificates.device_ca_cert().to_owned(), - Cow::Borrowed("certificates.device_ca_cert"), - ) + let device_ca_cert_path = if let Some(certificates) = settings.certificates() { + let path = certificates.device_ca_cert()?; + path.to_owned() } else { let certs_dir = settings.homedir().join("hsm").join("certs"); @@ -1324,65 +1332,14 @@ fn settings_certificates_expiry(check: &mut Check) -> Result now { - return Err(Context::new(format!( - "Device CA certificate in {} has not-before time {} which is in the future", - device_ca_cert_path_source, not_before, - )) - .into()); - } - - if not_after < now { - return Err(Context::new(format!( - "Device CA certificate in {} expired at {}", - device_ca_cert_path_source, not_after, - )) - .into()); - } - - if not_after < now + chrono::Duration::days(7) { - return Ok(CheckResult::Warning( - Context::new(format!( - "Device CA certificate in {} will expire soon ({})", - device_ca_cert_path_source, not_after, - )) - .into(), - )); - } - - Ok(CheckResult::Ok) + check_certificate_expiry(device_ca_cert_path) } fn settings_moby_runtime_uri(check: &mut Check) -> Result { @@ -1687,6 +1644,71 @@ fn edge_hub_ports_on_host(check: &mut Check) -> Result Result { + fn parse_openssl_time( + time: &openssl::asn1::Asn1TimeRef, + ) -> chrono::ParseResult> { + // openssl::asn1::Asn1TimeRef does not expose any way to convert the ASN1_TIME to a Rust-friendly type + // + // Its Display impl uses ASN1_TIME_print, so we convert it into a String and parse it back + // into a chrono::DateTime + let time = time.to_string(); + let time = chrono::NaiveDateTime::parse_from_str(&time, "%b %e %H:%M:%S %Y GMT")?; + Ok(chrono::DateTime::::from_utc(time, chrono::Utc)) + } + + let cert_path_source = cert_path.to_string_lossy().into_owned(); + let (not_after, not_before) = File::open(cert_path) + .map_err(failure::Error::from) + .and_then(|mut device_ca_cert_file| { + let mut device_ca_cert = vec![]; + device_ca_cert_file.read_to_end(&mut device_ca_cert)?; + let device_ca_cert = openssl::x509::X509::stack_from_pem(&device_ca_cert)?; + let device_ca_cert = &device_ca_cert[0]; + + let not_after = parse_openssl_time(device_ca_cert.not_after())?; + let not_before = parse_openssl_time(device_ca_cert.not_before())?; + + Ok((not_after, not_before)) + }) + .with_context(|_| { + format!( + "Could not parse {} as a valid certificate file", + cert_path_source, + ) + })?; + + let now = chrono::Utc::now(); + + if not_before > now { + return Err(Context::new(format!( + "Device CA certificate in {} has not-before time {} which is in the future", + cert_path_source, not_before, + )) + .into()); + } + + if not_after < now { + return Err(Context::new(format!( + "Device CA certificate in {} expired at {}", + cert_path_source, not_after, + )) + .into()); + } + + if not_after < now + chrono::Duration::days(7) { + return Ok(CheckResult::Warning( + Context::new(format!( + "Device CA certificate in {} will expire soon ({})", + cert_path_source, not_after, + )) + .into(), + )); + } + + Ok(CheckResult::Ok) +} + fn docker(docker_host_arg: &str, args: I) -> Result, (Option, failure::Error)> where I: IntoIterator, @@ -1851,7 +1873,7 @@ mod tests { fn config_file_checks_ok() { let mut runtime = tokio::runtime::current_thread::Runtime::new().unwrap(); - for filename in &["sample_settings.yaml", "sample_settings.tg.yaml"] { + for filename in &["sample_settings.yaml", "sample_settings.tg.filepaths.yaml"] { let config_file = format!( "{}/../edgelet-docker/test/{}/{}", env!("CARGO_MANIFEST_DIR"), diff --git a/edgelet/iotedged/src/error.rs b/edgelet/iotedged/src/error.rs index cbac47d8e7f..f3b09b00b20 100644 --- a/edgelet/iotedged/src/error.rs +++ b/edgelet/iotedged/src/error.rs @@ -152,6 +152,7 @@ impl From<&ErrorKind> for i32 { #[derive(Clone, Copy, Debug, PartialEq)] pub enum InitializeErrorReason { + CertificateSettings, CreateCertificateManager, CreateMasterEncryptionKey, CreateSettingsDirectory, @@ -169,6 +170,7 @@ pub enum InitializeErrorReason { HybridAuthKeyLoad, HybridAuthKeyInvalid, IncompatibleHsmVersion, + IdentityCertificateSettings, InvalidDeviceCertCredentials, InvalidDeviceConfig, InvalidHubConfig, @@ -203,6 +205,10 @@ pub enum ExternalProvisioningErrorReason { impl fmt::Display for InitializeErrorReason { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + InitializeErrorReason::CertificateSettings => { + write!(f, "Could not configure Edge gateway certificates") + } + InitializeErrorReason::CreateCertificateManager => { write!(f, "Could not create the certificate manager.") } @@ -265,6 +271,10 @@ impl fmt::Display for InitializeErrorReason { write!(f, "Incompatible HSM lib version") } + InitializeErrorReason::IdentityCertificateSettings => { + write!(f, "Could not configure Edge X.509 identity certificate") + } + InitializeErrorReason::InvalidDeviceCertCredentials => { write!(f, "Invalid identity certificate") } diff --git a/edgelet/iotedged/src/lib.rs b/edgelet/iotedged/src/lib.rs index d845e916da8..1ce13478252 100644 --- a/edgelet/iotedged/src/lib.rs +++ b/edgelet/iotedged/src/lib.rs @@ -263,7 +263,8 @@ where ); } - set_iot_edge_env_vars(&settings); + set_iot_edge_env_vars(&settings) + .context(ErrorKind::Initialize(InitializeErrorReason::LoadSettings))?; info!("Initializing hsm..."); let crypto = Crypto::new(hsm_lock.clone()) @@ -302,7 +303,7 @@ where ))?; macro_rules! start_edgelet { - ($key_store:ident, $provisioning_result:ident, $root_key:ident, $force_reprovision:ident) => {{ + ($key_store:ident, $provisioning_result:ident, $root_key:ident, $force_reprovision:ident, $id_cert_thumprint:ident,) => {{ info!("Finished provisioning edge device."); let runtime = init_runtime::( @@ -338,6 +339,7 @@ where &runtime, &crypto, &mut tokio_runtime, + $id_cert_thumprint, )?; let cfg = WorkloadData::new( @@ -388,7 +390,8 @@ where key_store, provisioning_result, root_key, - force_module_reprovision + force_module_reprovision, + None, ); } Provisioning::External(external) => { @@ -433,7 +436,8 @@ where derived_key_store, prov_result, memory_key, - force_module_reprovision + force_module_reprovision, + None, ); } else { let (derived_key_store, tpm_key) = @@ -442,7 +446,8 @@ where derived_key_store, prov_result, tpm_key, - force_module_reprovision + force_module_reprovision, + None, ); } } @@ -474,7 +479,8 @@ where key_store, provisioning_result, root_key, - force_module_reprovision + force_module_reprovision, + None, ); } AttestationMethod::SymmetricKey(ref symmetric_key_info) => { @@ -491,7 +497,8 @@ where key_store, provisioning_result, root_key, - force_module_reprovision + force_module_reprovision, + None, ); } AttestationMethod::X509(ref x509_info) => { @@ -518,13 +525,15 @@ where dps_path, &mut tokio_runtime, &key_bytes, - id_data.thumbprint, + id_data.thumbprint.clone(), )?; + let thumprint_op = Some(id_data.thumbprint.as_str()); start_edgelet!( key_store, provisioning_result, root_key, - force_module_reprovision + force_module_reprovision, + thumprint_op, ); } } @@ -536,7 +545,7 @@ where } } -fn set_iot_edge_env_vars(settings: &S) +fn set_iot_edge_env_vars(settings: &S) -> Result<(), Error> where S: RuntimeSettings, { @@ -553,16 +562,31 @@ where info!("Transparent gateway certificates not found, operating in quick start mode...") } Some(&c) => { - let path = c.device_ca_cert().as_os_str(); - info!("Configuring the Device CA certificate using {:?}.", path); + let path = c.device_ca_cert().context(ErrorKind::Initialize( + InitializeErrorReason::CertificateSettings, + ))?; + info!( + "Configuring the Device CA certificate using {:?}.", + path.as_os_str() + ); env::set_var(DEVICE_CA_CERT_KEY, path); - let path = c.device_ca_pk().as_os_str(); - info!("Configuring the Device private key using {:?}.", path); + let path = c.device_ca_pk().context(ErrorKind::Initialize( + InitializeErrorReason::CertificateSettings, + ))?; + info!( + "Configuring the Device private key using {:?}.", + path.as_os_str() + ); env::set_var(DEVICE_CA_PK_KEY, path); - let path = c.trusted_ca_certs().as_os_str(); - info!("Configuring the trusted CA certificates using {:?}.", path); + let path = c.trusted_ca_certs().context(ErrorKind::Initialize( + InitializeErrorReason::CertificateSettings, + ))?; + info!( + "Configuring the trusted CA certificates using {:?}.", + path.as_os_str() + ); env::set_var(TRUSTED_CA_CERTS_KEY, path); } }; @@ -586,12 +610,20 @@ where env::set_var(DPS_REGISTRATION_ID_ENV_KEY, val.to_string()); } - env::set_var(DPS_DEVICE_ID_CERT_ENV_KEY, x509_info.identity_cert()); - env::set_var(DPS_DEVICE_ID_KEY_ENV_KEY, x509_info.identity_pk()); + let path = x509_info.identity_cert().context(ErrorKind::Initialize( + InitializeErrorReason::IdentityCertificateSettings, + ))?; + env::set_var(DPS_DEVICE_ID_CERT_ENV_KEY, path.as_os_str()); + + let path = x509_info.identity_pk().context(ErrorKind::Initialize( + InitializeErrorReason::IdentityCertificateSettings, + ))?; + env::set_var(DPS_DEVICE_ID_KEY_ENV_KEY, path.as_os_str()); } } } info!("Finished configuring provisioning environment variables and certificates."); + Ok(()) } fn prepare_httpclient_and_identity_data( @@ -900,37 +932,36 @@ where Ok(key_bytes.to_vec()) } -fn compute_settings_digest(settings: &S) -> Result +fn compute_settings_digest( + settings: &S, + id_cert_thumbprint: Option<&str>, +) -> Result where S: RuntimeSettings + Serialize, { let mut s = serde_json::to_string(settings)?; - - if let Provisioning::Dps(dps) = settings.provisioning() { - if let AttestationMethod::X509(x509_info) = dps.attestation() { - let mut file = OpenOptions::new() - .read(true) - .open(x509_info.identity_cert())?; - let mut cert = String::new(); - file.read_to_string(&mut cert)?; - s.push_str(&cert); - } + if let Some(thumbprint) = id_cert_thumbprint { + s.push_str(thumbprint); } Ok(base64::encode(&Sha256::digest_str(&s))) } -fn diff_with_cached(settings: &S, path: &Path) -> bool +fn diff_with_cached(settings: &S, path: &Path, id_cert_thumbprint: Option<&str>) -> bool where S: RuntimeSettings + Serialize, { - fn diff_with_cached_inner(cached_settings: &S, path: &Path) -> Result + fn diff_with_cached_inner( + cached_settings: &S, + path: &Path, + id_cert_thumbprint: Option<&str>, + ) -> Result where S: RuntimeSettings + Serialize, { let mut file = OpenOptions::new().read(true).open(path)?; let mut buffer = String::new(); file.read_to_string(&mut buffer)?; - let encoded = compute_settings_digest(cached_settings)?; + let encoded = compute_settings_digest(cached_settings, id_cert_thumbprint)?; if encoded == buffer { debug!("Config state matches supplied config."); Ok(false) @@ -939,7 +970,7 @@ where } } - match diff_with_cached_inner(settings, path) { + match diff_with_cached_inner(settings, path, id_cert_thumbprint) { Ok(result) => result, Err(err) => { @@ -973,6 +1004,7 @@ fn check_settings_state( runtime: &M::ModuleRuntime, crypto: &C, tokio_runtime: &mut tokio::runtime::Runtime, + id_cert_thumbprint: Option<&str>, ) -> Result<(), Error> where M: MakeModuleRuntime + 'static, @@ -982,7 +1014,7 @@ where info!("Detecting if configuration file has changed..."); let path = subdir.join(filename); let mut reconfig_reqd = false; - let diff = diff_with_cached(settings, &path); + let diff = diff_with_cached(settings, &path, id_cert_thumbprint); if diff { info!("Change to configuration file detected."); reconfig_reqd = true; @@ -999,7 +1031,15 @@ where }; } if reconfig_reqd { - reconfigure::(subdir, filename, settings, runtime, crypto, tokio_runtime)?; + reconfigure::( + subdir, + filename, + settings, + runtime, + crypto, + tokio_runtime, + id_cert_thumbprint, + )?; } Ok(()) } @@ -1023,6 +1063,7 @@ fn reconfigure( runtime: &M::ModuleRuntime, crypto: &C, tokio_runtime: &mut tokio::runtime::Runtime, + id_cert_thumbprint: Option<&str>, ) -> Result<(), Error> where M: MakeModuleRuntime + 'static, @@ -1056,7 +1097,7 @@ where prepare_workload_ca(crypto)?; let mut file = File::create(path).context(ErrorKind::Initialize(InitializeErrorReason::SaveSettings))?; - let digest = compute_settings_digest(settings) + let digest = compute_settings_digest(settings, id_cert_thumbprint) .context(ErrorKind::Initialize(InitializeErrorReason::SaveSettings))?; file.write_all(digest.as_bytes()) .context(ErrorKind::Initialize(InitializeErrorReason::SaveSettings))?; @@ -1899,6 +1940,7 @@ mod tests { &runtime, &crypto, &mut tokio_runtime, + None, ); match result.unwrap_err().kind() { ErrorKind::Initialize(InitializeErrorReason::PrepareWorkloadCa) => (), @@ -1941,6 +1983,7 @@ mod tests { &runtime, &crypto, &mut tokio_runtime, + None, ); match result.unwrap_err().kind() { ErrorKind::Initialize(InitializeErrorReason::IssuerCAExpiration) => (), @@ -1983,6 +2026,7 @@ mod tests { &runtime, &crypto, &mut tokio_runtime, + None, ) .unwrap(); let expected = serde_json::to_string(&settings).unwrap(); @@ -2042,6 +2086,7 @@ mod tests { &runtime, &crypto, &mut tokio_runtime, + None, ) .unwrap(); let expected = serde_json::to_string(&settings).unwrap(); @@ -2101,6 +2146,7 @@ mod tests { &runtime, &crypto, &mut tokio_runtime, + None, ) .unwrap(); let expected = serde_json::to_string(&settings).unwrap(); @@ -2160,6 +2206,7 @@ mod tests { &runtime, &crypto, &mut tokio_runtime, + None, ) .unwrap(); let mut written = String::new(); @@ -2177,6 +2224,7 @@ mod tests { &runtime, &crypto, &mut tokio_runtime, + None, ) .unwrap(); let expected = serde_json::to_string(&settings1).unwrap(); @@ -2243,7 +2291,7 @@ mod tests { .unwrap() .write_all(base64_to_write.as_bytes()) .unwrap(); - assert!(!diff_with_cached(&settings, &path)); + assert!(!diff_with_cached(&settings, &path, None)); } #[test] @@ -2259,7 +2307,7 @@ mod tests { .write_all(base64_to_write.as_bytes()) .unwrap(); let settings = Settings::new(Some(Path::new(GOOD_SETTINGS_DPS_TPM1))).unwrap(); - assert!(!diff_with_cached(&settings, &path)); + assert!(!diff_with_cached(&settings, &path, None)); } #[test] @@ -2275,7 +2323,7 @@ mod tests { .write_all(base64_to_write.as_bytes()) .unwrap(); let settings = Settings::new(Some(Path::new(GOOD_SETTINGS_DPS_DEFAULT))).unwrap(); - assert!(!diff_with_cached(&settings, &path)); + assert!(!diff_with_cached(&settings, &path, None)); } #[test] @@ -2291,7 +2339,7 @@ mod tests { .write_all(base64_to_write.as_bytes()) .unwrap(); let settings = Settings::new(Some(Path::new(GOOD_SETTINGS))).unwrap(); - assert!(!diff_with_cached(&settings, &path)); + assert!(!diff_with_cached(&settings, &path, None)); } #[test] @@ -2307,13 +2355,13 @@ mod tests { .write_all(base64_to_write.as_bytes()) .unwrap(); let settings = Settings::new(Some(Path::new(GOOD_SETTINGS))).unwrap(); - assert!(diff_with_cached(&settings, &path)); + assert!(diff_with_cached(&settings, &path, None)); } #[test] fn diff_with_no_file_returns_true() { let settings = Settings::new(Some(Path::new(GOOD_SETTINGS))).unwrap(); - assert!(diff_with_cached(&settings, Path::new("i dont exist"))); + assert!(diff_with_cached(&settings, Path::new("i dont exist"), None)); } #[test] @@ -2358,6 +2406,14 @@ mod tests { .write_all(b"i pity the fool") .expect("Test cert private key file could not be written"); + let cert_uri = format!( + "file://{}", + cert_path.canonicalize().unwrap().to_str().unwrap() + ); + let pk_uri = format!( + "file://{}", + key_path.canonicalize().unwrap().to_str().unwrap() + ); let settings_yaml = json!({ "provisioning": { "source": "dps", @@ -2365,8 +2421,8 @@ mod tests { "scope_id": "i got no time for the jibba-jabba", "attestation": { "method": "x509", - "identity_cert": cert_path.to_str().unwrap(), - "identity_pk": key_path.to_str().unwrap(), + "identity_cert": cert_uri, + "identity_pk": pk_uri, }, }}) .to_string(); @@ -2394,7 +2450,7 @@ mod tests { } #[test] - fn dps_x509_auth_diff_also_checks_cert_file() { + fn dps_x509_auth_diff_also_checks_cert_thumbprint() { let tmp_dir = TempDir::new("blah").unwrap(); let cert_path = tmp_dir.path().join("test_cert"); let key_path = tmp_dir.path().join("test_key"); @@ -2404,21 +2460,23 @@ mod tests { let settings = Settings::new(Some(&settings_path)).unwrap(); let path = tmp_dir.path().join("cache"); - let base64_to_write = compute_settings_digest(&settings).unwrap(); + let base64_to_write = compute_settings_digest(&settings, Some("thumbprint-1")).unwrap(); File::create(&path) .unwrap() .write_all(base64_to_write.as_bytes()) .unwrap(); // check if there is no diff - assert_eq!(diff_with_cached(&settings, &path), false); + assert_eq!( + diff_with_cached(&settings, &path, Some("thumbprint-1")), + false + ); - // now modify only the cert file and test if there is a diff - File::create(&cert_path) - .unwrap() - .write_all(b"CN=B.A. Baracus") - .unwrap(); - assert_eq!(diff_with_cached(&settings, &path), true); + // now modify only the cert thumbprint and test if there is a diff + assert_eq!( + diff_with_cached(&settings, &path, Some("thumbprint-2")), + true + ); } #[test] diff --git a/scripts/windows/setup/IotEdgeSecurityDaemon.ps1 b/scripts/windows/setup/IotEdgeSecurityDaemon.ps1 index 85a0ed892d3..b42ba97c7bc 100644 --- a/scripts/windows/setup/IotEdgeSecurityDaemon.ps1 +++ b/scripts/windows/setup/IotEdgeSecurityDaemon.ps1 @@ -123,11 +123,6 @@ PS> Initialize-IoTEdge -Dps -ScopeId $scopeId -ContainerOs Windows -X509Identity -X509IdentityPrivateKey $x509IdentityPrivateKey -DeviceCACertificate $deviceCACertificate -DeviceCAPrivateKey $deviceCAPrivateKey -DeviceTrustbundle $deviceTrustbundle -.EXAMPLE - -PS> Initialize-IoTEdge -Dps -ScopeId $scopeId -RegistrationId $registrationId -ContainerOs Windows -AutoGenX509IdentityCertificate $true -DeviceCACertificate $deviceCACertificate -DeviceCAPrivateKey $deviceCAPrivateKey -DeviceTrustbundle $deviceTrustbundle - - .EXAMPLE PS> Initialize-IoTEdge -External -ExternalProvisioningEndpoint $externalProvisioningEndpoint -ContainerOs Windows -DeviceCACertificate $deviceCACertificate -DeviceCAPrivateKey $deviceCAPrivateKey -DeviceTrustbundle $deviceTrustbundle @@ -175,10 +170,6 @@ function Initialize-IoTEdge { [ValidateNotNullOrEmpty()] [String] $X509IdentityPrivateKey, - # Auto generate the X.509 identity certificate from the device CA - [Parameter(ParameterSetName = 'DPS')] - [bool] $AutoGenX509IdentityCertificate = $false, - # The Edge device CA certificate [ValidateNotNullOrEmpty()] [String] $DeviceCACertificate, @@ -472,11 +463,6 @@ PS> Install-IoTEdge -Dps -ScopeId $scopeId -ContainerOs Windows -X509IdentityCer PS> Install-IoTEdge -Dps -ScopeId $scopeId -ContainerOs Windows -X509IdentityCertificate $x509IdentityCertificate -X509IdentityPrivateKey $x509IdentityPrivateKey -DeviceCACertificate $deviceCACertificate -DeviceCAPrivateKey $deviceCAPrivateKey -DeviceTrustbundle $deviceTrustbundle -.EXAMPLE - -PS> Install-IoTEdge -Dps -ScopeId $scopeId -RegistrationId $registrationId -ContainerOs Windows -AutoGenX509IdentityCertificate $true -DeviceCACertificate $deviceCACertificate -DeviceCAPrivateKey $deviceCAPrivateKey -DeviceTrustbundle $deviceTrustbundle - - .EXAMPLE PS> Install-IoTEdge -External -ExternalProvisioningEndpoint $externalProvisioningEndpoint -ContainerOs Windows -DeviceCACertificate $deviceCACertificate -DeviceCAPrivateKey $deviceCAPrivateKey -DeviceTrustbundle $deviceTrustbundle @@ -524,10 +510,6 @@ function Install-IoTEdge { [ValidateNotNullOrEmpty()] [String] $X509IdentityPrivateKey, - # Auto generate the X.509 identity certificate from the device CA - [Parameter(ParameterSetName = 'DPS')] - [bool] $AutoGenX509IdentityCertificate = $false, - # The Edge device CA certificate [ValidateNotNullOrEmpty()] [String] $DeviceCACertificate, @@ -622,7 +604,6 @@ function Install-IoTEdge { if ($SymmetricKey) { $Params["-SymmetricKey"] = $SymmetricKey } if ($X509IdentityCertificate) { $Params["-X509IdentityCertificate"] = $X509IdentityCertificate } if ($X509IdentityPrivateKey) { $Params["-X509IdentityPrivateKey"] = $X509IdentityPrivateKey } - if ($AutoGenX509IdentityCertificate) { $Params["-AutoGenX509IdentityCertificate"] = $AutoGenX509IdentityCertificate } if ($DeviceCACertificate) { $Params["-DeviceCACertificate"] = $DeviceCACertificate } if ($DeviceCAPrivateKey) { $Params["-DeviceCAPrivateKey"] = $DeviceCAPrivateKey } if ($DeviceTrustbundle) { $Params["-DeviceTrustbundle"] = $DeviceTrustbundle } @@ -1563,9 +1544,6 @@ function Get-DpsProvisioningSettings { if ($SymmetricKey) { $attestationMethod = 'symmetric_key' } - elseif ($AutoGenX509IdentityCertificate) { - $attestationMethod = 'x509' - } elseif ($X509IdentityCertificate -or $X509IdentityPrivateKey) { $attestationMethod = 'x509' $idCertFilesProvided = $true @@ -1585,29 +1563,15 @@ function Get-DpsProvisioningSettings { } if ($attestationMethod -eq 'x509') { - if ($idCertFilesProvided) { - if (-Not (Test-Path -Path $X509IdentityCertificate)) { - Write-HostRed - Write-HostRed "Identity certificate file $X509IdentityCertificate not found." - throw - } - if (-Not (Test-Path -Path $X509IdentityPrivateKey)) { - Write-HostRed - Write-HostRed "Identity private file $X509IdentityPrivateKey not found." - throw - } + if (-Not (Test-Path -Path $X509IdentityCertificate)) { + Write-HostRed + Write-HostRed "Identity certificate file $X509IdentityCertificate not found." + throw } - else { - if ($X509IdentityCertificate -or $X509IdentityPrivateKey) { - Write-HostRed - Write-HostRed 'Cannot specify a device identity certificate and also set AutoGenX509IdentityCertificate as true.' - throw - } - if (-Not (Validate-GatewaySettings)) { - Write-HostRed - Write-HostRed 'Device CA certificate files are not found. These are required when using AutoGenX509IdentityCertificate.' - throw - } + if (-Not (Test-Path -Path $X509IdentityPrivateKey)) { + Write-HostRed + Write-HostRed "Identity private file $X509IdentityPrivateKey not found." + throw } } @@ -1661,10 +1625,12 @@ function Set-ProvisioningMode { $replacementContent += " symmetric_key: '$SymmetricKey'" } if ($X509IdentityCertificate) { - $replacementContent += " identity_cert: '$X509IdentityCertificate'" + $uri = ([System.Uri][System.IO.Path]::GetFullPath($X509IdentityCertificate)).AbsoluteUri + $replacementContent += " identity_cert: '$uri'" } if ($X509IdentityPrivateKey) { - $replacementContent += " identity_pk: '$X509IdentityPrivateKey'" + $uri = ([System.Uri][System.IO.Path]::GetFullPath($X509IdentityPrivateKey)).AbsoluteUri + $replacementContent += " identity_pk: '$uri'" } $configurationYaml = $configurationYaml -replace $selectionRegex, ($replacementContent -join "`n") @@ -1683,11 +1649,14 @@ function Set-Certificates { Update-ConfigYaml({ param($configurationYaml) $selectionRegex = '(?:[^\S\n]*#[^\S\n]*)?certificates:\s*#?\s*device_ca_cert:\s*".*"\s*#?\s*device_ca_pk:\s*".*"\s*#?\s*trusted_ca_certs:\s*".*"' + $certURI = ([System.Uri][System.IO.Path]::GetFullPath($DeviceCACertificate)).AbsoluteUri + $keyURI = ([System.Uri][System.IO.Path]::GetFullPath($DeviceCAPrivateKey)).AbsoluteUri + $tbURI = ([System.Uri][System.IO.Path]::GetFullPath($DeviceTrustbundle)).AbsoluteUri $replacementContent = @( "certificates:", - " device_ca_cert: '$DeviceCACertificate'", - " device_ca_pk: '$DeviceCAPrivateKey'", - " trusted_ca_certs: '$DeviceTrustbundle'") + " device_ca_cert: '$certURI'", + " device_ca_pk: '$keyURI'", + " trusted_ca_certs: '$tbURI'") $configurationYaml = ($configurationYaml -replace $selectionRegex, ($replacementContent -join "`n")) Write-HostGreen 'Configured device for manual provisioning.' return $configurationYaml diff --git a/smoke/IotEdgeQuickstart/details/IotedgedLinux.cs b/smoke/IotEdgeQuickstart/details/IotedgedLinux.cs index 3a30a8413fb..edd023c0a6e 100644 --- a/smoke/IotEdgeQuickstart/details/IotedgedLinux.cs +++ b/smoke/IotEdgeQuickstart/details/IotedgedLinux.cs @@ -207,9 +207,11 @@ public async Task Configure(DeviceProvisioningMethod method, string image, strin doc.ReplaceOrAdd("provisioning.attestation.symmetric_key", dps.SymmetricKey.Expect(() => new ArgumentException("Expected symmetric key"))); break; case DPSAttestationType.X509: + var certUri = new Uri(dps.DeviceIdentityCertificate.Expect(() => new ArgumentException("Expected path to identity certificate"))); + var keyUri = new Uri(dps.DeviceIdentityPrivateKey.Expect(() => new ArgumentException("Expected path to identity private key"))); doc.ReplaceOrAdd("provisioning.attestation.method", "x509"); - doc.ReplaceOrAdd("provisioning.attestation.identity_cert", dps.DeviceIdentityCertificate.Expect(() => new ArgumentException("Expected path to identity certificate"))); - doc.ReplaceOrAdd("provisioning.attestation.identity_pk", dps.DeviceIdentityPrivateKey.Expect(() => new ArgumentException("Expected path to identity private key"))); + doc.ReplaceOrAdd("provisioning.attestation.identity_cert", certUri.AbsoluteUri); + doc.ReplaceOrAdd("provisioning.attestation.identity_pk", keyUri.AbsoluteUri); break; default: doc.ReplaceOrAdd("provisioning.attestation.method", "tpm");