Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
08a4e21
Switch TLS to sslyze/nassl based reimplementation
mxsasha May 2, 2023
f3cb6d6
Update for new resolver
mxsasha Mar 18, 2025
dc21f98
Switch to latest pypi nassl/sslyze
mxsasha Jul 22, 2025
ee49321
Partial update to NCSC 2025
mxsasha Jun 9, 2025
7b529b5
FFDHE2024 -> insufficient, others -> phase out
mxsasha Jun 25, 2025
df08eb0
Remove ECDHE/DHE min key size - this is superfluous
mxsasha Jun 25, 2025
f6c6c61
Update SHA2 key exchange check to new requirements (must reject SHA1 …
mxsasha Jul 1, 2025
1e4b6a0
Update cert requirements, including RSA phase out for 2048
mxsasha Jul 1, 2025
ab431c8
API upd
mxsasha Jul 1, 2025
6fe5566
Initial work in RSA-PKCS check
mxsasha Jul 8, 2025
b67c9dd
Update client initiated renegotiation limits, incl DB changes for 3 s…
mxsasha Jul 22, 2025
634c3e0
Fix issues with sigalg check, some sigalgs were not supported
mxsasha Jul 24, 2025
83e0765
Fix cert curve detection
mxsasha Jul 28, 2025
80a2a4e
Add support for EMS
mxsasha Jul 28, 2025
6ae57e5
Cleanup
mxsasha Jul 29, 2025
ca2fc6e
Add sufficient>good separate status for cipher order
mxsasha Aug 4, 2025
cfc01c0
Fix formatting in pubkey
mxsasha Aug 4, 2025
90c4d1b
Add new checks to db/categories/template
mxsasha Aug 12, 2025
e7a40d3
Remove duplicate kex hash func
mxsasha Aug 21, 2025
0fbde9d
Finetune some labels
mxsasha Aug 21, 2025
21544d6
Update ems na_no_tls_1_2 status to success
mxsasha Aug 21, 2025
3873398
TO REVERT: force pushing to the registry (required due to double-PR)
mxsasha Aug 21, 2025
5d339c6
Fix batch tests
mxsasha Aug 22, 2025
e2a7124
Update release notes / openapi.yaml
mxsasha Sep 1, 2025
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: 1 addition & 1 deletion .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ env:
# determine whether this pull request has permissions to push to registry, or artifacts
# should be used to transfer images between jobs. Forked and dependabot builds don't
# have permission to push to registry.
use_registry: ${{ ! (github.event_name == 'pull_request' && (github.event.pull_request.head.repo.full_name != github.repository || startsWith(github.head_ref, 'dependabot/'))) }}
use_registry: true

jobs:
# builds all docker images in parallel
Expand Down
40 changes: 37 additions & 3 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,32 @@

_Compared to the latest 1.10 release._

### Feature changes

- ...
### TLS updates for NCSC 2025 guidelines

All tests were updated to match the
[2025-05 version of the NCSC TLS guidelines](https://www.ncsc.nl/documenten/publicaties/2025/juni/01/ict-beveiligingsrichtlijnen-voor-transport-layer-security-2025-05).
Most significant changes:

- The requirements on TLS versions, TLS authentication, curves, hashes, key exchange algorithms, FFDHE groups,
RSA key lengths, and bulk encryption algorithms were updated to match the new guidelines.
- A test for RSA PKCS#1 v1.5 was added (only PSS padding is sufficient).
- A test for Extended Master Secret (RFC7627) was added.
- Client-initiated renegotiation is now acceptable, if limited to less than 10.
- All checks on certificates apply only to the TODO TODO certificates.

### Other TLS updates

- Certificates that do not have OCSP enabled, which means stapling is not possible,
[are now detected as such](https://github.com/internetstandards/Internet.nl/issues/1641).
Several issues with OCSP stapling reliability were also resolved.
- Issues were fixed where the cipher order failed to detect some bad scenarios,
including some where servers preferred RSA over ECDHE, or CBC over POLY1305.
- CCM_8 ciphers are now detected when enabled on a server.
- OLD ciphers are no longer detected.
- The cipher order test no longer separates between "the server cipher order preference is wrong"
and "the server has no preference".


### Significant internal changes

Expand All @@ -18,7 +41,18 @@ _Compared to the latest 1.10 release._

### API changes

- ...
This release has API version 2.7.0.

The changes noted above are reflected in the API as well, e.g. which ciphers
are considered bad, which are listed in the API output, along with score impacts.
Additionally, the API structure changes are:
- OCSP stapling has a new status `not_in_cert`, for when a certificate does not have OCSP enabled,
therefore stapling is neither required nor possible.
- The cipher order status no longer returns `not_prescribed` or `not_seclevel` for new tests.
The insufficient statuses are now `bad` for preferring phase out over good and/or sufficient;
and `sufficient_above_good` for preferring sufficient over good.
- `extended_master_secret_status` and `kex_rsa_pkcs` were added to the TLS details.
- `client_reneg` in the TLS details was changed from a boolean to a new enum.


## 1.10.6
Expand Down
234 changes: 218 additions & 16 deletions checks/categories.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import Optional

from checks import scoring
from checks.models import TLSClientInitiatedRenegotiationStatus, KexRSAPKCSStatus, TLSExtendedMasterSecretStatus
from checks.scoring import (
ORDERED_STATUSES,
STATUS_ERROR,
Expand Down Expand Up @@ -184,6 +185,8 @@ def __init__(self, name="web-tls"):
WebTlsZeroRTT,
WebTlsOCSPStapling,
WebTlsKexHashFunc,
WebTlsKexRSAPKCSStatus,
WebTLSExtendedMasterSecret,
# WebTlsDaneRollover,
]
super().__init__(name, subtests)
Expand Down Expand Up @@ -256,6 +259,8 @@ def __init__(self, name="mail-tls"):
MailTlsDaneRollover,
MailTlsZeroRTT,
MailTlsKexHashFunc,
MailTlsKexRSAPKCSStatus,
MailTLSExtendedMasterSecret,
# MailTlsOCSPStapling, # Disabled for mail.
]
super().__init__(name, subtests)
Expand Down Expand Up @@ -1095,6 +1100,11 @@ def result_na(self):
self.verdict = "detail web tls cipher-order verdict na"
self.tech_data = ""

def result_sufficient_above_good(self):
self._status(STATUS_INFO)
self.verdict = "detail web tls cipher-order verdict sufficient-above-good"
self.tech_data = ""


class WebTlsVersion(Subtest):
def __init__(self):
Expand Down Expand Up @@ -1183,14 +1193,27 @@ def __init__(self):
model_score_field="client_reneg_score",
)

def result_good(self):
def save_result(self, status: TLSClientInitiatedRenegotiationStatus):
handlers = {
TLSClientInitiatedRenegotiationStatus.not_allowed: self.result_not_allowed,
TLSClientInitiatedRenegotiationStatus.allowed_with_low_limit: self.result_allowed_with_low_limit,
TLSClientInitiatedRenegotiationStatus.allowed_with_too_high_limit: self.result_allowed_with_too_high_limit,
}
return handlers[status]()

def result_not_allowed(self):
self._status(STATUS_SUCCESS)
self.verdict = "detail web tls renegotiation-client verdict good"
self.verdict = "detail web tls renegotiation-client verdict not-allowed"
self.tech_data = "detail tech data no"

def result_bad(self):
def result_allowed_with_low_limit(self):
self._status(STATUS_INFO)
self.verdict = "detail web tls renegotiation-client verdict allowed-with-low-limit"
self.tech_data = "detail tech data phase-out"

def result_allowed_with_too_high_limit(self):
self._status(STATUS_FAIL)
self.verdict = "detail web tls renegotiation-client verdict bad"
self.verdict = "detail web tls renegotiation-client verdict allowed-with-too-high-limit"
self.tech_data = "detail tech data yes"


Expand Down Expand Up @@ -1488,18 +1511,101 @@ def __init__(self):
def result_good(self):
self._status(STATUS_SUCCESS)
self.verdict = "detail web tls kex-hash-func verdict good"
self.tech_data = "detail tech data yes"
self.tech_data = "detail tech data good"

def result_bad(self):
self._status(STATUS_FAIL)
self.verdict = "detail web tls kex-hash-func verdict phase-out"
self.tech_data = "detail tech data no"
self.verdict = "detail web tls kex-hash-func verdict bad"
self.tech_data = "detail tech data insufficient"

def result_unknown(self):
self._status(STATUS_INFO)
self.verdict = "detail web tls kex-hash-func verdict other"
self.tech_data = "detail tech data not-applicable"

def result_phase_out(self):
self._status(STATUS_NOTICE)
self.verdict = "detail web tls kex-hash-func verdict phase-out"
self.tech_data = "detail tech data phase-out"


class WebTlsKexRSAPKCSStatus(Subtest):
def __init__(self):
super().__init__(
name="key_exchange_rsa_pkcs",
label="detail web tls key-exchange-rsa-pkcs label",
explanation="detail web tls key-exchange-rsa-pkcs exp",
tech_string="detail web tls key-exchange-rsa-pkcs tech table",
worst_status=scoring.TLS_KEX_RSA_PKCS_WORST_STATUS,
full_score=scoring.TLS_KEX_RSA_PKCS_GOOD,
model_score_field="key_exchange_rsa_pkcs_score",
)

def save_result(self, status: KexRSAPKCSStatus):
handlers = {
KexRSAPKCSStatus.good: self.result_good,
KexRSAPKCSStatus.bad: self.result_bad,
KexRSAPKCSStatus.unknown: self.result_unknown,
}
return handlers[status]()

def result_good(self):
self._status(STATUS_SUCCESS)
self.verdict = "detail web tls key-exchange-rsa-pkcs verdict good"
self.tech_data = "detail tech data good"

def result_bad(self):
self._status(STATUS_FAIL)
self.verdict = "detail web tls key-exchange-rsa-pkcs verdict bad"
self.tech_data = "detail tech data insufficient"

def result_unknown(self):
self._status(STATUS_INFO)
self.verdict = "detail web tls key-exchange-rsa-pkcs verdict other"
self.tech_data = "detail tech data not-applicable"


class WebTLSExtendedMasterSecret(Subtest):
def __init__(self):
super().__init__(
name="extended_master_secret",
label="detail web tls extended-master-secret label",
explanation="detail web tls extended-master-secret exp",
tech_string="detail web tls extended-master-secret tech table",
worst_status=scoring.TLS_EXTENDED_MASTER_SECRET_WORST_STATUS,
full_score=scoring.TLS_EXTENDED_MASTER_SECRET_GOOD,
model_score_field="extended_master_secret_score",
)

def save_result(self, status: TLSExtendedMasterSecretStatus):
handlers = {
TLSExtendedMasterSecretStatus.supported: self.result_good,
TLSExtendedMasterSecretStatus.na_no_tls_1_2: self.result_na_no_tls_1_2,
TLSExtendedMasterSecretStatus.not_supported: self.result_bad,
TLSExtendedMasterSecretStatus.unknown: self.result_unknown,
}
return handlers[status]()

def result_good(self):
self._status(STATUS_SUCCESS)
self.verdict = "detail web tls extended-master-secret verdict good"
self.tech_data = "detail tech data good"

def result_bad(self):
self._status(STATUS_FAIL)
self.verdict = "detail web tls extended-master-secret verdict bad"
self.tech_data = "detail tech data insufficient"

def result_unknown(self):
self._status(STATUS_INFO)
self.verdict = "detail web tls extended-master-secret verdict unknown"
self.tech_data = "detail tech data not-applicable"

def result_na_no_tls_1_2(self):
self._status(STATUS_SUCCESS)
self.verdict = "detail web tls extended-master-secret verdict na-no-tls-1-2"
self.tech_data = "detail tech data phase-out"


class MailTlsStarttlsExists(Subtest):
def __init__(self):
Expand Down Expand Up @@ -1676,6 +1782,11 @@ def result_na(self):
self.verdict = "detail mail tls cipher-order verdict na"
self.tech_data = ""

def result_sufficient_above_good(self):
self._status(STATUS_INFO)
self.verdict = "detail web tls cipher-order verdict sufficient-above-good"
self.tech_data = ""


class MailTlsVersion(Subtest):
def __init__(self):
Expand Down Expand Up @@ -1780,19 +1891,27 @@ def __init__(self):
model_score_field="client_reneg_score",
)

