Skip to content

Commit

Permalink
Fix RusticaAgent, New SSHCerts, Integration Tests (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
obelisk authored Feb 3, 2022
1 parent d052dcd commit cc66a70
Show file tree
Hide file tree
Showing 26 changed files with 538 additions and 101 deletions.
21 changes: 21 additions & 0 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file modified examples/example.db
Binary file not shown.
83 changes: 83 additions & 0 deletions examples/rustica_local_file_alt.toml
Original file line number Diff line number Diff line change
@@ -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"
4 changes: 2 additions & 2 deletions rustica-agent/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rustica-agent"
version = "0.8.1"
version = "0.8.3"
authors = ["Mitchell Grenier <[email protected]>"]
edition = "2018"

Expand All @@ -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"]}
Expand Down
49 changes: 28 additions & 21 deletions rustica-agent/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -171,41 +173,46 @@ 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<SigningFunction> = 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
if let Some(f) = &self.notification_function {
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"))
Expand Down
6 changes: 3 additions & 3 deletions rustica-agent/src/rustica/cert.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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,
Expand Down
9 changes: 2 additions & 7 deletions rustica-agent/src/sshagent/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,6 @@ pub enum Response {
Failure,
Identities(Vec<Identity>),
SignResponse {
algo_name: String,
signature: Vec<u8>,
},
}
Expand All @@ -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::<BigEndian>(buf.len() as u32)?;
Expand Down
4 changes: 2 additions & 2 deletions rustica/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rustica"
version = "0.8.2"
version = "0.8.3"
authors = ["Mitchell Grenier <[email protected]>"]
edition = "2018"

Expand Down Expand Up @@ -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"]}
Expand Down
4 changes: 2 additions & 2 deletions rustica/src/auth/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use super::{
RegisterKeyRequestProperties,
};

use sshcerts::ssh::{CertType, Extensions};
use sshcerts::ssh::{Certificate, CertType};

#[derive(Deserialize)]
pub struct LocalDatabase {
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 0 additions & 3 deletions rustica/src/auth/external.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
5 changes: 3 additions & 2 deletions rustica/src/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -31,7 +32,7 @@ pub struct Authorization {
pub valid_after: u64,
pub principals: Vec<String>,
pub hosts: Option<Vec<String>>,
pub extensions: Extensions,
pub extensions: HashMap<String, String>,
pub force_command: Option<String>,
pub force_source_ip: bool,
}
Expand Down
6 changes: 3 additions & 3 deletions rustica/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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)),
};
Expand All @@ -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) => {
Expand Down
2 changes: 1 addition & 1 deletion rustica/src/signing/amazon_kms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ impl AmazonKMSSigner {
})
}

pub async fn sign_certificate(&self, cert: Certificate) -> Result<Certificate, SigningError> {
pub async fn sign(&self, cert: Certificate) -> Result<Certificate, SigningError> {
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),
Expand Down
Loading

0 comments on commit cc66a70

Please sign in to comment.