added: renew command
This commit is contained in:
parent
02fbc55d93
commit
53f21fa668
@ -18,7 +18,7 @@ use jwt_compact::alg::{Hs256, Hs256Key};
|
|||||||
use jwt_compact::{AlgorithmExt, Token, UntrustedToken};
|
use jwt_compact::{AlgorithmExt, Token, UntrustedToken};
|
||||||
use rand::{thread_rng, Rng};
|
use rand::{thread_rng, Rng};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use ssh_key::{Certificate, PublicKey};
|
use ssh_key::{Certificate, Fingerprint, PublicKey};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tower::ServiceBuilder;
|
use tower::ServiceBuilder;
|
||||||
use tower_http::{trace::TraceLayer, ServiceBuilderExt};
|
use tower_http::{trace::TraceLayer, ServiceBuilderExt};
|
||||||
@ -135,6 +135,7 @@ pub async fn run(
|
|||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.typed_get(get_certs_identifier)
|
.typed_get(get_certs_identifier)
|
||||||
|
.typed_get(get_certs_pubkey)
|
||||||
.typed_put(put_cert_update)
|
.typed_put(put_cert_update)
|
||||||
.typed_get(get_cert_info)
|
.typed_get(get_cert_info)
|
||||||
.typed_post(post_certs_identifier);
|
.typed_post(post_certs_identifier);
|
||||||
@ -219,6 +220,22 @@ struct AuthClaims {
|
|||||||
identifier: String,
|
identifier: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn request_client_auth(enabled: bool, identifier: &str, jwt_key: &Hs256Key) -> ApiResult<()> {
|
||||||
|
use jwt_compact::{Claims, Header, TimeOptions};
|
||||||
|
if enabled {
|
||||||
|
let claims = Claims::new(AuthClaims {
|
||||||
|
identifier: identifier.into(),
|
||||||
|
})
|
||||||
|
.set_duration(&TimeOptions::default(), chrono::Duration::seconds(120));
|
||||||
|
let challenge = Hs256
|
||||||
|
.compact_token(Header::default(), &claims, &jwt_key)
|
||||||
|
.context("jwt sign")?;
|
||||||
|
return Err(ApiError::AuthenticationRequired(challenge));
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Retrieve an certificate for identifier
|
/// Retrieve an certificate for identifier
|
||||||
/// TODO: add option to require auth
|
/// TODO: add option to require auth
|
||||||
/// return Unauthorized with an challenge
|
/// return Unauthorized with an challenge
|
||||||
@ -233,16 +250,7 @@ async fn get_certs_identifier(
|
|||||||
..
|
..
|
||||||
}): State<ApiState>,
|
}): State<ApiState>,
|
||||||
) -> ApiResult<String> {
|
) -> ApiResult<String> {
|
||||||
use jwt_compact::{Claims, Header, TimeOptions};
|
request_client_auth(client_auth, &identifier, &jwt_key).await?;
|
||||||
|
|
||||||
if client_auth {
|
|
||||||
let claims = Claims::new(AuthClaims { identifier })
|
|
||||||
.set_duration(&TimeOptions::default(), chrono::Duration::seconds(120));
|
|
||||||
let challenge = Hs256
|
|
||||||
.compact_token(Header::default(), &claims, &jwt_key)
|
|
||||||
.context("jwt sign")?;
|
|
||||||
return Err(ApiError::AuthenticationRequired(challenge));
|
|
||||||
}
|
|
||||||
let certs = certs.lock().await;
|
let certs = certs.lock().await;
|
||||||
let cert = certs
|
let cert = certs
|
||||||
.get(&identifier)
|
.get(&identifier)
|
||||||
@ -250,24 +258,55 @@ async fn get_certs_identifier(
|
|||||||
Ok(cert.to_openssh().context("to openssh")?)
|
Ok(cert.to_openssh().context("to openssh")?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_certs_pubkey(
|
||||||
|
GetCertsPubkey { pubkey_hash }: GetCertsPubkey,
|
||||||
|
State(ApiState {
|
||||||
|
certs,
|
||||||
|
jwt_key: _,
|
||||||
|
client_auth: _,
|
||||||
|
..
|
||||||
|
}): State<ApiState>,
|
||||||
|
) -> ApiResult<Json<CertIds>> {
|
||||||
|
let certs = certs.lock().await;
|
||||||
|
let ids = certs
|
||||||
|
.values()
|
||||||
|
.filter(|cert| &cert.public_key().fingerprint(pubkey_hash.algorithm()) == &pubkey_hash)
|
||||||
|
.map(|cert| cert.key_id().to_string())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
Ok(Json(CertIds { ids }))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "info")]
|
#[cfg(feature = "info")]
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
struct CertInfo {
|
struct CertInfo {
|
||||||
principals: Vec<String>,
|
principals: Vec<String>,
|
||||||
ca: PublicKey,
|
ca: PublicKey,
|
||||||
|
ca_hash: Fingerprint,
|
||||||
identity: PublicKey,
|
identity: PublicKey,
|
||||||
|
identity_hash: Fingerprint,
|
||||||
key_id: String,
|
key_id: String,
|
||||||
expiry: SystemTime,
|
expiry: SystemTime,
|
||||||
|
renew_command: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&Certificate> for CertInfo {
|
impl From<&Certificate> for CertInfo {
|
||||||
fn from(cert: &Certificate) -> Self {
|
fn from(cert: &Certificate) -> Self {
|
||||||
|
let validity = cert.valid_after_time().duration_since(cert.valid_before_time()).unwrap();
|
||||||
|
let validity_days = validity.as_secs() / ((60*60) * 24);
|
||||||
|
let host_key = if cert.cert_type().is_host() {
|
||||||
|
" -h"
|
||||||
|
} else { "" };
|
||||||
|
let opts = cert.critical_options().iter().map(|(opt, val)| if val.is_empty() { opt.clone() } else { format!("{opt}={val}") }).map(|arg| format!("-O {arg}")).join(" ");
|
||||||
|
let renew_command = format!("ssh-keygen -s ./ca_key {host_key} -I {} -n {} -V {validity_days}d {opts}", cert.key_id(), cert.valid_principals().join(","));
|
||||||
CertInfo {
|
CertInfo {
|
||||||
principals: cert.valid_principals().to_vec(),
|
principals: cert.valid_principals().to_vec(),
|
||||||
ca: cert.signature_key().clone().into(),
|
ca: cert.signature_key().clone().into(),
|
||||||
|
ca_hash: cert.signature_key().fingerprint(ssh_key::HashAlg::Sha256),
|
||||||
identity: cert.public_key().clone().into(),
|
identity: cert.public_key().clone().into(),
|
||||||
|
identity_hash: cert.public_key().fingerprint(ssh_key::HashAlg::Sha256),
|
||||||
key_id: cert.key_id().to_string(),
|
key_id: cert.key_id().to_string(),
|
||||||
expiry: cert.valid_before_time(),
|
expiry: cert.valid_before_time(),
|
||||||
|
renew_command
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -395,7 +434,16 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn ca_pub() -> PublicKey {
|
fn ca_pub() -> PublicKey {
|
||||||
PublicKey::new(ca_key().public.into(), "TEST CA")
|
PublicKey::new(
|
||||||
|
ca_key().public.into(),
|
||||||
|
format!(
|
||||||
|
"TEST CA {}",
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_secs()
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn user_key() -> Ed25519Keypair {
|
fn user_key() -> Ed25519Keypair {
|
||||||
@ -421,7 +469,7 @@ mod tests {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.key_id("test_cert")
|
.key_id("test_cert")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.comment("A TEST CERT")
|
.comment(&format!("A TEST CERT, VALID FOR {}s", validity.as_secs()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
builder.sign(&ca_private).unwrap()
|
builder.sign(&ca_private).unwrap()
|
||||||
@ -455,9 +503,9 @@ mod tests {
|
|||||||
let user: PublicKey = user_key().public.into();
|
let user: PublicKey = user_key().public.into();
|
||||||
let (cert_first, cert_newer, cert_outdated) = {
|
let (cert_first, cert_newer, cert_outdated) = {
|
||||||
(
|
(
|
||||||
|
user_cert(ca.clone(), user.clone(), Duration::from_secs(300)),
|
||||||
|
user_cert(ca.clone(), user.clone(), Duration::from_secs(600)),
|
||||||
user_cert(ca.clone(), user.clone(), Duration::from_secs(30)),
|
user_cert(ca.clone(), user.clone(), Duration::from_secs(30)),
|
||||||
user_cert(ca.clone(), user.clone(), Duration::from_secs(60)),
|
|
||||||
user_cert(ca.clone(), user.clone(), Duration::from_secs(3)),
|
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
let res = put_cert_update(PutCert, State(state.clone()), CertificateBody(cert_first)).await;
|
let res = put_cert_update(PutCert, State(state.clone()), CertificateBody(cert_first)).await;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user