def was_tested(self):
self.worst_status = scoring.MAIL_TLS_CLIENT_RENEG_WORST_STATUS
def save_result(self, status: TLSClientInitiatedRenegotiationStatus):
handlers = {
TLSClientInitiatedRenegotiationStatus.not_allowed: self.result_not_allowed,
TLSClientInitiatedRenegotiationStatus.allowed_with_low_limit: self.result_allowed_with_low_limit,
TLSClientInitiatedRenegotiationStatus.allowed_with_too_high_limit: self.result_allowed_with_too_high_limit,
}
return handlers[status]()

def result_good(self):
self.was_tested()
def result_not_allowed(self):
self._status(STATUS_SUCCESS)
self.verdict = "detail mail tls renegotiation-client verdict good"
self.verdict = "detail mail tls renegotiation-client verdict not-allowed"
self.tech_data = "detail tech data no"

def result_bad(self):
self.was_tested()
def result_allowed_with_low_limit(self):
self._status(STATUS_INFO)
self.verdict = "detail mail tls renegotiation-client verdict allowed-with-low-limit"
self.tech_data = "detail tech data phase-out"

def result_allowed_with_too_high_limit(self):
self._status(STATUS_FAIL)
self.verdict = "detail mail tls renegotiation-client verdict bad"
self.verdict = "detail mail tls renegotiation-client verdict allowed-with-too-high-limit"
self.tech_data = "detail tech data yes"


