merge: feat-lints into main
Some checks failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-test Pipeline was successful
ci/woodpecker/push/server-audit Pipeline was successful
ci/woodpecker/push/server-lint Pipeline failed
ci/woodpecker/push/server-vet Pipeline failed
ci/woodpecker/push/server-test Pipeline was successful
ci/woodpecker/push/useragent-analyze Pipeline failed
Some checks failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-test Pipeline was successful
ci/woodpecker/push/server-audit Pipeline was successful
ci/woodpecker/push/server-lint Pipeline failed
ci/woodpecker/push/server-vet Pipeline failed
ci/woodpecker/push/server-test Pipeline was successful
ci/woodpecker/push/useragent-analyze Pipeline failed
This commit was merged in pull request #87.
This commit is contained in:
@@ -6,16 +6,17 @@ edition = "2024"
|
||||
[dependencies]
|
||||
ml-dsa = {workspace = true, optional = true }
|
||||
rand = {workspace = true, optional = true}
|
||||
base64 = {workspace = true, optional = true }
|
||||
memsafe = {version = "0.4.0", optional = true}
|
||||
hmac.workspace = true
|
||||
alloy.workspace = true
|
||||
x-wing = { version = "0.1.0-rc.0", features = ["zeroize"] }
|
||||
chrono.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
default = ["authn", "safecell"]
|
||||
authn = ["dep:ml-dsa", "dep:rand", "dep:base64"]
|
||||
authn = ["dep:ml-dsa", "dep:rand"]
|
||||
safecell = ["dep:memsafe"]
|
||||
|
||||
@@ -1,16 +1,65 @@
|
||||
use base64::{Engine as _, prelude::BASE64_STANDARD};
|
||||
use chrono::{DateTime, Utc};
|
||||
use hmac::digest::Digest;
|
||||
use ml_dsa::{
|
||||
EncodedVerifyingKey, Error, KeyGen, MlDsa87, Seed, Signature as MlDsaSignature,
|
||||
SigningKey as MlDsaSigningKey, VerifyingKey as MlDsaVerifyingKey, signature::Keypair as _,
|
||||
};
|
||||
use rand::RngExt;
|
||||
|
||||
pub static CLIENT_CONTEXT: &[u8] = b"arbiter_client";
|
||||
pub static USERAGENT_CONTEXT: &[u8] = b"arbiter_user_agent";
|
||||
|
||||
pub fn format_challenge(nonce: i32, pubkey: &[u8]) -> Vec<u8> {
|
||||
let concat_form = format!("{}:{}", nonce, BASE64_STANDARD.encode(pubkey));
|
||||
concat_form.into_bytes()
|
||||
const NONCE_SIZE: usize = 32;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
||||
#[error("invalid length: expected {expected} bytes, got {actual} bytes")]
|
||||
pub struct InvalidLength {
|
||||
pub expected: usize,
|
||||
pub actual: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AuthChallenge {
|
||||
pub nonce: [u8; NONCE_SIZE],
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl AuthChallenge {
|
||||
pub fn generate(rng: &mut impl rand::CryptoRng) -> Self {
|
||||
let timestamp = Utc::now();
|
||||
let nonce = {
|
||||
let mut array = [0; NONCE_SIZE];
|
||||
rng.fill(&mut array);
|
||||
array
|
||||
};
|
||||
|
||||
Self { nonce, timestamp }
|
||||
}
|
||||
|
||||
pub fn format(&self) -> Vec<u8> {
|
||||
{
|
||||
let mut buffer = Vec::from(self.nonce);
|
||||
|
||||
let stamp = self
|
||||
.timestamp
|
||||
.timestamp_nanos_opt()
|
||||
.expect("We would be long dead by the time this triggers :)");
|
||||
buffer.extend_from_slice(stamp.to_be_bytes().as_slice());
|
||||
|
||||
buffer
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_parts(nonce: &[u8], timestamp: i64) -> Result<Self, InvalidLength> {
|
||||
let random_nonce = nonce.as_array().ok_or(InvalidLength {
|
||||
expected: NONCE_SIZE,
|
||||
actual: nonce.len(),
|
||||
})?;
|
||||
Ok(Self {
|
||||
nonce: *random_nonce,
|
||||
timestamp: DateTime::from_timestamp_nanos(timestamp),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub type KeyParams = MlDsa87;
|
||||
@@ -36,12 +85,10 @@ impl PublicKey {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn verify(&self, nonce: i32, context: &[u8], signature: &Signature) -> bool {
|
||||
self.0.verify_with_context(
|
||||
&format_challenge(nonce, &self.to_bytes()),
|
||||
context,
|
||||
&signature.0,
|
||||
)
|
||||
pub fn verify(&self, challenge: &AuthChallenge, context: &[u8], signature: &Signature) -> bool {
|
||||
let challenge = challenge.format();
|
||||
self.0
|
||||
.verify_with_context(&challenge, context, &signature.0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,11 +122,14 @@ impl SigningKey {
|
||||
.map(Into::into)
|
||||
}
|
||||
|
||||
pub fn sign_challenge(&self, nonce: i32, context: &[u8]) -> Result<Signature, Error> {
|
||||
self.sign_message(
|
||||
&format_challenge(nonce, &self.public_key().to_bytes()),
|
||||
context,
|
||||
)
|
||||
pub fn sign_challenge(
|
||||
&self,
|
||||
challenge: &AuthChallenge,
|
||||
context: &[u8],
|
||||
) -> Result<Signature, Error> {
|
||||
let challenge = challenge.format();
|
||||
|
||||
self.sign_message(&challenge, context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,6 +190,8 @@ impl TryFrom<&'_ [u8]> for Signature {
|
||||
mod tests {
|
||||
use ml_dsa::{KeyGen, MlDsa87, signature::Keypair as _};
|
||||
|
||||
use crate::authn::AuthChallenge;
|
||||
|
||||
use super::{CLIENT_CONTEXT, PublicKey, Signature, SigningKey, USERAGENT_CONTEXT};
|
||||
|
||||
#[test]
|
||||
@@ -169,13 +221,13 @@ mod tests {
|
||||
fn challenge_verification_uses_context_and_canonical_key_bytes() {
|
||||
let key = SigningKey::generate();
|
||||
let public_key = key.public_key();
|
||||
let nonce = 17;
|
||||
let challenge = AuthChallenge::generate(&mut rand::rng());
|
||||
let signature = key
|
||||
.sign_challenge(nonce, CLIENT_CONTEXT)
|
||||
.sign_challenge(&challenge, CLIENT_CONTEXT)
|
||||
.expect("signature should be created");
|
||||
|
||||
assert!(public_key.verify(nonce, CLIENT_CONTEXT, &signature));
|
||||
assert!(!public_key.verify(nonce, USERAGENT_CONTEXT, &signature));
|
||||
assert!(public_key.verify(&challenge, CLIENT_CONTEXT, &signature));
|
||||
assert!(!public_key.verify(&challenge, USERAGENT_CONTEXT, &signature));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -185,10 +237,16 @@ mod tests {
|
||||
|
||||
assert_eq!(restored.public_key(), original.public_key());
|
||||
|
||||
let challenge = AuthChallenge::generate(&mut rand::rng());
|
||||
|
||||
let signature = restored
|
||||
.sign_challenge(9, CLIENT_CONTEXT)
|
||||
.sign_challenge(&challenge, CLIENT_CONTEXT)
|
||||
.expect("signature should be created");
|
||||
|
||||
assert!(restored.public_key().verify(9, CLIENT_CONTEXT, &signature));
|
||||
assert!(
|
||||
restored
|
||||
.public_key()
|
||||
.verify(&challenge, CLIENT_CONTEXT, &signature)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
pub use hmac::digest::Digest;
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub use hmac::digest::Digest;
|
||||
|
||||
/// Deterministically hash a value by feeding its fields into the hasher in a consistent order.
|
||||
#[diagnostic::on_unimplemented(
|
||||
note = "for local types consider adding `#[derive(arbiter_macros::Hashable)]` to your `{Self}` type",
|
||||
|
||||
@@ -3,3 +3,5 @@ pub mod authn;
|
||||
pub mod hashing;
|
||||
#[cfg(feature = "safecell")]
|
||||
pub mod safecell;
|
||||
|
||||
pub use x_wing;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::{any::type_name, fmt};
|
||||
|
||||
use memsafe::MemSafe;
|
||||
use std::{
|
||||
any::type_name,
|
||||
fmt,
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
|
||||
pub trait SafeCellHandle<T> {
|
||||
type CellRead<'a>: Deref<Target = T>
|
||||
|
||||
Reference in New Issue
Block a user