diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml new file mode 100644 index 0000000..da705bf --- /dev/null +++ b/.github/workflows/integration.yml @@ -0,0 +1,21 @@ +name: Integration Tests + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +env: + CARGO_TERM_COLOR: always + +jobs: + ubuntu-integration-test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Install libpcsc + run: sudo apt install -y libpcsclite-dev + - name: Run Integration Tests + run: ./tests/integration.sh diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 3ab473b..797b1ba 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -56,4 +56,4 @@ jobs: steps: - uses: actions/checkout@v2 - name: Build - run: cargo build --package=rustica --features="splunk,influx,local-db,amazon-kms" + run: cargo build --package=rustica --features="splunk,influx,local-db,amazon-kms" \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index e43bf07..039009b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1781,7 +1781,7 @@ dependencies = [ [[package]] name = "rustica" -version = "0.8.2" +version = "0.8.3" dependencies = [ "aws-config", "aws-sdk-kms", @@ -1812,7 +1812,7 @@ dependencies = [ [[package]] name = "rustica-agent" -version = "0.8.1" +version = "0.8.3" dependencies = [ "base64 0.12.3", "byteorder", @@ -2125,9 +2125,9 @@ dependencies = [ [[package]] name = "sshcerts" -version = "0.9.1" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1998030dbf62c4395095559c47fcbf32e1c2bea1dc2fec737a86730f0de86fc7" +checksum = "c3d170d1d0f2749220d73126609cd3b6e6179f0d5ed677d24cf4061dbfd2fb27" dependencies = [ "base64 0.13.0", "der-parser 5.1.2", diff --git a/examples/example.db b/examples/example.db index 2ebdecd..905a6e7 100644 Binary files a/examples/example.db and b/examples/example.db differ diff --git a/examples/rustica_local_file_alt.toml b/examples/rustica_local_file_alt.toml new file mode 100644 index 0000000..9294251 --- /dev/null +++ b/examples/rustica_local_file_alt.toml @@ -0,0 +1,83 @@ +# The certificate presented to connecting clients +server_cert = ''' +-----BEGIN CERTIFICATE----- +MIIBqjCCAVCgAwIBAgIJAOI2FtcQeixVMAoGCCqGSM49BAMCMBsxGTAXBgNVBAMM +EEVudGVycHJpc2VSb290Q0EwHhcNMjIwMTIwMDQwMjA2WhcNMjQwNDI0MDQwMjA2 +WjAxMRAwDgYDVQQDDAdydXN0aWNhMRAwDgYDVQQKDAdSdXN0aWNhMQswCQYDVQQG +EwJDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABAINhoFW/5twPqAHLxjFjmns +lE1jJMJQXmijymZTJxR0DsNZlwvUgNH+WYQFfq4IVMwypVHgyTYJO+lAAPEeyPOj +ZzBlMDUGA1UdIwQuMCyhH6QdMBsxGTAXBgNVBAMMEEVudGVycHJpc2VSb290Q0GC +CQCRg096sVtP0zAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE8DAUBgNVHREEDTALggls +b2NhbGhvc3QwCgYIKoZIzj0EAwIDSAAwRQIhAMfjW/PMrA9/cCg6O835sr22ZrNk +k/lFOODLqAJPbh3+AiAzeCUyrmxT5VTf6uyFoNT8zMoWSi79rudcdgl+32RqMg== +-----END CERTIFICATE----- +''' + +# The key for the certificate presented to clients +server_key = ''' +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgkTd0C69xWFX9PmVf +BeD0ySfG+O0e7p7SXR9xo/enbvahRANCAAQCDYaBVv+bcD6gBy8YxY5p7JRNYyTC +UF5oo8pmUycUdA7DWZcL1IDR/lmEBX6uCFTMMqVR4Mk2CTvpQADxHsjz +-----END PRIVATE KEY----- +''' + +# The CA certificate clients must have their identities signed by +client_ca_cert = ''' +-----BEGIN CERTIFICATE----- +MIIBMTCB2AIJAL3yJUJ7ShOJMAoGCCqGSM49BAMCMCExHzAdBgNVBAMMFkVudGVy +cHJpc2VDbGllbnRSb290Q0EwHhcNMjIwMTIwMDQwMjA2WhcNMzIwMTE4MDQwMjA2 +WjAhMR8wHQYDVQQDDBZFbnRlcnByaXNlQ2xpZW50Um9vdENBMFkwEwYHKoZIzj0C +AQYIKoZIzj0DAQcDQgAEmZbAXHUQEXKB/NmHCG0AcjQA0IsBph+jmcFbw9Na58Cv +PNZtmm8jQ3Q9R8e3faG6gZuXNe60q/Ea6a6jR5UryDAKBggqhkjOPQQDAgNIADBF +AiA7H67T6QVZq1pBs6MU6+f/4mxYbVkPi/aCqMthRin7qQIhAP3cYrcO8QibwSjx +QCYQlNrEWT9OVP/akN0OyFDQIYIU +-----END CERTIFICATE----- +''' + +# This is the listen address that will be used for the Rustica service +listen_address = "0.0.0.0:50052" + +# This setting controls if the agent has to prove that it +# controls the private key to Rustica. Setting this to true means a user needs +# to generate two signatures (one to Rustica, and one to the host). The +# advantage of using this, is a compromised host cannot get certificates +# from the server without physical interaction. +# +# A client will always need to sign the challenge from the host they +# are attempting to connect to however so a physical tap will always +# be required. +require_rustica_proof = false + + +# Rustica has many ways it can sign SSH certificates which are sent to +# clients. This method uses private keys embedded in the configuration +# file. This will mean the hosts which you want to login to via Rustica +# must respect the public portion of the user key variable below. +[signing."file"] +user_key = ''' +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACAipVtvuI/JPxiyNkRWEjiBE/JtQi8iGjz4sSeLmBMz/wAAAKhiepqoYnqa +qAAAAAtzc2gtZWQyNTUxOQAAACAipVtvuI/JPxiyNkRWEjiBE/JtQi8iGjz4sSeLmBMz/w +AAAEBHwGHZTQ6oGSiiz7kB6/g5g2mNWSX3U4e5WnVZFCv8jSKlW2+4j8k/GLI2RFYSOIET +8m1CLyIaPPixJ4uYEzP/AAAAIW9iZWxpc2tATWl0Y2hlbGxzLU1CUC5sb2NhbGRvbWFpbg +ECAwQ= +-----END OPENSSH PRIVATE KEY----- +''' + +host_key = ''' +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACAXAtkLkmySqYT2isdH0cROdrAzT2cGg9pL9eLpZwQnewAAAKhQSP5+UEj+ +fgAAAAtzc2gtZWQyNTUxOQAAACAXAtkLkmySqYT2isdH0cROdrAzT2cGg9pL9eLpZwQnew +AAAEAevZOed5UnsVdAASUn+sJ+dUfUnG1kQ1wRH9L758mSCxcC2QuSbJKphPaKx0fRxE52 +sDNPZwaD2kv14ulnBCd7AAAAIW9iZWxpc2tATWl0Y2hlbGxzLU1CUC5sb2NhbGRvbWFpbg +ECAwQ= +-----END OPENSSH PRIVATE KEY----- +''' + +[logging."stdout"] + +[authorization."database"] +path = "examples/example.db" \ No newline at end of file diff --git a/rustica-agent/Cargo.toml b/rustica-agent/Cargo.toml index 42b1a2d..75debea 100644 --- a/rustica-agent/Cargo.toml +++ b/rustica-agent/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustica-agent" -version = "0.8.1" +version = "0.8.3" authors = ["Mitchell Grenier "] edition = "2018" @@ -22,7 +22,7 @@ serde = "1.0.97" serde_derive = "1.0" sha2 = "0.9.2" # For Production -sshcerts = {version = "0.9.1", features = ["yubikey-support"]} +sshcerts = {version = "0.10.1", features = ["yubikey-support"]} # For Development # sshcerts = {git = "https://github.com/obelisk/sshcerts", features = ["yubikey-support"]} # sshcerts = {path = "../../sshcerts", features = ["yubikey-support"]} diff --git a/rustica-agent/src/lib.rs b/rustica-agent/src/lib.rs index 0268d89..1362717 100644 --- a/rustica-agent/src/lib.rs +++ b/rustica-agent/src/lib.rs @@ -12,7 +12,9 @@ pub use rustica::{ RefreshError::{ConfigurationError, SigningError} }; -use sshcerts::ssh::{Certificate, CertType, PrivateKey, SigningFunction}; + +use sshcerts::ssh::{Certificate, CertType, PrivateKey, SSHCertificateSigner}; +use sshcerts::utils::format_signature_for_ssh; use sshcerts::yubikey::piv::{AlgorithmId, SlotId, RetiredSlotId, TouchPolicy, PinPolicy, Yubikey}; use std::collections::HashMap; @@ -171,10 +173,10 @@ impl SshAgentHandler for Handler { // key is the same process as keys added afterwards, we do this to prevent duplication // of the private key based signing code. // TODO: @obelisk make this better - let signer: Option = if self.identities.contains_key(&pubkey) { - Some(self.identities[&pubkey].clone().into()) - } else if let Signatory::Direct(privkey) = &mut self.signatory { - Some(privkey.clone().into()) + let private_key: Option<&PrivateKey> = if self.identities.contains_key(&pubkey) { + Some(&self.identities[&pubkey]) + } else if let Signatory::Direct(privkey) = &self.signatory { + Some(privkey) } else if let Signatory::Yubikey(signer) = &mut self.signatory { // If using long lived certificates you might need to tap again here because you didn't have to // to get the certificate the first time @@ -182,30 +184,35 @@ impl SshAgentHandler for Handler { f() } - let signature = signer.yk.ssh_cert_signer(&data, &signer.slot).unwrap(); - // TODO: @obelisk Why is this magic value here - let signature = (&signature[27..]).to_vec(); - let pubkey = signer.yk.ssh_cert_fetch_pubkey(&signer.slot).unwrap(); - - return Ok(Response::SignResponse { - algo_name: String::from(pubkey.key_type.name), - signature, - }); + let pubkey = signer.yk.ssh_cert_fetch_pubkey(&signer.slot).unwrap(); + let signature = signer.yk.ssh_cert_signer(&data, &signer.slot).map_err(|_| AgentError::from("Yubikey signing error"))?; + + let signature = match format_signature_for_ssh(&pubkey, &signature) { + Some(s) => s, + None => return Err(AgentError::from("Signature could not be converted to SSH format")), + }; + + return Ok(Response::SignResponse { + signature, + }); } else { None }; - match signer { - Some(signer) => { - let sig = match signer(&data) { + match private_key { + Some(key) => { + let signature = match key.sign(&data) { None => return Err(AgentError::from("Signing Error")), - Some(signature) => signature.to_vec(), + Some(signature) => format_signature_for_ssh(&key.pubkey, &signature), + }; + + let signature = match signature { + Some(s) => s, + None => return Err(AgentError::from("Signature could not be converted to SSH format")) }; - let mut reader = sshcerts::ssh::Reader::new(&sig); Ok(Response::SignResponse { - algo_name: reader.read_string().unwrap(), - signature: reader.read_bytes().unwrap(), + signature, }) } None => Err(AgentError::from("Signing Error: No Valid Keys")) diff --git a/rustica-agent/src/rustica/cert.rs b/rustica-agent/src/rustica/cert.rs index ce1ff43..12348a5 100644 --- a/rustica-agent/src/rustica/cert.rs +++ b/rustica-agent/src/rustica/cert.rs @@ -1,7 +1,7 @@ use super::error::{RefreshError, ServerError}; use super::{CertificateRequest, Signatory, RusticaCert}; use crate::{CertificateConfig, RusticaServer}; -use sshcerts::ssh::{CriticalOptions, Extensions}; +use sshcerts::Certificate; use std::collections::HashMap; use std::time::SystemTime; @@ -19,8 +19,8 @@ impl RusticaServer { let request = tonic::Request::new(CertificateRequest { cert_type: options.cert_type as u32, key_id: String::from(""), // Rustica Server ignores this field - critical_options: HashMap::from(CriticalOptions::None), - extensions: HashMap::from(Extensions::Standard), + critical_options: HashMap::new(), + extensions: Certificate::standard_extensions(), servers: options.hosts.clone(), principals: options.principals.clone(), valid_before: current_timestamp + options.duration, diff --git a/rustica-agent/src/sshagent/protocol.rs b/rustica-agent/src/sshagent/protocol.rs index 07b30e5..767e1b1 100644 --- a/rustica-agent/src/sshagent/protocol.rs +++ b/rustica-agent/src/sshagent/protocol.rs @@ -160,7 +160,6 @@ pub enum Response { Failure, Identities(Vec), SignResponse { - algo_name: String, signature: Vec, }, } @@ -180,14 +179,10 @@ impl Response { write_message(&mut buf, identity.key_comment.as_bytes())?; } } - Response::SignResponse { ref algo_name, ref signature } => { + Response::SignResponse { ref signature } => { buf.write_u8(AgentMessageResponse::SignResponse as u8)?; - let mut full_sig = Vec::new(); - write_message(&mut full_sig, algo_name.as_bytes())?; - write_message(&mut full_sig, signature)?; - - write_message(&mut buf, full_sig.as_slice())?; + write_message(&mut buf, signature.as_slice())?; } } stream.write_u32::(buf.len() as u32)?; diff --git a/rustica/Cargo.toml b/rustica/Cargo.toml index 4666b54..fb17f63 100644 --- a/rustica/Cargo.toml +++ b/rustica/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustica" -version = "0.8.2" +version = "0.8.3" authors = ["Mitchell Grenier "] edition = "2018" @@ -29,7 +29,7 @@ ring = "0.16.20" serde = {version = "1.0", features = ["derive"]} sha2 = "0.9.2" # For Production -sshcerts = { version = "0.9.1", default-features = false, features = ["x509-support"] } +sshcerts = { version = "0.10.1", default-features = false, features = ["x509-support"] } # For Development # sshcerts = {git = "https://github.com/obelisk/sshcerts", branch="more_versatile_certificate_signing", default-features = false, features = ["x509-support"]} # sshcerts = {path = "../../sshcerts", features = ["x509-support"]} diff --git a/rustica/src/auth/database.rs b/rustica/src/auth/database.rs index 38a5f64..96a1720 100644 --- a/rustica/src/auth/database.rs +++ b/rustica/src/auth/database.rs @@ -14,7 +14,7 @@ use super::{ RegisterKeyRequestProperties, }; -use sshcerts::ssh::{CertType, Extensions}; +use sshcerts::ssh::{Certificate, CertType}; #[derive(Deserialize)] pub struct LocalDatabase { @@ -82,7 +82,7 @@ impl LocalDatabase { principals: if results[0].principal_unrestricted {req.principals.clone()} else {principals}, // When host is unrestricted we return None hosts: if results[0].host_unrestricted {None} else {hosts}, - extensions: Extensions::Standard, + extensions: Certificate::standard_extensions(), force_command: None, force_source_ip: false, valid_after: req.valid_after, diff --git a/rustica/src/auth/external.rs b/rustica/src/auth/external.rs index 3fd074e..c8dd581 100644 --- a/rustica/src/auth/external.rs +++ b/rustica/src/auth/external.rs @@ -4,7 +4,6 @@ use author::{AuthorizeRequest, AddIdentityDataRequest}; use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity}; use serde::Deserialize; -use sshcerts::ssh::Extensions; use super::{ Authorization, AuthorizationError, @@ -82,8 +81,6 @@ impl AuthServer { .map(|ext| (ext.strip_prefix("extension.").unwrap().to_string(), approval_response[ext].clone())) .collect(); - let extensions = Extensions::Custom(extensions); - let force_command = if approval_response.contains_key("force_command") { Some(approval_response["force_command"].clone()) } else { diff --git a/rustica/src/auth/mod.rs b/rustica/src/auth/mod.rs index 86928c8..21a25c7 100644 --- a/rustica/src/auth/mod.rs +++ b/rustica/src/auth/mod.rs @@ -4,9 +4,10 @@ pub mod external; pub use super::key::KeyAttestation; -use sshcerts::ssh::{CertType, Extensions}; +use sshcerts::ssh::CertType; use serde::Deserialize; +use std::collections::HashMap; use std::convert::TryInto; #[derive(Debug)] @@ -31,7 +32,7 @@ pub struct Authorization { pub valid_after: u64, pub principals: Vec, pub hosts: Option>, - pub extensions: Extensions, + pub extensions: HashMap, pub force_command: Option, pub force_source_ip: bool, } diff --git a/rustica/src/server.rs b/rustica/src/server.rs index f017836..9ce872b 100644 --- a/rustica/src/server.rs +++ b/rustica/src/server.rs @@ -18,7 +18,7 @@ use crate::yubikey::verify_certificate_chain; use crossbeam_channel::Sender; use sshcerts::ssh::{ - CertType, Certificate, CurveKind, CriticalOptions, PublicKey as SSHPublicKey, PublicKeyKind as SSHPublicKeyKind + CertType, Certificate, CurveKind, PublicKey as SSHPublicKey, PublicKeyKind as SSHPublicKeyKind }; use ring::signature::{UnparsedPublicKey, ECDSA_P256_SHA256_ASN1, ECDSA_P384_SHA384_ASN1, ED25519}; @@ -296,7 +296,7 @@ impl Rustica for RusticaServer { co.insert(String::from("source-address"), remote_addr.ip().to_string()); } - CriticalOptions::Custom(co) + co }, Err(_) => return Ok(create_response(RusticaServerError::Unknown)), }; @@ -310,7 +310,7 @@ impl Rustica for RusticaServer { .set_critical_options(critical_options.clone()) .set_extensions(authorization.extensions.clone()); - let cert = self.signer.sign_certificate(cert).await; + let cert = self.signer.sign(cert).await; let serialized_cert = match cert { Ok(cert) => { diff --git a/rustica/src/signing/amazon_kms.rs b/rustica/src/signing/amazon_kms.rs index c2d557f..e893448 100644 --- a/rustica/src/signing/amazon_kms.rs +++ b/rustica/src/signing/amazon_kms.rs @@ -111,7 +111,7 @@ impl AmazonKMSSigner { }) } - pub async fn sign_certificate(&self, cert: Certificate) -> Result { + pub async fn sign(&self, cert: Certificate) -> Result { let data = cert.tbs_certificate(); let (key_id, key_algo) = match &cert.cert_type { CertType::User => (&self.user_key_id, &self.user_key_signing_algorithm), diff --git a/rustica/src/signing/file.rs b/rustica/src/signing/file.rs index 1dd1c8c..2a811ba 100644 --- a/rustica/src/signing/file.rs +++ b/rustica/src/signing/file.rs @@ -1,6 +1,8 @@ -use sshcerts::{PublicKey, PrivateKey, ssh::CertType, ssh::SigningFunction}; +use sshcerts::{Certificate, PublicKey, PrivateKey, ssh::CertType}; use serde::Deserialize; +use super::SigningError; + #[derive(Deserialize)] pub struct FileSigner { /// The private key used to sign user certificates @@ -12,11 +14,13 @@ pub struct FileSigner { } impl FileSigner { - pub fn get_signer(&self, cert_type: CertType) -> SigningFunction { - match cert_type { - CertType::User => self.user_key.clone().into(), - CertType::Host => self.host_key.clone().into(), - } + pub fn sign(&self, cert: Certificate) -> Result { + let final_cert = match cert.cert_type { + CertType::User => cert.sign(&self.user_key), + CertType::Host => cert.sign(&self.host_key), + }; + + final_cert.map_err(|_| SigningError::SigningFailure) } pub fn get_signer_public_key(&self, cert_type: CertType) -> PublicKey { diff --git a/rustica/src/signing/mod.rs b/rustica/src/signing/mod.rs index 756e431..01acf96 100644 --- a/rustica/src/signing/mod.rs +++ b/rustica/src/signing/mod.rs @@ -78,27 +78,13 @@ impl std::fmt::Display for SigningError { impl SigningMechanism { /// Takes in a certificate and handles the getting a signature from the /// configured SigningMechanism. - pub async fn sign_certificate(&self, cert: Certificate) -> Result { + pub async fn sign(&self, cert: Certificate) -> Result { match self { - SigningMechanism::File(file) => { - let signer = file.get_signer(cert.cert_type); - match cert.sign(signer) { - Ok(c) => Ok(c), - Err(_) => Err(SigningError::SigningFailure) - } - }, + SigningMechanism::File(file) => file.sign(cert), #[cfg(feature = "yubikey-support")] - SigningMechanism::Yubikey(yubikey) => { - let signer = yubikey.get_signer(cert.cert_type); - match cert.sign(signer) { - Ok(c) => Ok(c), - Err(_) => Err(SigningError::SigningFailure) - } - }, + SigningMechanism::Yubikey(yubikey) => yubikey.sign(cert), #[cfg(feature = "amazon-kms")] - SigningMechanism::AmazonKMS(amazonkms) => { - amazonkms.sign_certificate(cert).await - }, + SigningMechanism::AmazonKMS(amazonkms) => amazonkms.sign(cert).await, } } diff --git a/rustica/src/signing/yubikey.rs b/rustica/src/signing/yubikey.rs index c2c4f52..d492829 100644 --- a/rustica/src/signing/yubikey.rs +++ b/rustica/src/signing/yubikey.rs @@ -1,4 +1,4 @@ -use sshcerts::{PublicKey, ssh::CertType, ssh::SigningFunction}; +use sshcerts::{Certificate, PublicKey, ssh::CertType}; use sshcerts::yubikey::piv::{SlotId, Yubikey}; use serde::Deserialize; use std::convert::TryFrom; @@ -23,35 +23,25 @@ pub struct YubikeySigner { impl YubikeySigner { - fn create_signer(&self, slot: SlotId) -> SigningFunction { - let yk = self.yubikey.clone(); - Box::new(move |buf: &[u8]| { - match yk.lock() { - Ok(_) => { - // Unfortunatly we need to create a new Yubikey here because otherwise - // everything will have to be mutable which causes an issue - // for the RusticaServer struct - let mut yk = Yubikey::new().unwrap(); - match yk.ssh_cert_signer(buf, &slot) { - Ok(sig) => Some(sig), - Err(_) => None, - } - }, - Err(e) => { - error!("Error in acquiring mutex for yubikey signing: {}", e); - None - } - } - }) - } - - pub fn get_signer(&self, cert_type: CertType) -> SigningFunction { - let slot = match cert_type { + pub fn sign(&self, cert: Certificate) -> Result { + let slot = match cert.cert_type { CertType::User => self.user_slot, CertType::Host => self.host_slot, }; - self.create_signer(slot) + match self.yubikey.lock() { + Ok(_) => { + // Unfortunatly we need to create a new Yubikey here because otherwise + // everything will have to be mutable which causes an issue + // for the RusticaServer struct + let mut yk = Yubikey::new().unwrap(); + match yk.ssh_cert_signer(&cert.tbs_certificate(), &slot) { + Ok(sig) => cert.add_signature(&sig).map_err(|_| SigningError::SigningFailure), + Err(_) => Err(SigningError::SigningFailure), + } + }, + Err(e) => Err(SigningError::AccessError(e.to_string())), + } } pub fn get_signer_public_key(&self, cert_type: CertType) -> Result { diff --git a/tests/integration.sh b/tests/integration.sh new file mode 100755 index 0000000..9817aa8 --- /dev/null +++ b/tests/integration.sh @@ -0,0 +1,176 @@ +#!/bin/bash +# Run integration tests for Rustica +# +# This works by setting up a Rustica server and RusticaAgent client then trying +# testing certificate pull functionality as well as SSH signing for logging +# into remote system. + +cleanup_and_exit () { + rm $SSH_AUTH_SOCK + docker kill rustica_test_ssh_server > /dev/null 2>&1 + docker rm rustica_test_ssh_server > /dev/null 2>&1 + exit $1 +} + + +# Build Rustica and RusticaAgent +cargo build --features=all + +# Build test SSH Server. This server trusts all the test keys in this folder as +# as well as the user key in rustica_local_file.toml. The reason we start up alt +# first is to run other tests on the manual key add functionality. +cd tests/ssh_server +docker build -t rustica_test_ssh_server:latest . +cd ../.. + +# Run test SSH Server +docker run --name rustica_test_ssh_server -p 2424:22 rustica_test_ssh_server:latest & + +# Verify that Rustica is not running and that this should fail +if ./target/debug/rustica-agent --config examples/rustica_agent_local.toml -i > /dev/null 2>&1; then + echo "FAIL: Some other Rustica instance is running!" + exit 1 +else + echo "PASS: No other Rustica instance appears to be running...starting one" +fi + +# Start a Rustica Server +./target/debug/rustica --config examples/rustica_local_file_alt.toml > /dev/null 2>&1 & +RUSTICA_PID=$! +sleep 2 + +# Test that we can fetch a certificate +if ./target/debug/rustica-agent --config examples/rustica_agent_local.toml -i > /dev/null 2>&1; then + echo "PASS: Successfully pulled a certificate from Rustica" +else + echo "FAIL: Could not pull a certificate from Rustica" + cleanup_and_exit 1 +fi + +# Test that we can fetch a certificate and write it to a file +if ./target/debug/rustica-agent --config examples/rustica_agent_local.toml -i -o /tmp/testing_cert > /dev/null 2>&1; then + echo "PASS: Successfully saved a certificate to a file" + if ssh-keygen -Lf /tmp/testing_cert > /dev/null; then + echo "PASS: Validated ssh-keygen parses saved certificate" + rm /tmp/testing_cert + else + echo "FAIL: ssh-keygen could not read the output certificate successfully" + cleanup_and_exit 1 + fi +else + echo "FAIL: Could not pull a certificate from Rustica" + cleanup_and_exit 1 +fi + +# Generate random socket +SOCKET_RND=$(head -n 5 /dev/urandom | shasum | head -c 10) +SOCKET_PATH="/tmp/rustica_agent_$SOCKET_RND" + +echo "PASS: Using the following socket path for this test run: $SOCKET_PATH" + +# Start RusticaAgent +./target/debug/rustica-agent --config examples/rustica_agent_local.toml --socket $SOCKET_PATH > /dev/null 2>&1 & +AGENT_PID=$! +sleep 2 + +chmod 600 tests/test_ec256 +chmod 600 tests/test_ec384 +chmod 600 tests/test_ed25519 + +SSH_AUTH_SOCK="$SOCKET_PATH" +export SSH_AUTH_SOCK; + +if ssh-add tests/test_ec256 > /dev/null 2>&1; then + echo "PASS: Added EC256 private key to RusticaAgent" +else + echo "FAIL: Could not add EC256 private key to RusticaAgent" + cleanup_and_exit 1 +fi + + +if ssh -o StrictHostKeyChecking=no testuser@localhost -p2424 -t 'exit' > /dev/null 2>&1; then + echo "PASS: RusticaAgent used manually added EC256 to connect to SSH Server" +else + echo "Fail: RusticaAgent failed using manually added EC256 to connect to SSH Server" + kill $AGENT_PID $RUSTICA_PID + wait $AGENT_PID $RUSTICA_PID > /dev/null 2>&1 + cleanup_and_exit 1 +fi + +# Restart RusticaAgent because it doesn't support key removal at this time +kill $AGENT_PID +wait $AGENT_PID 2>/dev/null +rm $SSH_AUTH_SOCK +./target/debug/rustica-agent --config examples/rustica_agent_local.toml --socket $SOCKET_PATH > /dev/null 2>&1 & +AGENT_PID=$! +sleep 2 + +if ssh-add tests/test_ec384 > /dev/null 2>&1; then + echo "PASS: Added EC384 private key to RusticaAgent" +else + echo "FAIL: Could not add EC384 private key to RusticaAgent" + kill $AGENT_PID $RUSTICA_PID + wait $AGENT_PID $RUSTICA_PID > /dev/null 2>&1 + cleanup_and_exit 1 +fi + +if ssh -o StrictHostKeyChecking=no testuser@localhost -p2424 -t 'exit' > /dev/null 2>&1; then + echo "PASS: RusticaAgent used manually added EC384 to connect to SSH Server" +else + echo "Fail: RusticaAgent failed using manually added EC384 to connect to SSH Server" + kill $AGENT_PID $RUSTICA_PID + wait $AGENT_PID $RUSTICA_PID > /dev/null 2>&1 + cleanup_and_exit 1 +fi + +# Restart RusticaAgent because it doesn't support key removal at this time +kill $AGENT_PID +wait $AGENT_PID 2>/dev/null +rm $SSH_AUTH_SOCK +./target/debug/rustica-agent --config examples/rustica_agent_local.toml --socket $SOCKET_PATH > /dev/null 2>&1 & +AGENT_PID=$! +sleep 2 + +if ssh-add tests/test_ed25519 > /dev/null 2>&1; then + echo "PASS: Added Ed25519 private key to RusticaAgent" +else + echo "FAIL: Could not add Ed25519 private key to RusticaAgent" + kill $AGENT_PID $RUSTICA_PID + wait $AGENT_PID $RUSTICA_PID > /dev/null 2>&1 + cleanup_and_exit 1 +fi + +if ssh -o StrictHostKeyChecking=no testuser@localhost -p2424 -t 'exit' > /dev/null 2>&1; then + echo "PASS: RusticaAgent used manually added Ed25519 to connect to SSH Server" +else + echo "Fail: RusticaAgent failed using manually added Ed25519 to connect to SSH Server" + kill $AGENT_PID $RUSTICA_PID + wait $AGENT_PID $RUSTICA_PID > /dev/null 2>&1 + cleanup_and_exit 1 +fi + +# Restart RusticaAgent because it doesn't support key removal at this time +rm $SSH_AUTH_SOCK +kill $AGENT_PID $RUSTICA_PID +wait $AGENT_PID $RUSTICA_PID > /dev/null 2>&1 + +./target/debug/rustica --config examples/rustica_local_file.toml > /dev/null 2>&1 & +RUSTICA_PID=$! +sleep 2 + +./target/debug/rustica-agent --config examples/rustica_agent_local.toml --socket $SOCKET_PATH > /dev/null 2>&1 & +AGENT_PID=$! +sleep 2 + +if ssh -o StrictHostKeyChecking=no testuser@localhost -p2424 -t 'exit' > /dev/null 2>&1; then + echo "PASS: RusticaAgent used Rustica server to connect to SSH Server" +else + echo "Fail: RusticaAgent failed using Rustica server to connect to SSH Server" + kill $AGENT_PID $RUSTICA_PID + wait $AGENT_PID $RUSTICA_PID > /dev/null 2>&1 + cleanup_and_exit 1 +fi + +kill $AGENT_PID $RUSTICA_PID +wait $AGENT_PID $RUSTICA_PID > /dev/null 2>&1 +cleanup_and_exit 0 \ No newline at end of file diff --git a/tests/ssh_server/Dockerfile b/tests/ssh_server/Dockerfile new file mode 100644 index 0000000..718da70 --- /dev/null +++ b/tests/ssh_server/Dockerfile @@ -0,0 +1,20 @@ +FROM ubuntu +USER root +RUN apt update && apt upgrade -y && apt install -y openssh-server + +# SSH Configuration +COPY sshd_config /etc/ssh/sshd_config +COPY user-ca.pub /etc/ssh/user-ca.pub +RUN chmod 600 /etc/ssh/user-ca.pub +RUN service ssh start + +# User Configuration +RUN useradd -m -d /home/testuser -s /bin/bash -g root -G sudo -u 1000 testuser +USER 1000 +RUN mkdir /home/testuser/.ssh +COPY authorized_keys /home/testuser/.ssh/authorized_keys + +USER root + +EXPOSE 22 +CMD ["/usr/sbin/sshd", "-D"] diff --git a/tests/ssh_server/authorized_keys b/tests/ssh_server/authorized_keys new file mode 100644 index 0000000..0f3cb52 --- /dev/null +++ b/tests/ssh_server/authorized_keys @@ -0,0 +1,3 @@ +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBA0ImLwIBPw8g/0jbZXmFP6d6XeNAlJyzgJeJw+Btjo7jfwwJKEXfJb/hgDA5/I6E6wBQ1KZ7LMUjpAsNQvv6Xc= +ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBNk5GZ6aXu1VBk2mbffXCRVvOlPyzsSeZRTILMdu6ligA7b3gELbcPDX8tckrBsjITCRlLI1LYxKNiqQ35QY9CjY+QfRsXGCw44ANfz6LQw2UX987gZmhsKGpN9vvBKfnA== +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICKlW2+4j8k/GLI2RFYSOIET8m1CLyIaPPixJ4uYEzP/ \ No newline at end of file diff --git a/tests/ssh_server/sshd_config b/tests/ssh_server/sshd_config new file mode 100644 index 0000000..07f32b8 --- /dev/null +++ b/tests/ssh_server/sshd_config @@ -0,0 +1,125 @@ +# $OpenBSD: sshd_config,v 1.103 2018/04/09 20:41:22 tj Exp $ + +# This is the sshd server system-wide configuration file. See +# sshd_config(5) for more information. + +# This sshd was compiled with PATH=/usr/bin:/bin:/usr/sbin:/sbin + +# The strategy used for options in the default sshd_config shipped with +# OpenSSH is to specify options with their default value where +# possible, but leave them commented. Uncommented options override the +# default value. + +#Include /etc/ssh/sshd_config.d/*.conf + +TrustedUserCAKeys /etc/ssh/user-ca.pub + +#Port 22 +#AddressFamily any +#ListenAddress 0.0.0.0 +#ListenAddress :: + +#HostKey /etc/ssh/ssh_host_rsa_key +#HostKey /etc/ssh/ssh_host_ecdsa_key +#HostKey /etc/ssh/ssh_host_ed25519_key + +# Ciphers and keying +#RekeyLimit default none + +# Logging +#SyslogFacility AUTH +#LogLevel INFO + +# Authentication: + +#LoginGraceTime 2m +#PermitRootLogin prohibit-password +#StrictModes yes +#MaxAuthTries 6 +#MaxSessions 10 + +PubkeyAuthentication yes + +# Expect .ssh/authorized_keys2 to be disregarded by default in future. +#AuthorizedKeysFile .ssh/authorized_keys .ssh/authorized_keys2 + +#AuthorizedPrincipalsFile none + +#AuthorizedKeysCommand none +#AuthorizedKeysCommandUser nobody + +# For this to work you will also need host keys in /etc/ssh/ssh_known_hosts +#HostbasedAuthentication no +# Change to yes if you don't trust ~/.ssh/known_hosts for +# HostbasedAuthentication +#IgnoreUserKnownHosts no +# Don't read the user's ~/.rhosts and ~/.shosts files +#IgnoreRhosts yes + +# To disable tunneled clear text passwords, change to no here! +PasswordAuthentication no +#PermitEmptyPasswords no + +# Change to yes to enable challenge-response passwords (beware issues with +# some PAM modules and threads) +ChallengeResponseAuthentication no + +# Kerberos options +#KerberosAuthentication no +#KerberosOrLocalPasswd yes +#KerberosTicketCleanup yes +#KerberosGetAFSToken no + +# GSSAPI options +#GSSAPIAuthentication no +#GSSAPICleanupCredentials yes +#GSSAPIStrictAcceptorCheck yes +#GSSAPIKeyExchange no + +# Set this to 'yes' to enable PAM authentication, account processing, +# and session processing. If this is enabled, PAM authentication will +# be allowed through the ChallengeResponseAuthentication and +# PasswordAuthentication. Depending on your PAM configuration, +# PAM authentication via ChallengeResponseAuthentication may bypass +# the setting of "PermitRootLogin without-password". +# If you just want the PAM account and session checks to run without +# PAM authentication, then enable this but set PasswordAuthentication +# and ChallengeResponseAuthentication to 'no'. +UsePAM yes + +#AllowAgentForwarding yes +#AllowTcpForwarding yes +#GatewayPorts no +X11Forwarding yes +#X11DisplayOffset 10 +#X11UseLocalhost yes +#PermitTTY yes +PrintMotd no +#PrintLastLog yes +#TCPKeepAlive yes +#PermitUserEnvironment no +#Compression delayed +#ClientAliveInterval 0 +#ClientAliveCountMax 3 +#UseDNS no +#PidFile /var/run/sshd.pid +#MaxStartups 10:30:100 +#PermitTunnel no +#ChrootDirectory none +#VersionAddendum none + +# no default banner path +#Banner none + +# Allow client to pass locale environment variables +AcceptEnv LANG LC_* + +# override default of no subsystems +Subsystem sftp /usr/lib/openssh/sftp-server + +# Example of overriding settings on a per-user basis +#Match User anoncvs +# X11Forwarding no +# AllowTcpForwarding no +# PermitTTY no +# ForceCommand cvs server diff --git a/tests/ssh_server/user-ca.pub b/tests/ssh_server/user-ca.pub new file mode 100644 index 0000000..16eb6bf --- /dev/null +++ b/tests/ssh_server/user-ca.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOGrtTSBxbrqYk1amwv/gAM1eg5Qwsddhszw8gduo9P6 obelisk@Mitchells-MBP.localdomain \ No newline at end of file diff --git a/tests/test_ec256 b/tests/test_ec256 new file mode 100644 index 0000000..3e1a013 --- /dev/null +++ b/tests/test_ec256 @@ -0,0 +1,9 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS +1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQQNCJi8CAT8PIP9I22V5hT+nel3jQJS +cs4CXicPgbY6O438MCShF3yW/4YAwOfyOhOsAUNSmeyzFI6QLDUL7+l3AAAAwMk1lDjJNZ +Q4AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBA0ImLwIBPw8g/0j +bZXmFP6d6XeNAlJyzgJeJw+Btjo7jfwwJKEXfJb/hgDA5/I6E6wBQ1KZ7LMUjpAsNQvv6X +cAAAAgHABbRLTsuNBOMkOx8wYub5CVtwP3555YViLpBhe6YXYAAAAhb2JlbGlza0BNaXRj +aGVsbHMtTUJQLmxvY2FsZG9tYWluAQIDBAUGBw== +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/test_ec384 b/tests/test_ec384 new file mode 100644 index 0000000..a830e55 --- /dev/null +++ b/tests/test_ec384 @@ -0,0 +1,11 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS +1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQTZORmeml7tVQZNpm331wkVbzpT8s7E +nmUUyCzHbupYoAO294BC23Dw1/LXJKwbIyEwkZSyNS2MSjYqkN+UGPQo2PkH0bFxgsOOAD +X8+i0MNlF/fO4GZobChqTfb7wSn5wAAADw/TXNfv01zX4AAAATZWNkc2Etc2hhMi1uaXN0 +cDM4NAAAAAhuaXN0cDM4NAAAAGEE2TkZnppe7VUGTaZt99cJFW86U/LOxJ5lFMgsx27qWK +ADtveAQttw8Nfy1ySsGyMhMJGUsjUtjEo2KpDflBj0KNj5B9GxcYLDjgA1/PotDDZRf3zu +BmaGwoak32+8Ep+cAAAAMGvgw0+thkQve6+Ugxxsms3Pn2vjL9cnulh6cIE6P7UMSiim/P +0SiOzi1o9RXi9T5gAAACFvYmVsaXNrQE1pdGNoZWxscy1NQlAubG9jYWxkb21haW4BAgME +BQYH +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/test_ed25519 b/tests/test_ed25519 new file mode 100644 index 0000000..83f7154 --- /dev/null +++ b/tests/test_ed25519 @@ -0,0 +1,8 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACAipVtvuI/JPxiyNkRWEjiBE/JtQi8iGjz4sSeLmBMz/wAAAKhiepqoYnqa +qAAAAAtzc2gtZWQyNTUxOQAAACAipVtvuI/JPxiyNkRWEjiBE/JtQi8iGjz4sSeLmBMz/w +AAAEBHwGHZTQ6oGSiiz7kB6/g5g2mNWSX3U4e5WnVZFCv8jSKlW2+4j8k/GLI2RFYSOIET +8m1CLyIaPPixJ4uYEzP/AAAAIW9iZWxpc2tATWl0Y2hlbGxzLU1CUC5sb2NhbGRvbWFpbg +ECAwQ= +-----END OPENSSH PRIVATE KEY-----