Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for certificates with expiration date #219

Merged
merged 5 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
}

#[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>>,
}
Comment on lines +40 to 45
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a bit late, but we could have gone the route of using actual x509 certificates and then use client authentication (mTLS) against the certifier, would have probably been less implementation effort and made it easier to develop 3rd party tools or alternative implementations.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could a x509 certificate be only valid for a single nodeID? A certificate should allow for only 1 poet registration per round.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, a certificate would be for a specific public key until a given expiration date. I don't think the effort to change it now is worth it though.


#[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(),
)
})?;

Check warning on line 63 in certifier/src/certifier.rs

View check run for this annotation

Codecov / codecov/patch

certifier/src/certifier.rs#L57-L63

Added lines #L57 - L63 were not covered by tests

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:?}")));
fasmat marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

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
Loading