mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-24 15:52:20 +02:00
fix(security): refresh client trust and publish validated SearXNG images
Repair SearXNG system and certifi stores, validate native amd64/arm64 runtime images through Compose, and publish the exact verified artifacts through a trusted CI job. Adopt released Python, certifi, grpcio, uv and Docker CLI trust fixes, with strict controls for all fourteen public incident chains. Part of #6784 and #6790. Co-authored-by: Léo <leograndcontact@gmail.com>
This commit is contained in:
Vendored
+7
-3
@@ -4,12 +4,16 @@ The public certificates embedded in `tests/test_ca_trust.py` exercise the Octobe
|
||||
|
||||
Sources: the [incident root list](https://emudhra.we-have-a-problem.com/roots.json) and the public Mozilla root certificates distributed in Debian's `ca-certificates` package.
|
||||
|
||||
`incident.json` contains the four published roots, sixteen [compromised intermediate certificates and cross-signs](https://emudhra.we-have-a-problem.com/impacted-intermediates.json), and the [twelve published server chains](https://emudhra.we-have-a-problem.com/chains.zip), retrieved on 2026-10-21. Certificates are deduplicated by SHA-256 of DER; each chain lists its leaf first. The additional cross-signed `emSign Root TLS CA - G3` issuer comes from those chains and has fingerprint `3094025792039a68af6e553686410bdab7c0d4a36b9084cba64202e35d6ebbdd`. Like the intermediates, it must not be accepted as a custom trust anchor. All data is public certificate material, with no private keys.
|
||||
`incident.json` contains the four published roots, sixteen [compromised intermediate certificates and cross-signs](https://emudhra.we-have-a-problem.com/impacted-intermediates.json), and the [fourteen published server chains](https://emudhra.we-have-a-problem.com/chains.zip), retrieved on 2026-10-21 with chains refreshed on 2026-10-23. The refresh adds the two DV G3B hosts without Certificate Transparency entries and preserves the original twelve chains. Certificates are deduplicated by SHA-256 of DER; each chain lists its leaf first. The additional cross-signed `emSign Root TLS CA - G3` issuer comes from those chains and has fingerprint `3094025792039a68af6e553686410bdab7c0d4a36b9084cba64202e35d6ebbdd`. Like the intermediates, it must not be accepted as a custom trust anchor. All data is public certificate material, with no private keys.
|
||||
|
||||
Downloaded source SHA-256 values:
|
||||
|
||||
- `roots.json`: `119f32cd81a6b5a4e3c4c2df5fe07daa90dc43cafa123a62f0394f1895d92380`
|
||||
- `impacted-intermediates.json`: `f67b3abe9b4879f4e91874ed041beccd71257f1db41c588c724ccf12aa90c59e`
|
||||
- `chains.zip`: `8a2edc81b6c51e3ad27d45ca1e441ec84c961e2dee0fc547f2a02c0f74315f95`
|
||||
- `chains.zip`: `7b949956955fc501a30f4d98d2f513d34c0bbbe92e29a968194c96e097ce2345`
|
||||
|
||||
The offline OpenSSL regression verifies server purpose, hostname and strict certificate rules with partial-chain trust, at `2026-10-21T12:00:00Z` (`1792584000`). Fixing the verification date keeps later certificate expiry from masking a broken mitigation. All twelve chains must verify against the affected positive-control roots and fail against the unaffected G1-only store. Adding the compromised DV G3B intermediate restores two paths, which the checker must reject. The regression makes no network requests and skips chain verification if the OpenSSL executable is unavailable; certificate-inventory tests still run using Python's standard library.
|
||||
The offline OpenSSL regression verifies server purpose, hostname and strict certificate rules with partial-chain trust, at `2026-10-23T12:00:00Z` (`1792756800`). Fixing the verification date keeps later certificate expiry from masking a broken mitigation. All fourteen chains must verify against the affected positive-control roots and fail against the unaffected G1-only store. Adding the compromised DV G3B intermediate restores four paths, including the two new hosts, which the checker must reject. The regression makes no network requests and skips chain verification if the OpenSSL executable is unavailable; certificate-inventory tests still run using Python's standard library.
|
||||
|
||||
The SearXNG derived-image validation uses the same `incident.json` inventory and fixed verification timestamp, checking each stored DER fingerprint before use. See [the image documentation](../../../docs/searxng-trust-image.md) for its validation targets and source provenance.
|
||||
|
||||
`emsign-g1.txt` is the unaffected public emSign Root CA - G1 certificate in PEM format, used by the SearXNG regressions to verify preservation of unrelated trust. Its fingerprint is SHA-256 `40f6af0346a99aa1cd1d555a4e9cce62c7f9634603ee406615833dc8c8d00367`.
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYD
|
||||
VQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBU
|
||||
ZWNobm9sb2dpZXMgTGltaXRlZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBH
|
||||
MTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgxODMwMDBaMGcxCzAJBgNVBAYTAklO
|
||||
MRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVkaHJhIFRlY2hub2xv
|
||||
Z2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIBIjAN
|
||||
BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQz
|
||||
f2N4aLTNLnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO
|
||||
8oG0x5ZOrRkVUkr+PHB1cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aq
|
||||
d7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHWDV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhM
|
||||
tTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ6DqS0hdW5TUaQBw+jSzt
|
||||
Od9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrHhQIDAQAB
|
||||
o0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQD
|
||||
AgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31x
|
||||
PaOfG1vR2vjTnGs2vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjM
|
||||
wiI/aTvFthUvozXGaCocV685743QNcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6d
|
||||
GNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q+Mri/Tm3R7nrft8EI6/6nAYH
|
||||
6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeihU80Bv2noWgby
|
||||
RQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx
|
||||
iN66zB+Afko=
|
||||
-----END CERTIFICATE-----
|
||||
+12
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"verification_time": 1792584000,
|
||||
"verification_time": 1792756800,
|
||||
"roots": {
|
||||
"7dd78d5f4f13459a83dff9abbba62edbaf6f2d102bf257fd712f4d9f2746ed8d": "emSign Root TLS CA - G3",
|
||||
"f2edaaff3d9b701fb4d14c9f285f8c0bad90fc3bcc1fa07d47f4718edaba34a5": "emSign Root CS CA - G3",
|
||||
@@ -26,6 +26,7 @@
|
||||
},
|
||||
"certificates": {
|
||||
"0164d7ceae5bb909c272af151ff44b1177ae9b47d9424b94f1d9558c30934d19": "-----BEGIN CERTIFICATE-----\nMIIDaDCCAu6gAwIBAgIPWIsuPCu4zaE+GhIJJUMhMAoGCCqGSM49BAMDMFYxCzAJ\nBgNVBAYTAklOMSUwIwYDVQQKDBxlTXVkaHJhIFRlY2hub2xvZ2llcyBMaW1pdGVk\nMSAwHgYDVQQDDBdlbVNpZ24gUm9vdCBUTFMgQ0EgLSBHMzAeFw0yNTA0MzAxNTM1\nMDlaFw00MDA0MjgxODMwMDBaMEAxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtlTXVk\naHJhIEluYzEbMBkGA1UEAwwSRU0gRFYgVExTIENBIC0gRzNDMHYwEAYHKoZIzj0C\nAQYFK4EEACIDYgAEOnp4YsjETTJN3WkDOmeCwO/u/kBV5ooSP6A3G6O9kZRRTuFy\npDPjYw8CvRiAN9nlFJ+bd9uUooHfmHsv6oiLdL3+j9pMEJnubKGVZv4+8OX2sJpV\nbMyLpQVh3xsqRB0Bo4IBljCCAZIwHwYDVR0jBBgwFoAU4TqrIXzx401UrtRyt/2L\nWUIgkFQwHQYDVR0OBBYEFK5+EFw8KZbBqEcnBVnx4D3U5QrzMB0GA1UdJQQWMBQG\nCCsGAQUFBwMBBggrBgEFBQcDAjBdBgNVHSAEVjBUMDkGCysGAQQBg44hAQABMCow\nKAYIKwYBBQUHAgEWHGh0dHA6Ly9yZXBvc2l0b3J5LmVtc2lnbi5jb20wDQYLKwYB\nBAGDjiEBAmQwCAYGZ4EMAQIBMHUGCCsGAQUFBwEBBGkwZzAkBggrBgEFBQcwAYYY\naHR0cDovL29jc3AtYy5lbXNpZ24uY29tMD8GCCsGAQUFBzAChjNodHRwOi8vcmVw\nb3NpdG9yeS5lbXNpZ24uY29tL2NlcnRzL0VNRFZUTFNDQUczQy5wN2MwNwYDVR0f\nBDAwLjAsoCqgKIYmaHR0cDovL2NybC5lbXNpZ24uY29tLz9yb290dGxzY2FnMy5j\ncmwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwCgYIKoZIzj0E\nAwMDaAAwZQIxAMEN2HWMocbuWdy1d33pH6JpMvP/V/DIS5PtZbv1Q7TXEbxrcyxg\nyuddOJscYDJJlAIwB4sH1BpbScDzkdD4tJBSwrG/1Mx82vb8YaySehNuu1+1oXNd\nAEKwaJ0mVzScy2+6\n-----END CERTIFICATE-----\n",
|
||||
"0cbfe22b824385823fd3bf4be4d131e8901342933a7feef1cf8b3110b8e44a70": "-----BEGIN CERTIFICATE-----\nMIICQTCCAcigAwIBAgIUG01Z/jOotSvB49K4K27kFjpdnWMwCgYIKoZIzj0EAwIw\nPzELMAkGA1UEBhMCTkwxEzARBgNVBAoMCmVNdWRocmEgQlYxGzAZBgNVBAMMEkVN\nIERWIFRMUyBDQSAtIEczQjAeFw0yNjEwMjIxMjU3MzFaFw0yNzA1MDUxMzAyMzFa\nMDQxMjAwBgNVBAMMKWR2LWczYi1ub2N0LWVtdWRocmEud2UtaGF2ZS1hLXByb2Js\nZW0uY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEI805yWr+RxanILifET+/\nGwHsXo7dp8ghA3OgwR0Mg11Bl6iYyOzldy4iU7kM+3qfDNsUrzxvkUkJZQpYieuC\n+aOBrDCBqTAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAK\nBggrBgEFBQcDATA0BgNVHREELTArgilkdi1nM2Itbm9jdC1lbXVkaHJhLndlLWhh\ndmUtYS1wcm9ibGVtLmNvbTAdBgNVHQ4EFgQUOlRrLQGIczrmr4H+Oak06aUmJ4Yw\nHwYDVR0jBBgwFoAU4h4v5xwZOQ5VS2LBICRXNSSKJRQwCgYIKoZIzj0EAwIDZwAw\nZAIwf4ekmRgXAxXP2AjpVY2f1bO42GC7Nnc80t4OpaWJeVdXpT+X3dOsLRwlIUm0\nYbMwAjBihTx7oHfaYB3KRnFuhKqlHYmUcOe9Hjxqs6bubZohhrqMQzPocyasDhJL\nUx3HsgQ=\n-----END CERTIFICATE-----\n",
|
||||
"21f57f83631a93c98e867050075746b3361e0433c9e0e969462852476784f6f0": "-----BEGIN CERTIFICATE-----\nMIICPzCCAcagAwIBAgIUH0gWfBRnOiSjcduti/cukjDGAjIwCgYIKoZIzj0EAwIw\nPzELMAkGA1UEBhMCTkwxEzARBgNVBAoMCmVNdWRocmEgQlYxGzAZBgNVBAMMEkVN\nIE9WIFRMUyBDQSAtIEczQjAeFw0yNjEwMjAwMjIyNDJaFw0yNzA1MDMwMjI3NDJa\nMDMxMTAvBgNVBAMMKG92LWczYi1hYWEtZW11ZGhyYS53ZS1oYXZlLWEtcHJvYmxl\nbS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQ3EfjVE+6fUm9gr7UoUv6N\nYPttIdILR9OMZnAdTj+RWT1Z4fp7dpC5mat1gbIo4J6EDelcOQQz5/oGeB3gFJKw\no4GrMIGoMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG\nCCsGAQUFBwMBMDMGA1UdEQQsMCqCKG92LWczYi1hYWEtZW11ZGhyYS53ZS1oYXZl\nLWEtcHJvYmxlbS5jb20wHQYDVR0OBBYEFKzQPp6gTBOWb0TBxZbkL5zsQn3oMB8G\nA1UdIwQYMBaAFJvYZVGv5+csN60I7mkZ2b83hK4KMAoGCCqGSM49BAMCA2cAMGQC\nMARKrRRBEWY2Ix1d80DGUvC7bWENP1Q9pf6BOGF4+84UiwV5+nAuht/+hdpAanWB\nsQIwJF2dW8tpPN6lTm1kDerzG6yJAJOz4drotaTthMF2+efuv5W2FL7Dr8qZSd1z\n201o\n-----END CERTIFICATE-----\n",
|
||||
"3094025792039a68af6e553686410bdab7c0d4a36b9084cba64202e35d6ebbdd": "-----BEGIN CERTIFICATE-----\nMIIDdDCCAvmgAwIBAgIQAJYaPvsTWVdu0EAFtGhKWzAKBggqhkjOPQQDAzBrMQsw\nCQYDVQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhy\nYSBUZWNobm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290\nIENBIC0gRzMwHhcNMjQwNzExMTU0MzM2WhcNMzkwNzA5MTgzMDAwWjBWMQswCQYD\nVQQGEwJJTjElMCMGA1UECgwcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEg\nMB4GA1UEAwwXZW1TaWduIFJvb3QgVExTIENBIC0gRzMwdjAQBgcqhkjOPQIBBgUr\ngQQAIgNiAARfD9w/q1cWxL6D9bMGH6CQ4u+gAV6GN9s+PwQx9Bl4hC+0iU8QOk8m\nWO/RwXQy10DCSxT8vSIJClHKp+Ao4qJKdYeV0SwZ7LMQziyLfxIUOY6Grja/sQFw\nLKYf8EdDXAyjggF1MIIBcTAfBgNVHSMEGDAWgBR8XQKEE9TMipuBzhccLikenEhj\nQjAdBgNVHQ4EFgQU4TqrIXzx401UrtRyt/2LWUIgkFQwHQYDVR0lBBYwFAYIKwYB\nBQUHAwEGCCsGAQUFBwMCMD0GA1UdIAQ2MDQwMgYEVR0gADAqMCgGCCsGAQUFBwIB\nFhxodHRwOi8vcmVwb3NpdG9yeS5lbXNpZ24uY29tMHgGCCsGAQUFBwEBBGwwajAi\nBggrBgEFBQcwAYYWaHR0cDovL29jc3AuZW1zaWduLmNvbTBEBggrBgEFBQcwAoY4\naHR0cDovL3JlcG9zaXRvcnkuZW1zaWduLmNvbS9jZXJ0cy9lbVNpZ25FQ0NSb290\nQ0FHMy5jcnQwMwYDVR0fBCwwKjAooCagJIYiaHR0cDovL2NybC5lbXNpZ24uY29t\nP1Jvb3RDQUczLmNybDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIB\nATAKBggqhkjOPQQDAwNpADBmAjEAt9CIpRGp46QHQYDUBnLKw+vvK0/h9vmilEAg\nTroUl/anH4x2rXHqiCtBTBfgKDegAjEA8WpG1LTZdAIL0FSYqxXeREaeTSxEofM0\n1jJH3jGvCXC79LU9UPJ2o3TU+UIj2gMV\n-----END CERTIFICATE-----\n",
|
||||
"346cdd962e97189b59aee5526d1ae8fa714bb1a64094dfa67bef768ee93f36b5": "-----BEGIN CERTIFICATE-----\nMIICODCCAb+gAwIBAgIUbWvtwSNK/Qro223vAqyz323+VBAwCgYIKoZIzj0EAwIw\nQDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC2VNdWRocmEgSW5jMRswGQYDVQQDDBJF\nTSBEViBUTFMgQ0EgLSBHM0MwHhcNMjYxMDIwMDA1OTM2WhcNMjcwNTAzMDEwNDM2\nWjAvMS0wKwYDVQQDDCRkdi1nM2MtZW11ZGhyYS53ZS1oYXZlLWEtcHJvYmxlbS5j\nb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAScdBQpNUOOCcag6YEVFccNRSFe\nUIl02iH53vDfoWPJFS005WzqF3CPDclB/kygP2gWF4pg/r5gPYdnI3/6Vdzro4Gn\nMIGkMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsG\nAQUFBwMBMC8GA1UdEQQoMCaCJGR2LWczYy1lbXVkaHJhLndlLWhhdmUtYS1wcm9i\nbGVtLmNvbTAdBgNVHQ4EFgQUBj2yGO/TzDOE/u/6QeUvcW8y1iMwHwYDVR0jBBgw\nFoAUrn4QXDwplsGoRycFWfHgPdTlCvMwCgYIKoZIzj0EAwIDZwAwZAIwBQOOQqG9\nccdzM/7XIlXLtota1esALaLmgEN/ZweosBIvk4xabHcERZInWVIwO45NAjBos8Ue\nNwDe1X3ahdK2lkxqBEoBMtUHJN18TnJKs2nK1gmXpMbmY5oKnAQQgCEZPA4=\n-----END CERTIFICATE-----\n",
|
||||
@@ -34,6 +35,7 @@
|
||||
"432ec25f0d28de9006d97bf752f17bbb2cd41f669933fcb072d9a1148d0261f3": "-----BEGIN CERTIFICATE-----\nMIIDaDCCAu6gAwIBAgIQAIAJkopkEHYBrwX6LxnxCTAKBggqhkjOPQQDAzBWMQsw\nCQYDVQQGEwJJTjElMCMGA1UECgwcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRl\nZDEgMB4GA1UEAwwXZW1TaWduIFJvb3QgVExTIENBIC0gRzMwHhcNMjUwNDMwMTUz\nOTMxWhcNNDAwNDI4MTgzMDAwWjBAMQswCQYDVQQGEwJVUzEUMBIGA1UECgwLZU11\nZGhyYSBJbmMxGzAZBgNVBAMMEkVNIEVWIFRMUyBDQSAtIEczQzB2MBAGByqGSM49\nAgEGBSuBBAAiA2IABC1W6Cmm+4LGSkaX2WCvlR6QZ66R+SbTQwbaSKiq63hptqZ+\nbSdCbgRCqgOzbhmI3/Y2UU7hzwf1uks/8qBmRa59MdWGatMao9JG6wsxd+6Zy3Vm\nxRbOdICKZo+R8MR/yqOCAZUwggGRMB8GA1UdIwQYMBaAFOE6qyF88eNNVK7Ucrf9\ni1lCIJBUMB0GA1UdDgQWBBSmJV6AqU2385Z8G8eR35K87kvrXzAdBgNVHSUEFjAU\nBggrBgEFBQcDAQYIKwYBBQUHAwIwXAYDVR0gBFUwUzA5BgsrBgEEAYOOIQEAATAq\nMCgGCCsGAQUFBwIBFhxodHRwOi8vcmVwb3NpdG9yeS5lbXNpZ24uY29tMA0GCysG\nAQQBg44hAQJ4MAcGBWeBDAEBMHUGCCsGAQUFBwEBBGkwZzAkBggrBgEFBQcwAYYY\naHR0cDovL29jc3AtYy5lbXNpZ24uY29tMD8GCCsGAQUFBzAChjNodHRwOi8vcmVw\nb3NpdG9yeS5lbXNpZ24uY29tL2NlcnRzL0VNRVZUTFNDQUczQy5wN2MwNwYDVR0f\nBDAwLjAsoCqgKIYmaHR0cDovL2NybC5lbXNpZ24uY29tLz9yb290dGxzY2FnMy5j\ncmwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwCgYIKoZIzj0E\nAwMDaAAwZQIwBy1wTnzH4lXDTy2PD91ysLjvnlVN3JbJmc1BIMEU8TOLkYVHibMc\n9HeyonikFSfmAjEA6uE3QRxlrZTz/1/lXPTEMUhoauAtycagtlNFp79kjhvNa2/f\nAal1GNxsOutLnr0Y\n-----END CERTIFICATE-----\n",
|
||||
"45a883f349aedfd5f3a649b5498ffb121712ba8b448f3f2a91dbc5a766fcddbe": "-----BEGIN CERTIFICATE-----\nMIIDzjCCAragAwIBAgIRAKAk/fS6Zlx4XOSgIEg4WLcwDQYJKoZIhvcNAQELBQAw\nezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G\nA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV\nBAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y\nODEyMzEyMzU5NTlaMD8xCzAJBgNVBAYTAk5MMRMwEQYDVQQKDAplTXVkaHJhIEJW\nMRswGQYDVQQDDBJFTSBEViBUTFMgQ0EgLSBHM0IwdjAQBgcqhkjOPQIBBgUrgQQA\nIgNiAATnntnASpdzxHNeUREdqFrCvV5kqzdLgOEsvCA47B5PDjuBL8mFnh5nr7BO\nL4vJf3SFumWqkR7opORbYtXMA9Vr5nizw4y03JN8okYOI4RTyJKTXSnUT5HhVga2\nPM7atGijggE1MIIBMTAfBgNVHSMEGDAWgBSgEQojPpbxB+zirynvgqV/0DCktDAd\nBgNVHQ4EFgQU4h4v5xwZOQ5VS2LBICRXNSSKJRQwDgYDVR0PAQH/BAQDAgEGMBIG\nA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC\nMDEGA1UdIAQqMCgwCAYGZ4EMAQIBMA0GCysGAQQBg44hAQABMA0GCysGAQQBg44h\nAQJkMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0FB\nQUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEF\nBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBCwUAA4IB\nAQBzQ1IoqpdotM6gFrR4Q3YrKifOLeaj5XXunldpVE/vZn2TxC2y0s8oYiLR5ICE\nwqVXSB7N6BM1Mp+Nif7Jbhb2IswAfSR4Iik8+B0iRmi7lYoNlycCgHjmsjOmZaxM\nQkPmjXAJbG5f4V41OsvJZmS7lI3r0IuvEDVX3GAZ13on6IImSs9ybNpHQ7NCqkMQ\n7wH4uAO1dtpFycgJSdHaCOhHjrox2BTYGLdpUrYwY3EOAjlZ8UGgsxs7mG+xquMl\nbN1LFURwfnkVMP3T/DBIZ/ys2+lNFyxK3FlArrV4/000uTnVB1lO8oGORYdXoT5V\nGUyRCEIDhF5TBUuKF4E3kf1h\n-----END CERTIFICATE-----\n",
|
||||
"4b2460a466f95fd40113bcdcca2ceb29b3b093921de45474da7df48874c630b1": "-----BEGIN CERTIFICATE-----\nMIIDWzCCAuGgAwIBAgIPVEt/kiwxriIKSpIpFvbmMAoGCCqGSM49BAMDMFUxCzAJ\nBgNVBAYTAklOMSUwIwYDVQQKDBxlTXVkaHJhIFRlY2hub2xvZ2llcyBMaW1pdGVk\nMR8wHQYDVQQDDBZlbVNpZ24gUm9vdCBDUyBDQSAtIEczMB4XDTI1MDQzMDE2Mzg0\nMFoXDTQwMDQyODE4MzAwMFowPzELMAkGA1UEBhMCVVMxFDASBgNVBAoMC2VNdWRo\ncmEgSW5jMRowGAYDVQQDDBFFTSBPViBDUyBDQSAtIEczQzB2MBAGByqGSM49AgEG\nBSuBBAAiA2IABLnw4GNHAqacpqrdRDz9YsQbm6SyCSRpfXl052kaEm+5avs+NML9\nO8X0gwYfJl3CpYQ2NHMjUQQ2so8E84YjYwZ5gOJQPgJTLWVGI1pxZaJZoe9Jb3Qb\nX97neHII5Lj676OCAYswggGHMB8GA1UdIwQYMBaAFGh0A/iFyo1UUFcdFuDI3kUN\nq80tMB0GA1UdDgQWBBTub3MpjW+/p7Mbw5U09hVWRHhmtzATBgNVHSUEDDAKBggr\nBgEFBQcDAzBeBgNVHSAEVzBVMDkGCysGAQQBg44hAQABMCowKAYIKwYBBQUHAgEW\nHGh0dHA6Ly9yZXBvc2l0b3J5LmVtc2lnbi5jb20wDgYMKwYBBAGDjiEBAoFIMAgG\nBmeBDAEEATB0BggrBgEFBQcBAQRoMGYwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3Nw\nLWMuZW1zaWduLmNvbTA+BggrBgEFBQcwAoYyaHR0cDovL3JlcG9zaXRvcnkuZW1z\naWduLmNvbS9jZXJ0cy9FTU9WQ1NDQUczQy5wN2MwNgYDVR0fBC8wLTAroCmgJ4Yl\naHR0cDovL2NybC5lbXNpZ24uY29tLz9yb290Y3NjYWczLmNybDAOBgNVHQ8BAf8E\nBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAKBggqhkjOPQQDAwNoADBlAjEA8yPg\n8sntOhLwV2QXJ5LEaKSpo/fnE3uOgTadK73fDhyjiqr0Io9GVuQTYqBPbQ2TAjBg\neuApmBS486+iei03lRrp139dPw6k28jjGNvWsAU7A1N86J1zPJVDEyVH+nMAWC0=\n-----END CERTIFICATE-----\n",
|
||||
"5510ecf5764dee8dfe9468ae2d7323da21fcd5530889906540a2f8725c6f74db": "-----BEGIN CERTIFICATE-----\nMIICSTCCAdCgAwIBAgIUO7oRaIOhMWD0jFgvxusAGNi5CI0wCgYIKoZIzj0EAwIw\nPzELMAkGA1UEBhMCTkwxEzARBgNVBAoMCmVNdWRocmEgQlYxGzAZBgNVBAMMEkVN\nIERWIFRMUyBDQSAtIEczQjAeFw0yNjEwMjIxMzE0NDhaFw0yNzA1MDUxMzE5NDha\nMDgxNjA0BgNVBAMMLWR2LWczYi1hYWEtbm9jdC1lbXVkaHJhLndlLWhhdmUtYS1w\ncm9ibGVtLmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJ0yLMf28vw2E6pO\nc74doinU47pwTYD6xZ60nkrGkqI05DxVKXblos+V5S0+bgDjT2qw1F39EW9EUj8b\nNQmmqv6jgbAwga0wDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0l\nBAwwCgYIKwYBBQUHAwEwOAYDVR0RBDEwL4ItZHYtZzNiLWFhYS1ub2N0LWVtdWRo\ncmEud2UtaGF2ZS1hLXByb2JsZW0uY29tMB0GA1UdDgQWBBTruPPaZ1PiEFfpqdLo\n8Ra5Ah5a2jAfBgNVHSMEGDAWgBTiHi/nHBk5DlVLYsEgJFc1JIolFDAKBggqhkjO\nPQQDAgNnADBkAjBUTUTymJylZiZRV/RqMayZdUPJKRDIcn9+sUPpG66AjVgx8M+h\nDGzxxgeroOSxzSwCMBM/r1y7g371RI10nDU16T0WEAO/Udqyyuc8mJ2LCQ+Gxv3c\njU2NXGQLsvosv6T0ww==\n-----END CERTIFICATE-----\n",
|
||||
"579480aea166290bd3c643589e795fc0d0e54e86505d180f1dff99efd3199512": "-----BEGIN CERTIFICATE-----\nMIIDzTCCArWgAwIBAgIRANj4aF5PK9nYJAKoWhwetzEwDQYJKoZIhvcNAQELBQAw\nezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G\nA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV\nBAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y\nODEyMzEyMzU5NTlaMD8xCzAJBgNVBAYTAk5MMRMwEQYDVQQKDAplTXVkaHJhIEJW\nMRswGQYDVQQDDBJFTSBFViBUTFMgQ0EgLSBHM0IwdjAQBgcqhkjOPQIBBgUrgQQA\nIgNiAAQl26haZNOX2gsQUunqVG5Ie+/qnJ2daKDF6tz+73Wmm7XW/AH0K/vKbrQL\nzWTAdn42/9Bdl6gTxaoMbJUw55z18IZkaPE4BkIKpchRhAseJSVA55DktlTDhS7W\nSvHoZX2jggE0MIIBMDAfBgNVHSMEGDAWgBSgEQojPpbxB+zirynvgqV/0DCktDAd\nBgNVHQ4EFgQUPtwg2YlGmId7hz/YgUeyXBXYRwYwDgYDVR0PAQH/BAQDAgEGMBIG\nA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC\nMDAGA1UdIAQpMCcwBwYFZ4EMAQEwDQYLKwYBBAGDjiEBAAEwDQYLKwYBBAGDjiEB\nAngwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20vQUFB\nQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUF\nBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZIhvcNAQELBQADggEB\nAJNFuAFW9Gnw/8+kCO4KIlZdnM1Sh2u+8cK0EYAHhXpb5FlGvYkcARigo5oS7VoM\n7FOYlTueV6yICZKNAYawZPJPhNCGK+rp3cQBEz6d0n8OZEYZov9lb3ll8ORgSDuf\nFVIW6Gagg/UnOamLBTOR8eD5V73SCme/B2cHUDw+m5E8cc+0Ujzz3Cn3ex5JrhXk\n2T3s8PoVzEmhTgGboFfcWO1EYhPPrYwdxPYRQIXAXUXhLEOVNL9ASbZJLFI8E1IM\nm7NwxVN2s38jWpaCUaaF0tg7hfiy7oe13rzJfHsdqldJVTTUTPWVx8TC4ju0VLet\nbAlwhwFT6AP8uIVMGUHNw24=\n-----END CERTIFICATE-----\n",
|
||||
"59cf0386b56bc2414a0c06241ea9e25e01a9c93bac7a596648747f9d0ded4906": "-----BEGIN CERTIFICATE-----\nMIICQTCCAcegAwIBAgIULuWnWW6kFV6QZxp2ls8r18NCjXkwCgYIKoZIzj0EAwIw\nQDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC2VNdWRocmEgSW5jMRswGQYDVQQDDBJF\nTSBPViBUTFMgQ0EgLSBHM0MwHhcNMjYxMDIwMDIyMjQyWhcNMjcwNTAzMDIyNzQy\nWjAzMTEwLwYDVQQDDChvdi1nM2MtYWFhLWVtdWRocmEud2UtaGF2ZS1hLXByb2Js\nZW0uY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEb7+5g2YHNYwH2eQERrZc\nTmVuf/RmvzsGGMSwXmZke6vn35Ut5HME7UOOU3Muy+BSfjfpo6fjL5KOs3Ndz67d\nOKOBqzCBqDAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAK\nBggrBgEFBQcDATAzBgNVHREELDAqgihvdi1nM2MtYWFhLWVtdWRocmEud2UtaGF2\nZS1hLXByb2JsZW0uY29tMB0GA1UdDgQWBBQ7oBzq3jpTuvntbIkzMV12Ps3O0TAf\nBgNVHSMEGDAWgBQ06uoM68kK2vnQxeLtm3jJh54bbDAKBggqhkjOPQQDAgNoADBl\nAjEA/Y7AZ4M2MYbN5Od9tREaUfFjZKoOjczfIKPjKNMjtGxTdL9WUXvAT4QDgktn\n/IJdAjAbORTkAXD5hggyG6K7p0MBz3lFZJb2l/LndZlrttJWBLP8QGrc44hrZZtK\nIq0d1dE=\n-----END CERTIFICATE-----\n",
|
||||
"5f1ded4dd590af74631290cf073b122a2aac6a1fa479778a0bd43579bd832699": "-----BEGIN CERTIFICATE-----\nMIIDzjCCAragAwIBAgIRAOlCI73pL5FOWrmsq/2sNxAwDQYJKoZIhvcNAQELBQAw\nezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G\nA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV\nBAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y\nODEyMzEyMzU5NTlaMD8xCzAJBgNVBAYTAk5MMRMwEQYDVQQKDAplTXVkaHJhIEJW\nMRswGQYDVQQDDBJFTSBPViBUTFMgQ0EgLSBHM0IwdjAQBgcqhkjOPQIBBgUrgQQA\nIgNiAARziXqg6zz9yosSQC/68KvE4SErr43ud4rJ7i1tuD+sKAxeBWOK56Ks1xwy\nKo42HN659pxDArndjXPRzGyqsswK7k4hbOl53VGKSvS2JuVoTN1lgDftDIVOTMEv\nTPhkbQ2jggE1MIIBMTAfBgNVHSMEGDAWgBSgEQojPpbxB+zirynvgqV/0DCktDAd\nBgNVHQ4EFgQUm9hlUa/n5yw3rQjuaRnZvzeErgowDgYDVR0PAQH/BAQDAgEGMBIG\nA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC\nMDEGA1UdIAQqMCgwCAYGZ4EMAQICMA0GCysGAQQBg44hAQABMA0GCysGAQQBg44h\nAQJuMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0FB\nQUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEF\nBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBCwUAA4IB\nAQALRoOSvkzRa+tN5Rje9W5GiXd/qsicd9N6Gnh0SWC7VyWH+HycIqAnFCMYZznj\nCeGusomFYHE42SHWr85JDIbgzy6y3dO4i9n9w0Bx7yrXWBbDzFS8VgA3+EY4Cg7y\nmtoC3RPOJVtaXXPZ+68X804g1Mj+3vCSHj2lNPVgco0OzSIOJ/KMg1R9MLBkiKzb\nbERAGZHejY0HBhYd76IINZN8z6bBNah2UdFYxsOisLynPQUkol6gRLum024/FzVl\nCCDfnUusrkr+mAfPAvD/Vb8bhpA7jYtepuU+2mIdShj5lLlvAwZphn+ypWycuo2x\n0k9mldz2dqI9nhCiY+TTIOOR\n-----END CERTIFICATE-----\n",
|
||||
@@ -64,11 +66,20 @@
|
||||
"7e02622ab6d745c1edb4b431bb1c551e667305c3dd3eb0c7cc6dfdb523807424",
|
||||
"45a883f349aedfd5f3a649b5498ffb121712ba8b448f3f2a91dbc5a766fcddbe"
|
||||
],
|
||||
"dv-g3b-aaa-noct-emudhra.we-have-a-problem.com": [
|
||||
"5510ecf5764dee8dfe9468ae2d7323da21fcd5530889906540a2f8725c6f74db",
|
||||
"45a883f349aedfd5f3a649b5498ffb121712ba8b448f3f2a91dbc5a766fcddbe"
|
||||
],
|
||||
"dv-g3b-emudhra.we-have-a-problem.com": [
|
||||
"9dcb21b1841d75313b3c888f4bb9cc01f8f51379a76fbaa241ca492b668839e7",
|
||||
"42c4accd12c4f59597925e1a08a7a90102608543809308d50ec427e118ba1493",
|
||||
"3094025792039a68af6e553686410bdab7c0d4a36b9084cba64202e35d6ebbdd"
|
||||
],
|
||||
"dv-g3b-noct-emudhra.we-have-a-problem.com": [
|
||||
"0cbfe22b824385823fd3bf4be4d131e8901342933a7feef1cf8b3110b8e44a70",
|
||||
"42c4accd12c4f59597925e1a08a7a90102608543809308d50ec427e118ba1493",
|
||||
"3094025792039a68af6e553686410bdab7c0d4a36b9084cba64202e35d6ebbdd"
|
||||
],
|
||||
"dv-g3c-aaa-emudhra.we-have-a-problem.com": [
|
||||
"9200d153c3cac272cc06bf68f9ea163fc24659fc3331e77668236fe00645ae52",
|
||||
"62b7f9868263f72f6911fc78ceaf72c1979158b7368bf648683bb2aa39b02e72"
|
||||
|
||||
@@ -132,7 +132,7 @@ class CATrustTests(unittest.TestCase):
|
||||
custom = directory / "custom.pem"
|
||||
fingerprint = next(iter(INCIDENT["intermediates"]))
|
||||
custom.write_text(clean.read_text() + INCIDENT["certificates"][fingerprint])
|
||||
self.assertEqual(len(INCIDENT["chains"]), 12)
|
||||
self.assertEqual(len(INCIDENT["chains"]), 14)
|
||||
custom_accepted = []
|
||||
for hostname, chain in INCIDENT["chains"].items():
|
||||
leaf, intermediates = directory / "leaf.pem", directory / "chain.pem"
|
||||
@@ -156,9 +156,11 @@ class CATrustTests(unittest.TestCase):
|
||||
self.assertIn("unable to get local issuer certificate", negative.stderr)
|
||||
if verify(custom).returncode == 0:
|
||||
custom_accepted.append(hostname)
|
||||
self.assertEqual(custom_accepted, [
|
||||
self.assertCountEqual(custom_accepted, [
|
||||
"dv-g3b-aaa-emudhra.we-have-a-problem.com",
|
||||
"dv-g3b-aaa-noct-emudhra.we-have-a-problem.com",
|
||||
"dv-g3b-emudhra.we-have-a-problem.com",
|
||||
"dv-g3b-noct-emudhra.we-have-a-problem.com",
|
||||
])
|
||||
checker.check_bundle(clean)
|
||||
for bundle in (affected, custom):
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Exercise removal using public X.509 certificates, without network access."""
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import re
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
import ssl
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SPEC = importlib.util.spec_from_file_location("searxng_repair", ROOT / "docker/searxng/repair_trust.py")
|
||||
repair = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(repair)
|
||||
INCIDENT = json.loads((ROOT / "tests/fixtures/ca-trust/incident.json").read_text())
|
||||
|
||||
|
||||
class SearxngTrustTests(unittest.TestCase):
|
||||
def test_inventory_matches_all_published_fixture_ca_fingerprints(self):
|
||||
expected = set(INCIDENT["roots"]) | set(INCIDENT["intermediates"]) | set(INCIDENT["cross_signed_roots"])
|
||||
self.assertEqual(set(repair.BLOCKED), expected)
|
||||
self.assertEqual(len(expected), 21)
|
||||
for fingerprint in expected:
|
||||
self.assertEqual(hashlib.sha256(ssl.PEM_cert_to_DER_cert(INCIDENT["certificates"][fingerprint])).hexdigest(), fingerprint)
|
||||
|
||||
def test_all_known_incident_certificates_are_removed_preserving_other_certificates(self):
|
||||
unaffected = (ROOT / "tests/fixtures/ca-trust/emsign-g1.txt").read_text()
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "bundle.pem"
|
||||
for fingerprint in repair.BLOCKED:
|
||||
with self.subTest(fingerprint=fingerprint):
|
||||
path.write_text(INCIDENT["certificates"][fingerprint] + "\n" + unaffected)
|
||||
self.assertEqual(repair.filter_bundle(path), 1)
|
||||
self.assertEqual(path.read_text().strip(), unaffected.strip())
|
||||
self.assertEqual(repair.check_bundle(path), 1)
|
||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||
context.load_verify_locations(cafile=str(path))
|
||||
self.assertEqual(len(context.get_ca_certs()), 1)
|
||||
self.assertEqual(repair.filter_bundle(path), 0)
|
||||
|
||||
def test_system_source_is_deleted_so_regeneration_cannot_restore_it(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "root.crt"
|
||||
path.write_text(INCIDENT["certificates"][next(iter(repair.BLOCKED))])
|
||||
self.assertEqual(repair.filter_bundle(path, allow_empty=True), 1)
|
||||
self.assertFalse(path.exists())
|
||||
|
||||
def test_bundle_cannot_be_emptied(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "bundle.pem"
|
||||
original = INCIDENT["certificates"][next(iter(repair.BLOCKED))]
|
||||
path.write_text(original)
|
||||
with self.assertRaisesRegex(ValueError, "refusing to empty"):
|
||||
repair.filter_bundle(path)
|
||||
self.assertEqual(path.read_text(), original)
|
||||
|
||||
def test_derived_base_matches_every_compose_pin(self):
|
||||
# The overlay corrects whatever image Compose selects; a pin bump that
|
||||
# skips the derived Dockerfile would silently run the older base.
|
||||
base = re.search(r"^FROM (\S+) AS base$", (ROOT / "docker/searxng/Dockerfile").read_text(), re.M)
|
||||
self.assertIsNotNone(base)
|
||||
for name in ("docker-compose.yml", "docker-compose.gpu-amd.yml", "docker-compose.gpu-nvidia.yml"):
|
||||
with self.subTest(compose=name):
|
||||
pins = re.findall(r"image:\s*(\S*searxng/searxng:\S+)", (ROOT / name).read_text())
|
||||
self.assertEqual(pins, [base.group(1)])
|
||||
version = base.group(1).split(":", 1)[1].split("-", 1)[0]
|
||||
overlay = (ROOT / "docker/searxng/compose.yml").read_text()
|
||||
self.assertIn(f"image: localhost/odysseus-searxng:{version}-ca-trust", overlay)
|
||||
|
||||
def test_empty_and_malformed_input_fail_closed(self):
|
||||
for text in ("", "not a certificate", "-----BEGIN CERTIFICATE-----\nbroken\n-----END CERTIFICATE-----"):
|
||||
with self.subTest(text=text), self.assertRaises((ValueError, OSError)):
|
||||
list(repair.certificates(text))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,106 @@
|
||||
"""A network failure must not be mistaken for rejection of an incident chain."""
|
||||
|
||||
import asyncio
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from types import ModuleType, SimpleNamespace
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
INCIDENT = json.loads((ROOT / "tests/fixtures/ca-trust/incident.json").read_text())
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def validator():
|
||||
repair = ModuleType("repair_trust")
|
||||
repair.check_bundle = lambda path: None
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"searxng_validation", ROOT / "docker/searxng/validate_image.py"
|
||||
)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
with patch.dict(sys.modules, {"repair_trust": repair}):
|
||||
spec.loader.exec_module(module)
|
||||
module.__file__ = str(ROOT / "tests/fixtures/ca-trust/validate_image.py")
|
||||
return module
|
||||
|
||||
|
||||
class RequestFailure(Exception):
|
||||
def __init__(self, code):
|
||||
super().__init__(f"curl error {code}")
|
||||
self.code = code
|
||||
|
||||
|
||||
@pytest.mark.parametrize("code", [60, 6, 28, 35, None])
|
||||
def test_live_probe_requires_certificate_errors_for_every_host(validator, code):
|
||||
hosts = []
|
||||
|
||||
async def get(url, **kwargs):
|
||||
if url == "https://pypi.org/simple/certifi/":
|
||||
return SimpleNamespace(status_code=200)
|
||||
hosts.append(url.removeprefix("https://").removesuffix("/"))
|
||||
assert kwargs["allow_redirects"] is False
|
||||
if code is not None:
|
||||
raise RequestFailure(code)
|
||||
return SimpleNamespace(status_code=200)
|
||||
|
||||
client = SimpleNamespace(get=get, aclose=AsyncMock())
|
||||
factory = ModuleType("searx.network.client")
|
||||
|
||||
def new_client(**kwargs):
|
||||
assert kwargs["verify"] is True
|
||||
return client
|
||||
|
||||
factory.new_client = new_client
|
||||
errors = ModuleType("curl_cffi.requests.exceptions")
|
||||
errors.RequestException = RequestFailure
|
||||
modules = {"searx.network.client": factory, "curl_cffi.requests.exceptions": errors}
|
||||
with patch.dict(sys.modules, modules), patch.object(sys, "path", list(sys.path)):
|
||||
if code == 60:
|
||||
asyncio.run(validator.check_https())
|
||||
assert set(hosts) == set(INCIDENT["chains"])
|
||||
else:
|
||||
expected = "unexpectedly accepted" if code is None else "inconclusive"
|
||||
with pytest.raises(RuntimeError, match=expected):
|
||||
asyncio.run(validator.check_https())
|
||||
client.aclose.assert_awaited_once()
|
||||
|
||||
|
||||
def test_offline_probe_rejects_openssl_execution_failure(validator):
|
||||
result = SimpleNamespace(returncode=1, stderr="openssl: unknown option", stdout="")
|
||||
with patch.dict(validator.os.environ, {"SSL_CERT_FILE": "/unused.pem", "SSL_CERT_DIR": "/unused"}), \
|
||||
patch.object(validator.subprocess, "run", return_value=result):
|
||||
with pytest.raises(RuntimeError, match="not a certificate trust rejection"):
|
||||
validator.check_chains()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("results", [[{"title": "ordinary result"}], []])
|
||||
def test_running_probe_checks_existing_service_without_repair_or_second_boot(validator, tmp_path, results):
|
||||
certificate = tmp_path / "01234567.0"
|
||||
certificate.write_text("certificate double")
|
||||
(tmp_path / "unrelated").write_text("ignored")
|
||||
health = io.BytesIO(b"OK")
|
||||
health.status = 200
|
||||
search = io.BytesIO(json.dumps({"results": results}).encode())
|
||||
with patch.dict(validator.os.environ, {"SSL_CERT_DIR": str(tmp_path), "GRANIAN_PORT": "8080"}), \
|
||||
patch.object(validator.urllib.request, "build_opener") as opener, \
|
||||
patch.object(validator, "check_chains") as chains, \
|
||||
patch.object(validator, "check_bundle") as bundle, \
|
||||
patch.object(validator, "check_https", new_callable=AsyncMock) as https, \
|
||||
patch.object(validator.subprocess, "Popen") as boot:
|
||||
opener.return_value.open.side_effect = [health, search]
|
||||
if results:
|
||||
validator.check_running(live=True)
|
||||
else:
|
||||
with pytest.raises(RuntimeError, match="no results"):
|
||||
validator.check_running(live=True)
|
||||
assert opener.return_value.open.call_args_list[0].args == ("http://127.0.0.1:8080/healthz",)
|
||||
chains.assert_called_once_with()
|
||||
bundle.assert_called_once_with(certificate)
|
||||
https.assert_awaited_once()
|
||||
boot.assert_not_called()
|
||||
@@ -0,0 +1,148 @@
|
||||
"""Publication must preserve the images that passed native, credential-free checks."""
|
||||
|
||||
import copy
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SPEC = importlib.util.spec_from_file_location("searxng_release", ROOT / ".github/scripts/searxng_release.py")
|
||||
release = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(release)
|
||||
SHA = "a" * 40
|
||||
DIGESTS = {"amd64": "sha256:" + "1" * 64, "arm64": "sha256:" + "2" * 64}
|
||||
|
||||
|
||||
def descriptor(snapshot=None, manifest=None, digests=None):
|
||||
snapshot = snapshot or release.source_snapshot(ROOT, SHA, release.REPOSITORY)
|
||||
manifest = manifest or {
|
||||
"mediaType": "application/vnd.oci.image.index.v1+json",
|
||||
"digest": "sha256:" + "3" * 64,
|
||||
"manifests": [
|
||||
{"digest": value, "platform": {"os": "linux", "architecture": arch}}
|
||||
for arch, value in DIGESTS.items()
|
||||
],
|
||||
}
|
||||
return release.release_descriptor(snapshot, copy.deepcopy(snapshot), manifest, SHA, "123", "sha-test", digests or DIGESTS)
|
||||
|
||||
|
||||
def test_release_binds_both_native_artifacts_and_immutable_index():
|
||||
value = descriptor()
|
||||
assert value["image"] == release.IMAGE + "@sha256:" + "3" * 64
|
||||
assert value["architecture_digests"] == DIGESTS
|
||||
assert value["platforms"] == ["linux/amd64", "linux/arm64"]
|
||||
assert set(value["source_inputs"]) == set(release.SOURCE_FILES)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("change", ["fork", "revision", "missing-input", "checksum"])
|
||||
def test_release_refuses_unmatched_source(change):
|
||||
snapshot = release.source_snapshot(ROOT, SHA, release.REPOSITORY)
|
||||
if change == "fork":
|
||||
snapshot["repository"] = "fork/odysseus"
|
||||
elif change == "revision":
|
||||
snapshot["source_revision"] = "b" * 40
|
||||
elif change == "missing-input":
|
||||
snapshot["source_inputs"].pop(release.SOURCE_FILES[0])
|
||||
else:
|
||||
snapshot["source_inputs"][release.SOURCE_FILES[0]] = "invalid"
|
||||
with pytest.raises(ValueError):
|
||||
descriptor(snapshot=snapshot)
|
||||
|
||||
|
||||
def test_release_refuses_different_native_build_inputs():
|
||||
snapshot = release.source_snapshot(ROOT, SHA, release.REPOSITORY)
|
||||
other = copy.deepcopy(snapshot)
|
||||
other["source_inputs"][release.SOURCE_FILES[0]] = "f" * 64
|
||||
with pytest.raises(ValueError, match="different source inputs"):
|
||||
release.release_descriptor(snapshot, other, {}, SHA, "123", "test", DIGESTS)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("change", ["missing-arm", "extra-platform", "wrong-os", "duplicate-digest", "wrong-digest", "moving-index"])
|
||||
def test_release_refuses_incomplete_or_substituted_index(change):
|
||||
manifest = {
|
||||
"mediaType": "application/vnd.oci.image.index.v1+json",
|
||||
"digest": "sha256:" + "3" * 64,
|
||||
"manifests": [{"digest": value, "platform": {"os": "linux", "architecture": arch}} for arch, value in DIGESTS.items()],
|
||||
}
|
||||
if change == "missing-arm":
|
||||
manifest["manifests"].pop()
|
||||
elif change == "extra-platform":
|
||||
manifest["manifests"].append({"digest": "sha256:" + "4" * 64, "platform": {"os": "unknown", "architecture": "unknown"}})
|
||||
elif change == "wrong-os":
|
||||
manifest["manifests"][1]["platform"]["os"] = "windows"
|
||||
elif change == "duplicate-digest":
|
||||
manifest["manifests"][1]["digest"] = DIGESTS["amd64"]
|
||||
elif change == "wrong-digest":
|
||||
manifest["manifests"][1]["digest"] = "sha256:" + "4" * 64
|
||||
else:
|
||||
manifest["digest"] = "latest"
|
||||
with pytest.raises(ValueError):
|
||||
descriptor(manifest=manifest)
|
||||
|
||||
|
||||
def test_ci_publication_boundary_and_native_checks():
|
||||
workflow = yaml.safe_load((ROOT / ".github/workflows/searxng-image.yml").read_text())
|
||||
events = workflow.get("on", workflow.get(True))
|
||||
assert "pull_request_target" not in events
|
||||
assert workflow["permissions"] == {"contents": "read"}
|
||||
jobs = workflow["jobs"]
|
||||
assert [name for name, job in jobs.items() if job.get("permissions", {}).get("packages") == "write"] == ["publish"]
|
||||
publish = jobs["publish"]
|
||||
assert publish["needs"] == "validate"
|
||||
assert "github.repository == 'odysseus-dev/odysseus'" in publish["if"]
|
||||
assert "github.event_name == 'push'" in publish["if"]
|
||||
assert "refs/heads/release/searxng-ca-trust" in publish["if"]
|
||||
assert "github.ref)" in publish["if"]
|
||||
steps = publish["steps"]
|
||||
assert next(i for i, step in enumerate(steps) if "docker load" in step.get("run", "")) < next(i for i, step in enumerate(steps) if "docker/login-action" in step.get("uses", ""))
|
||||
for name in ("validate", "verify-published"):
|
||||
matrix = jobs[name]["strategy"]["matrix"]["include"]
|
||||
assert {(row["arch"], row["runner"]) for row in matrix} == {("amd64", "ubuntu-latest"), ("arm64", "ubuntu-24.04-arm")}
|
||||
assert any("validate-searxng-image.sh" in step.get("run", "") for step in jobs[name]["steps"])
|
||||
assert not any("login-action" in step.get("uses", "") for step in jobs["verify-published"]["steps"])
|
||||
assert set(jobs["release"]["needs"]) == {"publish", "verify-published"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("failure,expected", [("none", 0), ("exec", 7), ("up", 1), ("existing-volume", 1)])
|
||||
def test_compose_probe_owns_only_its_temporary_resources(tmp_path, failure, expected):
|
||||
"""Exercise the real helper with a Docker double, including failure cleanup."""
|
||||
binary = tmp_path / "docker"
|
||||
calls = tmp_path / "calls.jsonl"
|
||||
binary.write_text('''#!/usr/bin/env python3
|
||||
import json, os, sys
|
||||
args = sys.argv[1:]
|
||||
with open(os.environ["DOCKER_CALLS"], "a") as log:
|
||||
log.write(json.dumps({"args": args, "volume": os.environ["SEARXNG_VALIDATION_VOLUME"], "secret": os.environ["SEARXNG_SECRET"]}) + "\\n")
|
||||
failure = os.environ["DOCKER_FAILURE"]
|
||||
if args[:2] == ["volume", "inspect"]:
|
||||
sys.exit(0 if failure == "existing-volume" else 1)
|
||||
if "exec" in args and failure == "exec":
|
||||
sys.exit(7)
|
||||
if "up" in args and failure == "up":
|
||||
sys.exit(9)
|
||||
''')
|
||||
binary.chmod(0o755)
|
||||
result = subprocess.run(["bash", str(ROOT / ".github/scripts/validate-searxng-image.sh"), "localhost/searxng:validated"], env={**os.environ, "PATH": str(tmp_path) + ":" + os.environ["PATH"], "DOCKER_CALLS": str(calls), "DOCKER_FAILURE": failure, "SEARXNG_SECRET": "operator-secret-must-not-propagate"}, capture_output=True, text=True)
|
||||
assert result.returncode == expected, result.stderr
|
||||
entries = [json.loads(line) for line in calls.read_text().splitlines()]
|
||||
assert all(entry["secret"] == "searxng-ci-validation-only" for entry in entries)
|
||||
commands = [entry["args"] for entry in entries]
|
||||
if failure == "existing-volume":
|
||||
assert len(commands) == 1
|
||||
return
|
||||
up = next(command for command in commands if "up" in command)
|
||||
assert up[up.index("--env-file") + 1] == "/dev/null"
|
||||
assert "--no-deps" in up and "--no-build" in up and "--wait" in up
|
||||
assert up[up.index("--pull") + 1] == "never"
|
||||
down = next(command for command in commands if "down" in command)
|
||||
assert "--volumes" not in down
|
||||
assert commands[-1] == ["volume", "rm", entries[-1]["volume"]]
|
||||
if failure != "up":
|
||||
execute = next(command for command in commands if "exec" in command)
|
||||
assert execute[-2:] == ["--running", "--live"]
|
||||
Reference in New Issue
Block a user