Add Rust extension module (myfsio_core) for SigV4, hashing, and validation hot paths
This commit is contained in:
@@ -17,6 +17,13 @@ from urllib.parse import quote, urlencode, urlparse, unquote
|
||||
from xml.etree.ElementTree import Element, SubElement, tostring, ParseError
|
||||
from defusedxml.ElementTree import fromstring
|
||||
|
||||
try:
|
||||
import myfsio_core as _rc
|
||||
_HAS_RUST = True
|
||||
except ImportError:
|
||||
_rc = None
|
||||
_HAS_RUST = False
|
||||
|
||||
from flask import Blueprint, Response, current_app, jsonify, request, g
|
||||
from werkzeug.http import http_date
|
||||
|
||||
@@ -192,11 +199,16 @@ _SIGNING_KEY_CACHE_MAX_SIZE = 256
|
||||
|
||||
|
||||
def clear_signing_key_cache() -> None:
|
||||
if _HAS_RUST:
|
||||
_rc.clear_signing_key_cache()
|
||||
with _SIGNING_KEY_CACHE_LOCK:
|
||||
_SIGNING_KEY_CACHE.clear()
|
||||
|
||||
|
||||
def _get_signature_key(key: str, date_stamp: str, region_name: str, service_name: str) -> bytes:
|
||||
if _HAS_RUST:
|
||||
return bytes(_rc.derive_signing_key(key, date_stamp, region_name, service_name))
|
||||
|
||||
cache_key = (key, date_stamp, region_name, service_name)
|
||||
now = time.time()
|
||||
|
||||
@@ -314,9 +326,13 @@ def _verify_sigv4_header(req: Any, auth_header: str) -> Principal | None:
|
||||
raise IamError("Required headers not signed")
|
||||
|
||||
credential_scope = f"{date_stamp}/{region}/{service}/aws4_request"
|
||||
string_to_sign = f"AWS4-HMAC-SHA256\n{amz_date}\n{credential_scope}\n{hashlib.sha256(canonical_request.encode('utf-8')).hexdigest()}"
|
||||
signing_key = _get_signature_key(secret_key, date_stamp, region, service)
|
||||
calculated_signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||
if _HAS_RUST:
|
||||
string_to_sign = _rc.build_string_to_sign(amz_date, credential_scope, canonical_request)
|
||||
calculated_signature = _rc.compute_signature(signing_key, string_to_sign)
|
||||
else:
|
||||
string_to_sign = f"AWS4-HMAC-SHA256\n{amz_date}\n{credential_scope}\n{hashlib.sha256(canonical_request.encode('utf-8')).hexdigest()}"
|
||||
calculated_signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||
|
||||
if not hmac.compare_digest(calculated_signature, signature):
|
||||
if current_app.config.get("DEBUG_SIGV4"):
|
||||
@@ -400,18 +416,15 @@ def _verify_sigv4_query(req: Any) -> Principal | None:
|
||||
payload_hash
|
||||
])
|
||||
|
||||
algorithm = "AWS4-HMAC-SHA256"
|
||||
credential_scope = f"{date_stamp}/{region}/{service}/aws4_request"
|
||||
hashed_request = hashlib.sha256(canonical_request.encode('utf-8')).hexdigest()
|
||||
string_to_sign = "\n".join([
|
||||
algorithm,
|
||||
amz_date,
|
||||
credential_scope,
|
||||
hashed_request
|
||||
])
|
||||
|
||||
signing_key = _get_signature_key(secret_key, date_stamp, region, service)
|
||||
calculated_signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||
if _HAS_RUST:
|
||||
string_to_sign = _rc.build_string_to_sign(amz_date, credential_scope, canonical_request)
|
||||
calculated_signature = _rc.compute_signature(signing_key, string_to_sign)
|
||||
else:
|
||||
hashed_request = hashlib.sha256(canonical_request.encode('utf-8')).hexdigest()
|
||||
string_to_sign = f"AWS4-HMAC-SHA256\n{amz_date}\n{credential_scope}\n{hashed_request}"
|
||||
calculated_signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||
|
||||
if not hmac.compare_digest(calculated_signature, signature):
|
||||
raise IamError("SignatureDoesNotMatch")
|
||||
|
||||
@@ -18,6 +18,13 @@ from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, BinaryIO, Dict, Generator, List, Optional
|
||||
|
||||
try:
|
||||
import myfsio_core as _rc
|
||||
_HAS_RUST = True
|
||||
except ImportError:
|
||||
_rc = None
|
||||
_HAS_RUST = False
|
||||
|
||||
# Platform-specific file locking
|
||||
if os.name == "nt":
|
||||
import msvcrt
|
||||
@@ -220,6 +227,11 @@ class ObjectStorage:
|
||||
raise BucketNotFoundError("Bucket does not exist")
|
||||
|
||||
def _validate_bucket_name(self, bucket_name: str) -> None:
|
||||
if _HAS_RUST:
|
||||
error = _rc.validate_bucket_name(bucket_name)
|
||||
if error:
|
||||
raise StorageError(error)
|
||||
return
|
||||
if len(bucket_name) < 3 or len(bucket_name) > 63:
|
||||
raise StorageError("Bucket name must be between 3 and 63 characters")
|
||||
if not re.match(r"^[a-z0-9][a-z0-9.-]*[a-z0-9]$", bucket_name):
|
||||
@@ -2133,6 +2145,18 @@ class ObjectStorage:
|
||||
|
||||
@staticmethod
|
||||
def _sanitize_object_key(object_key: str, max_length_bytes: int = 1024) -> Path:
|
||||
if _HAS_RUST:
|
||||
error = _rc.validate_object_key(object_key, max_length_bytes, os.name == "nt")
|
||||
if error:
|
||||
raise StorageError(error)
|
||||
normalized = unicodedata.normalize("NFC", object_key)
|
||||
candidate = Path(normalized)
|
||||
if candidate.is_absolute():
|
||||
raise StorageError("Absolute object keys are not allowed")
|
||||
if getattr(candidate, "drive", ""):
|
||||
raise StorageError("Object key cannot include a drive letter")
|
||||
return Path(*candidate.parts) if candidate.parts else candidate
|
||||
|
||||
if not object_key:
|
||||
raise StorageError("Object key required")
|
||||
if "\x00" in object_key:
|
||||
@@ -2146,7 +2170,7 @@ class ObjectStorage:
|
||||
candidate = Path(object_key)
|
||||
if ".." in candidate.parts:
|
||||
raise StorageError("Object key contains parent directory references")
|
||||
|
||||
|
||||
if candidate.is_absolute():
|
||||
raise StorageError("Absolute object keys are not allowed")
|
||||
if getattr(candidate, "drive", ""):
|
||||
@@ -2174,6 +2198,8 @@ class ObjectStorage:
|
||||
|
||||
@staticmethod
|
||||
def _compute_etag(path: Path) -> str:
|
||||
if _HAS_RUST:
|
||||
return _rc.md5_file(str(path))
|
||||
checksum = hashlib.md5()
|
||||
with path.open("rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(8192), b""):
|
||||
|
||||
Reference in New Issue
Block a user