added: test
This commit is contained in:
parent
cd82dd9deb
commit
93c4b43135
@ -33,3 +33,6 @@ url = { version = "2.3.1", optional = true }
|
|||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
ssh-key = { git = "https://github.com/a-dma/SSH.git", branch = "u2f_signatures" }
|
ssh-key = { git = "https://github.com/a-dma/SSH.git", branch = "u2f_signatures" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3.3.0"
|
||||||
|
|
||||||
|
112
src/api.rs
112
src/api.rs
@ -4,7 +4,7 @@ use std::collections::HashMap;
|
|||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::path::{self, PathBuf};
|
use std::path::{self, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::SystemTime;
|
use std::time::{Duration, SystemTime};
|
||||||
|
|
||||||
use crate::certs::{load_cert_by_id, read_certs, read_pubkey, store_cert};
|
use crate::certs::{load_cert_by_id, read_certs, read_pubkey, store_cert};
|
||||||
use crate::env_key;
|
use crate::env_key;
|
||||||
@ -19,7 +19,8 @@ use axum_extra::routing::{
|
|||||||
};
|
};
|
||||||
use clap::{Args, Parser};
|
use clap::{Args, Parser};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use ssh_key::{Certificate, PublicKey};
|
use ssh_key::private::Ed25519Keypair;
|
||||||
|
use ssh_key::{certificate, Certificate, PrivateKey, 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};
|
||||||
@ -298,3 +299,110 @@ async fn put_cert_update(
|
|||||||
certs.lock().await.insert(cert.key_id().to_string(), cert);
|
certs.lock().await.insert(cert.key_id().to_string(), cert);
|
||||||
Ok(format!("{} -> {}", prev_serial, serial))
|
Ok(format!("{} -> {}", prev_serial, serial))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::env::temp_dir;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn ca_key() -> Ed25519Keypair {
|
||||||
|
Ed25519Keypair::from_seed(&[0u8; 32])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ca_key2() -> Ed25519Keypair {
|
||||||
|
Ed25519Keypair::from_seed(&[10u8; 32])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ca_pub() -> PublicKey {
|
||||||
|
PublicKey::new(ca_key().public.into(), "TEST CA")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn user_key() -> Ed25519Keypair {
|
||||||
|
Ed25519Keypair::from_seed(&[1u8; 32])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn user_cert(ca: Ed25519Keypair, user_key: PublicKey) -> Certificate {
|
||||||
|
let ca_private: PrivateKey = ca.into();
|
||||||
|
let unix_time = |time: SystemTime| -> u64 {
|
||||||
|
time.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_secs()
|
||||||
|
};
|
||||||
|
let mut builder = certificate::Builder::new(
|
||||||
|
[0u8; 16],
|
||||||
|
user_key,
|
||||||
|
unix_time(SystemTime::now()),
|
||||||
|
unix_time(SystemTime::now() + Duration::from_secs(30)),
|
||||||
|
);
|
||||||
|
|
||||||
|
builder
|
||||||
|
.valid_principal("git")
|
||||||
|
.unwrap()
|
||||||
|
.key_id("test_cert")
|
||||||
|
.unwrap()
|
||||||
|
.comment("A TEST CERT")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
builder.sign(&ca_private).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn api_state() -> ApiState {
|
||||||
|
let ca: PublicKey = ca_pub();
|
||||||
|
ApiState {
|
||||||
|
ca,
|
||||||
|
certs: Default::default(),
|
||||||
|
cert_dir: dbg!(temp_dir()),
|
||||||
|
validation_args: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_certificate() {
|
||||||
|
let valid_cert = user_cert(ca_key(), user_key().public.into());
|
||||||
|
let ca_pub: PublicKey = ca_pub();
|
||||||
|
assert!(valid_cert
|
||||||
|
.validate(&[ca_pub.fingerprint(Default::default())])
|
||||||
|
.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn routes() -> anyhow::Result<()> {
|
||||||
|
let state = api_state();
|
||||||
|
let valid_cert = user_cert(ca_key(), user_key().public.into());
|
||||||
|
let invalid_cert = user_cert(ca_key2(), user_key().public.into());
|
||||||
|
let res = put_cert_update(
|
||||||
|
PutCert,
|
||||||
|
State(state.clone()),
|
||||||
|
CertificateBody(valid_cert.clone()),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(dbg!(res).is_ok());
|
||||||
|
assert_eq!(state.certs.lock().await.get("test_cert"), Some(&valid_cert));
|
||||||
|
let res = put_cert_update(
|
||||||
|
PutCert,
|
||||||
|
State(state.clone()),
|
||||||
|
CertificateBody(invalid_cert.clone()),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(matches!(res, Err(ApiError::CertificateInvalid)));
|
||||||
|
|
||||||
|
let cert = get_certs_identifier(
|
||||||
|
GetCert {
|
||||||
|
identifier: "test_cert".into(),
|
||||||
|
},
|
||||||
|
State(state.clone()),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(cert, valid_cert.to_openssh()?);
|
||||||
|
let res = get_certs_identifier(
|
||||||
|
GetCert {
|
||||||
|
identifier: "missing_cert".into(),
|
||||||
|
},
|
||||||
|
State(state.clone()),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(matches!(res, Err(ApiError::CertificateNotFound)));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
36
src/certs.rs
36
src/certs.rs
@ -8,11 +8,19 @@ use std::{
|
|||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
use tracing::{instrument, trace};
|
use tracing::{instrument, trace};
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum CertError {
|
||||||
|
#[error("missing key id")]
|
||||||
|
NoKID,
|
||||||
|
#[error("missing ca identifier (comment)")]
|
||||||
|
NoCAComment,
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn read_certs(
|
pub async fn read_certs(
|
||||||
ca: &PublicKey,
|
ca: &PublicKey,
|
||||||
path: impl AsRef<Path>,
|
path: impl AsRef<Path>,
|
||||||
) -> anyhow::Result<Vec<Certificate>> {
|
) -> anyhow::Result<Vec<Certificate>> {
|
||||||
let ca_dir = path.as_ref().join(ca_dir(ca));
|
let ca_dir = path.as_ref().join(ca_dir(ca)?);
|
||||||
if !ca_dir.exists() {
|
if !ca_dir.exists() {
|
||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
}
|
}
|
||||||
@ -60,13 +68,19 @@ pub async fn read_pubkey(path: impl AsRef<Path>) -> anyhow::Result<PublicKey> {
|
|||||||
.with_context(|| format!("parse '{}' as public key", string_repr))
|
.with_context(|| format!("parse '{}' as public key", string_repr))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ca_dir(ca: &PublicKey) -> String {
|
fn ca_dir(ca: &PublicKey) -> Result<String, CertError> {
|
||||||
ca.comment().to_string()
|
if ca.comment().is_empty() {
|
||||||
|
return Err(CertError::NoCAComment);
|
||||||
|
}
|
||||||
|
Ok(ca.comment().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
fn cert_path(ca: &PublicKey, identifier: &str) -> String {
|
fn cert_path(ca: &PublicKey, identifier: &str) -> Result<String, CertError> {
|
||||||
format!("{}/{}-cert.pub", ca_dir(ca), identifier)
|
if identifier.is_empty() {
|
||||||
|
return Err(CertError::NoKID);
|
||||||
|
}
|
||||||
|
Ok(format!("{}/{}-cert.pub", ca_dir(ca)?, identifier))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
@ -76,11 +90,15 @@ pub async fn store_cert(
|
|||||||
cert: &Certificate,
|
cert: &Certificate,
|
||||||
) -> anyhow::Result<PathBuf> {
|
) -> anyhow::Result<PathBuf> {
|
||||||
// TODO: proper store
|
// TODO: proper store
|
||||||
let path = cert_dir.as_ref().join(cert_path(&ca, cert.key_id()));
|
let path = cert_dir.as_ref().join(cert_path(&ca, cert.key_id())?);
|
||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent() {
|
||||||
fs::create_dir_all(parent).await?;
|
fs::create_dir_all(parent)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("mkdir -p {parent:?}"))?;
|
||||||
}
|
}
|
||||||
fs::write(&path, cert.to_openssh().context("encode cert")?).await?;
|
fs::write(&path, cert.to_openssh().context("encode cert")?)
|
||||||
|
.await
|
||||||
|
.context("write cert")?;
|
||||||
Ok(path)
|
Ok(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,7 +107,7 @@ pub async fn load_cert_by_id(
|
|||||||
ca: &PublicKey,
|
ca: &PublicKey,
|
||||||
identifier: &str,
|
identifier: &str,
|
||||||
) -> anyhow::Result<Option<Certificate>> {
|
) -> anyhow::Result<Option<Certificate>> {
|
||||||
let path = cert_dir.as_ref().join(cert_path(ca, identifier));
|
let path = cert_dir.as_ref().join(cert_path(ca, identifier)?);
|
||||||
load_cert(&path).await
|
load_cert(&path).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user