diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000000..489cc2be27ac --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# Default owners for everything in this fork. +* @duksh diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index ff9dac37f881..cd670687506a 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -25,6 +25,9 @@ jobs: benchmark: runs-on: ubuntu-latest timeout-minutes: 15 + # Benchmarks check out and run code from the upstream repository; not + # useful (and unnecessary CI surface) on forks. + if: github.repository_owner == 'pyca' steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 timeout-minutes: 3 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a99963ce4e6..83cc39a30031 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -185,7 +185,10 @@ jobs: - {IMAGE: "ubuntu-rolling:aarch64", NOXSESSION: "tests", RUNNER: "ubuntu-24.04-arm"} - {IMAGE: "ubuntu-rolling:armv7l", NOXSESSION: "tests", RUNNER: "ubuntu-24.04-arm"} - - {IMAGE: "ubuntu-rolling:ppc64le", NOXSESSION: "tests", RUNNER: "ubuntu-24.04-ppc64le"} + # Removed on this fork: ubuntu-24.04-ppc64le runners are never + # allocated outside the pyca org, so the leg queues forever and + # keeps all-green from reporting. + # - {IMAGE: "ubuntu-rolling:ppc64le", NOXSESSION: "tests", RUNNER: "ubuntu-24.04-ppc64le"} timeout-minutes: 15 env: RUSTUP_HOME: /root/.rustup @@ -395,6 +398,9 @@ jobs: linux-downstream: runs-on: ubuntu-latest + # Downstream tests execute third-party repositories' code; skip them on + # forks (all-green allows this skip below). + if: github.repository_owner == 'pyca' strategy: fail-fast: false matrix: @@ -515,6 +521,8 @@ jobs: uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2 with: jobs: ${{ toJSON(needs) }} + # linux-downstream is skipped on forks. + allowed-skips: linux-downstream - run: pip install -c ci-constraints-requirements.txt coverage[toml] if: ${{ always() }} - name: Download coverage data diff --git a/.github/workflows/linkcheck.yml b/.github/workflows/linkcheck.yml index 698a7908eeca..e44ec0a47b83 100644 --- a/.github/workflows/linkcheck.yml +++ b/.github/workflows/linkcheck.yml @@ -18,6 +18,10 @@ jobs: docs-linkcheck: runs-on: ubuntu-latest name: "linkcheck" + # The weekly scheduled run fails on forks whenever an external site + # blocks CI traffic or is slow; keep it upstream-only. PR runs (for + # changes to docs/conf.py or this file) still work everywhere. + if: github.repository_owner == 'pyca' || github.event_name == 'pull_request' timeout-minutes: 20 steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml index db46ceeae941..c2b7eece1a58 100644 --- a/.github/workflows/pypi-publish.yml +++ b/.github/workflows/pypi-publish.yml @@ -27,7 +27,9 @@ jobs: # We're not actually verifying that the triggering push event was for a # tag, because github doesn't expose enough information to do so. # wheel-builder.yml currently only has push events for tags. - if: github.event_name == 'workflow_dispatch' || (github.event.workflow_run.event == 'push' && github.event.workflow_run.conclusion == 'success') + # The repository_owner guard prevents this job from ever attempting a + # publish (or minting OIDC tokens/attestations) when run from a fork. + if: github.repository_owner == 'pyca' && (github.event_name == 'workflow_dispatch' || (github.event.workflow_run.event == 'push' && github.event.workflow_run.conclusion == 'success')) permissions: id-token: "write" attestations: "write" diff --git a/.github/workflows/wheel-builder.yml b/.github/workflows/wheel-builder.yml index a0cb2413f251..0d25cba48903 100644 --- a/.github/workflows/wheel-builder.yml +++ b/.github/workflows/wheel-builder.yml @@ -27,6 +27,10 @@ jobs: sdist: runs-on: ubuntu-latest name: sdists + # Guard release builds against running from a fork (tag pushes and manual + # dispatches). PR runs are kept so workflow changes remain testable. + # Every other job needs this one, so skipping it skips the whole chain. + if: github.repository_owner == 'pyca' || github.event_name == 'pull_request' steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: @@ -82,8 +86,10 @@ jobs: - { NAME: "manylinux_2_31_armv7l", CONTAINER: "cryptography-manylinux_2_31:armv7l", RUNNER: "ubuntu-24.04-arm" } - - { NAME: "manylinux_2_28_ppc64le", CONTAINER: "cryptography-manylinux_2_28:ppc64le", RUNNER: "ubuntu-24.04-ppc64le" } - - { NAME: "manylinux_2_34_ppc64le", CONTAINER: "cryptography-manylinux_2_34:ppc64le", RUNNER: "ubuntu-24.04-ppc64le" } + # Removed on this fork: ubuntu-24.04-ppc64le runners are never + # allocated outside the pyca org, so these legs queue forever. + # - { NAME: "manylinux_2_28_ppc64le", CONTAINER: "cryptography-manylinux_2_28:ppc64le", RUNNER: "ubuntu-24.04-ppc64le" } + # - { NAME: "manylinux_2_34_ppc64le", CONTAINER: "cryptography-manylinux_2_34:ppc64le", RUNNER: "ubuntu-24.04-ppc64le" } exclude: # There are no readily available musllinux PyPy distributions @@ -102,11 +108,11 @@ jobs: - PYTHON: { VERSION: "pp311-pypy311_pp73" } MANYLINUX: { NAME: "manylinux_2_31_armv7l", CONTAINER: "cryptography-manylinux_2_31:armv7l", RUNNER: "ubuntu-24.04-arm" } - # No PyPy on ppc64le - - PYTHON: { VERSION: "pp311-pypy311_pp73" } - MANYLINUX: { NAME: "manylinux_2_34_ppc64le", CONTAINER: "cryptography-manylinux_2_34:ppc64le", RUNNER: "ubuntu-24.04-ppc64le" } - - PYTHON: { VERSION: "pp311-pypy311_pp73" } - MANYLINUX: { NAME: "manylinux_2_28_ppc64le", CONTAINER: "cryptography-manylinux_2_28:ppc64le", RUNNER: "ubuntu-24.04-ppc64le" } + # No PyPy on ppc64le (ppc64le legs removed on this fork, see above) + # - PYTHON: { VERSION: "pp311-pypy311_pp73" } + # MANYLINUX: { NAME: "manylinux_2_34_ppc64le", CONTAINER: "cryptography-manylinux_2_34:ppc64le", RUNNER: "ubuntu-24.04-ppc64le" } + # - PYTHON: { VERSION: "pp311-pypy311_pp73" } + # MANYLINUX: { NAME: "manylinux_2_28_ppc64le", CONTAINER: "cryptography-manylinux_2_28:ppc64le", RUNNER: "ubuntu-24.04-ppc64le" } name: "${{ matrix.PYTHON.VERSION }} for ${{ matrix.MANYLINUX.NAME }}" steps: diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000000..a1543e1c8b12 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,24 @@ +# Security Policy + +This repository is a fork of [pyca/cryptography](https://github.com/pyca/cryptography). +It does **not** publish releases, wheels, or packages anywhere. If you obtained +`cryptography` from PyPI or a distribution, it did not come from this fork. + +## Reporting a vulnerability in the cryptography library + +Vulnerabilities in the library itself should be reported **upstream**, following +the upstream security policy: https://cryptography.io/en/latest/security/ +(report privately via https://github.com/pyca/cryptography/security/advisories/new — +not in a public issue tracker). + +## Reporting an issue specific to this fork + +For issues that exist only in this fork (for example, its CI configuration or +repository setup), please open a private security advisory on this repository: +https://github.com/duksh/cryptography/security/advisories/new + +## Supported versions + +This fork tracks the upstream `main` branch and ships nothing; there are no +supported release versions here. See the upstream policy for supported versions +of the actual library. diff --git a/SECURITY_ANALYSIS.md b/SECURITY_ANALYSIS.md new file mode 100644 index 000000000000..023be953492b --- /dev/null +++ b/SECURITY_ANALYSIS.md @@ -0,0 +1,217 @@ +# Security Analysis: duksh/cryptography + +A cybersecurity reinforcement analysis of this repository (a fork of +[pyca/cryptography](https://github.com/pyca/cryptography), v50.0.0-dev1, synced with +upstream as of 2026-07-22). Three areas were audited: CI/supply-chain posture, the +code-level cryptographic surface, and testing/fuzzing assurance. + +## Executive summary + +The upstream project is one of the best-hardened codebases in the Python ecosystem, and +this fork inherits that posture: SHA-pinned Actions, least-privilege workflow permissions, +OIDC trusted publishing with PEP 740 attestations, hash-pinned build requirements, +memory-safe Rust parsers, an enforced 100% coverage gate, and differential test vectors +run across five TLS backends. The most actionable reinforcement opportunities are +**fork-specific**: the release/publish pipeline is not guarded against running in a fork, +and the vulnerability-reporting channel points at the upstream project. Beyond that, +there are meaningful gaps in dependency auditing, code scanning, fuzzing, and a handful of +code-level hardening items. + +Findings are prioritized P1 (act first) through P4. + +## Baseline strengths (verified) + +| Area | Evidence | +|---|---| +| Action pinning | Every third-party action pinned to a full commit SHA (e.g. `ci.yml:72`, `pypi-publish.yml:60`) | +| Workflow permissions | Top-level `permissions: contents: read` in all main workflows; job-level elevation only where needed (`pypi-publish.yml:31-33`) | +| Checkout hygiene | `persist-credentials: false` on test/build checkouts; no `pull_request_target`; no self-hosted runners | +| Publishing | OIDC Trusted Publishing (`uv publish --trusted-publishing=always`, `pypi-publish.yml:77`); no PyPI token secret exists; PEP 740 attestations via `astral-sh/attest-action`; SBOM embedded in wheels (`wheel-builder.yml:151`) | +| Dependency pinning | `.github/requirements/build-requirements.txt` and `uv-requirements.txt` hash-pinned and consumed with `--require-hashes`; `Cargo.lock` committed with checksums; daily Dependabot for actions/cargo/uv incl. transitive deps | +| Memory safety | ASN.1/X.509/key-parsing crates are `#![forbid(unsafe_code)]` (`cryptography-x509`, `cryptography-x509-verification`, `cryptography-key-parsing`); all unsafe blocks require `// SAFETY:` comments (`clippy::undocumented_unsafe_blocks` denied); `overflow-checks = true` in release (`Cargo.toml`) | +| Test assurance | Enforced 100% combined Python+Rust coverage (`.github/bin/merge_rust_coverage.py:138-142`); wycheproof + x509-limbo vectors (pinned refs) in every matrix leg; matrix spans OpenSSL 3.0.x–master, LibreSSL, BoringSSL, AWS-LC, FIPS, `no-legacy`, free-threaded Python | +| Weak-crypto quarantine | Legacy ciphers isolated in `hazmat/decrepit/`; constant-time compares via `hmac.compare_digest` / `CRYPTO_memcmp`; OpenSSL < 3.0 is a hard compile error (`src/_cffi_src/openssl/cryptography.py:55-60`) | + +## P1 — Fork-specific risks + +### 1.1 The publish pipeline is not owner-guarded + +All scheduled bot workflows are gated with `if: github.repository_owner == 'pyca'` +(`auto-close-stale.yml:9`, `lock.yml:9`, `boring-open-awslc-bump.yml:13`, +`downstream-version-bump.yml:13`, `x509-limbo-version-bump.yml:13`), so they no-op on this +fork. The **release path is not**: + +- `wheel-builder.yml:11-13` triggers on any tag push matching `*.*` / `*.*.*` — no owner + guard. Pushing a tag like `1.0` to this fork starts a full multi-platform wheel build. +- `pypi-publish.yml:14-16` chains from Wheel Builder completion via `workflow_run`, and + its gate (`pypi-publish.yml:30`) checks only the event type and conclusion — not the + repository owner. It then requests `id-token: write` + `attestations: write` and runs + `uv publish --trusted-publishing=always`. + +On this fork the publish would only succeed if a PyPI Trusted Publisher were ever +configured for `duksh/cryptography` — safety currently rests entirely on that PyPI-side +absence, with no in-repo defense. + +**Recommendation:** add `if: github.repository_owner == 'pyca'` (or the fork owner, if +the fork intends to publish under a different name) to the `wheel-builder.yml` tag +trigger jobs and the `pypi-publish.yml` publish job, and/or disable these workflows in +the fork's Actions settings. + +### 1.2 No valid vulnerability-reporting channel + +- There is no `SECURITY.md`; the policy in `docs/security.rst:60-67` directs reporters to + the **upstream pyca** advisory page (`pyca/cryptography/security/advisories/new`). +- There is no `CODEOWNERS` file. + +**Recommendation:** add a root `SECURITY.md` stating where issues in *this fork* should +be reported (or explicitly deferring to upstream and stating the fork ships no releases), +and a `CODEOWNERS` if more than one person maintains the fork. + +### 1.3 Third-party code execution in CI + +`ci.yml:402-451` (downstream tests: paramiko, twisted, mitmproxy, scapy, certbot, +aws-encryption-sdk, sigstore-python) and `benchmark.yml:38` check out and execute +external repositories' code in this repo's CI context. Refs are pinned to commit SHAs and +the token is read-only, so exposure is limited — but on a fork this is unnecessary attack +surface and compute. + +**Recommendation:** owner-gate or disable the downstream and benchmark jobs on the fork. + +## P2 — Supply-chain hardening + +### 2.1 One unhashed install path — and it is in the publish job + +`ci-constraints-requirements.txt` is version-pinned but **not** hash-pinned (unlike the +`.github/requirements/*.txt` files). It is used to bootstrap `uv`/`nox` in CI +(`ci.yml:150,212,478`) and, notably, in the publish job (`pypi-publish.yml:58`). + +**Recommendation:** compile it with `--generate-hashes`, or bootstrap the publish job +from the already-hashed `.github/requirements/uv-requirements.txt`. + +### 2.2 No dependency-audit gates + +`Cargo.lock` is committed, but there is no `cargo-deny` / `cargo-audit` / `cargo-vet` +configuration or CI job for Rust advisory/license checking, and no `pip-audit` job — +even though `docs/security.rst:8-13` recommends osv.dev tooling to downstream users. + +**Recommendation:** add a scheduled + PR workflow running `cargo deny check advisories` +(plus optional `pip-audit` against the hashed requirements files). + +### 2.3 No code-scanning workflows + +No CodeQL, semgrep, or OpenSSF Scorecard integration exists in the repository. + +**Recommendation:** add CodeQL (Python; Rust once stable) and the Scorecard action with +results published to the Security tab. Scorecard also directly measures several items in +this report (pinning, token permissions, fuzzing, SAST). + +### 2.4 Minor items + +- `pypi-publish.yml:19` defines `PUBLISH_REQUIREMENTS_PATH=.github/requirements/publish-requirements.txt`, + which **does not exist** and is never used — dead reference; remove it or restore a + hash-pinned publish requirements file. +- `dtolnay/rust-toolchain` is SHA-pinned but lacks a version comment (`ci.yml:85`), + making drift review harder. +- `dependabot.yml` sets `open-pull-requests-limit: 1024` per ecosystem — effectively + unbounded automated-PR volume. + +## P3 — Code-level hardening + +### 3.1 No secret zeroization + +There is no `zeroize` crate usage (0 references in `Cargo.toml`/`Cargo.lock`) and no +explicit cleansing of key or plaintext buffers in `src/rust`. Secret material relies on +OpenSSL's internal handling and Rust drop semantics; owned intermediate buffers (derived +keys, seeds, decrypted PKCS#12 material) are not scrubbed. + +**Recommendation:** adopt `zeroize` (or explicit `OPENSSL_cleanse` calls through the FFI +shim) for owned secret buffers, starting in `cryptography-openssl`. + +### 3.2 Rust lint gaps + +- No `#![deny(unsafe_op_in_unsafe_fn)]` anywhere — unsafe operations inside `unsafe fn` + bodies are not individually flagged. +- No `[workspace.lints]` table; lint policy is per-crate inner attributes, so a new crate + could silently omit them. There are 66 unsafe blocks across 16 files (concentrated in + the `cryptography-openssl` FFI shim: `mlkem.rs`, `mldsa.rs`, `aead.rs`, `hmac.rs`). + +**Recommendation:** add a workspace lints table with `unsafe_op_in_unsafe_fn = "deny"` +and inherit it in all crates; keep `forbid(unsafe_code)` in every crate that doesn't +strictly need FFI. + +### 3.3 Legacy algorithm exposure gaps + +- `CAST5`, `IDEA`, `SEED`, and `Blowfish` are re-exported from + `src/cryptography/hazmat/primitives/ciphers/algorithms.py:8-28` **without** deprecation + wrappers — only `Camellia`, `ARC4`, and `TripleDES` warn (`algorithms.py:70-103`). +- `MD5` (`hashes.py:281`), `SHA1` (`hashes.py:101`), and DSA key generation + (`dsa.py:166-179`) carry no deprecation gating. +- RSA generation floor is 1024 bits (`rsa.py:180-181`), below the modern 2048 + recommendation; key loading enforces no floor. + +**Recommendation:** extend `utils.deprecated` wrappers to the remaining decrepit +re-exports; consider a `CryptographyDeprecationWarning` on RSA generation below 2048 and +on DSA generation. (For a fork, these could also be hard errors if no legacy +compatibility is needed.) + +### 3.4 No minimum-version assertion for alternative backends + +Only OpenSSL proper has a hard compile-time floor +(`src/_cffi_src/openssl/cryptography.py:55-60`). LibreSSL, BoringSSL, and AWS-LC are +detected by feature cfg only (`src/rust/build.rs:36-49`) — an old LibreSSL could compile +without any version assertion. + +**Recommendation:** add explicit minimum-version `#error`/build-fail gates for the +alternative backends matching what CI actually tests. + +### 3.5 Documented validation bypass (informational) + +`unsafe_skip_rsa_key_validation` (`src/rust/src/backend/keys.rs:12-26`) is a public +keyword-only flag that skips RSA key consistency checks on load. It is clearly named and +defaults to `False`; no change proposed, but any downstream code setting it should be +audited. + +## P4 — Assurance depth + +### 4.1 No fuzzing + +No cargo-fuzz/libFuzzer targets, no OSS-Fuzz or ClusterFuzzLite configuration exist in +the repo. The hand-rolled ASN.1 parsers (X.509 certificates/CRLs/CSRs, PKCS#7, PKCS#12) +are memory-safe Rust, but panics and logic/differential bugs remain findable by fuzzing. + +**Recommendation:** add `cargo fuzz` targets for `load_der_x509_certificate`, +`load_der_x509_crl`, `load_der_x509_csr`, and the PKCS#7/PKCS#12 parsers, and wire +ClusterFuzzLite into CI for PR-time fuzzing. + +### 4.2 No sanitizer CI legs + +No ASAN/UBSAN/MSAN jobs exist; memory-safety assurance at the CFFI/OpenSSL boundary rests +on Rust's guarantees plus backend diversity. + +**Recommendation:** add one ASAN+UBSAN matrix leg (nightly Rust `-Zsanitizer=address` +with an ASAN-built OpenSSL) on a scheduled workflow if PR latency is a concern. + +### 4.3 Minor + +`mypy` is strict-ish but not maximal (`pyproject.toml:152-164` — no +`disallow_untyped_defs`). + +## Remediation roadmap + +| # | Action | Priority | Effort | +|---|---|---|---| +| 1 | Owner-guard `wheel-builder.yml` + `pypi-publish.yml` (or disable on fork) | P1 | Small | +| 2 | Add `SECURITY.md` (+ `CODEOWNERS`) for the fork | P1 | Small | +| 3 | Owner-gate downstream/benchmark third-party CI jobs | P1 | Small | +| 4 | Hash-pin `ci-constraints-requirements.txt`; fix dead `PUBLISH_REQUIREMENTS_PATH` | P2 | Small | +| 5 | Add `cargo deny check advisories` (+ optional `pip-audit`) workflow | P2 | Small | +| 6 | Add CodeQL + OpenSSF Scorecard workflows | P2 | Small | +| 7 | Workspace `[lints]` with `unsafe_op_in_unsafe_fn = "deny"` | P3 | Small | +| 8 | Deprecation wrappers for CAST5/IDEA/SEED/Blowfish re-exports; DSA/small-RSA warnings | P3 | Medium | +| 9 | Minimum-version gates for LibreSSL/BoringSSL/AWS-LC | P3 | Medium | +| 10 | Adopt `zeroize` for owned secret buffers | P3 | Medium | +| 11 | cargo-fuzz targets + ClusterFuzzLite for the ASN.1/PKCS parsers | P4 | Medium | +| 12 | Scheduled ASAN/UBSAN CI leg | P4 | Medium | + +Items 8–10 change library behavior/internals and are best contributed upstream rather +than carried as fork-only patches. diff --git a/docs/conf.py b/docs/conf.py index e277d0f2c1c9..a04f6438394c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -213,6 +213,10 @@ r"https://github.com", # 403s r"https://e-trust.gosuslugi.ru/", + # Returns 403 to CI/cloud IP ranges + r"https://eprint.iacr.org", + # Consistently exceeds the linkcheck timeout from CI runners + r"https://alexgaynor.net", ] autosectionlabel_prefix_document = True