security(memory): zeroization of bootstrap token
Some checks failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-lint Pipeline was successful
ci/woodpecker/pr/server-test Pipeline failed

This commit is contained in:
CleverWild
2026-06-18 17:16:06 +02:00
parent a8e4a710f1
commit dc03923c24
6 changed files with 25 additions and 20 deletions

9
server/Cargo.lock generated
View File

@@ -787,6 +787,7 @@ dependencies = [
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"x25519-dalek 2.0.1", "x25519-dalek 2.0.1",
"zeroize",
] ]
[[package]] [[package]]
@@ -6261,18 +6262,18 @@ dependencies = [
[[package]] [[package]]
name = "zeroize" name = "zeroize"
version = "1.8.2" version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
dependencies = [ dependencies = [
"zeroize_derive", "zeroize_derive",
] ]
[[package]] [[package]]
name = "zeroize_derive" name = "zeroize_derive"
version = "1.4.3" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@@ -36,6 +36,7 @@ chrono.workspace = true
kameo.workspace = true kameo.workspace = true
chacha20poly1305 = { version = "0.10.1", features = ["std"] } chacha20poly1305 = { version = "0.10.1", features = ["std"] }
argon2 = { version = "0.5.3", features = ["zeroize"] } argon2 = { version = "0.5.3", features = ["zeroize"] }
zeroize = "1.9"
restructed = "0.2.2" restructed = "0.2.2"
strum = { version = "0.28.0", features = ["derive"] } strum = { version = "0.28.0", features = ["derive"] }
pem = "3.0.6" pem = "3.0.6"

View File

@@ -7,6 +7,7 @@ use kameo::{Actor, messages};
use rand::{RngExt, distr::Alphanumeric, make_rng, rngs::StdRng}; use rand::{RngExt, distr::Alphanumeric, make_rng, rngs::StdRng};
use subtle::ConstantTimeEq as _; use subtle::ConstantTimeEq as _;
use thiserror::Error; use thiserror::Error;
use zeroize::Zeroizing;
const TOKEN_LENGTH: usize = 64; const TOKEN_LENGTH: usize = 64;
@@ -40,7 +41,7 @@ pub enum Error {
#[derive(Actor)] #[derive(Actor)]
pub struct Bootstrapper { pub struct Bootstrapper {
token: Option<String>, token: Option<Zeroizing<String>>,
} }
impl Bootstrapper { impl Bootstrapper {
@@ -56,7 +57,7 @@ impl Bootstrapper {
let token = if row_count == 0 { let token = if row_count == 0 {
let token = generate_token().await?; let token = generate_token().await?;
Some(token) Some(Zeroizing::new(token))
} else { } else {
None None
}; };
@@ -65,10 +66,8 @@ impl Bootstrapper {
} }
} }
#[messages]
impl Bootstrapper { impl Bootstrapper {
#[message] fn is_correct_token(&self, token: &str) -> bool {
pub fn is_correct_token(&self, token: String) -> bool {
self.token.as_ref().is_some_and(|expected| { self.token.as_ref().is_some_and(|expected| {
let expected_bytes = expected.as_bytes(); let expected_bytes = expected.as_bytes();
let token_bytes = token.as_bytes(); let token_bytes = token.as_bytes();
@@ -77,10 +76,13 @@ impl Bootstrapper {
bool::from(choice) bool::from(choice)
}) })
} }
}
#[messages]
impl Bootstrapper {
#[message] #[message]
pub fn consume_token(&mut self, token: String) -> bool { pub fn consume_token(&mut self, token: Zeroizing<String>) -> bool {
if self.is_correct_token(token) { if self.is_correct_token(&token) {
self.token = None; self.token = None;
true true
} else { } else {
@@ -93,6 +95,6 @@ impl Bootstrapper {
impl Bootstrapper { impl Bootstrapper {
#[message] #[message]
pub fn get_token(&self) -> Option<String> { pub fn get_token(&self) -> Option<String> {
self.token.clone() self.token.as_ref().map(|token| token.to_string())
} }
} }

View File

@@ -18,6 +18,7 @@ use arbiter_proto::{
use async_trait::async_trait; use async_trait::async_trait;
use tonic::Status; use tonic::Status;
use tracing::warn; use tracing::warn;
use zeroize::Zeroizing;
pub(super) struct AuthTransportAdapter<'a> { pub(super) struct AuthTransportAdapter<'a> {
pub(super) bi: &'a mut GrpcBi<OperatorRequest, OperatorResponse>, pub(super) bi: &'a mut GrpcBi<OperatorRequest, OperatorResponse>,
@@ -171,7 +172,7 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
Some(auth::Inbound::AuthChallengeRequest { Some(auth::Inbound::AuthChallengeRequest {
pubkey, pubkey,
bootstrap_token, bootstrap_token: bootstrap_token.map(Zeroizing::new),
}) })
} }
AuthRequestPayload::ChallengeSolution(ProtoAuthChallengeSolution { signature }) => { AuthRequestPayload::ChallengeSolution(ProtoAuthChallengeSolution { signature }) => {

View File

@@ -1,6 +1,7 @@
use super::{Credentials, OperatorConnection}; use super::{Credentials, OperatorConnection};
use arbiter_crypto::authn::{self, AuthChallenge}; use arbiter_crypto::authn::{self, AuthChallenge};
use arbiter_proto::transport::Bi; use arbiter_proto::transport::Bi;
use zeroize::Zeroizing;
use state::{ use state::{
AuthContext, AuthError, AuthEvents, AuthStateMachine, AuthStates, ChallengeRequest, AuthContext, AuthError, AuthEvents, AuthStateMachine, AuthStates, ChallengeRequest,
@@ -14,7 +15,7 @@ mod state;
pub enum Inbound { pub enum Inbound {
AuthChallengeRequest { AuthChallengeRequest {
pubkey: authn::PublicKey, pubkey: authn::PublicKey,
bootstrap_token: Option<String>, bootstrap_token: Option<Zeroizing<String>>,
}, },
AuthChallengeSolution { AuthChallengeSolution {
signature: Vec<u8>, signature: Vec<u8>,

View File

@@ -9,6 +9,7 @@ use crate::{
}; };
use arbiter_crypto::authn::{self, AuthChallenge, OPERATOR_CONTEXT}; use arbiter_crypto::authn::{self, AuthChallenge, OPERATOR_CONTEXT};
use arbiter_proto::transport::Bi; use arbiter_proto::transport::Bi;
use zeroize::Zeroizing;
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl}; use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
@@ -16,13 +17,13 @@ use tracing::error;
pub(super) struct ChallengeRequest { pub(super) struct ChallengeRequest {
pub(super) pubkey: authn::PublicKey, pub(super) pubkey: authn::PublicKey,
pub(super) bootstrap_token: Option<String>, pub(super) bootstrap_token: Option<Zeroizing<String>>,
} }
pub struct ChallengeContext { pub struct ChallengeContext {
pub(super) challenge: AuthChallenge, pub(super) challenge: AuthChallenge,
pub(super) pubkey: authn::PublicKey, pub(super) pubkey: authn::PublicKey,
pub(super) bootstrap_token: Option<String>, pub(super) bootstrap_token: Option<Zeroizing<String>>,
} }
pub(super) struct ChallengeSolution { pub(super) struct ChallengeSolution {
@@ -152,15 +153,13 @@ where
} }
// Resolve client id: bootstrap (consume token + register) or lookup // Resolve client id: bootstrap (consume token + register) or lookup
let id = match bootstrap_token { let id = match bootstrap_token.clone() {
Some(token) => { Some(token) => {
let token_ok: bool = self let token_ok: bool = self
.conn .conn
.actors .actors
.bootstrapper .bootstrapper
.ask(ConsumeToken { .ask(ConsumeToken { token })
token: token.clone(),
})
.await .await
.map_err(|e| { .map_err(|e| {
error!(?e, "Failed to consume bootstrap token"); error!(?e, "Failed to consume bootstrap token");