mod common; use axum::body::Body; use axum::http::{Request, StatusCode}; use form_data_builder::FormData; use ssh_key::{Algorithm, Certificate}; use tower::ServiceExt; use uuid::uuid; use sshca::server::make_app; use common::setup; use common::token; const ED25519_KEY: &str = concat!( "ssh-ed25519 ", "AAAAC3NzaC1lZDI1NTE5AAAAIAsFEmrNIoRHHUayEO0NdAIgtMvci/wME07h+A5XSNJy", ); const DSA_KEY: &str = concat!( "ssh-dss ", "AAAAB3NzaC1kc3MAAACBALNAS+fZjaWt4q+MAgjf6HREFoYjgoSVJUUCtNmRGhND85msVtla", "kll2gLzL6n6TWyiToARlThoTFu1ZDoGYauDL7iDXrGB6VWJEOQZ3TEMHLFYPziW02AbjR9GI", "ptsF42D0bTTvvaIaBIhOTjWAUjuFIhAKhPkcj+udIcyH8CG1AAAAFQCpbXQSlxOvd4J92j2C", "rWDYVGoK8wAAAIAfHiV6/glGZrDRztJmw1hfwbmiNPxaoSGkB+Necfkj0fZrlyLj8sLJIbGQ", "w0dJMATZdRHw3Ql4R5IOu7sBfX1KQW++onT4ads/Xtl6vwfsjO2e/a6Y1ib9JCIOGJxNAAUC", "JU0Fm0TSv2Nn6UTICAarp1eKALimqkvy1+ygBWjprgAAAIEAic5EpZH9wpgzvl9kPW531yrz", "IOlCcXsJFPqQxUThrB2o1g3Rjpscd9kCw5UlPu6GGLk4aSN3UxeIKymTuKiEi7tvP1Tj/Bv5", "tEc4rhfmrBAfAST09oRFDsELufsOAlTrJ0uk2LhtN14H1RBv9qPR5PQKTEYslyvXG1f8itNQ", "YnQ=" ); fn make_test_request_body(key: &[u8], name: &str) -> (Body, String) { let mut form = FormData::new(Vec::new()); form.write_file( "pubkey", key, Some(name.as_ref()), "application/octet-stream", ) .unwrap(); let content_type = form.content_type_header(); let body = Body::from(form.finish().unwrap()); (body, content_type) } fn make_test_request(body: Body, content_type: &str) -> Request { let hostname = "test.example.org"; let machine_id = uuid!("b75e9126-d73a-4ae0-9a0d-63cb3552e6cd"); let token = token::make_token(hostname, machine_id); Request::builder() .uri("/host/sign") .method("POST") .header("Authorization", format!("Bearer {}", token)) .header("Host", "sshca.example.org") .header("Content-Type", content_type) .body(body) .unwrap() } #[tokio::test] async fn test_sign() { let (ctx, config) = setup::setup().await.unwrap(); let app = make_app(config); let (body, content_type) = make_test_request_body( ED25519_KEY.as_bytes(), "ssh_host_ed25519_key.pub", ); let req = make_test_request(body, &content_type); let res = app.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::OK); let body = hyper::body::to_bytes(res.into_body()).await.unwrap(); let cert = Certificate::from_openssh(std::str::from_utf8(&body).unwrap()) .unwrap(); assert_eq!(cert.algorithm(), Algorithm::Ed25519); cert.validate(&[ctx.host_ca_fingerprint()]).unwrap(); } #[tokio::test] async fn test_sign_invalid() { let (_ctx, config) = setup::setup().await.unwrap(); let (body, content_type) = make_test_request_body( "this is not a valid openssh key".as_bytes(), "ssh_host_ecdsa_key.pub", ); let app = make_app(config); let req = make_test_request(body, &content_type); let res = app.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::BAD_REQUEST); let body = hyper::body::to_bytes(res.into_body()).await.unwrap(); assert_eq!( body, concat!( "Could not parse SSH key: ", "Base64 encoding error: invalid Base64 encoding", ) ); } #[tokio::test] async fn test_sign_nokey() { let (_ctx, config) = setup::setup().await.unwrap(); let mut form = FormData::new(Vec::new()); let content_type = form.content_type_header(); let body = Body::from(form.finish().unwrap()); let app = make_app(config); let req = make_test_request(body, &content_type); let res = app.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::BAD_REQUEST); let body = hyper::body::to_bytes(res.into_body()).await.unwrap(); assert_eq!(body, "No SSH public key provided in request",); } #[tokio::test] async fn test_sign_mangled() { let (_ctx, config) = setup::setup().await.unwrap(); let app = make_app(config); let mut form = FormData::new(Vec::new()); form.write_file( "pubkey", ED25519_KEY.as_bytes(), Some("ssh_host_ed25519_key.pub".as_ref()), "application/octet-stream", ) .unwrap(); let content_type = form.content_type_header(); let mut form_bytes = form.finish().unwrap(); form_bytes.truncate(19); let body = Body::from(form_bytes); let req = make_test_request(body, &content_type); let res = app.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::BAD_REQUEST); let body = hyper::body::to_bytes(res.into_body()).await.unwrap(); assert_eq!(body, "Error parsing `multipart/form-data` request",); } #[tokio::test] async fn test_sign_bad_request() { let (_ctx, config) = setup::setup().await.unwrap(); let app = make_app(config); let content_type = "text/plain"; let body = Body::from("test"); let req = make_test_request(body, content_type); let res = app.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::BAD_REQUEST); let body = hyper::body::to_bytes(res.into_body()).await.unwrap(); assert_eq!(body, "Invalid `boundary` for `multipart/form-data` request",); } #[tokio::test] async fn test_sign_dsa() { let (_ctx, config) = setup::setup().await.unwrap(); let app = make_app(config); let (body, content_type) = make_test_request_body(DSA_KEY.as_bytes(), "ssh_host_dsa_key.pub"); let req = make_test_request(body, &content_type); let res = app.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::BAD_REQUEST); let body = hyper::body::to_bytes(res.into_body()).await.unwrap(); assert_eq!(body, "Unsupported key algorithm: ssh-dss"); } #[tokio::test] async fn test_sign_failure() { let (_ctx, mut config) = setup::setup().await.unwrap(); config.ca.host.private_key_file = "bogus".into(); let app = make_app(config); let (body, content_type) = make_test_request_body( ED25519_KEY.as_bytes(), "ssh_host_ed25519_key.pub", ); let req = make_test_request(body, &content_type); let res = app.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::SERVICE_UNAVAILABLE); let body = hyper::body::to_bytes(res.into_body()).await.unwrap(); assert_eq!(body, "Service Unavailable"); } #[tokio::test] async fn test_sign_unauthorized() { // Deliberately drop the TestContext so the machine ID file gets deleted, // which will cause authentication to fail. let (_, config) = setup::setup().await.unwrap(); let app = make_app(config); let (body, content_type) = make_test_request_body( ED25519_KEY.as_bytes(), "ssh_host_ed25519_key.pub", ); let req = make_test_request(body, &content_type); let res = app.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::UNAUTHORIZED); let body = hyper::body::to_bytes(res.into_body()).await.unwrap(); assert_eq!(body, "Unauthorized"); }