Skip to content

Commit c053db2

Browse files
Merge #78
78: feat: add nilauth client r=mfontanini a=mfontanini This adds a client for nilauth (formerly authentication-service). Not much to add, basically the same as the nuc-py one. Co-authored-by: Matias Fontanini <[email protected]>
2 parents 3f3a942 + 1a3625e commit c053db2

File tree

6 files changed

+266
-7
lines changed

6 files changed

+266
-7
lines changed

Cargo.lock

Lines changed: 23 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ members = [
6262
"libs/nada-value",
6363
"libs/node-api",
6464
"libs/grpc-channel",
65-
"libs/client-core",
65+
"libs/client-core",
66+
"libs/nilauth-client",
6667
]
6768

6869
exclude = ["wasm-workspace"]

libs/nilauth-client/Cargo.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[package]
2+
name = "nilauth-client"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
async-trait = "0.1"
8+
chrono = { version = "0.4", features = ["serde"] }
9+
hex = { version = "0.4", features = ["serde"] }
10+
nillion-chain-client = { path = "../nillion-chain/client" }
11+
nillion-nucs = { path = "../nucs" }
12+
rand = "0.8"
13+
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
14+
serde = { version = "1", features = ["derive"] }
15+
serde_json = "1"
16+
thiserror = "1"
17+
18+
[dev-dependencies]
19+
tokio = { version = "1.44", features = ["rt-multi-thread", "macros"] }
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
use nilauth_client::client::{DefaultNilauthClient, NilauthClient};
2+
use nillion_chain_client::{client::NillionChainClient, key::NillionChainPrivateKey};
3+
use nillion_nucs::k256::SecretKey;
4+
5+
#[tokio::main]
6+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
7+
let payment_key = NillionChainPrivateKey::from_bytes(
8+
b"\x97\xf4\x98\x89\xfc\xee\xd8\x8a\x9c\xdd\xdb\x16\xa1a\xd1?j\x120|+9\x16?<<9|<-$4",
9+
)?;
10+
let mut payer = NillionChainClient::new("http://localhost:26648".to_string(), payment_key).await?;
11+
let client = DefaultNilauthClient::new("http://127.0.0.1:30921");
12+
let key = SecretKey::random(&mut rand::thread_rng());
13+
client.pay_subscription(&mut payer, &key.public_key()).await?;
14+
let token = client.request_token(&key).await?;
15+
println!("{token}");
16+
Ok(())
17+
}

