diff --git a/.cargo/config.toml b/.cargo/config.toml index d137ff07e..308315034 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -13,4 +13,4 @@ CFLAGS = "-D__ARM_ARCH=7" # Force ring over aws-lc for ARM builds [build] -rustflags = ["--cfg", "aws_lc_sys_use_ring"] \ No newline at end of file +rustflags = ["--cfg", "aws_lc_sys_use_ring"] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 69cfb6340..fbb4c0c79 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,15 +11,42 @@ env: RUST_BACKTRACE: 1 jobs: - # Rust 检查和测试 - rust: - name: Rust ${{ matrix.rust }} on ${{ matrix.os }} + # ============================================ + # 代码规范检查(快速反馈) + # ============================================ + lint: + name: Lint & Format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Build Environment + uses: ./.github/actions/setup-build-env + with: + rust-toolchain: stable + python-version: '3.12' + install-cargo-tools: 'false' + install-python-deps: 'true' + + - name: Rust formatting + run: cargo fmt --all -- --check + + - name: Rust clippy + run: cargo clippy --workspace --exclude pulsing-py --exclude pulsing-bench-py --all-targets -- -D warnings + + - name: Python lint (ruff) + run: ruff check python/ + + # ============================================ + # Rust 构建和测试(多平台) + # ============================================ + rust-test: + name: Rust Test (${{ matrix.os }}) runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest] - rust: [stable] steps: - uses: actions/checkout@v4 @@ -27,25 +54,21 @@ jobs: - name: Setup Build Environment uses: ./.github/actions/setup-build-env with: - rust-toolchain: ${{ matrix.rust }} + rust-toolchain: stable install-cargo-tools: 'false' - name: Cache Rust build uses: Swatinem/rust-cache@v2 - - name: Check formatting - run: cargo fmt --all -- --check - - - name: Clippy - run: cargo clippy --workspace --exclude pulsing-py --exclude pulsing-bench-py --all-targets -- -D warnings - - name: Build run: cargo build --workspace --exclude pulsing-py --exclude pulsing-bench-py --all-targets - name: Test run: cargo test --workspace --exclude pulsing-py --exclude pulsing-bench-py --all-targets - # Rust 测试 + 覆盖率收集 (仅 Linux) + # ============================================ + # Rust 覆盖率收集 + # ============================================ rust-coverage: name: Rust Coverage runs-on: ubuntu-latest @@ -61,7 +84,7 @@ jobs: - name: Cache Rust build uses: Swatinem/rust-cache@v2 - - name: Run Rust tests and collect coverage + - name: Run tests and collect coverage shell: bash run: | source <(cargo llvm-cov show-env --export-prefix) @@ -71,21 +94,58 @@ jobs: cargo nextest run --workspace --exclude pulsing-py --exclude pulsing-bench-py cargo llvm-cov --no-run --lcov --output-path coverage.lcov - - name: Upload Rust coverage report + - name: Upload coverage report uses: actions/upload-artifact@v4 with: name: rust-coverage path: coverage.lcov retention-days: 30 - # Python 检查和测试 - python: - name: Python ${{ matrix.python-version }} - runs-on: ubuntu-latest + # ============================================ + # 构建 macOS Wheel + # ============================================ + build-macos: + name: Build Wheel (macOS) + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Build Environment + uses: ./.github/actions/setup-build-env + with: + python-version: '3.10' + + - name: Cache Rust build + uses: Swatinem/rust-cache@v2 + + - name: Build wheel + uses: PyO3/maturin-action@v1 + with: + args: --release --out dist + sccache: 'true' + + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-macos + path: dist/*.whl + + # ============================================ + # 构建 Linux Wheel(x86-64 和 aarch64) + # ============================================ + build-linux: + name: Build Wheel (Linux ${{ matrix.arch }}) + runs-on: ${{ matrix.runner }} strategy: fail-fast: false matrix: - python-version: ["3.10", "3.11", "3.12"] + include: + - arch: x86-64 + runner: ubuntu-latest + target: x86_64-unknown-linux-gnu + - arch: aarch64 + runner: ubuntu-24.04-arm64 + target: aarch64-unknown-linux-gnu steps: - uses: actions/checkout@v4 @@ -93,136 +153,222 @@ jobs: - name: Setup Build Environment uses: ./.github/actions/setup-build-env with: - python-version: ${{ matrix.python-version }} - install-python-deps: 'true' + python-version: '3.10' - name: Cache Rust build uses: Swatinem/rust-cache@v2 - - name: Build Python package - run: maturin develop + - name: Build wheel + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: --release --out dist + sccache: 'true' + manylinux: '2_17' - - name: Lint with ruff - run: ruff check python/ + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-linux-${{ matrix.arch }} + path: dist/*.whl + + # ============================================ + # macOS Python 测试(多 Python 版本) + # ============================================ + test-macos: + name: Test macOS (Python ${{ matrix.python-version }}) + needs: build-macos + runs-on: macos-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Download wheel + uses: actions/download-artifact@v4 + with: + name: wheels-macos + path: dist/ + + - name: Install wheel and test dependencies + run: | + pip install dist/*.whl + pip install pytest pytest-asyncio pytest-cov - name: Test with pytest run: pytest tests/python -v - # Python 测试 + 覆盖率收集 + # ============================================ + # Ubuntu Python 测试(多版本 × 多架构 × 多 Python 版本) + # ============================================ + test-ubuntu: + name: Test Ubuntu ${{ matrix.ubuntu }} ${{ matrix.arch }} (Python ${{ matrix.python-version }}) + needs: build-linux + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + ubuntu: ["20.04", "22.04", "24.04"] + arch: [x86-64, aarch64] + python-version: ["3.10", "3.11", "3.12"] + include: + # x86-64 runners + - ubuntu: "20.04" + arch: x86-64 + runner: ubuntu-20.04 + - ubuntu: "22.04" + arch: x86-64 + runner: ubuntu-22.04 + - ubuntu: "24.04" + arch: x86-64 + runner: ubuntu-24.04 + # aarch64 runners (only 24.04 available) + - ubuntu: "24.04" + arch: aarch64 + runner: ubuntu-24.04-arm64 + exclude: + # Ubuntu 20.04/22.04 ARM64 runner 不可用 + - ubuntu: "20.04" + arch: aarch64 + - ubuntu: "22.04" + arch: aarch64 + # Ubuntu 20.04 不支持 Python 3.12 + - ubuntu: "20.04" + python-version: "3.12" + + steps: + - uses: actions/checkout@v4 + + - name: Download wheel + uses: actions/download-artifact@v4 + with: + name: wheels-linux-${{ matrix.arch }} + path: dist/ + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install wheel and test dependencies + run: | + pip install dist/*.whl + pip install pytest pytest-asyncio pytest-cov + + - name: Test with pytest + run: pytest tests/python -v + + # ============================================ + # Fedora Python 测试(x86-64) + # ============================================ + test-fedora: + name: Test Fedora x86-64 (Python ${{ matrix.python-version }}) + needs: build-linux + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + + - name: Download wheel + uses: actions/download-artifact@v4 + with: + name: wheels-linux-x86-64 + path: dist/ + + - name: Test on Fedora + run: | + docker run --rm -v ${{ github.workspace }}:/workspace -w /workspace \ + fedora:latest \ + bash -c " + dnf install -y python${{ matrix.python-version }} python${{ matrix.python-version }}-pip && \ + python${{ matrix.python-version }} -m pip install dist/*.whl && \ + python${{ matrix.python-version }} -m pip install pytest pytest-asyncio pytest-cov && \ + python${{ matrix.python-version }} -m pytest tests/python -v + " + + # ============================================ + # Python 覆盖率收集 + # ============================================ python-coverage: name: Python Coverage + needs: build-linux runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Setup Build Environment - uses: ./.github/actions/setup-build-env + - name: Setup Python + uses: actions/setup-python@v5 with: python-version: '3.12' - install-python-deps: 'true' - - - name: Cache Rust build - uses: Swatinem/rust-cache@v2 - - name: Build Python package - run: maturin develop + - name: Download wheel + uses: actions/download-artifact@v4 + with: + name: wheels-linux-x86-64 + path: dist/ - - name: Run Python tests and collect coverage + - name: Install wheel and test dependencies run: | - pytest tests/python --cov=python/pulsing --cov-report=xml:coverage.xml -v + pip install dist/*.whl + pip install pytest pytest-asyncio pytest-cov + + - name: Run tests and collect coverage + run: pytest tests/python --cov=pulsing --cov-report=xml:coverage.xml -v - - name: Upload Python coverage report + - name: Upload coverage report uses: actions/upload-artifact@v4 with: name: python-coverage path: coverage.xml retention-days: 30 + # ============================================ # 上传覆盖率到 Codecov + # ============================================ codecov: - name: Upload Coverage to Codecov + name: Upload Coverage needs: [rust-coverage, python-coverage] runs-on: ubuntu-latest if: always() steps: - uses: actions/checkout@v4 - - name: Download Rust coverage report + - name: Download Rust coverage uses: actions/download-artifact@v4 with: name: rust-coverage path: . continue-on-error: true - - name: Download Python coverage report + - name: Download Python coverage uses: actions/download-artifact@v4 with: name: python-coverage path: . continue-on-error: true - - name: Check coverage files exist + - name: Check coverage files run: | echo "=== Coverage files check ===" - if [ -f coverage.lcov ]; then - echo "✓ coverage.lcov exists ($(wc -l < coverage.lcov) lines)" - else - echo "✗ coverage.lcov not found" - fi - if [ -f coverage.xml ]; then - echo "✓ coverage.xml exists ($(wc -l < coverage.xml) lines)" - else - echo "✗ coverage.xml not found" - fi - - - name: Upload coverage reports to Codecov + [ -f coverage.lcov ] && echo "✓ coverage.lcov exists" || echo "✗ coverage.lcov not found" + [ -f coverage.xml ] && echo "✓ coverage.xml exists" || echo "✗ coverage.xml not found" + + - name: Upload to Codecov uses: codecov/codecov-action@v4 with: files: coverage.lcov,coverage.xml token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: false verbose: true - - # Pre-commit 检查 - pre-commit: - name: Pre-commit - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - uses: pre-commit/action@v3.0.1 - with: - extra_args: --all-files - - # 构建检查 - build: - name: Build wheels on ${{ matrix.os }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, macos-latest] - - steps: - - uses: actions/checkout@v4 - - - name: Setup Build Environment - uses: ./.github/actions/setup-build-env - with: - python-version: '3.12' - - - name: Cache Rust build - uses: Swatinem/rust-cache@v2 - - - name: Build wheel - uses: PyO3/maturin-action@v1 - with: - args: --release --out dist - sccache: 'true' - - - name: Upload wheels - uses: actions/upload-artifact@v4 - with: - name: wheels-${{ matrix.os }} - path: dist/*.whl diff --git a/Cargo.lock b/Cargo.lock index 3b3178165..4a4f21112 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,6 +110,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a88aab2464f1f25453baa7a07c84c5b7684e274054ba06817f382357f77a288" dependencies = [ "aws-lc-sys", + "untrusted 0.7.1", "zeroize", ] @@ -199,6 +200,15 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.19.0" @@ -335,6 +345,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -369,6 +388,16 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "darling" version = "0.20.11" @@ -467,6 +496,16 @@ dependencies = [ "syn", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "dirs" version = "6.0.0" @@ -736,6 +775,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -1737,6 +1786,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "aws-lc-rs", "bincode", "bytes", "dashmap", @@ -1749,11 +1799,11 @@ dependencies = [ "opentelemetry_sdk", "rand 0.9.2", "rcgen", - "ring", "rustls", "rustls-pemfile", "serde", "serde_json", + "sha2", "thiserror 2.0.17", "time", "tokio", @@ -2221,7 +2271,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.16", "libc", - "untrusted", + "untrusted 0.9.0", "windows-sys 0.52.0", ] @@ -2288,7 +2338,7 @@ dependencies = [ "aws-lc-rs", "ring", "rustls-pki-types", - "untrusted", + "untrusted 0.9.0", ] [[package]] @@ -2396,6 +2446,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -2997,6 +3058,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + [[package]] name = "unicode-ident" version = "1.0.22" @@ -3036,6 +3103,12 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index 9ef44916d..1e0e62cb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,10 +75,10 @@ reqwest = { version = "0.12", default-features = false, features = [ reqwest-eventsource = "0.6" # TLS -rustls = { version = "0.23", default-features = false, features = ["ring", "std"] } +rustls = { version = "0.23", default-features = false, features = ["aws-lc-rs", "std"] } tokio-rustls = { version = "0.26" } rcgen = { version = "0.13" } -ring = { version = "0.17" } +aws-lc-rs = { version = "1.8" } rustls-pemfile = { version = "2" } time = { version = "0.3" } @@ -94,3 +94,7 @@ tabled = "=0.14" tokenizers = { version = "0.21", features = ["http"] } hf-hub = { version = "0.4", features = ["tokio"] } vergen-gitcl = "1.0" +pkcs8 = { version = "0.10", features = ["alloc"] } +sha2 = "0.10" +ed25519-dalek = { version = "2.0", features = ["pkcs8"] } +der = "0.7" diff --git a/crates/pulsing-actor/Cargo.toml b/crates/pulsing-actor/Cargo.toml index c2791a88f..4f2ab62f6 100644 --- a/crates/pulsing-actor/Cargo.toml +++ b/crates/pulsing-actor/Cargo.toml @@ -15,7 +15,7 @@ integration = [] # Enable OTLP exporter for sending traces to Jaeger/Tempo otlp = ["opentelemetry-otlp"] # Enable TLS support with passphrase-derived certificates -tls = ["dep:rustls", "dep:tokio-rustls", "dep:rcgen", "dep:ring", "dep:rustls-pemfile", "dep:time"] +tls = ["dep:rustls", "dep:tokio-rustls", "dep:rcgen", "dep:aws-lc-rs", "dep:rustls-pemfile", "dep:time", "dep:sha2"] # Enable test helpers module for testing in downstream crates test-helper = [] @@ -63,9 +63,10 @@ http-body-util = { workspace = true } rustls = { workspace = true, optional = true } tokio-rustls = { workspace = true, optional = true } rcgen = { workspace = true, optional = true } -ring = { workspace = true, optional = true } +aws-lc-rs = { workspace = true, optional = true } rustls-pemfile = { workspace = true, optional = true } time = { workspace = true, optional = true } +sha2 = { version = "0.10", optional = true } [dev-dependencies] tokio-test = { workspace = true } diff --git a/crates/pulsing-actor/src/transport/http2/tls.rs b/crates/pulsing-actor/src/transport/http2/tls.rs index 2fdc14d63..1ebcaf7ce 100644 --- a/crates/pulsing-actor/src/transport/http2/tls.rs +++ b/crates/pulsing-actor/src/transport/http2/tls.rs @@ -4,13 +4,11 @@ use rcgen::{ BasicConstraints, Certificate, CertificateParams, DnType, ExtendedKeyUsagePurpose, IsCa, KeyPair, KeyUsagePurpose, SerialNumber, PKCS_ED25519, }; -use ring::digest::{digest, SHA256}; -use ring::hkdf::{self, HKDF_SHA256}; -use ring::signature::{Ed25519KeyPair, KeyPair as RingKeyPair}; -use rustls::crypto::ring::default_provider; +use rustls::crypto::aws_lc_rs::default_provider; use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer, ServerName}; use rustls::server::WebPkiClientVerifier; use rustls::{ClientConfig, RootCertStore, ServerConfig}; +use sha2::{Digest, Sha256}; use std::sync::OnceLock; static CRYPTO_PROVIDER_INSTALLED: OnceLock<()> = OnceLock::new(); @@ -70,8 +68,10 @@ impl TlsConfig { .with_client_auth_cert(vec![node_cert_der], node_key_der) .map_err(|e| anyhow::anyhow!("Failed to build client config: {}", e))?; - let hash = digest(&SHA256, passphrase.as_bytes()); - let passphrase_hash = hex_encode(&hash.as_ref()[..8]); + let hash = Sha256::digest(passphrase.as_bytes()); + let hash_slice = hash.as_slice(); + let hash_bytes = &hash_slice[..8]; + let passphrase_hash = hex_encode(hash_bytes); Ok(Self { acceptor: TlsAcceptor::from(Arc::new(server_config)), @@ -205,110 +205,65 @@ fn generate_node_cert( Ok((cert, node_key)) } -/// Helper struct for HKDF output length -struct HkdfLen(usize); - -impl hkdf::KeyType for HkdfLen { - fn len(&self) -> usize { - self.0 - } -} - -/// Derive a 32-byte seed using HKDF +/// Derive a 32-byte seed using SHA256 fn derive_seed(passphrase: &str, info: &[u8]) -> anyhow::Result<[u8; 32]> { - let salt = hkdf::Salt::new(HKDF_SHA256, HKDF_SALT); - let prk = salt.extract(passphrase.as_bytes()); + let mut hasher = Sha256::new(); + hasher.update(HKDF_SALT); + hasher.update(passphrase.as_bytes()); + hasher.update(info); + let hash = hasher.finalize(); let mut seed = [0u8; 32]; - prk.expand(&[info], HkdfLen(32)) - .map_err(|_| anyhow::anyhow!("HKDF expand failed"))? - .fill(&mut seed) - .map_err(|_| anyhow::anyhow!("HKDF fill failed"))?; - + seed.copy_from_slice(&hash); Ok(seed) } /// Generate a deterministic Ed25519 key pair from a seed /// -/// Ed25519 natively supports deterministic key generation from a 32-byte seed. +/// Manually constructs PKCS#8 DER format for Ed25519 keys per RFC 8410. fn generate_deterministic_key_pair(seed: &[u8; 32]) -> anyhow::Result { - // Create Ed25519 key pair from seed using ring - let ed25519_key = Ed25519KeyPair::from_seed_unchecked(seed) - .map_err(|e| anyhow::anyhow!("Failed to create Ed25519 key from seed: {}", e))?; - - // Get the PKCS#8 v2 DER encoding - let pkcs8_der = create_ed25519_pkcs8_der(seed, ed25519_key.public_key().as_ref())?; - - // Convert to PrivateKeyDer and import into rcgen - let private_key_der = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(pkcs8_der)); - - let key_pair = KeyPair::from_der_and_sign_algo(&private_key_der, &PKCS_ED25519) - .map_err(|e| anyhow::anyhow!("Failed to create key pair from DER: {}", e))?; + // Manually construct PKCS#8 DER for Ed25519 (RFC 8410) + // PrivateKeyInfo ::= SEQUENCE { + // version INTEGER (0), + // algorithm AlgorithmIdentifier, + // privateKey OCTET STRING (contains OCTET STRING with seed) + // } + + // Inner OCTET STRING containing the 32-byte seed + let mut inner_private_key = Vec::new(); + inner_private_key.push(0x04); // OCTET STRING tag + inner_private_key.push(32); // length + inner_private_key.extend_from_slice(seed); + + // Outer OCTET STRING containing the inner OCTET STRING + let mut outer_private_key = Vec::new(); + outer_private_key.push(0x04); // OCTET STRING tag + outer_private_key.push(inner_private_key.len() as u8); + outer_private_key.extend_from_slice(&inner_private_key); + + // Algorithm identifier for Ed25519 (OID 1.3.101.112) + let algo_id: &[u8] = &[0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70]; + + // Build content: version + algorithm + privateKey + let mut content = Vec::new(); + content.extend_from_slice(&[0x02, 0x01, 0x00]); // INTEGER 0 (version) + content.extend_from_slice(algo_id); // AlgorithmIdentifier + content.extend_from_slice(&outer_private_key); // privateKey + + // Wrap in SEQUENCE + let mut pkcs8_der = Vec::new(); + pkcs8_der.push(0x30); // SEQUENCE tag + pkcs8_der.push(content.len() as u8); + pkcs8_der.extend_from_slice(&content); + + // Create rcgen KeyPair from the DER + let private_key_der = rustls::pki_types::PrivatePkcs8KeyDer::from(pkcs8_der.as_slice()); + let key_pair = KeyPair::from_pkcs8_der_and_sign_algo(&private_key_der, &PKCS_ED25519) + .map_err(|e| anyhow::anyhow!("Failed to create rcgen KeyPair: {}", e))?; Ok(key_pair) } -/// Create PKCS#8 v1 DER encoding for an Ed25519 private key -/// -/// RFC 8410 defines the format for Ed25519 keys: -/// - privateKey contains CurvePrivateKey which is an OCTET STRING -/// - The 32-byte seed needs to be wrapped in an OCTET STRING -fn create_ed25519_pkcs8_der(seed: &[u8; 32], _public_key: &[u8]) -> anyhow::Result> { - // OID for Ed25519: 1.3.101.112 - let ed25519_oid: &[u8] = &[0x06, 0x03, 0x2b, 0x65, 0x70]; - - // Build algorithm identifier: SEQUENCE { OID ed25519 } - let algo_seq = wrap_in_sequence(ed25519_oid); - - // CurvePrivateKey ::= OCTET STRING (the 32-byte seed) - // This needs to be wrapped in OCTET STRING for the privateKey field - let inner_private_key = wrap_in_octet_string(seed); - let private_key_octet = wrap_in_octet_string(&inner_private_key); - - // Build PKCS#8 structure (version 0) - let mut pkcs8_content = Vec::new(); - // Version INTEGER 0 - pkcs8_content.extend_from_slice(&[0x02, 0x01, 0x00]); - // Algorithm identifier - pkcs8_content.extend_from_slice(&algo_seq); - // Private key (double-wrapped OCTET STRING) - pkcs8_content.extend_from_slice(&private_key_octet); - - Ok(wrap_in_sequence(&pkcs8_content)) -} - -/// Wrap data in an ASN.1 SEQUENCE -fn wrap_in_sequence(data: &[u8]) -> Vec { - let mut result = Vec::new(); - result.push(0x30); // SEQUENCE tag - write_length(&mut result, data.len()); - result.extend_from_slice(data); - result -} - -/// Wrap data in an ASN.1 OCTET STRING -fn wrap_in_octet_string(data: &[u8]) -> Vec { - let mut result = Vec::new(); - result.push(0x04); // OCTET STRING tag - write_length(&mut result, data.len()); - result.extend_from_slice(data); - result -} - -/// Write ASN.1 DER length encoding -fn write_length(output: &mut Vec, len: usize) { - if len < 128 { - output.push(len as u8); - } else if len < 256 { - output.push(0x81); - output.push(len as u8); - } else { - output.push(0x82); - output.push((len >> 8) as u8); - output.push(len as u8); - } -} - /// Convert bytes to hex string fn hex_encode(bytes: &[u8]) -> String { bytes.iter().map(|b| format!("{:02x}", b)).collect()