Skip to content

Commit

Permalink
feat(jx): add support for signing election data (#95)
Browse files Browse the repository at this point in the history
  • Loading branch information
eventualbuddha authored Mar 27, 2024
1 parent 558909f commit ec48e96
Show file tree
Hide file tree
Showing 22 changed files with 739 additions and 229 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

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

158 changes: 147 additions & 11 deletions apps/cacvote-jx-terminal/backend/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,33 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::time::Duration;

use async_stream::try_stream;
use axum::body::Bytes;
use axum::response::sse::{Event, KeepAlive};
use axum::response::Sse;
use axum::routing::post;
use axum::Json;
use axum::{extract::DefaultBodyLimit, routing::get, Router};
use axum::{extract::State, http::StatusCode, response::IntoResponse};
use futures_core::Stream;
use serde_json::json;
use sqlx::PgPool;
use tokio::time::sleep;
use tower_http::services::{ServeDir, ServeFile};
use tower_http::trace::TraceLayer;
use tracing::Level;
use types_rs::cacvote::{Election, Payload, SessionData, SignedObject};
use types_rs::election::ElectionDefinition;
use uuid::Uuid;

use crate::config::{Config, MAX_REQUEST_SIZE};
use crate::smartcard;
use crate::{db, smartcard};

// type AppState = (Config, PgPool, smartcard::StatusGetter);
type AppState = (Config, PgPool, smartcard::DynStatusGetter);
type AppState = (Config, PgPool, smartcard::DynSmartcard);

/// Prepares the application with all the routes. Run the application with
/// `app::run(…)` once you have it.
pub(crate) fn setup(
pool: PgPool,
config: Config,
smartcard_status: smartcard::DynStatusGetter,
) -> Router {
pub(crate) fn setup(pool: PgPool, config: Config, smartcard: smartcard::DynSmartcard) -> Router {
let _entered = tracing::span!(Level::DEBUG, "Setting up application").entered();

let router = match &config.public_dir {
Expand All @@ -49,9 +52,11 @@ pub(crate) fn setup(
router
.route("/api/status", get(get_status))
.route("/api/status-stream", get(get_status_stream))
.route("/api/elections", get(get_elections))
.route("/api/elections", post(create_election))
.layer(DefaultBodyLimit::max(MAX_REQUEST_SIZE))
.layer(TraceLayer::new_for_http())
.with_state((config, pool, smartcard_status))
.with_state((config, pool, smartcard))
}

/// Runs an application built by `app::setup(…)`.
Expand All @@ -69,13 +74,144 @@ async fn get_status() -> impl IntoResponse {
}

async fn get_status_stream(
State((_, _pool, _smartcard_status)): State<AppState>,
State((_, _pool, smartcard)): State<AppState>,
) -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
Sse::new(try_stream! {
let mut last_card_details = None;

loop {
yield Event::default();
sleep(Duration::from_secs(1)).await;
let new_card_details = smartcard.get_card_details();

if new_card_details != last_card_details {
last_card_details = new_card_details.clone();
yield Event::default().json_data(SessionData {
jurisdiction_code: new_card_details.map(|d| d.card_details.jurisdiction_code()),
}).unwrap();
}

sleep(Duration::from_millis(100)).await;
}
})
.keep_alive(KeepAlive::default())
}

async fn get_elections(State((_, pool, _)): State<AppState>) -> impl IntoResponse {
let mut connection = match pool.acquire().await {
Ok(connection) => connection,
Err(e) => {
tracing::error!("error getting database connection: {e}");
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({ "error": "error getting database connection" })),
);
}
};

let elections = match db::get_elections(&mut connection).await {
Ok(elections) => elections,
Err(e) => {
tracing::error!("error getting elections from database: {e}");
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({ "error": "error getting elections from database" })),
);
}
};

(StatusCode::OK, Json(json!({ "elections": elections })))
}

async fn create_election(
State((_, pool, smartcard)): State<AppState>,
body: Bytes,
) -> impl IntoResponse {
let jurisdiction_code = match smartcard.get_card_details() {
Some(card_details) => card_details.card_details.jurisdiction_code(),
None => {
tracing::error!("no card details found");
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({ "error": "no card details found" })),
);
}
};

let election_definition = match ElectionDefinition::try_from(&body[..]) {
Ok(election_definition) => election_definition,
Err(e) => {
tracing::error!("error parsing election definition: {e}");
return (
StatusCode::BAD_REQUEST,
Json(json!({ "error": format!("error parsing election definition: {e}") })),
);
}
};

let mut connection = match pool.acquire().await {
Ok(connection) => connection,
Err(e) => {
tracing::error!("error getting database connection: {e}");
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({ "error": "error getting database connection" })),
);
}
};

