Skip to content

Commit

Permalink
revised verify checkpoint endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
calvinrp committed Nov 14, 2023
1 parent a6e1047 commit a45e149
Show file tree
Hide file tree
Showing 3 changed files with 41 additions and 111 deletions.
48 changes: 6 additions & 42 deletions crates/api/src/v1/monitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,52 +11,31 @@ pub struct CheckpointVerificationResponse {
/// The checkpoint verification state.
pub checkpoint: CheckpointVerificationState,
/// The checkpoint signature verification state.
pub signature: CheckpointSignatureVerificationState,
pub signature: CheckpointVerificationState,
/// Optional, retry after specified number of seconds.
#[serde(skip_serializing_if = "Option::is_none")]
pub retry_after: Option<u16>,
}

/// Represents checkpoint verification state.
#[derive(Eq, PartialEq, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum CheckpointVerificationState {
/// The checkpoint signature is unverified and could be valid or invalid.
/// The checkpoint is unverified and could be valid or invalid.
#[serde(rename_all = "camelCase")]
Unverified,
/// The checkpoint is verified.
#[serde(rename_all = "camelCase")]
Verified,
/// The checkpoint log length does not exist.
#[serde(rename_all = "camelCase")]
NotFound,
/// The checkpoint is invalid.
#[serde(rename_all = "camelCase")]
Invalid,
}

/// Represents checkpoint signature verification state.
#[derive(Eq, PartialEq, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum CheckpointSignatureVerificationState {
/// The checkpoint signature is unverified and could be valid or invalid.
#[serde(rename_all = "camelCase")]
Unverified,
/// The checkpoint signature is verified.
#[serde(rename_all = "camelCase")]
Verified,
/// The checkpoint signature key ID is known but not authorized to sign checkpoints.
#[serde(rename_all = "camelCase")]
Unauthorized,
/// The checkpoint signature is not valid. The key ID could be not known or the signature is incorrect.
#[serde(rename_all = "camelCase")]
Invalid,
}

