-
-
Notifications
You must be signed in to change notification settings - Fork 75
[change] Migrate from PyOpenSSL to cryptography #131 #194
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Replaced PyOpenSSL with cryptography with retained functionalities. Fixes #131
nemesifier
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Quick review
PEM validation now catches generic Exception, which can hide the real cause of failures and make debugging harder.
The use of default_backend() is deprecated in recent cryptography versions and may cause warnings or issues in the future.
The issuer check (cert.issuer == ca_cert.subject) may be too strict and could reject otherwise valid certificates due to encoding or ordering differences.
Extension parsing is somewhat permissive and may silently ignore unsupported or malformed values instead of failing fast.
What do you think?
|
@nemesifier Instead of a generic Yeah, I have removed all references to This was a bit tricky, you are totally right here. I initially tried issuer_identity = set((attr.oid, str(attr.value).strip().lower()) for attr in cert.issuer)
ca_identity = set((attr.oid, str(attr.value).strip().lower()) for attr in ca_cert.subject)We can make it failfast by checking immediately for required structure and supported values, like: for ext in self.extensions:
if not all(k in ext for k in ("name", "critical", "value")):
raise ValidationError("Each extension must have 'name', 'critical', and 'value'.")
if ext["name"] == "extendedKeyUsage":
allowed = ["serverAuth", "clientAuth"]
provided = [v.strip() for v in ext["value"].split(",")]
for p in provided:
if p not in allowed:
raise ValidationError(f"Unsupported EKU: {p}") |
nemesifier
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not exhaustive but I was able to scan at least 50% of the code.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
📝 WalkthroughWalkthroughReplaces pyOpenSSL usage with the cryptography library across models, tests, CRL generation, PEM validation, certificate building/signing, verification, extension handling, and renewal propagation; requirements drop pyOpenSSL. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Model as Model method
participant Builder as CertificateBuilder
participant Crypto as Cryptography primitives
participant CAKey as CA Private Key
participant DB as Database
rect rgba(0,128,96,0.08)
Note over Model,Builder: Certificate generation / renewal (cryptography)
Model->>Builder: call _get_subject(), set subject, issuer, serial, validity
Model->>Crypto: generate or load private/public key
Model->>Builder: add public_key
Model->>Builder: call _add_extensions(builder, public_key)
Builder->>CAKey: sign(builder, chosen hash algorithm)
CAKey-->>Builder: cryptography.x509.Certificate (signed)
Builder->>DB: persist PEM (certificate ± private key)
DB-->>Model: persisted certificate record
end
alt Renewal with issued certs
Note over Model: propagate renew to children
Model->>Model: iterate issued_certificates / cert_set
Model->>Builder: regenerate child certs signed by renewed CA
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
📜 Recent review detailsConfiguration used: defaults Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🔇 Additional comments (10)
Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
♻️ Duplicate comments (3)
django_x509/tests/test_cert.py (1)
16-24: DRY violation: Same duplication as in test_ca.py.As noted in past review comments, these time constants and helper function should be moved to a shared module.
django_x509/base/models.py (1)
444-449: Replace broadexcept Exceptionwith specific cryptography exceptions.This duplicates the concern from past review comments. The broad exception catch can mask unrelated errors. Use specific exceptions like
InvalidSignaturefromcryptography.exceptions.🔎 Proposed fix
+from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm # ... in _verify_ca method: try: if isinstance(ca_pubkey, rsa.RSAPublicKey): ca_pubkey.verify( cert.signature, cert.tbs_certificate_bytes, padding.PKCS1v15(), cert.signature_hash_algorithm, ) elif isinstance(ca_pubkey, ec.EllipticCurvePublicKey): ca_pubkey.verify( cert.signature, cert.tbs_certificate_bytes, ec.ECDSA(cert.signature_hash_algorithm), ) else: ca_pubkey.verify(cert.signature, cert.tbs_certificate_bytes) - except Exception: + except InvalidSignature: raise ValidationError( _("Cryptographic signature verification failed: CA does not match.") - ) + ) from Nonedjango_x509/tests/test_ca.py (1)
20-33: DRY violation:datetime_to_stringand time constants are duplicated.Per the past review comment, these constants (
generalized_time,utc_time) and thedatetime_to_stringfunction are duplicated betweentest_ca.pyandtest_cert.py. They should be moved to a shared location like__init__.py.🔎 Proposed refactor - move to django_x509/tests/__init__.py
In
django_x509/tests/__init__.py:generalized_time = "%Y%m%d%H%M%SZ" utc_time = "%y%m%d%H%M%SZ" def datetime_to_string(datetime_): if datetime_.year < 2050: return datetime_.strftime(utc_time) return datetime_.strftime(generalized_time)Then import in both test files:
from . import TestX509Mixin, datetime_to_string, generalized_time, utc_time
🧹 Nitpick comments (3)
django_x509/base/models.py (2)
618-650: Removedefault_backend()usage in CRL generation for consistency.Similar to the earlier comment,
default_backend()is deprecated and should be removed for consistency with modern cryptography library usage.🔎 Proposed fix
def crl(self): """ Returns up to date CRL of this CA """ - backend = default_backend() ca_cert = x509.load_pem_x509_certificate(self.certificate.encode()) - # import ipdb; ipdb.set_trace() - pkey_kwargs = {"backend": backend, "password": None} + pkey_kwargs = {"password": None} if self.passphrase: pkey_kwargs["password"] = self.passphrase.encode() private_key = serialization.load_pem_private_key( self.private_key.encode(), **pkey_kwargs ) now = timezone.now() builder = ( x509.CertificateRevocationListBuilder() .issuer_name(ca_cert.subject) .last_update(now) .next_update(now + timedelta(days=1)) ) for cert in self.get_revoked_certs(): revoked = ( x509.RevokedCertificateBuilder() .serial_number(int(cert.serial_number)) .revocation_date(now) .add_extension( x509.CRLReason(x509.ReasonFlags.unspecified), critical=False, ) - .build(backend) + .build() ) builder = builder.add_revoked_certificate(revoked) crl = builder.sign( private_key=private_key, algorithm=hashes.SHA256(), - backend=backend, ) return crl.public_bytes(encoding=serialization.Encoding.PEM)Also, there's a leftover debug comment
# import ipdb; ipdb.set_trace()at line 620 that should be removed.
197-199: Removedefault_backend()call to align with cryptography library best practices.In cryptography ≥3.1,
default_backend()is no longer required—the backend is automatically provided. The project uses cryptography ~46.0.3, which supports calling the API without the backend parameter. The import ofdefault_backendcan also be removed.Proposed change
if self.certificate: return x509.load_pem_x509_certificate( - self.certificate.encode("utf-8"), default_backend() + self.certificate.encode("utf-8") )Also remove the import:
-from cryptography.hazmat.backends import default_backenddjango_x509/tests/test_ca.py (1)
211-253: Consider using direct attribute assignment instead ofsetattr.Per static analysis (B010), using
setattrwith a constant attribute is no safer than direct assignment. While this pattern is common in tests for temporarily modifying settings, direct assignment is more readable.🔎 Example refactor
def test_basic_constraints_not_critical(self): - setattr(app_settings, "CA_BASIC_CONSTRAINTS_CRITICAL", False) + app_settings.CA_BASIC_CONSTRAINTS_CRITICAL = False ca = self._create_ca() ext = ca.x509.extensions.get_extension_for_class(x509.BasicConstraints) self.assertFalse(ext.critical) - setattr(app_settings, "CA_BASIC_CONSTRAINTS_CRITICAL", True) + app_settings.CA_BASIC_CONSTRAINTS_CRITICAL = True
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
django_x509/base/models.pydjango_x509/tests/test_ca.pydjango_x509/tests/test_cert.pyrequirements.txt
💤 Files with no reviewable changes (1)
- requirements.txt
🧰 Additional context used
🧬 Code graph analysis (1)
django_x509/tests/test_cert.py (4)
django_x509/base/models.py (8)
x509(192-199)_verify_ca(401-449)pkey(224-233)x509_text(202-221)_get_subject(330-352)save(183-189)renew(568-575)renew(601-611)django_x509/tests/__init__.py (3)
TestX509Mixin(23-66)_create_cert(43-66)_create_ca(24-41)django_x509/models.py (1)
Cert(16-23)tests/openwisp2/sample_x509/models.py (1)
Cert(22-28)
🪛 Gitleaks (8.30.0)
django_x509/tests/test_ca.py
[high] 65-74: Identified a Private Key, which may compromise cryptographic security and sensitive data encryption.
(private-key)
🪛 Ruff (0.14.10)
django_x509/tests/test_ca.py
215-215: Do not call setattr with a constant attribute value. It is not any safer than normal property access.
Replace setattr with assignment
(B010)
218-218: Do not call setattr with a constant attribute value. It is not any safer than normal property access.
Replace setattr with assignment
(B010)
222-222: Do not call setattr with a constant attribute value. It is not any safer than normal property access.
Replace setattr with assignment
(B010)
225-225: Do not call setattr with a constant attribute value. It is not any safer than normal property access.
Replace setattr with assignment
(B010)
229-229: Do not call setattr with a constant attribute value. It is not any safer than normal property access.
Replace setattr with assignment
(B010)
239-239: Do not call setattr with a constant attribute value. It is not any safer than normal property access.
Replace setattr with assignment
(B010)
243-243: Do not call setattr with a constant attribute value. It is not any safer than normal property access.
Replace setattr with assignment
(B010)
246-246: Do not call setattr with a constant attribute value. It is not any safer than normal property access.
Replace setattr with assignment
(B010)
253-253: Do not call setattr with a constant attribute value. It is not any safer than normal property access.
Replace setattr with assignment
(B010)
django_x509/tests/test_cert.py
236-236: Do not call setattr with a constant attribute value. It is not any safer than normal property access.
Replace setattr with assignment
(B010)
240-240: Do not call setattr with a constant attribute value. It is not any safer than normal property access.
Replace setattr with assignment
(B010)
243-243: Do not call setattr with a constant attribute value. It is not any safer than normal property access.
Replace setattr with assignment
(B010)
248-250: Do not call setattr with a constant attribute value. It is not any safer than normal property access.
Replace setattr with assignment
(B010)
472-472: Possible hardcoded password assigned to argument: "passphrase"
(S106)
django_x509/base/models.py
244-244: Do not catch blind exception: Exception
(BLE001)
263-263: Do not catch blind exception: Exception
(BLE001)
427-429: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
446-446: Do not catch blind exception: Exception
(BLE001)
🔇 Additional comments (13)
django_x509/base/models.py (4)
6-11: LGTM - Proper cryptography imports for the migration.The imports correctly bring in the necessary cryptography components for certificate handling, including x509 module, hash algorithms, serialization, asymmetric key types, and OIDs.
280-328: LGTM - Clean migration to cryptography CertificateBuilder.The
_generatemethod properly:
- Clears cached properties before regeneration
- Uses
rsa.generate_private_keywith appropriate parameters- Builds certificates using
x509.CertificateBuilderwith correct subject/issuer handling- Maps digest algorithms correctly
- Handles passphrase encryption with
BestAvailableEncryption
330-352: LGTM - Subject construction using NameOID attributes.The
_get_subjectmethod correctly maps model fields to X.509 NameOIDs and only includes non-empty values. The string coercion handles foreign key redefinition in subclasses.
607-611: LGTM - CA renewal properly propagates to child certificates.The renewal logic correctly falls back between
issued_certificatesandcert_setrelations and renews all child certificates with the updated CA.django_x509/tests/test_ca.py (4)
65-74: Test private key is acceptable for testing purposes.The Gitleaks static analysis flags this as a private key exposure. However, this is intentionally a test-only private key used for import testing and poses no security risk. This is a false positive that can be safely ignored.
76-109: LGTM - Test correctly validates cryptography-based CA creation.The test properly verifies:
- Serial number matches between model and x509 object
- BasicConstraints path_length is set correctly
- Subject attributes are extracted via OID-based lookups
- Issuer equals subject for self-signed CA
- Certificate version is v3
- BasicConstraints extension is critical and marks CA as true
637-668: LGTM - Comprehensive CA renewal test with child certificate verification.The test properly validates:
- CA certificate, key, and validity_end are updated after renewal
- All child certificates are renewed with new certificates, keys, and serial numbers
- Validity end dates are extended
693-700: LGTM - Good addition to verify serial number synchronization.This test ensures the serial number stored in the model matches the serial number embedded in the PEM certificate after renewal, catching potential bugs in the renewal order.
django_x509/tests/test_cert.py (5)
96-132: LGTM - Comprehensive certificate creation test with cryptography.The test properly validates:
- Certificate and private key are generated
- Serial number and version are correct
- Subject attributes match model fields via OID lookups
- Issuer matches CA subject
- CA signature verification works
- BasicConstraints marks it as non-CA
268-285: LGTM - Extension test correctly validates nsCertType and extendedKeyUsage.The test verifies:
- nsCertType extension is added with correct OID and value
- extendedKeyUsage extension has correct criticality
- CLIENT_AUTH OID is present in extended key usage
Note: This test will need updating if the nsCertType fix (to respect the input value) is implemented in models.py - currently it checks for hardcoded
b"\x03\x02\x07\x80"which matches the hardcoded implementation.
324-342: LGTM - Good test coverage for_get_subjectwith None attributes.This test properly verifies that:
- Only set attributes appear in the subject
- Empty/unset attributes are excluded
- Dynamically adding attributes works correctly
402-424: Good: Added proper failure assertion for serial clash test.The
else: self.fail(...)clause ensures the test fails if no ValidationError is raised, making the test more robust.
477-498: LGTM - Certificate renewal test properly verifies CA is unchanged.The test correctly validates:
- Certificate, key, validity_end, and serial_number are updated
- The associated CA remains unchanged after cert renewal
- Uses
refresh_from_db()to ensure database state is verified
Replaced hardcoded client bitstring with dynamic bitmask mapping. Fixes #131
Will address this in next commit. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🧹 Nitpick comments (1)
django_x509/base/models.py (1)
447-459: Consider adding value validation in extension format check.The current implementation only validates the structure of extensions (dict with required keys) but doesn't validate the values. While
_add_extensionsperforms some validation later (e.g., fornsCertType), catching invalid extension values early would provide better error messages to users and fail faster.🔎 Suggested approach
Consider adding validation for extension values in this method:
- Validate that
nameis a supported extension type- Validate that
valueis appropriate for the extension type- Validate that
criticalis a booleanThis would catch configuration errors earlier in the flow rather than during certificate generation.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
django_x509/base/models.py
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
- GitHub Check: Python==3.11 | django~=4.2.0
- GitHub Check: Python==3.10 | django~=5.1.0
- GitHub Check: Python==3.12 | django~=5.0.0
- GitHub Check: Python==3.10 | django~=5.2.0
- GitHub Check: Python==3.12 | django~=5.2.0
- GitHub Check: Python==3.13 | django~=5.2.0
- GitHub Check: Python==3.11 | django~=5.2.0
- GitHub Check: Python==3.11 | django~=5.0.0
🔇 Additional comments (2)
django_x509/base/models.py (2)
619-629: LGTM: CA renewal with child certificate propagation.The renewal logic correctly handles both the CA and its issued certificates. The use of
getattrto support bothissued_certificatesandcert_setprovides backward compatibility while allowing for the relationship name to be customized.
631-667: LGTM: CRL generation migrated correctly to cryptography.The CRL generation has been properly migrated to use the cryptography library's
CertificateRevocationListBuilder. The implementation correctly handles password-protected private keys, builds revoked certificate entries, and signs the CRL.
Replaced permissive filtering with explicit validation for EKU and nsCertType. Fixes #131
| KEY_LENGTH_CHOICES = ( | ||
| ("512", "512"), | ||
| ("1024", "1024"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| KEY_LENGTH_CHOICES = ( | |
| ("512", "512"), | |
| ("1024", "1024"), | |
| KEY_LENGTH_CHOICES = ( | |
| ("224", "224"), | |
| ("256", "256"), | |
| ("384", "384"), | |
| ("512", "512"), | |
| ("1024", "1024"), |
now that we are switching to cryptography and using cert.signature_hash_algorithm.name.lower() just by adding these key length values to the KEY_LENGTH_CHOICES should also directly solve #118
nemesifier
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work @stktyagi, thanks also to @davidjmemmett for this support on this!
Replaced PyOpenSSL with cryptography with retained functionalities.
Fixes #131
Checklist
Reference to Existing Issue
Closes #131.
Description of Changes
Migrated the cryptographic engine from PyOpenSSL to the cryptography library and resolved the renewal bug by fixing the serial number update order.
Summary by CodeRabbit
Refactor
New Features
Chores
Tests
✏️ Tip: You can customize this high-level summary in your review settings.