Skip to content

Commit

Permalink
Merge pull request #219 from spacemeshos/certifier-support-expiration
Browse files Browse the repository at this point in the history
Support for certificates with expiration date
  • Loading branch information
poszu authored Apr 18, 2024
2 parents 69da0db + 9d9a2f2 commit efb8fac
Show file tree
Hide file tree
Showing 9 changed files with 380 additions and 56 deletions.
26 changes: 26 additions & 0 deletions Cargo.lock

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

6 changes: 6 additions & 0 deletions certifier/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ serde_json = "1.0.115"
base64 = "0.22.0"
axum-prometheus = "0.6.1"
tower = { version = "0.4.13", features = ["limit"] }
duration-str = { version = "0.7.1", default-features = false, features = [
"serde",
"time",
] }
parity-scale-codec = { version = "3.6.9", features = ["derive", "serde"] }
mockall = "0.12.1"

[dev-dependencies]
reqwest = { version = "0.12.3", features = ["json"] }
Expand Down
7 changes: 6 additions & 1 deletion certifier/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ The config structure is defined [here](src/configuration.rs). An example config:
```yaml
listen: "127.0.0.1:8080"
signing_key: <BASE64-encoded ed25519 private key>
certificate_expiration: 2w
post_cfg:
k1: 26
k2: 37
Expand All @@ -48,6 +49,10 @@ randomx_mode: Fast
Each field can also be provided as env variable prefixed with CERTIFIER. For example, `CERTIFIER_SIGNING_KEY`.

##### Expiring certificates
The certificates don't expire by default. To create certificates that expire after certain time duration,
set `certificate_expiration` field in the config. It understands units supported by the [duration_str](https://docs.rs/duration-str/0.7.1/duration_str/index.html) crate (i.e "1d", "2w").

##### Concurrency limit
It's important to configure the maximum number of requests that will be processed in parallel.
The POST verification is heavy on CPU and hence a value higher than the number of CPU cores might lead to drop in performance and increase latency.
Expand All @@ -74,4 +79,4 @@ Run `certifier generate-keys` to obtain randomly generated new keys.
```

## Log level
The log level can be controlled via `RUST_LOG` enviroment variable. It can be set to [error, warn, info, debug, trace, off].
The log level can be controlled via `RUST_LOG` enviroment variable. It can be set to [error, warn, info, debug, trace, off].
242 changes: 207 additions & 35 deletions certifier/src/certifier.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
use std::sync::Arc;
use std::time::{Duration, SystemTime};

use axum::http::StatusCode;
use axum::{extract::State, Json};
use axum::{routing::post, Router};
use ed25519_dalek::{Signer, SigningKey};
use ed25519_dalek::{Signature, Signer, SigningKey};
use parity_scale_codec::{Compact, Decode, Encode};
use post::config::{InitConfig, ProofConfig};
use post::pow::randomx::PoW;
use post::verification::{Mode, Verifier};
use post::verification::Mode;
use serde::{Deserialize, Serialize};
use serde_with::{base64::Base64, serde_as};
use tracing::instrument;

use crate::configuration::RandomXMode;
use crate::time::unix_timestamp;

