security(bootstrap): use SafeCell for token storage instead of zeroize
This commit is contained in:
1
server/Cargo.lock
generated
1
server/Cargo.lock
generated
@@ -788,7 +788,6 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"x25519-dalek 2.0.1",
|
"x25519-dalek 2.0.1",
|
||||||
"zeroize",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ 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"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::db::{self, DatabasePool, schema};
|
use crate::db::{self, DatabasePool, schema};
|
||||||
|
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
|
||||||
use arbiter_proto::{BOOTSTRAP_PATH, home_path};
|
use arbiter_proto::{BOOTSTRAP_PATH, home_path};
|
||||||
|
|
||||||
use diesel::QueryDsl;
|
use diesel::QueryDsl;
|
||||||
@@ -10,7 +11,6 @@ use std::path::{Path, PathBuf};
|
|||||||
use subtle::ConstantTimeEq as _;
|
use subtle::ConstantTimeEq as _;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
use zeroize::Zeroizing;
|
|
||||||
|
|
||||||
const TOKEN_LENGTH: usize = 64;
|
const TOKEN_LENGTH: usize = 64;
|
||||||
|
|
||||||
@@ -26,18 +26,23 @@ async fn write_token_file(path: &Path, content: &str) -> Result<(), std::io::Err
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn generate_token(path: &Path) -> Result<String, std::io::Error> {
|
async fn generate_token(path: &Path) -> Result<SafeCell<[u8; TOKEN_LENGTH]>, std::io::Error> {
|
||||||
let token = UnwrapErr(SysRng)
|
let mut cell = SafeCell::new([0u8; TOKEN_LENGTH]);
|
||||||
.sample_iter(Alphanumeric)
|
{
|
||||||
.take(TOKEN_LENGTH)
|
let mut buf = cell.write();
|
||||||
.fold(String::default(), |mut accum, char| {
|
for (slot, b) in buf
|
||||||
accum += char.to_string().as_str();
|
.iter_mut()
|
||||||
accum
|
.zip(UnwrapErr(SysRng).sample_iter(Alphanumeric))
|
||||||
});
|
{
|
||||||
|
*slot = b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
write_token_file(path, &token).await?;
|
let token_str = cell.read_inline(|buf| String::from_utf8_lossy(buf.as_ref()).into_owned());
|
||||||
|
|
||||||
Ok(token)
|
write_token_file(path, &token_str).await?;
|
||||||
|
|
||||||
|
Ok(cell)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
@@ -54,7 +59,7 @@ pub enum Error {
|
|||||||
|
|
||||||
#[derive(Actor)]
|
#[derive(Actor)]
|
||||||
pub struct Bootstrapper {
|
pub struct Bootstrapper {
|
||||||
token: Option<Zeroizing<String>>,
|
token: Option<SafeCell<[u8; TOKEN_LENGTH]>>,
|
||||||
token_path: Option<PathBuf>,
|
token_path: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +77,7 @@ impl Bootstrapper {
|
|||||||
let (token, token_path) = if row_count == 0 {
|
let (token, token_path) = if row_count == 0 {
|
||||||
let path = home_path()?.join(BOOTSTRAP_PATH);
|
let path = home_path()?.join(BOOTSTRAP_PATH);
|
||||||
let token = generate_token(&path).await?;
|
let token = generate_token(&path).await?;
|
||||||
(Some(Zeroizing::new(token)), Some(path))
|
(Some(token), Some(path))
|
||||||
} else {
|
} else {
|
||||||
(None, None)
|
(None, None)
|
||||||
};
|
};
|
||||||
@@ -82,13 +87,9 @@ impl Bootstrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Bootstrapper {
|
impl Bootstrapper {
|
||||||
fn is_correct_token(&self, token: &str) -> bool {
|
fn is_correct_token(&mut self, token: &[u8]) -> bool {
|
||||||
self.token.as_ref().is_some_and(|expected| {
|
self.token.as_mut().is_some_and(|expected| {
|
||||||
let expected_bytes = expected.as_bytes();
|
expected.read_inline(|exp| bool::from(exp.as_ref().ct_eq(token)))
|
||||||
let token_bytes = token.as_bytes();
|
|
||||||
|
|
||||||
let choice = expected_bytes.ct_eq(token_bytes);
|
|
||||||
bool::from(choice)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,13 +97,13 @@ impl Bootstrapper {
|
|||||||
#[messages]
|
#[messages]
|
||||||
impl Bootstrapper {
|
impl Bootstrapper {
|
||||||
#[message]
|
#[message]
|
||||||
pub async fn consume_token(&mut self, token: Zeroizing<String>) -> bool {
|
pub async fn consume_token(&mut self, token: Vec<u8>) -> bool {
|
||||||
if self.is_correct_token(&token) {
|
if self.is_correct_token(&token) {
|
||||||
self.token = None;
|
self.token = None;
|
||||||
if let Some(path) = self.token_path.take() {
|
if let Some(path) = self.token_path.take()
|
||||||
if let Err(e) = tokio::fs::remove_file(&path).await {
|
&& let Err(e) = tokio::fs::remove_file(&path).await
|
||||||
warn!(error = ?e, path = ?path, "Failed to delete bootstrap token file after consumption");
|
{
|
||||||
}
|
warn!(error = ?e, path = ?path, "Failed to delete bootstrap token file after consumption");
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
@@ -114,7 +115,9 @@ impl Bootstrapper {
|
|||||||
#[messages]
|
#[messages]
|
||||||
impl Bootstrapper {
|
impl Bootstrapper {
|
||||||
#[message]
|
#[message]
|
||||||
pub fn get_token(&self) -> Option<String> {
|
pub fn get_token(&mut self) -> Option<String> {
|
||||||
self.token.as_ref().map(|token| token.to_string())
|
self.token
|
||||||
|
.as_mut()
|
||||||
|
.map(|cell| cell.read_inline(|buf| String::from_utf8_lossy(buf.as_ref()).into_owned()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ 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>,
|
||||||
@@ -172,7 +171,7 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
|
|||||||
|
|
||||||
Some(auth::Inbound::AuthChallengeRequest {
|
Some(auth::Inbound::AuthChallengeRequest {
|
||||||
pubkey,
|
pubkey,
|
||||||
bootstrap_token: bootstrap_token.map(Zeroizing::new),
|
bootstrap_token: bootstrap_token.map(String::into_bytes),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
AuthRequestPayload::ChallengeSolution(ProtoAuthChallengeSolution { signature }) => {
|
AuthRequestPayload::ChallengeSolution(ProtoAuthChallengeSolution { signature }) => {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
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,
|
||||||
@@ -15,7 +14,7 @@ mod state;
|
|||||||
pub enum Inbound {
|
pub enum Inbound {
|
||||||
AuthChallengeRequest {
|
AuthChallengeRequest {
|
||||||
pubkey: authn::PublicKey,
|
pubkey: authn::PublicKey,
|
||||||
bootstrap_token: Option<Zeroizing<String>>,
|
bootstrap_token: Option<Vec<u8>>,
|
||||||
},
|
},
|
||||||
AuthChallengeSolution {
|
AuthChallengeSolution {
|
||||||
signature: Vec<u8>,
|
signature: Vec<u8>,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ 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;
|
||||||
@@ -17,13 +16,12 @@ 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<Zeroizing<String>>,
|
pub(super) bootstrap_token: Option<Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
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<Zeroizing<String>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) struct ChallengeSolution {
|
pub(super) struct ChallengeSolution {
|
||||||
@@ -80,11 +78,16 @@ async fn register_key(db: &DatabasePool, pubkey: &authn::PublicKey) -> Result<i3
|
|||||||
pub(super) struct AuthContext<'a, T: ?Sized> {
|
pub(super) struct AuthContext<'a, T: ?Sized> {
|
||||||
pub(super) conn: &'a mut OperatorConnection,
|
pub(super) conn: &'a mut OperatorConnection,
|
||||||
pub(super) transport: &'a mut T,
|
pub(super) transport: &'a mut T,
|
||||||
|
bootstrap_token: Option<Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, T: ?Sized> AuthContext<'a, T> {
|
impl<'a, T: ?Sized> AuthContext<'a, T> {
|
||||||
pub(super) const fn new(conn: &'a mut OperatorConnection, transport: &'a mut T) -> Self {
|
pub(super) const fn new(conn: &'a mut OperatorConnection, transport: &'a mut T) -> Self {
|
||||||
Self { conn, transport }
|
Self {
|
||||||
|
conn,
|
||||||
|
transport,
|
||||||
|
bootstrap_token: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +112,8 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.bootstrap_token = bootstrap_token;
|
||||||
|
|
||||||
let challenge = AuthChallenge::generate(&mut rand::rng());
|
let challenge = AuthChallenge::generate(&mut rand::rng());
|
||||||
|
|
||||||
self.transport
|
self.transport
|
||||||
@@ -121,20 +126,12 @@ where
|
|||||||
Error::Transport
|
Error::Transport
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(ChallengeContext {
|
Ok(ChallengeContext { challenge, pubkey })
|
||||||
challenge,
|
|
||||||
pubkey,
|
|
||||||
bootstrap_token,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn verify_solution(
|
async fn verify_solution(
|
||||||
&mut self,
|
&mut self,
|
||||||
ChallengeContext {
|
ChallengeContext { challenge, pubkey }: &ChallengeContext,
|
||||||
challenge,
|
|
||||||
pubkey,
|
|
||||||
bootstrap_token,
|
|
||||||
}: &ChallengeContext,
|
|
||||||
ChallengeSolution { solution }: ChallengeSolution,
|
ChallengeSolution { solution }: ChallengeSolution,
|
||||||
) -> Result<Credentials, Self::Error> {
|
) -> Result<Credentials, Self::Error> {
|
||||||
let signature = authn::Signature::try_from(solution.as_slice()).map_err(|()| {
|
let signature = authn::Signature::try_from(solution.as_slice()).map_err(|()| {
|
||||||
@@ -153,7 +150,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Resolve client id: bootstrap (consume token + register) or lookup
|
// Resolve client id: bootstrap (consume token + register) or lookup
|
||||||
let id = match bootstrap_token.clone() {
|
let id = match self.bootstrap_token.take() {
|
||||||
Some(token) => {
|
Some(token) => {
|
||||||
let token_ok: bool = self
|
let token_ok: bool = self
|
||||||
.conn
|
.conn
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ pub async fn bootstrap_token_auth() {
|
|||||||
test_transport
|
test_transport
|
||||||
.send(auth::Inbound::AuthChallengeRequest {
|
.send(auth::Inbound::AuthChallengeRequest {
|
||||||
pubkey: verifying_key(&new_key).into(),
|
pubkey: verifying_key(&new_key).into(),
|
||||||
bootstrap_token: Some(token),
|
bootstrap_token: Some(token.into_bytes()),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -231,7 +231,7 @@ pub async fn bootstrap_invalid_token_auth() {
|
|||||||
test_transport
|
test_transport
|
||||||
.send(auth::Inbound::AuthChallengeRequest {
|
.send(auth::Inbound::AuthChallengeRequest {
|
||||||
pubkey: verifying_key(&new_key).into(),
|
pubkey: verifying_key(&new_key).into(),
|
||||||
bootstrap_token: Some("invalid_token".to_owned()),
|
bootstrap_token: Some(b"invalid_token".to_vec()),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user