Support RSA keys and configure external ChuSQL submodules

This commit is contained in:
TheBadRoger committed 2026-10-15 20:41:23 +08:00
1 parent a631d75cee
commit d4caf05370
18 files changed
+648 -41

No files matched your search

+2 -1
View File
@@ -9,13 +9,14 @@ argon2 = "0.5"
axum = "0.8"
base64 = "0.22"
chrono = { version = "0.4", features = ["serde"] }
chusql-driver = { path = "../../crates/chusql-driver" }
chusql-driver = { path = "../../external/ChuSQL-RustDriver" }
fs2 = "0.4"
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp"] }
rand = "0.8"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_yaml = "0.9"
ssh-key = { version = "0.6.7", default-features = false, features = ["std"] }
tokio = { version = "1", features = ["full"] }
pulldown-cmark = "0.13"
tracing = "0.1"
+31 -19
View File
@@ -10,10 +10,7 @@ use axum::{
http::{HeaderMap, HeaderValue, StatusCode, header},
response::{IntoResponse, Response},
};
use base64::{
Engine,
engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD},
};
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
use rand::RngCore;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
@@ -428,23 +425,38 @@ pub async fn logout(State(app): State<Arc<App>>, headers: HeaderMap) -> ApiResul
}
pub fn public_key(value: &str) -> Result<String> {
ensure!(
value.len() < 512 && !value.contains(['\n', '\r']),
value.len() <= 4096 && !value.contains(['\n', '\r']),
"公钥格式无效"
);
let parts: Vec<_> = value.split_whitespace().collect();
ensure!(
parts.len() >= 2 && parts[0] == "ssh-ed25519",
"仅支持 Ed25519 公钥,禁止 authorized_keys 选项"
);
let bytes = STANDARD.decode(parts[1])?;
ensure!(
bytes.len() == 51
&& bytes[..4] == [0, 0, 0, 11]
&& &bytes[4..15] == b"ssh-ed25519"
&& bytes[15..19] == [0, 0, 0, 32],
"Ed25519 公钥内容无效"
);
Ok(format!("ssh-ed25519 {}", STANDARD.encode(bytes)))
let key = ssh_key::PublicKey::from_openssh(value)
.context("SSH 公钥格式无效,禁止 authorized_keys 选项")?;
match key.key_data() {
ssh_key::public::KeyData::Ed25519(_) => {}
ssh_key::public::KeyData::Rsa(rsa) => {
let modulus = rsa.n.as_positive_bytes().context("RSA 模数必须为正数")?;
let first = modulus.first().context("RSA 模数为空")?;
let bits = modulus.len() * 8 - first.leading_zeros() as usize;
ensure!(
(2048..=8192).contains(&bits) && modulus.last().is_some_and(|b| b & 1 == 1),
"RSA 模数必须为 2048–8192 位正奇数"
);
let exponent = rsa.e.as_positive_bytes().context("RSA 指数必须为正数")?;
ensure!(exponent.len() <= 4, "RSA 指数过大");
let exponent = exponent.iter().fold(0_u32, |n, b| (n << 8) | u32::from(*b));
ensure!(
exponent >= 3 && exponent % 2 == 1,
"RSA 指数必须为至少 3 的奇数"
);
}
_ => anyhow::bail!("仅支持 Ed25519 或 RSA 公钥"),
}
// Drop comments and options: the stored value is exactly the parsed public key.
Ok(format!(
"{} {}",
key.algorithm().as_str(),
key.to_bytes()
.map(|bytes| base64::engine::general_purpose::STANDARD.encode(bytes))?
))
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
+52
View File
@@ -253,6 +253,58 @@ fn ssh_keys_cannot_inject_options_or_commands() {
assert!(accounts::public_key(&format!("{key}\nssh-rsa AAA")).is_err());
assert!(accounts::public_key("ssh-ed25519 AAAA").is_err());
}
fn rsa_key(bits: usize, exponent: &[u8]) -> String {
let mut modulus = vec![0xff; bits.div_ceil(8)];
modulus[0] >>= (8 - bits % 8) % 8;
if modulus[0] & 0x80 != 0 {
modulus.insert(0, 0);
}
let mut bytes = Vec::new();
for field in [b"ssh-rsa".as_slice(), exponent, modulus.as_slice()] {
bytes.extend((field.len() as u32).to_be_bytes());
bytes.extend(field);
}
format!(
"ssh-rsa {}",
base64::engine::general_purpose::STANDARD.encode(bytes)
)
}
#[test]
fn rsa_keys_enforce_strength_encoding_and_safe_canonical_form() {
for bits in [2048, 3072, 4096, 8192] {
let key = rsa_key(bits, &[1, 0, 1]);
assert_eq!(
accounts::public_key(&format!("{key} user comment")).unwrap(),
key
);
}
for key in [
rsa_key(1024, &[1, 0, 1]),
rsa_key(2047, &[1, 0, 1]),
rsa_key(8193, &[1, 0, 1]),
rsa_key(2048, &[2]),
rsa_key(2048, &[1]),
rsa_key(2048, &[0x80]),
rsa_key(2048, &[1, 0, 0, 0, 1]),
] {
assert!(accounts::public_key(&key).is_err());
}
let key = rsa_key(2048, &[1, 0, 1]);
assert!(accounts::public_key(&key.replace("ssh-rsa", "ssh-ed25519")).is_err());
assert!(accounts::public_key(&format!("command=\"sh\" {key}")).is_err());
let encoded = key.split_whitespace().nth(1).unwrap();
let mut bytes = base64::engine::general_purpose::STANDARD
.decode(encoded)
.unwrap();
bytes.push(0);
assert!(
accounts::public_key(&format!(
"ssh-rsa {}",
base64::engine::general_purpose::STANDARD.encode(bytes)
))
.is_err()
);
}
async fn app(dir: &std::path::Path) -> Arc<App> {
let config = dir.join("publishers.json");
let hash = accounts::hash_password("Administrator-test-123".into())