Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Default owners for everything in this fork.
* @duksh
3 changes: 3 additions & 0 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/linkcheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/pypi-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
20 changes: 13 additions & 7 deletions .github/workflows/wheel-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down
24 changes: 24 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -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.
217 changes: 217 additions & 0 deletions SECURITY_ANALYSIS.md
Original file line number Diff line number Diff line change
@@ -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.
Comment on lines +15 to +18

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Update the report to reflect this PR’s remediation.

These sections still state that publishing is unguarded, no SECURITY.md/CODEOWNERS exists, and benchmark/downstream jobs run on forks. The final workflows and documentation now implement those controls, so either mark these findings as remediated or label the report as a pre-remediation snapshot; otherwise it documents an incorrect security posture.

Also applies to: 37-78

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SECURITY_ANALYSIS.md` around lines 15 - 18, Update SECURITY_ANALYSIS.md to
reflect the implemented remediation: mark the findings about fork-unguarded
publishing, missing SECURITY.md/CODEOWNERS, and benchmark/downstream jobs
running on forks as remediated, or clearly label the document as a
pre-remediation snapshot. Ensure the report no longer presents these controls as
absent in the final security posture.


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.
4 changes: 4 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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