libs/nilauth-client/src/client.rs

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
use async_trait::async_trait;
2+
use chrono::{DateTime, Utc};
3+
use nillion_chain_client::{client::NillionChainClient, transactions::TokenAmount};
4+
use nillion_nucs::k256::{
5+
ecdsa::{signature::Signer, Signature, SigningKey},
6+
sha2::{Digest, Sha256},
7+
PublicKey, SecretKey,
8+
};
9+
use serde::{Deserialize, Serialize};
10+
use std::time::Duration;
11+
12+
const TOKEN_REQUEST_EXPIRATION: Duration = Duration::from_secs(60);
13+
14+
/// An interface to interact with nilauth.
15+
#[async_trait]
16+
pub trait NilauthClient {
17+
/// Get information about the nilauth instance.
18+
async fn about(&self) -> Result<About, AboutError>;
19+
20+
/// Request a token for the given private key.
21+
async fn request_token(&self, key: &SecretKey) -> Result<String, RequestTokenError>;
22+
23+
/// Pay for a subscription.
24+
async fn pay_subscription(
25+
&self,
26+
payments_client: &mut NillionChainClient,
27+
key: &PublicKey,
28+
) -> Result<TxHash, PaySubscriptionError>;
29+
}
30+
31+
/// An error when requesting a token.
32+
#[derive(Debug, thiserror::Error)]
33+
pub enum RequestTokenError {
34+
#[error("fetching server's about: {0}")]
35+
About(#[from] AboutError),
36+
37+
#[error("serde: {0}")]
38+
Serde(#[from] serde_json::Error),
39+
40+
#[error("invalid public key")]
41+
InvalidPublicKey,
42+
43+
#[error("request: {0}")]
44+
Request(#[from] reqwest::Error),
45+
}
46+
47+
/// An error when paying a subscription.
48+
#[derive(Debug, thiserror::Error)]
49+
pub enum PaySubscriptionError {
50+
#[error("fetching server's about: {0}")]
51+
About(#[from] AboutError),
52+
53+
#[error("serde: {0}")]
54+
Serde(#[from] serde_json::Error),
55+
56+
#[error("invalid public key")]
57+
InvalidPublicKey,
58+
59+
#[error("request: {0}")]
60+
Request(#[from] reqwest::Error),
61+
62+
#[error("making payment: {0}")]
63+
Payment(String),
64+
}
65+
66+
/// An error when requesting the information about a nilauth instance.
67+
#[derive(Debug, thiserror::Error)]
68+
pub enum AboutError {
69+
#[error("request: {0}")]
70+
Request(#[from] reqwest::Error),
71+
}
72+
73+
/// The default nilauth client that hits the actual service.
74+
pub struct DefaultNilauthClient {
75+
client: reqwest::Client,
76+
base_url: String,
77+
}
78+
79+
impl DefaultNilauthClient {
80+
pub fn new(base_url: impl Into<String>) -> Self {
81+
Self { client: reqwest::Client::new(), base_url: base_url.into() }
82+
}
83+
84+
fn make_url(&self, path: &str) -> String {
85+
let base_url = &self.base_url;
86+
format!("{base_url}{path}")
87+
}
88+
}
89+
90+
#[async_trait]
91+
impl NilauthClient for DefaultNilauthClient {
92+
async fn about(&self) -> Result<About, AboutError> {
93+
let url = self.make_url("/about");
94+
let about = self.client.get(url).send().await?.json().await?;
95+
Ok(about)
96+
}
97+
98+
async fn request_token(&self, key: &SecretKey) -> Result<String, RequestTokenError> {
99+
let about = self.about().await?;
100+
let payload = CreateNucRequestPayload {
101+
nonce: rand::random(),
102+
expires_at: Utc::now() + TOKEN_REQUEST_EXPIRATION,
103+
target_public_key: about.public_key,
104+
};
105+
let payload = serde_json::to_string(&payload)?;
106+
let signature: Signature = SigningKey::from(key).sign(payload.as_bytes());
107+
108+
let public_key =
109+
key.public_key().to_sec1_bytes().as_ref().try_into().map_err(|_| RequestTokenError::InvalidPublicKey)?;
110+
let request =
111+
CreateNucRequest { public_key, signature: signature.to_bytes().into(), payload: payload.into_bytes() };
112+
let url = self.make_url("/api/v1/nucs/create");
113+
let response: CreateNucResponse =
114+
self.client.post(url).json(&request).send().await?.error_for_status()?.json().await?;
115+
Ok(response.token)
116+
}
117+
118+
async fn pay_subscription(
119+
&self,
120+
payments_client: &mut NillionChainClient,
121+
key: &PublicKey,
122+
) -> Result<TxHash, PaySubscriptionError> {
123+
let about = self.about().await?;
124+
let payload = ValidatePaymentRequestPayload { nonce: rand::random(), service_public_key: about.public_key };
125+
let payload = serde_json::to_string(&payload)?;
126+
let hash = Sha256::digest(&payload);
127+
let tx_hash = payments_client
128+
.pay_for_resource(TokenAmount::Unil(1), hash.to_vec())
129+
.await
130+
.map_err(|e| PaySubscriptionError::Payment(e.to_string()))?;
131+
132+
let public_key = key.to_sec1_bytes().as_ref().try_into().map_err(|_| PaySubscriptionError::InvalidPublicKey)?;
133+
let url = self.make_url("/api/v1/payments/validate");
134+
let request = ValidatePaymentRequest { tx_hash: tx_hash.clone(), payload: payload.into_bytes(), public_key };
135+
self.client.post(url).json(&request).send().await?.error_for_status()?;
136+
Ok(TxHash(tx_hash))
137+
}
138+
}
139+
140+
/// A transaction hash.
141+
#[derive(Clone, Debug, PartialEq)]
142+
pub struct TxHash(pub String);
143+
144+
/// Information about a nilauth server.
145+
#[derive(Clone, Deserialize)]
146+
pub struct About {
147+
/// The server's public key.
148+
#[serde(deserialize_with = "hex::serde::deserialize")]
149+
pub public_key: [u8; 33],
150+
}
151+
152+
#[derive(Serialize)]
153+
struct CreateNucRequest {
154+
#[serde(serialize_with = "hex::serde::serialize")]
155+
public_key: [u8; 33],
156+
157+
#[serde(serialize_with = "hex::serde::serialize")]
158+
signature: [u8; 64],
159+
160+
#[serde(serialize_with = "hex::serde::serialize")]
161+
payload: Vec<u8>,
162+
}
163+
164+
#[derive(Serialize)]
165+
struct CreateNucRequestPayload {
166+
// A nonce, to add entropy.
167+
#[serde(serialize_with = "hex::serde::serialize")]
168+
nonce: [u8; 16],
169+
170+
// When this payload is no longer considered valid, to prevent reusing this forever if it
171+
// leaks.
172+
#[serde(serialize_with = "chrono::serde::ts_seconds::serialize")]
173+
expires_at: DateTime<Utc>,
174+
175+
// Our public key, to ensure this request can't be redirected to another authority service.
176+
#[serde(serialize_with = "hex::serde::serialize")]
177+
target_public_key: [u8; 33],
178+
}
179+
180+
#[derive(Debug, Deserialize)]
181+
struct CreateNucResponse {
182+
token: String,
183+
}
184+
185+
#[derive(Serialize)]
186+
struct ValidatePaymentRequest {
187+
tx_hash: String,
188+
189+
#[serde(serialize_with = "hex::serde::serialize")]
190+
payload: Vec<u8>,
191+
192+
#[serde(serialize_with = "hex::serde::serialize")]
193+
public_key: [u8; 33],
194+
}
195+
196+
#[derive(Serialize)]
197+
struct ValidatePaymentRequestPayload {
198+
#[allow(dead_code)]
199+
#[serde(serialize_with = "hex::serde::serialize")]
200+
nonce: [u8; 16],
201+
202+
#[serde(serialize_with = "hex::serde::serialize")]
203+
service_public_key: [u8; 33],
204+
}

libs/nilauth-client/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod client;

0 commit comments

Comments
 (0)