let payload = Payload::Election(Election {
jurisdiction_code,
election_definition,
});
let serialized_payload = match serde_json::to_vec(&payload) {
Ok(serialized_payload) => serialized_payload,
Err(e) => {
tracing::error!("error serializing payload: {e}");
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({ "error": "error serializing payload" })),
);
}
};

let signed = match smartcard.sign(&serialized_payload, None) {
Ok(signed) => signed,
Err(e) => {
tracing::error!("error signing payload: {e}");
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({ "error": "error signing payload" })),
);
}
};
let certificates: Vec<u8> = match signed
.cert_stack
.iter()
.map(|cert| cert.to_pem())
.collect::<Result<Vec<_>, _>>()
{
Ok(certificates) => certificates.concat(),
Err(e) => {
tracing::error!("error converting certificates to PEM: {e}");
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({ "error": "error converting certificates to PEM" })),
);
}
};
let signed_object = SignedObject {
id: Uuid::new_v4(),
payload: serialized_payload,
certificates,
signature: signed.data,
};

if let Err(e) = db::add_object(&mut connection, &signed_object).await {
tracing::error!("error adding object to database: {e}");
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({ "error": "error adding object to database" })),
);
}

(StatusCode::CREATED, Json(json!({ "id": signed_object.id })))
}
77 changes: 76 additions & 1 deletion apps/cacvote-jx-terminal/backend/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,46 @@ pub(crate) async fn setup(config: &Config) -> color_eyre::Result<PgPool> {
Ok(pool)
}

pub(crate) async fn get_elections(
connection: &mut sqlx::PgConnection,
) -> color_eyre::eyre::Result<Vec<types_rs::cacvote::Election>> {
let objects = sqlx::query_as!(
SignedObject,
r#"
SELECT
id,
payload,
certificates,
signature
FROM objects
WHERE object_type = 'Election'
"#,
)
.fetch_all(connection)
.await?;

let mut elections = Vec::new();

for object in objects {
let payload = match object.try_to_inner() {
Ok(payload) => {
tracing::debug!("got object payload: {payload:?}");
payload
}
Err(err) => {
tracing::error!("unable to parse object payload: {err:?}");
continue;
}
};

if let types_rs::cacvote::Payload::Election(election) = payload {
elections.push(election);
}
}

Ok(elections)
}

#[tracing::instrument(skip(connection, object))]
pub async fn add_object_from_server(
connection: &mut sqlx::PgConnection,
Expand All @@ -48,7 +88,7 @@ pub async fn add_object_from_server(
bail!("No jurisdiction found");
};

let object_type = object.try_to_inner()?.object_type;
let object_type = object.try_to_inner()?.object_type();

sqlx::query!(
r#"
Expand All @@ -70,6 +110,41 @@ pub async fn add_object_from_server(
Ok(object.id)
}

#[tracing::instrument(skip(connection, object))]
pub async fn add_object(
connection: &mut sqlx::PgConnection,
object: &SignedObject,
) -> color_eyre::Result<Uuid> {
if !object.verify()? {
bail!("Unable to verify signature/certificates")
}

let Some(jurisdiction_code) = object.jurisdiction_code() else {
bail!("No jurisdiction found");
};

let object_type = object.try_to_inner()?.object_type();

sqlx::query!(
r#"
INSERT INTO objects (id, jurisdiction, object_type, payload, certificates, signature)
VALUES ($1, $2, $3, $4, $5, $6)
"#,
&object.id,
jurisdiction_code.as_str(),
object_type,
&object.payload,
&object.certificates,
&object.signature
)
.execute(connection)
.await?;

tracing::info!("Created object with id {}", object.id);

Ok(object.id)
}

#[tracing::instrument(skip(connection, entries))]
pub(crate) async fn add_journal_entries(
connection: &mut sqlx::PgConnection,
Expand Down
8 changes: 4 additions & 4 deletions apps/cacvote-jx-terminal/backend/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ mod log;
mod smartcard;
mod sync;

use crate::smartcard::StatusGetter;
use crate::smartcard::Smartcard;

#[tokio::main]
async fn main() -> color_eyre::Result<()> {
Expand All @@ -65,7 +65,7 @@ async fn main() -> color_eyre::Result<()> {
let pool = db::setup(&config).await?;
sync::sync_periodically(&pool, config.clone()).await;
let smartcard_watcher = Watcher::watch();
let smartcard_status = StatusGetter::new(smartcard_watcher.readers_with_cards());
let smartcard_status = Arc::new(smartcard_status) as smartcard::DynStatusGetter;
app::run(app::setup(pool, config.clone(), smartcard_status), &config).await
let smartcard = Smartcard::new(smartcard_watcher.readers_with_cards());
let smartcard = Arc::new(smartcard) as smartcard::DynSmartcard;
app::run(app::setup(pool, config.clone(), smartcard), &config).await
}
Loading

0 comments on commit ec48e96

Please sign in to comment.