Expand Down Expand Up @@ -2069,10 +2188,93 @@ def result_bad(self):
def result_unknown(self):
self.was_tested()
self._status(STATUS_INFO)
self.verdict = "detail mail tls kex-hash-func verdict other"
self.verdict = "detail mail tls kex-hash-func verdict unknown"
self.tech_data = "detail tech data not-applicable"

def result_phase_out(self):
self._status(STATUS_NOTICE)
self.verdict = "detail web tls kex-hash-func verdict phase-out"
self.tech_data = "detail tech data phase-out"


class MailTlsKexRSAPKCSStatus(Subtest):
def __init__(self):
super().__init__(
name="key_exchange_rsa_pkcs",
label="detail mail tls key-exchange-rsa-pkcs label",
explanation="detail mail tls key-exchange-rsa-pkcs exp",
tech_string="detail mail tls key-exchange-rsa-pkcs tech table",
worst_status=scoring.TLS_KEX_RSA_PKCS_WORST_STATUS,
full_score=scoring.TLS_KEX_RSA_PKCS_GOOD,
model_score_field="key_exchange_rsa_pkcs_score",
)

def save_result(self, status: KexRSAPKCSStatus):
handlers = {
KexRSAPKCSStatus.good: self.result_good,
KexRSAPKCSStatus.bad: self.result_bad,
KexRSAPKCSStatus.unknown: self.result_unknown,
}
return handlers[status]()

