1
0
mirror of https://github.com/bitwarden/server synced 2026-01-28 23:36:12 +00:00

Reader API implementation

This commit is contained in:
Matt Gibson
2026-01-15 10:18:01 -08:00
parent da229d6b3f
commit 1205133682
14 changed files with 392 additions and 13 deletions

15
akd/Cargo.lock generated
View File

@@ -62,6 +62,7 @@ dependencies = [
"hex",
"log",
"protobuf",
"serde",
"tokio",
]
@@ -79,6 +80,8 @@ dependencies = [
"protobuf",
"protobuf-codegen",
"protobuf-parse",
"serde",
"serde_bytes",
"tokio",
"zeroize",
]
@@ -953,6 +956,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
dependencies = [
"pkcs8",
"serde",
"signature",
]
@@ -2255,6 +2259,7 @@ dependencies = [
"anyhow",
"axum",
"bitwarden-akd-configuration",
"bitwarden-encoding",
"chrono",
"common",
"config",
@@ -2531,6 +2536,16 @@ dependencies = [
"typeid",
]
[[package]]
name = "serde_bytes"
version = "0.11.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8"
dependencies = [
"serde",
"serde_core",
]
[[package]]
name = "serde_core"
version = "1.0.228"

View File

@@ -91,7 +91,7 @@ impl VrfKeyConfig {
Ok(blake3::hash(&root_key_bytes).as_bytes().to_vec())
}
pub fn root_key_type(&self) -> VrfRootKeyType {
pub(crate) fn root_key_type(&self) -> VrfRootKeyType {
match self {
#[cfg(test)]
VrfKeyConfig::ConstantVrfKey => VrfRootKeyType::None,

View File

@@ -1,5 +1,7 @@
use akd::Directory;
use akd::{directory::ReadOnlyDirectory, Directory};
use akd_storage::{AkdDatabase, VrfKeyDatabase};
use bitwarden_akd_configuration::BitwardenV1Configuration;
pub type BitAkdDirectory = Directory<BitwardenV1Configuration, AkdDatabase, VrfKeyDatabase>;
pub type ReadOnlyBitAkdDirectory =
ReadOnlyDirectory<BitwardenV1Configuration, AkdDatabase, VrfKeyDatabase>;

View File

@@ -10,7 +10,7 @@ keywords.workspace = true
tokio = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
akd = { workspace = true }
akd = { workspace = true, features = ["serde_serialization"] }
akd_storage = { workspace = true }
anyhow = { workspace = true }
bitwarden-akd-configuration = { workspace = true }
@@ -20,6 +20,7 @@ config = { workspace = true }
uuid = { workspace = true }
axum = { workspace = true }
chrono = { workspace = true }
bitwarden-encoding = { workspace = true }
[lints]
workspace = true

View File

@@ -1,5 +1,5 @@
use akd::directory::ReadOnlyDirectory;
use akd_storage::{AkdDatabase, ReadOnlyPublishQueueType, VrfKeyDatabase};
use akd_storage::{AkdDatabase, VrfKeyDatabase};
use anyhow::{Context, Result};
use axum::Router;
use bitwarden_akd_configuration::BitwardenV1Configuration;
@@ -10,12 +10,14 @@ mod config;
mod routes;
pub use crate::config::ApplicationConfig;
pub use routes::response_types;
#[derive(Clone)]
struct AppState {
// Add any shared state here, e.g., database connections
directory: ReadOnlyDirectory<BitwardenV1Configuration, AkdDatabase, VrfKeyDatabase>,
publish_queue: ReadOnlyPublishQueueType,
// TODO: use this to allow for unique failures for lookup and key history requests that have pending updates
// publish_queue: ReadOnlyPublishQueueType,
}
#[instrument(skip_all, name = "reader_start")]
@@ -23,7 +25,7 @@ pub async fn start(
config: ApplicationConfig,
shutdown_rx: &Receiver<()>,
) -> Result<tokio::task::JoinHandle<Result<()>>> {
let (directory, _, publish_queue) = config
let (directory, _, _) = config
.storage
.initialize_readonly_directory::<BitwardenV1Configuration>()
.await
@@ -34,14 +36,14 @@ pub async fn start(
let handle = tokio::spawn(async move {
let app_state = AppState {
directory: directory,
publish_queue: publish_queue,
// publish_queue: publish_queue,
};
let app = Router::new()
.merge(crate::routes::api_routes())
.with_state(app_state);
let listener = TcpListener::bind((&config.socket_address()))
let listener = TcpListener::bind(&config.socket_address())
.await
.context("Socket bind failed")?;
info!(

View File

@@ -0,0 +1,36 @@
use axum::{extract::State, http::StatusCode, Json};
use serde::{Deserialize, Serialize};
use tracing::{error, info, instrument};
use crate::{routes::Response, AppState};
#[derive(Debug, Serialize, Deserialize)]
pub struct AuditRequest {
pub start_epoch: u64,
pub end_epoch: u64,
}
pub type AuditData = akd::AppendOnlyProof;
#[instrument(skip_all)]
pub async fn audit_handler(
State(AppState { directory, .. }): State<AppState>,
Json(AuditRequest {
start_epoch,
end_epoch,
}): Json<AuditRequest>,
) -> (StatusCode, Json<Response<AuditData>>) {
info!(
start_epoch,
end_epoch, "Handling epoch audit request request"
);
let audit_proof = directory.audit(start_epoch, end_epoch).await;
match audit_proof {
Ok(audit_proof) => (StatusCode::OK, Json(Response::success(audit_proof))),
Err(e) => {
error!(err = ?e, "Failed to perform epoch audit");
(StatusCode::INTERNAL_SERVER_ERROR, Json(Response::fail(e)))
}
}
}

View File

@@ -0,0 +1,47 @@
use axum::{extract::State, http::StatusCode, Json};
use serde::{Deserialize, Serialize};
use tracing::{error, info, instrument};
use crate::{
routes::{get_epoch_hash::EpochData, lookup::AkdLabelB64, Response},
AppState,
};
#[derive(Debug, Serialize, Deserialize)]
pub struct BatchLookupRequest {
/// An array of labels to look up. Each label is encoded as base64.
pub labels_b64: Vec<AkdLabelB64>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BatchLookupData {
pub lookup_proofs: Vec<akd::LookupProof>,
pub epoch_data: EpochData,
}
#[instrument(skip_all)]
pub async fn batch_lookup_handler(
State(AppState { directory, .. }): State<AppState>,
Json(BatchLookupRequest { labels_b64 }): Json<BatchLookupRequest>,
) -> (StatusCode, Json<Response<BatchLookupData>>) {
info!("Handling get public key request");
let labels = labels_b64
.into_iter()
.map(|label_b64| label_b64.into())
.collect::<Vec<akd::AkdLabel>>();
let lookup_proofs = directory.batch_lookup(&labels).await;
match lookup_proofs {
Ok((lookup_proofs, epoch_hash)) => (
StatusCode::OK,
Json(Response::success(BatchLookupData {
lookup_proofs,
epoch_data: epoch_hash.into(),
})),
),
Err(e) => {
error!(err = ?e, "Failed to get AKD public key");
(StatusCode::INTERNAL_SERVER_ERROR, Json(Response::fail(e)))
}
}
}

View File

@@ -0,0 +1,40 @@
use akd::EpochHash;
use axum::{extract::State, http::StatusCode, Json};
use serde::{Deserialize, Serialize};
use tracing::{error, info, instrument};
use crate::{routes::Response, AppState};
#[derive(Debug, Serialize, Deserialize)]
pub struct EpochData {
pub epoch: u64,
pub epoch_hash_b64: bitwarden_encoding::B64,
}
impl From<EpochHash> for EpochData {
fn from(epoch_hash: EpochHash) -> Self {
EpochData {
epoch: epoch_hash.0,
epoch_hash_b64: bitwarden_encoding::B64::from(epoch_hash.1.as_ref()),
}
}
}
#[instrument(skip_all)]
pub async fn get_epoch_hash_handler(
State(AppState { directory, .. }): State<AppState>,
) -> (StatusCode, Json<Response<EpochData>>) {
info!("Handling get epoch hash request");
let epoch_hash = directory.get_epoch_hash().await;
match epoch_hash {
Ok(epoch_hash) => (
StatusCode::OK,
Json(Response::<EpochData>::success(epoch_hash.into())),
),
Err(e) => {
error!(err = ?e, "Failed to get current AKD epoch hash");
(StatusCode::INTERNAL_SERVER_ERROR, Json(Response::fail(e)))
}
}
}

View File

@@ -0,0 +1,26 @@
use axum::{extract::State, http::StatusCode, Json};
use tracing::{error, info, instrument};
use crate::{routes::Response, AppState};
/// Public key encoded as a base64 string
pub type PublicKeyData = bitwarden_encoding::B64;
#[instrument(skip_all)]
pub async fn get_public_key_handler(
State(AppState { directory, .. }): State<AppState>,
) -> (StatusCode, Json<Response<PublicKeyData>>) {
info!("Handling get public key request");
let public_key = directory.get_public_key().await;
match public_key {
Ok(public_key) => (
StatusCode::OK,
Json(Response::success(public_key.as_ref().into())),
),
Err(e) => {
error!(err = ?e, "Failed to get AKD public key");
(StatusCode::INTERNAL_SERVER_ERROR, Json(Response::fail(e)))
}
}
}

View File

@@ -1,17 +1,19 @@
use axum::Json;
use axum::{http::StatusCode, Json};
use serde::{Deserialize, Serialize};
use tracing::{info, instrument};
use crate::routes::Response;
#[derive(Debug, Serialize, Deserialize)]
pub struct ServerHealth {
pub struct HealthData {
time: String,
}
#[instrument(skip_all)]
pub async fn health_handler() -> Json<ServerHealth> {
pub async fn health_handler() -> (StatusCode, Json<Response<HealthData>>) {
info!("Handling server health request");
let time = chrono::Utc::now().to_rfc3339();
Json(ServerHealth { time })
(StatusCode::OK, Json(Response::success(HealthData { time })))
}

View File

@@ -0,0 +1,70 @@
use axum::{extract::State, http::StatusCode, Json};
use serde::{Deserialize, Serialize};
use tracing::{error, info, instrument};
use crate::{
routes::{get_epoch_hash::EpochData, Response},
AppState,
};
#[derive(Debug, Serialize, Deserialize)]
pub struct KeyHistoryRequest {
/// the label to look up encoded as an uppercase hex string
pub label: akd::AkdLabel,
pub history_params: HistoryParams,
}
/// The parameters that dictate how much of the history proof to return to the consumer
/// (either a complete history, or some limited form).
#[derive(Copy, Clone, Serialize, Deserialize, Debug)]
#[serde(tag = "type")]
pub enum HistoryParams {
/// Returns a complete history for a label
Complete,
/// Returns up to the most recent N updates for a label
MostRecent(usize),
/// Returns all updates since a specified epoch (inclusive)
SinceEpoch(u64),
}
impl From<HistoryParams> for akd::HistoryParams {
fn from(params: HistoryParams) -> Self {
match params {
HistoryParams::Complete => akd::HistoryParams::Complete,
HistoryParams::MostRecent(n) => akd::HistoryParams::MostRecent(n),
HistoryParams::SinceEpoch(epoch) => akd::HistoryParams::SinceEpoch(epoch),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct HistoryData {
pub history_proof: akd::HistoryProof,
pub epoch_data: EpochData,
}
#[instrument(skip_all)]
pub async fn key_history_handler(
State(AppState { directory, .. }): State<AppState>,
Json(KeyHistoryRequest {
label,
history_params,
}): Json<KeyHistoryRequest>,
) -> (StatusCode, Json<Response<HistoryData>>) {
info!("Handling get public key request");
let history_proof = directory.key_history(&label, history_params.into()).await;
match history_proof {
Ok((history_proof, epoch_hash)) => (
StatusCode::OK,
Json(Response::success(HistoryData {
history_proof: history_proof,
epoch_data: epoch_hash.into(),
})),
),
Err(e) => {
error!(err = ?e, "Failed to get AKD public key");
(StatusCode::INTERNAL_SERVER_ERROR, Json(Response::fail(e)))
}
}
}

View File

@@ -0,0 +1,52 @@
use axum::{extract::State, http::StatusCode, Json};
use serde::{Deserialize, Serialize};
use tracing::{error, info, instrument};
use crate::{
routes::{get_epoch_hash::EpochData, Response},
AppState,
};
#[derive(Debug, Serialize, Deserialize)]
pub struct AkdLabelB64(bitwarden_encoding::B64);
impl From<AkdLabelB64> for akd::AkdLabel {
fn from(label_b64: AkdLabelB64) -> Self {
akd::AkdLabel(label_b64.0.into_bytes())
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LookupRequest {
/// the label to look up encoded as base64
pub label_b64: AkdLabelB64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LookupData {
pub lookup_proof: akd::LookupProof,
pub epoch_data: EpochData,
}
#[instrument(skip_all)]
pub async fn lookup_handler(
State(AppState { directory, .. }): State<AppState>,
Json(LookupRequest { label_b64 }): Json<LookupRequest>,
) -> (StatusCode, Json<Response<LookupData>>) {
info!("Handling get public key request");
let lookup_proof = directory.lookup(label_b64.into()).await;
match lookup_proof {
Ok((lookup_proof, epoch_hash)) => (
StatusCode::OK,
Json(Response::success(LookupData {
lookup_proof,
epoch_data: epoch_hash.into(),
})),
),
Err(e) => {
error!(err = ?e, "Failed to get AKD public key");
(StatusCode::INTERNAL_SERVER_ERROR, Json(Response::fail(e)))
}
}
}

View File

@@ -1,9 +1,86 @@
use akd::errors::AkdError;
use axum::routing::get;
use serde::{Deserialize, Serialize};
mod audit;
mod batch_lookup;
mod get_epoch_hash;
mod get_public_key;
mod health;
mod key_history;
mod lookup;
pub mod response_types;
use crate::AppState;
pub fn api_routes() -> axum::Router<AppState> {
axum::Router::new().route("/health", get(health::health_handler))
axum::Router::new()
.route("/health", get(health::health_handler))
.route("/public_key", get(get_public_key::get_public_key_handler))
.route("/epoch_hash", get(get_epoch_hash::get_epoch_hash_handler))
.route("/lookup", get(lookup::lookup_handler))
.route("/key_history", get(key_history::key_history_handler))
.route("/batch_lookup", get(batch_lookup::batch_lookup_handler))
.route("/audit", get(audit::audit_handler))
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Response<T> {
success: bool,
data: Option<T>,
error: Option<ResponseError>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ResponseError {
pub akd_error_type: String,
pub message: String,
}
impl From<AkdError> for ResponseError {
fn from(err: AkdError) -> Self {
ResponseError {
akd_error_type: match &err {
AkdError::TreeNode(_) => "TreeNode".to_string(),
AkdError::Directory(err) => match err {
akd::errors::DirectoryError::Verification(_) => "VerificationError".to_string(),
akd::errors::DirectoryError::InvalidEpoch(_) => "InvalidEpoch".to_string(),
akd::errors::DirectoryError::ReadOnlyDirectory(_) => {
"ReadOnlyDirectory".to_string()
}
akd::errors::DirectoryError::Publish(_) => "Publish".to_string(),
},
AkdError::AzksErr(_) => "AzksErr".to_string(),
AkdError::Vrf(_) => "Vrf".to_string(),
AkdError::Storage(err) => match err {
akd::errors::StorageError::NotFound(_) => "NotFound".to_string(),
akd::errors::StorageError::Transaction(_) => "Transaction".to_string(),
akd::errors::StorageError::Connection(_) => "Connection".to_string(),
akd::errors::StorageError::Other(_) => "Other".to_string(),
},
AkdError::AuditErr(_) => "AuditErr".to_string(),
AkdError::Parallelism(_) => "Parallelism".to_string(),
AkdError::TestErr(_) => "TestErr".to_string(),
},
message: err.to_string(),
}
}
}
impl<'a, T: Serialize + Deserialize<'a>> Response<T> {
fn success(data: T) -> Self {
Self {
success: true,
data: Some(data),
error: None,
}
}
fn fail(err: AkdError) -> Self {
Self {
success: false,
data: None,
error: Some(err.into()),
}
}
}

View File

@@ -0,0 +1,9 @@
use crate::routes::Response;
pub type AuditResponse = Response<crate::routes::audit::AuditData>;
pub type BatchLookupData = Response<crate::routes::batch_lookup::BatchLookupData>;
pub type HealthData = Response<crate::routes::health::HealthData>;
pub type HistoryData = Response<crate::routes::key_history::HistoryData>;
pub type LookupData = Response<crate::routes::lookup::LookupData>;
pub type EpochData = Response<crate::routes::get_epoch_hash::EpochData>;
pub type PublicKeyData = Response<crate::routes::get_public_key::PublicKeyData>;