/// Represents a monitor API error.
#[non_exhaustive]
#[derive(Debug, Error)]
pub enum MonitorError {
/// Instruct to retry after specified number of seconds.
#[error("retry after {0} seconds")]
RetryAfter(u16),
/// An error with a message occurred.
#[error("{message}")]
Message {
Expand All @@ -71,7 +50,6 @@ impl MonitorError {
/// Returns the HTTP status code of the error.
pub fn status(&self) -> u16 {
match self {
Self::RetryAfter(_) => 503,
Self::Message { status, .. } => *status,
}
}
Expand All @@ -80,25 +58,12 @@ impl MonitorError {
#[derive(Serialize, Deserialize)]
#[serde(untagged, rename_all = "camelCase")]
enum RawError<'a> {
#[serde(rename_all = "camelCase")]
RetryAfter {
status: u16,
retry_after: u16,
},
Message {
status: u16,
message: Cow<'a, str>,
},
Message { status: u16, message: Cow<'a, str> },
}

impl Serialize for MonitorError {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
Self::RetryAfter(seconds) => RawError::RetryAfter {
status: 503,
retry_after: *seconds,
}
.serialize(serializer),
Self::Message { status, message } => RawError::Message {
status: *status,
message: Cow::Borrowed(message),
Expand All @@ -114,7 +79,6 @@ impl<'de> Deserialize<'de> for MonitorError {
D: serde::Deserializer<'de>,
{
match RawError::deserialize(deserializer)? {
RawError::RetryAfter { retry_after, .. } => Ok(Self::RetryAfter(retry_after)),
RawError::Message { status, message } => Ok(Self::Message {
status,
message: message.into_owned(),
Expand Down
52 changes: 12 additions & 40 deletions crates/server/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,7 @@ paths:
security: []
tags:
- monitor
description: Verify checkpoint from the registry. The server may return a `503` status code and instruct to retry the request.
description: Verify checkpoint from the registry. The client must interpret the response body to determine the verification status.
parameters:
- name: Warg-Registry
in: header
Expand All @@ -575,38 +575,14 @@ paths:
$ref: "#/components/schemas/SignedCheckpoint"
responses:
"200":
description: The checkpoint was successfully verified.
description: The checkpoint verification request was processed. The client must interpret the response body to determine the verification status.
headers:
Warg-Registry:
$ref: "#/components/headers/WargRegistryHeader"
content:
application/json:
schema:
$ref: "#/components/schemas/CheckpointVerificationResponse"
"503":
description: A requested entity was not found.
headers:
Warg-Registry:
$ref: "#/components/headers/WargRegistryHeader"
Retry-After:
$ref: "#/components/headers/RetryAfter"
content:
application/json:
schema:
type: object
additionalProperties: false
required:
- status
- retryAfter
properties:
status:
type: integer
description: The HTTP status code for the error.
example: 404
retryAfter:
type: integer
description: Instructs to retry after specified number of seconds.
example: 60
default:
description: An error occurred when processing the request.
headers:
Expand Down Expand Up @@ -656,12 +632,6 @@ components:
schema:
type: string
example: registry.example.com
RetryAfter:
description: Instructs to retry the request after specified number of seconds.
required: false
schema:
type: integer
example: 60
schemas:
Error:
type: object
Expand Down Expand Up @@ -1360,20 +1330,22 @@ components:
description: |
The verification of the checkpoint's `logLength`, `logRoot` and `mapRoot`:
* `unverified`: checkpoint may be valid or invalid;
* `unverified`: checkpoint may be valid or invalid; if `retryAfter` is provided, the client should retry the request;
* `verified`: `logLength`, `logRoot` and `mapRoot` are verified;
* `notFound`: checkpoint with the specified `logLength` does not exist;
* `invalid`: checkpoint with the specified `logLength` does not match the correct `logRoot` and `mapRoot`;
enum: [unverified, verified, notFound, invalid]
* `invalid`: checkpoint with the specified `logLength` was either not produced or does not match the correct `logRoot` and `mapRoot`;
enum: [unverified, verified, invalid]
example: verified
signature:
type: string
description: |
The verification of the checkpoint's `keyId` and `signature`:
* `unverified`: checkpoint's signature may be valid or invalid;
* `unverified`: checkpoint's signature may be valid or invalid; if `retryAfter` is provided, the client should retry the request;
* `verified`: checkpoint's signature is verified;
* `unauthorized`: checkpoint's signature `keyId` does not have authorization to sign checkpoints; authorization may have been revoked or never granted;
* `invalid`: checkpoint's signature `keyId` is not known or the signature itself is invalid;
enum: [unverified, verified, unauthorized, invalid]
* `invalid`: `keyId` is not known or does not have authorization (could have been revoke or never granted) to sign checkpoints or the signature itself is invalid;
enum: [unverified, verified, invalid]
example: verified
retryAfter:
type: integer
description: If either `checkpoint` or `signature` is `unverified` status, then the server may instruct the client to retry the request after the specified number of seconds.
example: 60
52 changes: 23 additions & 29 deletions crates/server/src/api/v1/monitor.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
use super::{Json, RegistryHeader};
use crate::datastore::DataStoreError;
use crate::services::CoreService;
use axum::http::{header, StatusCode};
use axum::http::StatusCode;
use axum::{debug_handler, extract::State, response::IntoResponse, routing::post, Router};
use warg_api::v1::monitor::{
CheckpointSignatureVerificationState, CheckpointVerificationResponse,
CheckpointVerificationState, MonitorError,
CheckpointVerificationResponse, CheckpointVerificationState, MonitorError,
};
use warg_crypto::hash::Sha256;
use warg_protocol::registry::{LogId, TimestampedCheckpoint};
Expand Down Expand Up @@ -33,6 +32,7 @@ struct MonitorApiError(MonitorError);
impl From<DataStoreError> for MonitorApiError {
fn from(e: DataStoreError) -> Self {
tracing::error!("unexpected data store error: {e}");

Self(MonitorError::Message {
status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
message: "an error occurred while processing the request".into(),
Expand All @@ -42,19 +42,7 @@ impl From<DataStoreError> for MonitorApiError {

impl IntoResponse for MonitorApiError {
fn into_response(self) -> axum::response::Response {
match self.0 {
MonitorError::RetryAfter(seconds) => {
let mut headers = header::HeaderMap::new();
headers.insert(header::RETRY_AFTER, seconds.into());
(
StatusCode::from_u16(self.0.status()).unwrap(),
headers,
Json(self.0),
)
.into_response()
}
_ => (StatusCode::from_u16(self.0.status()).unwrap(), Json(self.0)).into_response(),
}
(StatusCode::from_u16(self.0.status()).unwrap(), Json(self.0)).into_response()
}
}

Expand All @@ -64,7 +52,11 @@ async fn verify_checkpoint(
RegistryHeader(_registry_header): RegistryHeader,
Json(body): Json<SerdeEnvelope<TimestampedCheckpoint>>,
) -> Result<Json<CheckpointVerificationResponse>, MonitorApiError> {
// look up checkpoint
// check checkpoint:
// - if `log_length` not found, `checkpoint` is `Invalid`;
// - if `log_root` or `map_root` is incorrect, `checkpoint` is `Invalid`;
// - if `key_id` and `signature` does not match previously stored value,
// `signature` is `Unverified` and will check against the operator log;
let (checkpoint_verification, mut signature_verification) = match config
.core_service
.store()
Expand All @@ -86,38 +78,39 @@ async fn verify_checkpoint(
let signature_verification = if checkpoint.signature() == body.signature()
&& checkpoint.key_id() == body.key_id()
{
CheckpointSignatureVerificationState::Verified
CheckpointVerificationState::Verified
} else {
// set to Unverified and check against operator log keys below
CheckpointSignatureVerificationState::Unverified
CheckpointVerificationState::Unverified
};

(checkpoint_verification, signature_verification)
}
Err(DataStoreError::CheckpointNotFound(_)) => (
CheckpointVerificationState::NotFound,
CheckpointSignatureVerificationState::Unverified,
CheckpointVerificationState::Invalid,
CheckpointVerificationState::Unverified,
),
Err(error) => return Err(MonitorApiError::from(error)),
};

// if Unverified, check signature against keys in operator log
if signature_verification == CheckpointSignatureVerificationState::Unverified {
// if `Unverified`, check signature against keys in operator log:
//
// - if `key_id` is not known or it does not have permission to sign checkpoints or
// the `signature` is invalid, `signature` is `Invalid`;
if signature_verification == CheckpointVerificationState::Unverified {
match config
.core_service
.store()
.verify_timestamped_checkpoint_signature(&LogId::operator_log::<Sha256>(), &body)
.await
{
Ok(_) => {
signature_verification = CheckpointSignatureVerificationState::Verified;
signature_verification = CheckpointVerificationState::Verified;
}
Err(DataStoreError::UnknownKey(_))
| Err(DataStoreError::SignatureVerificationFailed(_)) => {
signature_verification = CheckpointSignatureVerificationState::Invalid;
}
Err(DataStoreError::KeyUnauthorized(_)) => {
signature_verification = CheckpointSignatureVerificationState::Unauthorized;
| Err(DataStoreError::SignatureVerificationFailed(_))
| Err(DataStoreError::KeyUnauthorized(_)) => {
signature_verification = CheckpointVerificationState::Invalid;
}
Err(error) => return Err(MonitorApiError::from(error)),
};
Expand All @@ -126,5 +119,6 @@ async fn verify_checkpoint(
Ok(Json(CheckpointVerificationResponse {
checkpoint: checkpoint_verification,
signature: signature_verification,
retry_after: None,
}))
}

0 comments on commit a45e149

Please sign in to comment.