#[derive(Debug, Deserialize, Serialize)]
pub struct CertifyRequest {
Expand All @@ -20,71 +23,240 @@ pub struct CertifyRequest {
}

#[serde_as]
#[derive(Debug, Serialize)]
struct CertifyResponse {
#[derive(Debug, Serialize, Deserialize)]
pub struct CertifyResponse {
/// The certificate as scale-encoded `Certificate` struct
#[serde_as(as = "Base64")]
signature: Vec<u8>,
pub certificate: Vec<u8>,
/// Signature of the certificate
#[serde_as(as = "Base64")]
pub_key: Vec<u8>,
pub signature: Vec<u8>,
/// The public key of the certifier that signed the certificate
#[serde_as(as = "Base64")]
pub pub_key: Vec<u8>,
}

#[derive(Debug, Decode, Encode)]
pub struct Certificate {
// ID of the node being certified
pub pub_key: Vec<u8>,
/// Unix timestamp
pub expiration: Option<Compact<u64>>,
}

#[instrument(skip(state))]
async fn certify(
State(state): State<Arc<AppState>>,
State(state): State<Arc<Certifier>>,
Json(req): Json<CertifyRequest>,
) -> Result<Json<CertifyResponse>, (StatusCode, String)> {
tracing::debug!("certifying");

let pub_key = req.metadata.node_id;
let my_id = state.signer.verifying_key().to_bytes();
let s = state.clone();
let result = tokio::task::spawn_blocking(move || s.certify(&req.proof, &req.metadata))
.await
.map_err(|e| {
tracing::error!("internal error verifying proof: {e:?}");
(
StatusCode::INTERNAL_SERVER_ERROR,
"error verifying proof".into(),
)
})?;

let result = tokio::task::spawn_blocking(move || {
s.verifier
.verify(&req.proof, &req.metadata, &s.cfg, &s.init_cfg, Mode::All)
})
.await
.map_err(|e| {
tracing::error!("internal error verifying proof: {e:?}");
(
StatusCode::INTERNAL_SERVER_ERROR,
"error verifying proof".into(),
)
})?;

result.map_err(|e| (StatusCode::FORBIDDEN, format!("invalid proof: {e:?}")))?;

// Sign the nodeID
let response = CertifyResponse {
signature: state.signer.sign(&pub_key).to_vec(),
pub_key: my_id.to_vec(),
};
Ok(Json(response))
match result {
Ok(result) => {
let response = CertifyResponse {
certificate: result.0.to_vec(),
signature: result.1.to_vec(),
pub_key: state.signer.verifying_key().to_bytes().to_vec(),
};
Ok(Json(response))
}
Err(e) => {
return Err((StatusCode::FORBIDDEN, format!("invalid proof: {e:?}")));
}
}
}

struct AppState {
verifier: Verifier,
#[mockall::automock]
trait Verifier {
fn verify(
&self,
proof: &post::prove::Proof<'static>,
metadata: &post::metadata::ProofMetadata,
) -> Result<(), String>;
}

struct PostVerifier {
verifier: post::verification::Verifier,
cfg: ProofConfig,
init_cfg: InitConfig,
}

impl Verifier for PostVerifier {
fn verify(
&self,
proof: &post::prove::Proof<'_>,
metadata: &post::metadata::ProofMetadata,
) -> Result<(), String> {
self.verifier
.verify(proof, metadata, &self.cfg, &self.init_cfg, Mode::All)
.map_err(|e| format!("{e:?}"))
}
}

struct Certifier {
verifier: Arc<dyn Verifier + Send + Sync>,
signer: SigningKey,
expiry: Option<Duration>,
}

impl Certifier {
pub fn certify(
&self,
proof: &post::prove::Proof<'static>,
metadata: &post::metadata::ProofMetadata,
) -> Result<(Vec<u8>, Signature), String> {
self.verifier.verify(proof, metadata)?;

let cert = self.create_certificate(&metadata.node_id);
let cert_encoded = cert.encode();
let signature = self.signer.sign(&cert_encoded);

Ok((cert_encoded.to_vec(), signature))
}

fn create_certificate(&self, id: &[u8; 32]) -> Certificate {
let expiration = self
.expiry
.map(|exp| unix_timestamp(SystemTime::now() + exp));
Certificate {
pub_key: id.to_vec(),
expiration: expiration.map(Compact),
}
}
}

pub fn new(
cfg: ProofConfig,
init_cfg: InitConfig,
signer: SigningKey,
randomx_mode: RandomXMode,
expiry: Option<Duration>,
) -> Router {
let state = AppState {
verifier: Verifier::new(Box::new(
let verifier = Arc::new(PostVerifier {
verifier: post::verification::Verifier::new(Box::new(
PoW::new(randomx_mode.into()).expect("creating RandomX PoW verifier"),
)),
cfg,
init_cfg,
});
let certifier = Certifier {
verifier,
signer,
expiry,
};

Router::new()
.route("/certify", post(certify))
.with_state(Arc::new(state))
.with_state(Arc::new(certifier))
}

#[cfg(test)]
mod tests {
use std::{
sync::Arc,
time::{Duration, SystemTime},
};

use crate::time::unix_timestamp;

use super::{Certificate, Certifier, MockVerifier};
use ed25519_dalek::SigningKey;
use parity_scale_codec::Decode;
use post::{metadata::ProofMetadata, prove::Proof};

#[test]
fn certify_invalid_post() {
let mut verifier = MockVerifier::new();
verifier
.expect_verify()
.returning(|_, _| Err("invalid".to_string()));

let certifier = Certifier {
verifier: Arc::new(verifier),
signer: SigningKey::generate(&mut rand::rngs::OsRng),
expiry: None,
};

let proof = Proof {
nonce: 0,
indices: std::borrow::Cow::Owned(vec![1, 2, 3]),
pow: 0,
};

let metadata = ProofMetadata {
node_id: [7; 32],
commitment_atx_id: [0u8; 32],
challenge: [0; 32],
num_units: 1,
};

certifier
.certify(&proof, &metadata)
.expect_err("certification should fail");
}

#[test]
fn ceritify_valid_post() {
let mut verifier = MockVerifier::new();
verifier.expect_verify().returning(|_, _| Ok(()));
let certifier = Certifier {
verifier: Arc::new(verifier),
signer: SigningKey::generate(&mut rand::rngs::OsRng),
expiry: None,
};

let proof = Proof {
nonce: 0,
indices: std::borrow::Cow::Owned(vec![1, 2, 3]),
pow: 0,
};

let metadata = ProofMetadata {
node_id: [7; 32],
commitment_atx_id: [0u8; 32],
challenge: [0; 32],
num_units: 1,
};

let (encoded, signature) = certifier
.certify(&proof, &metadata)
.expect("certification should succeed");

certifier
.signer
.verify(&encoded, &signature)
.expect("signature should be valid");

let cert = Certificate::decode(&mut encoded.as_slice())
.expect("decoding certificate should succeed");
assert!(cert.expiration.is_none());
}

#[test]
fn create_cert_with_expiry() {
let expiry = Duration::from_secs(60 * 60);
let certifier = Certifier {
verifier: Arc::new(MockVerifier::new()),
signer: SigningKey::generate(&mut rand::rngs::OsRng),
expiry: Some(expiry),
};

let started = SystemTime::now();
let cert = certifier.create_certificate(&[7u8; 32]);

let expiration = cert.expiration.unwrap().0;
assert!(expiration >= unix_timestamp(started + expiry));
assert!(expiration <= unix_timestamp(SystemTime::now() + expiry));
}
}
Loading

0 comments on commit efb8fac

Please sign in to comment.