Support RSA keys and configure external ChuSQL submodules
This commit is contained in:
1 parent
a631d75cee
commit
d4caf05370
18 files changed
+648
-41
No files matched your search
+2
-1
@@ -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
@@ -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)]
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in new issue
Block a user