user/login: Request signed cert from SSHCA

The `sshca-cli user login` command now requests a signed certificate
from the SSHCA server.  Given a valid OpenID Connect identity token and
an SSH public key, the server will return a signed certificate, valid
for a predetermined (usually short) period of time.  The principals
listed in the certificate are derived from the ID token.
dev/auto-reload
Dustin 2023-11-22 07:20:12 -06:00
parent 3b55f7418e
commit 123ca813a7
4 changed files with 178 additions and 2 deletions

78
Cargo.lock generated
View File

@ -223,6 +223,16 @@ dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]]
name = "combine"
version = "4.6.6"
@ -871,6 +881,15 @@ dependencies = [
"serde",
]
[[package]]
name = "inout"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
dependencies = [
"generic-array",
]
[[package]]
name = "ipnet"
version = "2.9.0"
@ -1279,6 +1298,20 @@ dependencies = [
"sha2",
]
[[package]]
name = "p521"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2"
dependencies = [
"base16ct",
"ecdsa",
"elliptic-curve",
"primeorder",
"rand_core",
"sha2",
]
[[package]]
name = "password-hash"
version = "0.5.0"
@ -1600,6 +1633,7 @@ dependencies = [
"pkcs1",
"pkcs8",
"rand_core",
"sha2",
"signature",
"spki",
"subtle",
@ -1889,6 +1923,48 @@ dependencies = [
"der",
]
[[package]]
name = "ssh-cipher"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f"
dependencies = [
"cipher",
"ssh-encoding",
]
[[package]]
name = "ssh-encoding"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15"
dependencies = [
"base64ct",
"pem-rfc7468",
"sha2",
]
[[package]]
name = "ssh-key"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01f8f4ea73476c0aa5d5e6a75ce1e8634e2c3f82005ef3bbed21547ac57f2bf7"
dependencies = [
"ed25519-dalek",
"p256",
"p384",
"p521",
"rand_core",
"rsa",
"sec1",
"sha2",
"signature",
"ssh-cipher",
"ssh-encoding",
"subtle",
"zeroize",
]
[[package]]
name = "sshca-cli"
version = "0.1.1"
@ -1903,6 +1979,8 @@ dependencies = [
"reqwest",
"serde",
"serde_json",
"ssh-encoding",
"ssh-key",
"tera",
"thiserror",
"tokio",

View File

@ -18,6 +18,8 @@ openidconnect = { version = "3.4.0", default-features = false, features = ["reqw
reqwest = { version = "0.11.22", features = ["multipart"] }
serde = { version = "1.0.190", features = ["derive"] }
serde_json = "1.0.108"
ssh-encoding = "0.2.0"
ssh-key = { version = "0.6.4", features = ["ed25519"] }
tera = { version = "1.19.1", default-features = false }
thiserror = "1.0.50"
tokio = { version = "1.33.0", features = ["rt", "macros"] }

74
src/user/cert.rs Normal file
View File

@ -0,0 +1,74 @@
//! SSHCA User Certificates
//!
//! The SSHCA server will sign SSH certificates for a user given a
//! supported public key and a valid OIDC Identity token.
use reqwest::multipart::{Form, Part};
use reqwest::{StatusCode, Url};
use ssh_key::{Certificate, PublicKey};
use tracing::{debug, error, info};
/// Error type for issues requesting a signed SSH certificate
#[derive(Debug, thiserror::Error)]
pub enum SignError {
#[error("{0}")]
BadUrl(String),
#[error("SSH key error: {0}")]
SshKey(#[from] ssh_key::Error),
#[error("HTTP error: {0}")]
Http(#[from] reqwest::Error),
#[error("Bad request: {0}")]
BadRequest(String),
}
/// Request a signed SSH certificate for a given public key
///
/// This function requests the SSHCA server to sign a certificate for
/// the specified SSH public key. The server will use the provided
/// identity token to authorize the request and return a certificate
/// for the user based on the token subject.
///
/// If an error occurs while requesting the certificate, [`SignError`]
/// is returned.
pub async fn sign_key(
token: &str,
pubkey: &PublicKey,
) -> Result<Certificate, SignError> {
let url = crate::get_sshca_server_url().map_err(SignError::BadUrl)?;
let mut url =
Url::parse(&url).map_err(|e| SignError::BadUrl(e.to_string()))?;
url.path_segments_mut()
.map_err(|_| SignError::BadUrl("Invalid URL: missing host".into()))?
.pop_if_empty()
.push("user")
.push("sign");
let key_str = pubkey.to_openssh()?;
let form = Form::new().part("pubkey", Part::text(key_str));
let client = reqwest::Client::new();
info!("Requesting SSH user certificate");
debug!("Request: POST {}", url);
let res = client
.post(url)
.header("Authorization", format!("Bearer {}", token))
.multipart(form)
.send()
.await?;
debug!("Response: {:?} {}", &res.version(), &res.status());
match res.error_for_status_ref() {
Ok(_) => (),
Err(e) if e.status() == Some(StatusCode::BAD_REQUEST) => {
let msg = res.text().await.unwrap_or_else(|e| e.to_string());
error!("{}: {}", e, msg);
return Err(SignError::BadRequest(format!("{}\n{}", e, msg)));
}
Err(e) => {
error!("{}", e);
return Err(e.into());
}
};
let cert = Certificate::from_openssh(&res.text().await?)?;
Ok(cert)
}

View File

@ -2,11 +2,15 @@
//!
//! The `sshca user` sub-command handles user-based operations, such
//! as signing an SSH user certificate.
mod cert;
mod login;
use std::time::Duration;
use argh::FromArgs;
use ssh_key::rand_core::OsRng;
use ssh_key::{Algorithm, PrivateKey};
use tracing::debug;
use crate::MainResult;
@ -33,8 +37,16 @@ struct LoginArgs {
callback_listen_address: Option<String>,
/// oidc callback timeout, in seconds (default: 300)
#[argh(option, short = 't')]
#[argh(option, short = 'T')]
callback_timeout: Option<u64>,
/// ssh key type
#[argh(option, short = 't', default = "default_key_type()")]
key_type: String,
}
fn default_key_type() -> String {
"ssh-ed25519".into()
}
/// Main entry point for `sshca user`
@ -46,14 +58,24 @@ pub(crate) async fn main(args: Args) -> MainResult {
/// Entry point for `sshca user login`
async fn login(args: LoginArgs) -> MainResult {
let algo = Algorithm::new(&args.key_type)?;
let listen = match args.callback_listen_address {
Some(s) => Some(s.parse()?),
None => None,
};
let timeout = args.callback_timeout.map(Duration::from_secs);
let url = super::get_sshca_server_url()?;
let config = login::get_oidc_config(&url).await?;
let token = login::login(config, listen, timeout).await?;
println!("{}", token);
debug!("Generatring new {} key pair", algo);
let privkey = PrivateKey::random(&mut OsRng, algo)
.map_err(|e| format!("Error generating key pair: {0}", e))?;
let pubkey = privkey.public_key();
let cert = cert::sign_key(&token, pubkey).await?;
if let Ok(c) = cert.to_openssh() {
println!("{}", c);
}
Ok(())
}