Consolidate workspace deps and migrate sha1 to sha2
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-05-07 21:11:14 +08:00
parent 9146e5b8ec
commit df80876f60
41 changed files with 949 additions and 342 deletions

View File

@@ -473,26 +473,23 @@ mod tests {
error::Error,
fs,
path::{Path, PathBuf},
time::SystemTime,
};
use axum::{
body::Body,
http::{Request, StatusCode},
};
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
use hmac::{Hmac, Mac};
use http_body_util::BodyExt;
use httpdate::fmt_http_date;
use reqwest::{Method, multipart};
use serde_json::{Value, json};
use sha1::Sha1;
use sha2::{Digest, Sha256};
use shared_kernel::new_uuid_simple_string;
use tower::ServiceExt;
use crate::{app::build_router, config::AppConfig, state::AppState};
type HmacSha1 = Hmac<Sha1>;
type HmacSha256 = Hmac<Sha256>;
#[test]
fn asset_history_kind_support_includes_puzzle_cover_image() {
@@ -653,8 +650,13 @@ mod tests {
Value::String("private".to_string())
);
assert_eq!(
payload["data"]["upload"]["formFields"]["OSSAccessKeyId"],
Value::String("test-access-key-id".to_string())
payload["data"]["upload"]["formFields"]["x-oss-signature-version"],
Value::String("OSS4-HMAC-SHA256".to_string())
);
assert!(
payload["data"]["upload"]["formFields"]["x-oss-credential"]
.as_str()
.is_some_and(|value| value.starts_with("test-access-key-id/"))
);
assert!(payload["data"]["upload"].get("publicUrl").is_none());
}
@@ -702,7 +704,7 @@ mod tests {
assert!(
payload["data"]["read"]["signedUrl"]
.as_str()
.is_some_and(|value| value.contains("OSSAccessKeyId=test-access-key-id"))
.is_some_and(|value| value.contains("x-oss-signature-version=OSS4-HMAC-SHA256"))
);
}
@@ -1410,13 +1412,26 @@ mod tests {
.oss_access_key_secret
.as_deref()
.ok_or_else(|| std::io::Error::other("缺少 oss access key secret"))?;
let date = fmt_http_date(SystemTime::now());
let canonical_resource = match object_key.map(str::trim).filter(|value| !value.is_empty()) {
Some(object_key) => format!("/{bucket}/{}", object_key.trim_start_matches('/')),
None => format!("/{bucket}/"),
};
let string_to_sign = format!("{}\n\n\n{}\n{}", method.as_str(), date, canonical_resource);
let signature = sign_oss_string(access_key_secret, &string_to_sign)?;
let signed_at = time::OffsetDateTime::now_utc();
let signed_at_text = build_oss_v4_signature_date(signed_at);
let signature_scope = build_oss_v4_signature_scope(endpoint, signed_at)?;
let object_path = object_key.map(str::trim).filter(|value| !value.is_empty());
let canonical_uri = build_oss_v4_canonical_uri(bucket, object_path);
let payload_hash = "UNSIGNED-PAYLOAD";
let canonical_headers =
format!("host:{bucket}.{endpoint}\nx-oss-content-sha256:{payload_hash}\nx-oss-date:{signed_at_text}\n");
let additional_headers = "host";
let canonical_request = format!(
"{}\n{}\n\n{}\n{}\n{}",
method.as_str(),
canonical_uri,
canonical_headers,
additional_headers,
payload_hash
);
let string_to_sign =
build_oss_v4_string_to_sign(&signed_at_text, &signature_scope, &canonical_request);
let signature = sign_oss_v4_content(access_key_secret, &signature_scope, &string_to_sign)?;
let target_url = match object_key.map(str::trim).filter(|value| !value.is_empty()) {
Some(object_key) => build_object_url(config, object_key)?,
None => reqwest::Url::parse(&format!("https://{bucket}.{endpoint}/"))?,
@@ -1424,18 +1439,147 @@ mod tests {
let response = client
.request(method, target_url)
.header("Date", date)
.header("Authorization", format!("OSS {access_key_id}:{signature}"))
.header("x-oss-content-sha256", payload_hash)
.header("x-oss-date", signed_at_text)
.header(
"Authorization",
format!(
"OSS4-HMAC-SHA256 Credential={access_key_id}/{signature_scope},AdditionalHeaders={additional_headers},Signature={signature}"
),
)
.send()
.await?;
Ok(response)
}
fn sign_oss_string(secret: &str, content: &str) -> Result<String, Box<dyn Error>> {
let mut signer = HmacSha1::new_from_slice(secret.as_bytes())?;
fn build_oss_v4_signature_scope(
endpoint: &str,
signed_at: time::OffsetDateTime,
) -> Result<String, Box<dyn Error>> {
let date = signed_at.date().to_string().replace('-', "");
let region = endpoint
.trim()
.split('.')
.next()
.and_then(|segment| segment.strip_prefix("oss-"))
.ok_or_else(|| std::io::Error::other("OSS endpoint 无法解析 region"))?;
Ok(format!("{date}/{region}/oss/aliyun_v4_request"))
}
fn build_oss_v4_signature_date(signed_at: time::OffsetDateTime) -> String {
let date = signed_at.date().to_string().replace('-', "");
let time = signed_at
.time()
.to_string()
.split('.')
.next()
.unwrap_or("00:00:00")
.replace(':', "");
debug_assert_eq!(time.len(), 6);
format!("{date}T{time}Z")
}
fn build_oss_v4_canonical_uri(bucket: &str, object_key: Option<&str>) -> String {
match object_key.map(str::trim).filter(|value| !value.is_empty()) {
Some(object_key) => format!(
"/{}/{}",
encode_oss_url_query_value(bucket),
encode_oss_url_path(object_key.trim_start_matches('/'))
),
None => format!("/{}/", encode_oss_url_query_value(bucket)),
}
}
fn build_oss_v4_string_to_sign(
signature_date: &str,
signature_scope: &str,
canonical_request: &str,
) -> String {
format!(
"OSS4-HMAC-SHA256\n{signature_date}\n{signature_scope}\n{}",
sha256_hex(canonical_request.as_bytes())
)
}
fn sign_oss_v4_content(
secret: &str,
signature_scope: &str,
content: &str,
) -> Result<String, Box<dyn Error>> {
let signing_key = build_oss_v4_signing_key(secret, signature_scope)?;
let mut signer = HmacSha256::new_from_slice(&signing_key)?;
signer.update(content.as_bytes());
Ok(BASE64_STANDARD.encode(signer.finalize().into_bytes()))
Ok(hex_lower(&signer.finalize().into_bytes()))
}
fn build_oss_v4_signing_key(
secret: &str,
signature_scope: &str,
) -> Result<Vec<u8>, Box<dyn Error>> {
let mut parts = signature_scope.split('/');
let date = parts
.next()
.ok_or_else(|| std::io::Error::other("OSS V4 scope 缺少日期"))?;
let region = parts
.next()
.ok_or_else(|| std::io::Error::other("OSS V4 scope 缺少 region"))?;
let service = parts
.next()
.ok_or_else(|| std::io::Error::other("OSS V4 scope 缺少 service"))?;
let request = parts
.next()
.ok_or_else(|| std::io::Error::other("OSS V4 scope 缺少 request"))?;
let date_key = hmac_sha256_raw(format!("aliyun_v4{secret}").as_bytes(), date)?;
let region_key = hmac_sha256_raw(&date_key, region)?;
let service_key = hmac_sha256_raw(&region_key, service)?;
hmac_sha256_raw(&service_key, request)
}
fn hmac_sha256_raw(key: &[u8], content: &str) -> Result<Vec<u8>, Box<dyn Error>> {
let mut signer = HmacSha256::new_from_slice(key)?;
signer.update(content.as_bytes());
Ok(signer.finalize().into_bytes().to_vec())
}
fn sha256_hex(content: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(content);
hex_lower(&hasher.finalize())
}
fn hex_lower(bytes: &[u8]) -> String {
bytes
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>()
}
fn encode_oss_url_path(path: &str) -> String {
path.split('/')
.map(encode_oss_url_query_value)
.collect::<Vec<_>>()
.join("/")
}
fn encode_oss_url_query_value(value: &str) -> String {
let mut encoded = String::with_capacity(value.len());
for byte in value.bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
encoded.push(byte as char)
}
_ => {
use std::fmt::Write as _;
let _ = write!(&mut encoded, "%{byte:02X}");
}
}
}
encoded
}
fn ensure_success_status(status: u16, message: &str) -> Result<(), Box<dyn Error>> {