Skip to content

Commit ac6d9ed

Browse files
committed
chore: documentation, dependencies, security and CI
1 parent 289638c commit ac6d9ed

34 files changed

+654
-277
lines changed

.flake8

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[flake8]
2+
max-line-length = 120
3+
extend-ignore = E203, W503
4+
exclude =
5+
.git,
6+
__pycache__,
7+
.venv,
8+
venv,
9+
build,
10+
dist,
11+
*.egg-info
12+
per-file-ignores =
13+
__init__.py:F401
14+
pymdoccbor/tests/*:E501

.github/workflows/code-quality.yml

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Code quality: flake8, isort, bandit
2+
name: Code quality
3+
4+
on:
5+
push:
6+
branches: ["*"]
7+
pull_request:
8+
branches: ["*"]
9+
10+
jobs:
11+
lint:
12+
name: flake8
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
- uses: actions/setup-python@v5
17+
with:
18+
python-version: "3.12"
19+
- name: Create venv and install
20+
run: |
21+
python -m venv env
22+
source env/bin/activate
23+
pip install --upgrade pip flake8
24+
if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi
25+
pip install -e .
26+
- name: Run flake8
27+
run: |
28+
source env/bin/activate
29+
flake8 pymdoccbor
30+
31+
isort:
32+
name: isort
33+
runs-on: ubuntu-latest
34+
steps:
35+
- uses: actions/checkout@v4
36+
- uses: actions/setup-python@v5
37+
with:
38+
python-version: "3.12"
39+
- name: Create venv and install
40+
run: |
41+
python -m venv env
42+
source env/bin/activate
43+
pip install --upgrade pip isort
44+
pip install -e .
45+
- name: Check import order with isort
46+
run: |
47+
source env/bin/activate
48+
isort pymdoccbor --check-only --diff
49+
50+
bandit:
51+
name: Bandit security scan
52+
runs-on: ubuntu-latest
53+
steps:
54+
- uses: actions/checkout@v4
55+
- uses: actions/setup-python@v5
56+
with:
57+
python-version: "3.12"
58+
- name: Create venv and install
59+
run: |
60+
python -m venv env
61+
source env/bin/activate
62+
pip install --upgrade pip bandit
63+
pip install -e .
64+
- name: Run Bandit security scan
65+
run: |
66+
source env/bin/activate
67+
bandit -r -x pymdoccbor/tests pymdoccbor -f txt
68+
69+
radon:
70+
name: Radon complexity
71+
runs-on: ubuntu-latest
72+
steps:
73+
- uses: actions/checkout@v4
74+
- uses: actions/setup-python@v5
75+
with:
76+
python-version: "3.12"
77+
- name: Create venv and install
78+
run: |
79+
python -m venv env
80+
source env/bin/activate
81+
pip install --upgrade pip radon
82+
pip install -e .
83+
- name: Run Radon (maintainability index, no fail)
84+
run: |
85+
source env/bin/activate
86+
radon cc pymdoccbor -a -n B || true
87+
- name: Run Radon (raw metrics)
88+
run: |
89+
source env/bin/activate
90+
radon raw pymdoccbor -s || true
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Security audit of Python dependencies (known vulnerabilities)
2+
name: Dependency security
3+
4+
on:
5+
push:
6+
branches: ["*"]
7+
pull_request:
8+
branches: ["*"]
9+
10+
jobs:
11+
pip-audit:
12+
name: pip-audit
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- uses: actions/setup-python@v5
18+
with:
19+
python-version: "3.12"
20+
21+
- name: Create venv and install
22+
run: |
23+
python -m venv env
24+
source env/bin/activate
25+
pip install --upgrade pip pip-audit
26+
pip install "cbor2>=5.4.0" "cbor-diag>=1.1.0" "pycose>=1.0.1"
27+
pip install -r requirements-dev.txt
28+
29+
# Exit 1 on any vulnerability (fail CI). --skip-editable whitelists pymdoccbor (local package, not on PyPI).
30+
# Ignore only unfixable: ecdsa CVE-2024-23342 (no upstream fix; see docs/SECURITY-DEPENDENCIES.md).
31+
- name: Run pip-audit (dependencies)
32+
run: |
33+
source env/bin/activate
34+
pip-audit --skip-editable --ignore-vuln CVE-2024-23342

.github/workflows/python-app.yml

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# This workflow will install Python dependencies, run tests and lint with a single version of Python
1+
# This workflow installs dependencies, runs examples and tests (lint is in code-quality workflow)
22
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
33

44
name: pymdoccbor
@@ -23,32 +23,39 @@ jobs:
2323
- "3.12"
2424

2525
steps:
26-
- uses: actions/checkout@v2
26+
- uses: actions/checkout@v4
2727
- name: Set up Python ${{ matrix.python-version }}
28-
uses: actions/setup-python@v2
28+
uses: actions/setup-python@v5
2929
with:
3030
python-version: ${{ matrix.python-version }}
3131
- name: Install system package
3232
run: |
3333
sudo apt update
34-
sudo apt install python3-dev libssl-dev libffi-dev make automake gcc g++
35-
- name: Install dependencies
34+
sudo apt install python3-dev libssl-dev libffi-dev make automake gcc g++
35+
- name: Create venv and install
3636
run: |
37-
python -m pip install --upgrade pip
37+
python -m venv env
38+
source env/bin/activate
39+
pip install --upgrade pip setuptools
3840
if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi
3941
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
4042
if [ -f requirements-customizations.txt ]; then pip install -r requirements-customizations.txt; fi
41-
python -m pip install -U setuptools
42-
python -m pip install -e .
43-
- name: Lint with flake8
43+
pip install -e .
44+
- name: Run examples
4445
run: |
45-
# stop the build if there are Python syntax errors or undefined names
46-
flake8 pymdoccbor --count --select=E9,F63,F7,F82 --show-source --statistics
47-
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
48-
flake8 pymdoccbor --count --exit-zero --statistics --max-line-length 160
49-
- name: Tests
46+
source env/bin/activate
47+
chmod +x scripts/run_examples.sh
48+
./scripts/run_examples.sh
49+
- name: Run README and docs examples
5050
run: |
51-
pytest --cov
52-
- name: Bandit Security Scan
51+
source env/bin/activate
52+
python scripts/run_doc_examples.py
53+
- name: Tests with coverage
5354
run: |
54-
bandit -r -x pymdoccbor/test* pymdoccbor/*
55+
source env/bin/activate
56+
pytest --cov=pymdoccbor --cov-report=xml --cov-report=term-missing
57+
- name: Upload coverage to Codecov
58+
uses: codecov/codecov-action@v4
59+
with:
60+
file: ./coverage.xml
61+
fail_ci_if_error: false

README.md

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,20 @@ mdoc = mdoci.new(
7171
`MdocCborIssuer` must be initialized with a private key.
7272
The method `.new()` gets the user attributes, devicekeyinfo and doctype.
7373

74-
````
74+
````python
7575
import os
76+
from datetime import datetime, timezone, timedelta
7677

7778
from pymdoccbor.mdoc.issuer import MdocCborIssuer
7879

80+
CERT_INFO = {
81+
"country_name": "IT",
82+
"organization_name": "Example Issuer",
83+
"common_name": "Example mDL",
84+
"not_valid_before": datetime.now(timezone.utc) - timedelta(days=1),
85+
"not_valid_after": datetime.now(timezone.utc) + timedelta(days=365),
86+
}
87+
7988
PKEY = {
8089
'KTY': 'EC2',
8190
'CURVE': 'P_256',
@@ -85,39 +94,36 @@ PKEY = {
8594
}
8695

8796
PID_DATA = {
88-
"eu.europa.ec.eudiw.pid.1": {
89-
"family_name": "Raffaello",
90-
"given_name": "Mascetti",
91-
"birth_date": "1922-03-13",
92-
"birth_place": "Rome",
93-
"birth_country": "IT"
94-
}
97+
"eu.europa.ec.eudiw.pid.1": {
98+
"family_name": "Raffaello",
99+
"given_name": "Mascetti",
100+
"birth_date": "1922-03-13",
101+
"birth_place": "Rome",
102+
"birth_country": "IT"
95103
}
104+
}
96105

97106
mdoci = MdocCborIssuer(
98107
private_key=PKEY,
99-
alg = "ES256"
108+
alg="ES256",
109+
cert_info=CERT_INFO,
100110
)
101111

102112
mdoc = mdoci.new(
103113
doctype="eu.europa.ec.eudiw.pid.1",
104114
data=PID_DATA,
105115
devicekeyinfo=PKEY,
106-
validity = {"issuance_date": "2025-01-17", "expiry_date": "2025-11-13" },
107-
# cert_path="/path/"
116+
validity={"issuance_date": "2025-01-17", "expiry_date": "2025-11-13"},
108117
)
109118

110-
mdoc
111-
>> returns a python dictionay
112-
113-
mdoc.dump()
114-
>> returns mdoc MSO bytes
119+
assert mdoc
120+
# >> mdoc returns a python dictionary (signed mdoc)
115121

116-
mdoci.dump()
117-
>> returns mdoc bytes
122+
assert mdoci.dump()
123+
# >> mdoci.dumps() returns mdoc bytes
118124

119-
mdoci.dumps()
120-
>> returns AF Binary mdoc string representation
125+
assert mdoci.dumps()
126+
# >> mdoci.dumps() returns mdoc bytes
121127
````
122128

123129
### Issue an MSO alone

docs/FIX_ERRORS_FIELD.md

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -76,31 +76,53 @@ All tests pass: **36/36 passed**
7676

7777
### With errors field (status != 0)
7878
```python
79+
import os
80+
from datetime import datetime, timezone, timedelta
81+
from pymdoccbor.mdoc.issuer import MdocCborIssuer
7982
from pymdoccbor.mdoc.verifier import MobileDocument
8083

81-
document = {
82-
'docType': 'eu.europa.ec.eudi.pid.1',
83-
'issuerSigned': {...},
84-
'errors': {
85-
'eu.europa.ec.eudi.pid.1': {
86-
'missing_element': 1
87-
}
88-
}
84+
CERT_INFO = {
85+
"country_name": "IT",
86+
"organization_name": "Example",
87+
"common_name": "Example mDL",
88+
"not_valid_before": datetime.now(timezone.utc) - timedelta(days=1),
89+
"not_valid_after": datetime.now(timezone.utc) + timedelta(days=365),
8990
}
91+
PKEY = {"KTY": "EC2", "CURVE": "P_256", "ALG": "ES256", "D": os.urandom(32), "KID": b"kid"}
92+
DATA = {"org.micov.medical.1": {"family_name": "Test", "given_name": "User"}}
93+
94+
issuer = MdocCborIssuer(private_key=PKEY, alg="ES256", cert_info=CERT_INFO)
95+
issuer.new(data=DATA, doctype="org.micov.medical.1", validity={"issuance_date": "2025-01-01", "expiry_date": "2025-12-31"})
96+
document = issuer.signed["documents"][0]
97+
document["errors"] = {"org.micov.medical.1": {"missing_element": 1}}
9098

91-
doc = MobileDocument(**document) # ✅ Works now!
92-
print(doc.errors) # {'eu.europa.ec.eudi.pid.1': {'missing_element': 1}}
99+
doc = MobileDocument(**document)
100+
assert doc.errors == {"org.micov.medical.1": {"missing_element": 1}}
93101
```
94102

95103
### Without errors field (status == 0)
96104
```python
97-
document = {
98-
'docType': 'eu.europa.ec.eudi.pid.1',
99-
'issuerSigned': {...}
105+
import os
106+
from datetime import datetime, timezone, timedelta
107+
from pymdoccbor.mdoc.issuer import MdocCborIssuer
108+
from pymdoccbor.mdoc.verifier import MobileDocument
109+
110+
CERT_INFO = {
111+
"country_name": "IT",
112+
"organization_name": "Example",
113+
"common_name": "Example mDL",
114+
"not_valid_before": datetime.now(timezone.utc) - timedelta(days=1),
115+
"not_valid_after": datetime.now(timezone.utc) + timedelta(days=365),
100116
}
117+
PKEY = {"KTY": "EC2", "CURVE": "P_256", "ALG": "ES256", "D": os.urandom(32), "KID": b"kid"}
118+
DATA = {"org.micov.medical.1": {"family_name": "Test", "given_name": "User"}}
119+
120+
issuer = MdocCborIssuer(private_key=PKEY, alg="ES256", cert_info=CERT_INFO)
121+
issuer.new(data=DATA, doctype="org.micov.medical.1", validity={"issuance_date": "2025-01-01", "expiry_date": "2025-12-31"})
122+
document = issuer.signed["documents"][0]
101123

102-
doc = MobileDocument(**document) # ✅ Still works
103-
print(doc.errors) # {}
124+
doc = MobileDocument(**document)
125+
assert doc.errors == {}
104126
```
105127

106128
## ISO 18013-5 Reference

docs/SECURITY-DEPENDENCIES.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Dependency security and known issues
2+
3+
This document describes known unfixable or accepted dependency vulnerabilities and possible alternatives.
4+
5+
## ecdsa CVE-2024-23342 (Minerva timing attack, no fix)
6+
7+
**Status:** Ignored in `pip-audit` via `--ignore-vuln CVE-2024-23342`. No upstream fix is planned.
8+
9+
**What it is:** A Minerva timing attack on the P-256 curve. Using `ecdsa.SigningKey.sign_digest()` and measuring timing can leak the internal nonce and potentially the private key. Affects ECDSA signing, key generation, and ECDH. **ECDSA signature verification is not affected.**
10+
11+
**Why we have it:** The `ecdsa` package is a transitive dependency of [pycose](https://github.com/TimothyClaeys/pycose). Pycose uses `ecdsa` for deterministic ECDSA (RFC 6979); it uses [cryptography](https://cryptography.io) for other operations.
12+
13+
**Alternatives / mitigations:**
14+
15+
1. **Keep ignoring in CI**
16+
Document the risk (as here) and continue with `--ignore-vuln CVE-2024-23342`. The python-ecdsa project [considers side-channel attacks out of scope](https://github.com/tlsfuzzer/python-ecdsa/issues/330); there is no planned fix.
17+
18+
2. **Prefer Ed25519 (EdDSA) where possible**
19+
This codebase and pycose support EdDSA (e.g. `EdDSA` / Ed25519), which is not affected by this CVE. When you control key and algorithm choice, prefer Ed25519 for new use.
20+
21+
3. **Limit exposure**
22+
If your use case only **verifies** MSO/mdoc signatures (no signing with P-256 in process), the vulnerable code path (signing/keygen) may not be exercised in your deployment. Verification is explicitly unaffected.
23+
24+
4. **Upstream change**
25+
Ask or contribute to pycose to use `cryptography` for deterministic ECDSA (ES256/ES384/ES512) instead of `ecdsa`, so the dependency can be dropped once pycose supports it.
26+
27+
5. **Alternative COSE library**
28+
Switching to another COSE implementation that does not depend on `ecdsa` would remove the vulnerability from the dependency tree; this would require a larger change in this project.
29+
30+
## cryptography (previously ignored; now resolved)
31+
32+
**Status:** No longer ignored. With current dependency resolution, `pip install` picks `cryptography>=42`, so the previously known cryptography CVEs (e.g. PYSEC-2024-225, CVE-2023-50782, CVE-2024-0727, GHSA-h4gh-qq45-vh27) are addressed by the default resolution.
33+
34+
If you ever see cryptography-related vulns again in CI (e.g. after adding a new dependency that pins `cryptography<42`), options are:
35+
36+
- Prefer upgrading the constraining dependency (e.g. ensure pycose’s transitive deps use versions that allow `cryptography>=42`; [pyhpke](https://pypi.org/project/pyhpke/) 0.6.x already requires `cryptography>=42.0.1,<47`).
37+
- As a last resort, re-add temporary `--ignore-vuln` for the specific cryptography advisories and track the issue until the dependency tree can be updated.

0 commit comments

Comments
 (0)