def result_good(self):
self._status(STATUS_SUCCESS)
self.verdict = "detail mail tls key-exchange-rsa-pkcs verdict good"
self.tech_data = "detail tech data good"

def result_bad(self):
self._status(STATUS_FAIL)
self.verdict = "detail mail tls key-exchange-rsa-pkcs verdict bad"
self.tech_data = "detail tech data insufficient"

def result_unknown(self):
self._status(STATUS_INFO)
self.verdict = "detail mail tls key-exchange-rsa-pkcs verdict unknown"
self.tech_data = "detail tech data not-applicable"


class MailTLSExtendedMasterSecret(Subtest):
def __init__(self):
super().__init__(
name="extended_master_secret",
label="detail mail tls extended-master-secret label",
explanation="detail mail tls extended-master-secret exp",
tech_string="detail mail tls extended-master-secret tech table",
worst_status=scoring.TLS_EXTENDED_MASTER_SECRET_WORST_STATUS,
full_score=scoring.TLS_EXTENDED_MASTER_SECRET_GOOD,
model_score_field="extended_master_secret_score",
)

def save_result(self, status: TLSExtendedMasterSecretStatus):
handlers = {
TLSExtendedMasterSecretStatus.supported: self.result_good,
TLSExtendedMasterSecretStatus.na_no_tls_1_2: self.result_na_no_tls_1_2,
TLSExtendedMasterSecretStatus.not_supported: self.result_bad,
TLSExtendedMasterSecretStatus.unknown: self.result_unknown,
}
return handlers[status]()

def result_good(self):
self._status(STATUS_SUCCESS)
self.verdict = "detail mail tls extended-master-secret verdict good"
self.tech_data = "detail tech data good"

def result_bad(self):
self._status(STATUS_FAIL)
self.verdict = "detail mail tls extended-master-secret verdict bad"
self.tech_data = "detail tech data insufficient"

def result_unknown(self):
self._status(STATUS_INFO)
self.verdict = "detail mail tls extended-master-secret verdict unknown"
self.tech_data = "detail tech data not-applicable"

def result_na_no_tls_1_2(self):
self._status(STATUS_SUCCESS)
self.verdict = "detail mail tls extended-master-secret verdict na-no-tls-1-2"
self.tech_data = "detail tech data phase-out"


class MailTlsDaneExists(Subtest):
def __init__(self):
super().__init__(
Expand Down
Loading