feat(auth): limited RSA support for signing
see server/clippy.toml
This commit is contained in:
@@ -5,6 +5,9 @@ edition = "2024"
|
||||
repository = "https://git.markettakers.org/MarketTakers/arbiter"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
diesel = { version = "2.3.6", features = ["chrono", "returning_clauses_for_sqlite_3_35", "serde_json", "time", "uuid"] }
|
||||
diesel-async = { version = "0.7.4", features = [
|
||||
@@ -43,6 +46,9 @@ restructed = "0.2.2"
|
||||
strum = { version = "0.27.2", features = ["derive"] }
|
||||
pem = "3.0.6"
|
||||
k256.workspace = true
|
||||
rsa.workspace = true
|
||||
sha2.workspace = true
|
||||
spki.workspace = true
|
||||
alloy.workspace = true
|
||||
arbiter-tokens-registry.path = "../arbiter-tokens-registry"
|
||||
|
||||
|
||||
@@ -52,6 +52,12 @@ fn parse_pubkey(key_type: ProtoKeyType, pubkey: Vec<u8>) -> Result<AuthPublicKey
|
||||
.map_err(|_| Error::InvalidAuthPubkeyEncoding)?;
|
||||
Ok(AuthPublicKey::EcdsaSecp256k1(key))
|
||||
}
|
||||
ProtoKeyType::Rsa => {
|
||||
use rsa::pkcs8::DecodePublicKey as _;
|
||||
let key = rsa::RsaPublicKey::from_public_key_der(&pubkey)
|
||||
.map_err(|_| Error::InvalidAuthPubkeyEncoding)?;
|
||||
Ok(AuthPublicKey::Rsa(key))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,22 +11,30 @@ use crate::{
|
||||
db::{models::KeyType, schema},
|
||||
};
|
||||
|
||||
/// Abstraction over Ed25519 / ECDSA-secp256k1 public keys used during the auth handshake.
|
||||
/// Abstraction over Ed25519 / ECDSA-secp256k1 / RSA public keys used during the auth handshake.
|
||||
#[derive(Clone)]
|
||||
pub enum AuthPublicKey {
|
||||
Ed25519(ed25519_dalek::VerifyingKey),
|
||||
/// Compressed SEC1 public key; signature bytes are raw 64-byte (r||s).
|
||||
EcdsaSecp256k1(k256::ecdsa::VerifyingKey),
|
||||
/// RSA-2048+ public key (Windows Hello / KeyCredentialManager); signature bytes are PSS+SHA-256.
|
||||
Rsa(rsa::RsaPublicKey),
|
||||
}
|
||||
|
||||
impl AuthPublicKey {
|
||||
/// Canonical bytes stored in DB and echoed back in the challenge.
|
||||
/// Ed25519: raw 32 bytes. ECDSA: SEC1 compressed 33 bytes.
|
||||
/// Ed25519: raw 32 bytes. ECDSA: SEC1 compressed 33 bytes. RSA: DER-encoded SPKI.
|
||||
pub fn to_stored_bytes(&self) -> Vec<u8> {
|
||||
match self {
|
||||
AuthPublicKey::Ed25519(k) => k.to_bytes().to_vec(),
|
||||
// SEC1 compressed (33 bytes) is the natural compact format for secp256k1
|
||||
AuthPublicKey::EcdsaSecp256k1(k) => k.to_encoded_point(true).as_bytes().to_vec(),
|
||||
AuthPublicKey::Rsa(k) => {
|
||||
use rsa::pkcs8::EncodePublicKey as _;
|
||||
k.to_public_key_der()
|
||||
.expect("rsa SPKI encoding is infallible")
|
||||
.to_vec()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +42,7 @@ impl AuthPublicKey {
|
||||
match self {
|
||||
AuthPublicKey::Ed25519(_) => KeyType::Ed25519,
|
||||
AuthPublicKey::EcdsaSecp256k1(_) => KeyType::EcdsaSecp256k1,
|
||||
AuthPublicKey::Rsa(_) => KeyType::Rsa,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,6 +170,15 @@ impl AuthStateMachineContext for AuthContext<'_> {
|
||||
})?;
|
||||
vk.verify(&formatted, &sig).is_ok()
|
||||
}
|
||||
AuthPublicKey::Rsa(pk) => {
|
||||
use rsa::signature::Verifier as _;
|
||||
let verifying_key = rsa::pss::VerifyingKey::<sha2::Sha256>::new(pk.clone());
|
||||
let sig = rsa::pss::Signature::try_from(solution.as_slice()).map_err(|_| {
|
||||
error!(?solution, "Invalid RSA signature bytes");
|
||||
Error::InvalidChallengeSolution
|
||||
})?;
|
||||
verifying_key.verify(&formatted, &sig).is_ok()
|
||||
}
|
||||
};
|
||||
|
||||
Ok(valid)
|
||||
@@ -266,6 +284,13 @@ impl AuthStateMachineContext for AuthContext<'_> {
|
||||
.expect("ecdsa key was already validated in parse_auth_event"),
|
||||
)
|
||||
}
|
||||
crate::db::models::KeyType::Rsa => {
|
||||
use rsa::pkcs8::DecodePublicKey as _;
|
||||
AuthPublicKey::Rsa(
|
||||
rsa::RsaPublicKey::from_public_key_der(&bytes)
|
||||
.expect("rsa key was already validated in parse_auth_event"),
|
||||
)
|
||||
}
|
||||
};
|
||||
Ok(rebuilt)
|
||||
}
|
||||
|
||||
@@ -36,9 +36,9 @@ pub mod types {
|
||||
SqliteTimestamp(dt)
|
||||
}
|
||||
}
|
||||
impl Into<chrono::DateTime<Utc>> for SqliteTimestamp {
|
||||
fn into(self) -> chrono::DateTime<Utc> {
|
||||
self.0
|
||||
impl From<SqliteTimestamp> for chrono::DateTime<Utc> {
|
||||
fn from(ts: SqliteTimestamp) -> Self {
|
||||
ts.0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,12 +75,13 @@ pub mod types {
|
||||
|
||||
/// Key algorithm stored in the `useragent_client.key_type` column.
|
||||
/// Values must stay stable — they are persisted in the database.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromSqlRow, AsExpression)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromSqlRow, AsExpression, strum::FromRepr)]
|
||||
#[diesel(sql_type = Integer)]
|
||||
#[repr(i32)]
|
||||
pub enum KeyType {
|
||||
Ed25519 = 1,
|
||||
EcdsaSecp256k1 = 2,
|
||||
Rsa = 3,
|
||||
}
|
||||
|
||||
impl ToSql<Integer, Sqlite> for KeyType {
|
||||
@@ -100,11 +101,9 @@ pub mod types {
|
||||
let Some(SqliteType::Long) = bytes.value_type() else {
|
||||
return Err("Expected Integer for KeyType".into());
|
||||
};
|
||||
match bytes.read_long() {
|
||||
1 => Ok(KeyType::Ed25519),
|
||||
2 => Ok(KeyType::EcdsaSecp256k1),
|
||||
other => Err(format!("Unknown KeyType discriminant: {other}").into()),
|
||||
}
|
||||
let discriminant = bytes.read_long();
|
||||
KeyType::from_repr(discriminant as i32)
|
||||
.ok_or_else(|| format!("Unknown KeyType discriminant: {discriminant}").into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ use tracing::info;
|
||||
|
||||
use crate::{
|
||||
actors::{
|
||||
client::{self, ClientError, ClientConnection as ClientConnectionProps, connect_client},
|
||||
user_agent::{self, UserAgentConnection, TransportResponseError, connect_user_agent},
|
||||
client::{self, ClientConnection as ClientConnectionProps, ClientError, connect_client},
|
||||
user_agent::{self, TransportResponseError, UserAgentConnection, connect_user_agent},
|
||||
},
|
||||
context::ServerContext,
|
||||
};
|
||||
@@ -89,7 +89,8 @@ fn client_auth_error_status(value: &client::auth::Error) -> Status {
|
||||
|
||||
fn user_agent_error_status(value: TransportResponseError) -> Status {
|
||||
match value {
|
||||
TransportResponseError::MissingRequestPayload | TransportResponseError::UnexpectedRequestPayload => {
|
||||
TransportResponseError::MissingRequestPayload
|
||||
| TransportResponseError::UnexpectedRequestPayload => {
|
||||
Status::invalid_argument("Expected message with payload")
|
||||
}
|
||||
TransportResponseError::InvalidStateForUnsealEncryptedKey => {
|
||||
@@ -99,7 +100,9 @@ fn user_agent_error_status(value: TransportResponseError) -> Status {
|
||||
Status::invalid_argument("client_pubkey must be 32 bytes")
|
||||
}
|
||||
TransportResponseError::StateTransitionFailed => Status::internal("State machine error"),
|
||||
TransportResponseError::KeyHolderActorUnreachable => Status::internal("Vault is not available"),
|
||||
TransportResponseError::KeyHolderActorUnreachable => {
|
||||
Status::internal("Vault is not available")
|
||||
}
|
||||
TransportResponseError::Auth(ref err) => auth_error_status(err),
|
||||
TransportResponseError::ConnectionRegistrationFailed => {
|
||||
Status::internal("Failed registering connection")
|
||||
|
||||
Reference in New Issue
Block a user