feat(cert-manager): Reiter „Dateien“ mit mehreren Dateien und ein Parser für RSA und EC – Durchstich
- Ein gemeinsamer Arbeitsbereich im Browser: jede Auswahl hängt an, eine zweite Datei ersetzt nie die erste - Ein Parser (node:crypto) für RSA- und EC-Zertifikate als PEM, DER und TRUSTED CERTIFICATE, Rolle je Datei - Neue Route analyze (mehrere Dateien, 30 x 5 MiB, zusammen 20 MiB); alte Routen, Service und Web-Reiter entfernt - Test-PKI-Fixtures (RSA und EC, Ketten, Schlüssel, CSR, PFX) mit Erzeugungsskript Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
# Fixtures des Zertifikat-Managers
|
||||
|
||||
Nur Testdaten (quick-261009-ikt). Keine echten Zertifikate, keine echten Schluessel.
|
||||
|
||||
- Eine eigene Test-PKI: RSA (Wurzel, zweite Wurzel, Zwischenzertifikat, dazu Varianten
|
||||
gleichen Namens: cross-signiert, abgelaufen, Lockvogel mit anderem Schluessel) und
|
||||
EC (Wurzel P-384, Zwischenzertifikat P-384, Server P-256).
|
||||
- Passwort aller geschuetzten Dateien (verschluesselte Schluessel, PFX, ZIP): `Test-Pass-123`.
|
||||
Die Datei `rsa-nopass.pfx` hat ein leeres Passwort.
|
||||
- Die Schluessel der CAs liegen nur waehrend der Erzeugung in einem temporaeren Ordner und
|
||||
werden geloescht. Committet sind ausschliesslich Schluessel der Server-Zertifikate.
|
||||
- Dateinamen enden nie auf `.key` (die `.gitignore` ignoriert `*.key` wegen des Updater-Schluessels).
|
||||
- Neu erzeugen: `bash apps/api/src/cert-manager/__fixtures__/make-fixtures.sh` (OpenSSL 3.4 oder neuer).
|
||||
Achtung: Eine Neuerzeugung ersetzt alle Zertifikate (neue Schluessel, neue Fingerabdruecke);
|
||||
die Specs lesen die Fingerabdruecke deshalb aus den Dateien und nennen keine festen Werte.
|
||||
- Die Specs lesen die Dateien mit `readFileSync(join(__dirname, '__fixtures__', ...))` und rufen
|
||||
OpenSSL nie auf (die CI hat es nicht). Das Pruefskript `e2e-cert.sh` nutzt OpenSSL.
|
||||
- Hinweis fuer einen kuenftigen Secret-Scanner: diesen Ordner freigeben (Testschluessel).
|
||||
@@ -0,0 +1,25 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEIzCCAwugAwIBAgICAMswDQYJKoZIhvcNAQELBQAwODEVMBMGA1UECgwMVGVz
|
||||
c2VyYSBUZXN0MR8wHQYDVQQDDBZUZXNzZXJhIFRlc3QgSW50ZXIgUlNBMCAXDTI2
|
||||
MTAwOTEyNDIwMloYDzIxMjYwOTE1MTI0MjAyWjA6MRUwEwYDVQQKDAxUZXNzZXJh
|
||||
IFRlc3QxITAfBgNVBAMMGGFpYS1wcml2YXRlLmV4YW1wbGUudGVzdDCCASIwDQYJ
|
||||
KoZIhvcNAQEBBQADggEPADCCAQoCggEBAKqqSQ1+lWWU/3YG2/1JjZLBm7yg1JXY
|
||||
/F9tHMqmzrkrLzqp42a72fvigDTjQQOP3jGujyrIBHfeCNjylIKZuyXJ3o47z4ZL
|
||||
B6eg2MNwMWEVYVYLLUydA0bKvEfuDbLxV2I8OiAFbGn9kDhAP/cniBm13HDyxjKm
|
||||
Cj+ZBweXwr+eJ/amNZhYDDrcWy4r6n0qg+CtUlSuj5qXHgrdKxEEVnfwhUyPNT4h
|
||||
KtTVQZWW4RoRMcHEmQLwPmf+SQ0F1NAN9XkxGbVRT17475vDiUgq2cnq/aWwbZ/1
|
||||
D1IyqLXEgZUFqMfZMr2SE4CiPuLsEqXjDzMbvJf2ZP2XKsWlYL9RD1kCAwEAAaOC
|
||||
ATEwggEtMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoG
|
||||
CCsGAQUFBwMBMB0GA1UdDgQWBBS9erSIVCsbwhwEHYRLbDlKknZTHzAfBgNVHSME
|
||||
GDAWgBTh7bvQ9X9iHKYm2zw9/nacQORtnzAjBgNVHREEHDAaghhhaWEtcHJpdmF0
|
||||
ZS5leGFtcGxlLnRlc3QwgZIGCCsGAQUFBwEBBIGFMIGCMCYGCCsGAQUFBzAChhpo
|
||||
dHRwOi8vMTI3LjAuMC4xL2ludGVyLmNlcjApBggrBgEFBQcwAoYdaHR0cDovLzE2
|
||||
OS4yNTQuMTY5LjI1NC9sYXRlc3QwLQYIKwYBBQUHMAKGIWxkYXA6Ly9sZGFwLmV4
|
||||
YW1wbGUudGVzdC9jbj1pbnRlcjANBgkqhkiG9w0BAQsFAAOCAQEAWXPmZqdtxGWU
|
||||
m9uXsHlf89Nx/lfHv/gmY276Wm+k1Kjtv03kv4XS5P5yqcbwkDH5ywoYL6X7FEgP
|
||||
R61iSu5lpyEIcRjOOwXIEchnDruX8RI7rbrPAsJ6ZY6i1tck+WBb1QcD36qnwar7
|
||||
vH9GQuINNKLVEDeVz2fB3/wSdxc0jEEMegWvMnHtZ4fC0YtKx84LPhsyBOEkSu6n
|
||||
vXeeyb+0s4/UBkClJSbpVEaG2kIkq8JOokEQCgRZ1Np9n1PObPSSFSY2OKEYot92
|
||||
ao4UNr9Pq6QqkfQ5rthfA5H7em6LE/poUviugEXc6BURnPwmVb+FK/1ni1TDZTbo
|
||||
WO7eSAZz5Q==
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,38 @@
|
||||
-----BEGIN PKCS7-----
|
||||
MIIGuQYJKoZIhvcNAQcCoIIGqjCCBqYCAQExADALBgkqhkiG9w0BBwGgggaOMIIC
|
||||
TjCCAdWgAwIBAgICAZEwCgYIKoZIzj0EAwIwNzEVMBMGA1UECgwMVGVzc2VyYSBU
|
||||
ZXN0MR4wHAYDVQQDDBVUZXNzZXJhIFRlc3QgSW50ZXIgRUMwIBcNMjYxMDA5MTI0
|
||||
MjAyWhgPMjEyNjA5MTUxMjQyMDJaMDExFTATBgNVBAoMDFRlc3NlcmEgVGVzdDEY
|
||||
MBYGA1UEAwwPZWMuZXhhbXBsZS50ZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcD
|
||||
QgAEeY7t+BxIXCNmolAyuLxdVSFmnFc1+Ipj9Xalvuhrk6b00/msgJIQ4+Dx2vci
|
||||
T9lGjfPJvlZqOfNNt+VVmsV7x6OB1DCB0TAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB
|
||||
/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAdBgNVHQ4EFgQUsPnAEeAumf2R
|
||||
qSu3lGGuUQVDhzowHwYDVR0jBBgwFoAUGgwrSpk97+BQ9zfXSImoWjrgG90wGgYD
|
||||
VR0RBBMwEYIPZWMuZXhhbXBsZS50ZXN0MEAGCCsGAQUFBwEBBDQwMjAwBggrBgEF
|
||||
BQcwAoYkaHR0cDovL3BraS5leGFtcGxlLnRlc3QvZWMtaW50ZXIuY2VyMAoGCCqG
|
||||
SM49BAMCA2cAMGQCMCOFzkLRLlGyCixYx9ufTb0k/5IxkRjrwDax7OFTSoN9+0lo
|
||||
Jv1Hp/3mGeS65HOf1AIwc17jU4Nh+478Hoaktv8dgN/qoetQgiOx/IaOb4O5shKM
|
||||
k+hRxoYYeJ/MrndtBFjUMIICRjCCAcugAwIBAgICAS0wCgYIKoZIzj0EAwIwNjEV
|
||||
MBMGA1UECgwMVGVzc2VyYSBUZXN0MR0wGwYDVQQDDBRUZXNzZXJhIFRlc3QgUm9v
|
||||
dCBFQzAgFw0yNjEwMDkxMjQyMDJaGA8yMTI2MDkxNTEyNDIwMlowNzEVMBMGA1UE
|
||||
CgwMVGVzc2VyYSBUZXN0MR4wHAYDVQQDDBVUZXNzZXJhIFRlc3QgSW50ZXIgRUMw
|
||||
djAQBgcqhkjOPQIBBgUrgQQAIgNiAASCaV+fIBVW12edBlTT95heB5iuYizTNmTP
|
||||
QUgu2u/XL7MIuIiy1DCsvKEwAPM62wvKab8xP4VsURLd2bGDKQ6nKfJyCITOG/iT
|
||||
UsAhTYfkWM4tFPie5Xczmj2prnq6NaKjgagwgaUwEgYDVR0TAQH/BAgwBgEB/wIB
|
||||
ADAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBoMK0qZPe/gUPc310iJqFo64Bvd
|
||||
MB8GA1UdIwQYMBaAFABpT8YziNytqiBtP8IJbB2w4h5FMD8GCCsGAQUFBwEBBDMw
|
||||
MTAvBggrBgEFBQcwAoYjaHR0cDovL3BraS5leGFtcGxlLnRlc3QvZWMtcm9vdC5j
|
||||
ZXIwCgYIKoZIzj0EAwIDaQAwZgIxAKvXNAcWTnuiUtQRcq5dVpttiFy1B0OuHEYS
|
||||
/UjdljB6oc2Ea8xJiCIXQODu/qOtJQIxAMrvX6coL2rvAj19zNrF2jboSoRcYEgE
|
||||
YxyCzo5X9+XGC1sOiBDYzlRPX+9+AovkrzCCAe4wggF1oAMCAQICFHEw11UEFUZO
|
||||
zbui78XQHRBvsrHGMAoGCCqGSM49BAMCMDYxFTATBgNVBAoMDFRlc3NlcmEgVGVz
|
||||
dDEdMBsGA1UEAwwUVGVzc2VyYSBUZXN0IFJvb3QgRUMwIBcNMjYxMDA5MTI0MjAy
|
||||
WhgPMjEyNjA5MTUxMjQyMDJaMDYxFTATBgNVBAoMDFRlc3NlcmEgVGVzdDEdMBsG
|
||||
A1UEAwwUVGVzc2VyYSBUZXN0IFJvb3QgRUMwdjAQBgcqhkjOPQIBBgUrgQQAIgNi
|
||||
AARPBGScIkXNVouGImWgBFylQVaFbJCYXVpcQQQ6L6xNkfgVOZf8zmNkb3pxMWx6
|
||||
l4mCmaGhmRupJRhFimLupWs5PVvBAkqWQtok2mldEk8e+5+/tq82kxgnOSHkE7/V
|
||||
10+jQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQW
|
||||
BBQAaU/GM4jcraogbT/CCWwdsOIeRTAKBggqhkjOPQQDAgNnADBkAjAvOiOt6Ojs
|
||||
gSeYeH7CnydetEZWH9kg+KzeVySa9DNTG9GpfZfcws4y6VwFVJDyaysCMFFm/wJL
|
||||
rJOOowf5AZNqssvzq0gsoHvo8Je/EqDON9irSf2gBVluy+oJU7DP8yu7mTEA
|
||||
-----END PKCS7-----
|
||||
Binary file not shown.
@@ -0,0 +1,43 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICTjCCAdWgAwIBAgICAZEwCgYIKoZIzj0EAwIwNzEVMBMGA1UECgwMVGVzc2Vy
|
||||
YSBUZXN0MR4wHAYDVQQDDBVUZXNzZXJhIFRlc3QgSW50ZXIgRUMwIBcNMjYxMDA5
|
||||
MTI0MjAyWhgPMjEyNjA5MTUxMjQyMDJaMDExFTATBgNVBAoMDFRlc3NlcmEgVGVz
|
||||
dDEYMBYGA1UEAwwPZWMuZXhhbXBsZS50ZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0D
|
||||
AQcDQgAEeY7t+BxIXCNmolAyuLxdVSFmnFc1+Ipj9Xalvuhrk6b00/msgJIQ4+Dx
|
||||
2vciT9lGjfPJvlZqOfNNt+VVmsV7x6OB1DCB0TAMBgNVHRMBAf8EAjAAMA4GA1Ud
|
||||
DwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAdBgNVHQ4EFgQUsPnAEeAu
|
||||
mf2RqSu3lGGuUQVDhzowHwYDVR0jBBgwFoAUGgwrSpk97+BQ9zfXSImoWjrgG90w
|
||||
GgYDVR0RBBMwEYIPZWMuZXhhbXBsZS50ZXN0MEAGCCsGAQUFBwEBBDQwMjAwBggr
|
||||
BgEFBQcwAoYkaHR0cDovL3BraS5leGFtcGxlLnRlc3QvZWMtaW50ZXIuY2VyMAoG
|
||||
CCqGSM49BAMCA2cAMGQCMCOFzkLRLlGyCixYx9ufTb0k/5IxkRjrwDax7OFTSoN9
|
||||
+0loJv1Hp/3mGeS65HOf1AIwc17jU4Nh+478Hoaktv8dgN/qoetQgiOx/IaOb4O5
|
||||
shKMk+hRxoYYeJ/MrndtBFjU
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICRjCCAcugAwIBAgICAS0wCgYIKoZIzj0EAwIwNjEVMBMGA1UECgwMVGVzc2Vy
|
||||
YSBUZXN0MR0wGwYDVQQDDBRUZXNzZXJhIFRlc3QgUm9vdCBFQzAgFw0yNjEwMDkx
|
||||
MjQyMDJaGA8yMTI2MDkxNTEyNDIwMlowNzEVMBMGA1UECgwMVGVzc2VyYSBUZXN0
|
||||
MR4wHAYDVQQDDBVUZXNzZXJhIFRlc3QgSW50ZXIgRUMwdjAQBgcqhkjOPQIBBgUr
|
||||
gQQAIgNiAASCaV+fIBVW12edBlTT95heB5iuYizTNmTPQUgu2u/XL7MIuIiy1DCs
|
||||
vKEwAPM62wvKab8xP4VsURLd2bGDKQ6nKfJyCITOG/iTUsAhTYfkWM4tFPie5Xcz
|
||||
mj2prnq6NaKjgagwgaUwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMC
|
||||
AQYwHQYDVR0OBBYEFBoMK0qZPe/gUPc310iJqFo64BvdMB8GA1UdIwQYMBaAFABp
|
||||
T8YziNytqiBtP8IJbB2w4h5FMD8GCCsGAQUFBwEBBDMwMTAvBggrBgEFBQcwAoYj
|
||||
aHR0cDovL3BraS5leGFtcGxlLnRlc3QvZWMtcm9vdC5jZXIwCgYIKoZIzj0EAwID
|
||||
aQAwZgIxAKvXNAcWTnuiUtQRcq5dVpttiFy1B0OuHEYS/UjdljB6oc2Ea8xJiCIX
|
||||
QODu/qOtJQIxAMrvX6coL2rvAj19zNrF2jboSoRcYEgEYxyCzo5X9+XGC1sOiBDY
|
||||
zlRPX+9+Aovkrw==
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIB7jCCAXWgAwIBAgIUcTDXVQQVRk7Nu6LvxdAdEG+yscYwCgYIKoZIzj0EAwIw
|
||||
NjEVMBMGA1UECgwMVGVzc2VyYSBUZXN0MR0wGwYDVQQDDBRUZXNzZXJhIFRlc3Qg
|
||||
Um9vdCBFQzAgFw0yNjEwMDkxMjQyMDJaGA8yMTI2MDkxNTEyNDIwMlowNjEVMBMG
|
||||
A1UECgwMVGVzc2VyYSBUZXN0MR0wGwYDVQQDDBRUZXNzZXJhIFRlc3QgUm9vdCBF
|
||||
QzB2MBAGByqGSM49AgEGBSuBBAAiA2IABE8EZJwiRc1Wi4YiZaAEXKVBVoVskJhd
|
||||
WlxBBDovrE2R+BU5l/zOY2RvenExbHqXiYKZoaGZG6klGEWKYu6lazk9W8ECSpZC
|
||||
2iTaaV0STx77n7+2rzaTGCc5IeQTv9XXT6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAO
|
||||
BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFABpT8YziNytqiBtP8IJbB2w4h5FMAoG
|
||||
CCqGSM49BAMCA2cAMGQCMC86I63o6OyBJ5h4fsKfJ160RlYf2SD4rN5XJJr0M1Mb
|
||||
0al9l9zCzjLpXAVUkPJrKwIwUWb/Akusk46jB/kBk2qyy/OrSCyge+jwl78SoM43
|
||||
2KtJ/aAFWW7L6glTsM/zK7uZ
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,15 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICRjCCAcugAwIBAgICAS0wCgYIKoZIzj0EAwIwNjEVMBMGA1UECgwMVGVzc2Vy
|
||||
YSBUZXN0MR0wGwYDVQQDDBRUZXNzZXJhIFRlc3QgUm9vdCBFQzAgFw0yNjEwMDkx
|
||||
MjQyMDJaGA8yMTI2MDkxNTEyNDIwMlowNzEVMBMGA1UECgwMVGVzc2VyYSBUZXN0
|
||||
MR4wHAYDVQQDDBVUZXNzZXJhIFRlc3QgSW50ZXIgRUMwdjAQBgcqhkjOPQIBBgUr
|
||||
gQQAIgNiAASCaV+fIBVW12edBlTT95heB5iuYizTNmTPQUgu2u/XL7MIuIiy1DCs
|
||||
vKEwAPM62wvKab8xP4VsURLd2bGDKQ6nKfJyCITOG/iTUsAhTYfkWM4tFPie5Xcz
|
||||
mj2prnq6NaKjgagwgaUwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMC
|
||||
AQYwHQYDVR0OBBYEFBoMK0qZPe/gUPc310iJqFo64BvdMB8GA1UdIwQYMBaAFABp
|
||||
T8YziNytqiBtP8IJbB2w4h5FMD8GCCsGAQUFBwEBBDMwMTAvBggrBgEFBQcwAoYj
|
||||
aHR0cDovL3BraS5leGFtcGxlLnRlc3QvZWMtcm9vdC5jZXIwCgYIKoZIzj0EAwID
|
||||
aQAwZgIxAKvXNAcWTnuiUtQRcq5dVpttiFy1B0OuHEYS/UjdljB6oc2Ea8xJiCIX
|
||||
QODu/qOtJQIxAMrvX6coL2rvAj19zNrF2jboSoRcYEgEYxyCzo5X9+XGC1sOiBDY
|
||||
zlRPX+9+Aovkrw==
|
||||
-----END CERTIFICATE-----
|
||||
Binary file not shown.
@@ -0,0 +1,6 @@
|
||||
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||
MIGxMBwGCiqGSIb3DQEMAQMwDgQI4J4Nb3UgXXgCAggABIGQ/epVZs7aeAwW7aai
|
||||
uGri0OKmYLfOjjcEHU/uvbAFhSFn8qQ+d+E6jlToOIR7ziS2RzUpPTWxmZ/08j2C
|
||||
SL4tPlS6TZBtVMAAqfgAwYLOv6B3gbmL3WJ/T3shOf6qtJ9cklv7AjG3+ivPtfTb
|
||||
YKUAkgRYJmuXQ6BxXwLE8wqy08DPJuw7RwTka2/zBjCRq9pP
|
||||
-----END ENCRYPTED PRIVATE KEY-----
|
||||
@@ -0,0 +1,8 @@
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
Proc-Type: 4,ENCRYPTED
|
||||
DEK-Info: AES-256-CBC,A541E4CD144B840064BB86BAF3666FFC
|
||||
|
||||
6S1gYfeZtkThiAKqZH2E4TMQppoaEBtVHSMKCqsfDDu5eIll2p/91NbtcC4L5WO8
|
||||
NNyTeofOqe8TWt+6zrbJEjqob5Q4673JthJayFPOZeKMRhpAPe2Bg442vFBPYiJl
|
||||
CwRyxJZAbf6oL8ukg6xMHT7iDyJBS32MK64owR4aBSg=
|
||||
-----END EC PRIVATE KEY-----
|
||||
Binary file not shown.
@@ -0,0 +1,5 @@
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIDEFEkJwvCAUZT60LWQ1hQi/maGFIVMg5tD6f+FAXim3oAoGCCqGSM49
|
||||
AwEHoUQDQgAEeY7t+BxIXCNmolAyuLxdVSFmnFc1+Ipj9Xalvuhrk6b00/msgJIQ
|
||||
4+Dx2vciT9lGjfPJvlZqOfNNt+VVmsV7xw==
|
||||
-----END EC PRIVATE KEY-----
|
||||
@@ -0,0 +1,5 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgMQUSQnC8IBRlPrQt
|
||||
ZDWFCL+ZoYUhUyDm0Pp/4UBeKbehRANCAAR5ju34HEhcI2aiUDK4vF1VIWacVzX4
|
||||
imP1dqW+6GuTpvTT+ayAkhDj4PHa9yJP2UaN88m+Vmo580235VWaxXvH
|
||||
-----END PRIVATE KEY-----
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIIBGjCBwAIBADAxMRUwEwYDVQQKDAxUZXNzZXJhIFRlc3QxGDAWBgNVBAMMD2Vj
|
||||
LmV4YW1wbGUudGVzdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHmO7fgcSFwj
|
||||
ZqJQMri8XVUhZpxXNfiKY/V2pb7oa5Om9NP5rICSEOPg8dr3Ik/ZRo3zyb5Wajnz
|
||||
TbflVZrFe8egLTArBgkqhkiG9w0BCQ4xHjAcMBoGA1UdEQQTMBGCD2VjLmV4YW1w
|
||||
bGUudGVzdDAKBggqhkjOPQQDAgNJADBGAiEAq1WTh8ixRI26kN4p6gmIE0h1TOua
|
||||
hDLUu8FA9tXK73oCIQDAGX/FC2qjMuobhP+DX0Gre5D8B18VZyAaMiuBWCC/mg==
|
||||
-----END CERTIFICATE REQUEST-----
|
||||
Binary file not shown.
@@ -0,0 +1,15 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICTjCCAdWgAwIBAgICAZEwCgYIKoZIzj0EAwIwNzEVMBMGA1UECgwMVGVzc2Vy
|
||||
YSBUZXN0MR4wHAYDVQQDDBVUZXNzZXJhIFRlc3QgSW50ZXIgRUMwIBcNMjYxMDA5
|
||||
MTI0MjAyWhgPMjEyNjA5MTUxMjQyMDJaMDExFTATBgNVBAoMDFRlc3NlcmEgVGVz
|
||||
dDEYMBYGA1UEAwwPZWMuZXhhbXBsZS50ZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0D
|
||||
AQcDQgAEeY7t+BxIXCNmolAyuLxdVSFmnFc1+Ipj9Xalvuhrk6b00/msgJIQ4+Dx
|
||||
2vciT9lGjfPJvlZqOfNNt+VVmsV7x6OB1DCB0TAMBgNVHRMBAf8EAjAAMA4GA1Ud
|
||||
DwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAdBgNVHQ4EFgQUsPnAEeAu
|
||||
mf2RqSu3lGGuUQVDhzowHwYDVR0jBBgwFoAUGgwrSpk97+BQ9zfXSImoWjrgG90w
|
||||
GgYDVR0RBBMwEYIPZWMuZXhhbXBsZS50ZXN0MEAGCCsGAQUFBwEBBDQwMjAwBggr
|
||||
BgEFBQcwAoYkaHR0cDovL3BraS5leGFtcGxlLnRlc3QvZWMtaW50ZXIuY2VyMAoG
|
||||
CCqGSM49BAMCA2cAMGQCMCOFzkLRLlGyCixYx9ufTb0k/5IxkRjrwDax7OFTSoN9
|
||||
+0loJv1Hp/3mGeS65HOf1AIwc17jU4Nh+478Hoaktv8dgN/qoetQgiOx/IaOb4O5
|
||||
shKMk+hRxoYYeJ/MrndtBFjU
|
||||
-----END CERTIFICATE-----
|
||||
Binary file not shown.
@@ -0,0 +1,13 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIB7jCCAXWgAwIBAgIUcTDXVQQVRk7Nu6LvxdAdEG+yscYwCgYIKoZIzj0EAwIw
|
||||
NjEVMBMGA1UECgwMVGVzc2VyYSBUZXN0MR0wGwYDVQQDDBRUZXNzZXJhIFRlc3Qg
|
||||
Um9vdCBFQzAgFw0yNjEwMDkxMjQyMDJaGA8yMTI2MDkxNTEyNDIwMlowNjEVMBMG
|
||||
A1UECgwMVGVzc2VyYSBUZXN0MR0wGwYDVQQDDBRUZXNzZXJhIFRlc3QgUm9vdCBF
|
||||
QzB2MBAGByqGSM49AgEGBSuBBAAiA2IABE8EZJwiRc1Wi4YiZaAEXKVBVoVskJhd
|
||||
WlxBBDovrE2R+BU5l/zOY2RvenExbHqXiYKZoaGZG6klGEWKYu6lazk9W8ECSpZC
|
||||
2iTaaV0STx77n7+2rzaTGCc5IeQTv9XXT6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAO
|
||||
BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFABpT8YziNytqiBtP8IJbB2w4h5FMAoG
|
||||
CCqGSM49BAMCA2cAMGQCMC86I63o6OyBJ5h4fsKfJ160RlYf2SD4rN5XJJr0M1Mb
|
||||
0al9l9zCzjLpXAVUkPJrKwIwUWb/Akusk46jB/kBk2qyy/OrSCyge+jwl78SoM43
|
||||
2KtJ/aAFWW7L6glTsM/zK7uZ
|
||||
-----END CERTIFICATE-----
|
||||
Binary file not shown.
@@ -0,0 +1,184 @@
|
||||
#!/usr/bin/env bash
|
||||
# Test-only PKI for quick 261009-ikt (Zertifikat-Manager).
|
||||
# Erzeugt alle Fixtures dieses Ordners mit OpenSSL (>= 3.4: -legacy, -not_before).
|
||||
# Die Schluessel der CAs liegen nur in einem mktemp-Ordner, den ein trap loescht;
|
||||
# committet werden nur Schluessel der Server-Zertifikate.
|
||||
# Passwort aller geschuetzten Fixtures: Test-Pass-123
|
||||
# Aufruf: bash apps/api/src/cert-manager/__fixtures__/make-fixtures.sh
|
||||
set -euo pipefail
|
||||
|
||||
OUT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
T="$(mktemp -d)"
|
||||
trap 'rm -rf "$T"' EXIT
|
||||
PW='Test-Pass-123'
|
||||
DAYS=36500
|
||||
|
||||
# Alte, erzeugte Dateien entfernen (Skript und README bleiben).
|
||||
find "$OUT" -maxdepth 1 -type f ! -name make-fixtures.sh ! -name README.md -delete
|
||||
|
||||
rsa_key() { openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "$1" 2>/dev/null; }
|
||||
ec_key() { openssl genpkey -algorithm EC -pkeyopt "ec_paramgen_curve:$2" -pkeyopt ec_param_enc:named_curve -out "$1" 2>/dev/null; }
|
||||
|
||||
# csr <key> <subject> <out>
|
||||
csr() { openssl req -new -key "$1" -subj "$2" -out "$3"; }
|
||||
|
||||
# selfsign <key> <subject> <extfile> <out>
|
||||
selfsign() {
|
||||
csr "$1" "$2" "$T/self.csr"
|
||||
openssl x509 -req -in "$T/self.csr" -signkey "$1" -days "$DAYS" -extfile "$3" -out "$4" 2>/dev/null
|
||||
}
|
||||
|
||||
# sign <csr> <issuer-cert> <issuer-key> <serial> <extfile> <out> [extra openssl x509 args]
|
||||
sign() {
|
||||
local c="$1" ic="$2" ik="$3" sn="$4" ext="$5" out="$6"
|
||||
shift 6
|
||||
openssl x509 -req -in "$c" -CA "$ic" -CAkey "$ik" -set_serial "$sn" -extfile "$ext" "$@" -out "$out" 2>/dev/null
|
||||
}
|
||||
|
||||
ext_root() {
|
||||
cat > "$1" <<'X'
|
||||
basicConstraints = critical,CA:TRUE
|
||||
keyUsage = critical,keyCertSign,cRLSign
|
||||
subjectKeyIdentifier = hash
|
||||
X
|
||||
}
|
||||
|
||||
ext_inter() { # <file> <aia-url> [ski]
|
||||
cat > "$1" <<X
|
||||
basicConstraints = critical,CA:TRUE,pathlen:0
|
||||
keyUsage = critical,keyCertSign,cRLSign
|
||||
subjectKeyIdentifier = ${3:-hash}
|
||||
authorityKeyIdentifier = keyid:always
|
||||
authorityInfoAccess = caIssuers;URI:$2
|
||||
X
|
||||
}
|
||||
|
||||
ext_leaf() { # <file> <san> <aia-list>
|
||||
cat > "$1" <<X
|
||||
basicConstraints = critical,CA:FALSE
|
||||
keyUsage = critical,digitalSignature,keyEncipherment
|
||||
extendedKeyUsage = serverAuth
|
||||
subjectKeyIdentifier = hash
|
||||
authorityKeyIdentifier = keyid:always
|
||||
subjectAltName = $2
|
||||
authorityInfoAccess = $3
|
||||
X
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- RSA PKI
|
||||
rsa_key "$T/rsa-root.key"
|
||||
rsa_key "$T/rsa-root2.key"
|
||||
rsa_key "$T/rsa-inter.key"
|
||||
rsa_key "$T/rsa-decoy.key"
|
||||
rsa_key "$OUT/rsa-leaf-key.pem"
|
||||
rsa_key "$T/rsa-leaf-noaki.key"
|
||||
rsa_key "$T/rsa-self.key"
|
||||
|
||||
ext_root "$T/ext-root"
|
||||
selfsign "$T/rsa-root.key" "/O=Tessera Test/CN=Tessera Test Root RSA" "$T/ext-root" "$OUT/rsa-root.pem"
|
||||
selfsign "$T/rsa-root2.key" "/O=Tessera Test/CN=Tessera Test Root RSA 2" "$T/ext-root" "$OUT/rsa-root2.pem"
|
||||
|
||||
csr "$T/rsa-inter.key" "/O=Tessera Test/CN=Tessera Test Inter RSA" "$T/rsa-inter.csr"
|
||||
ext_inter "$T/ext-inter" "http://pki.example.test/rsa-root.cer"
|
||||
sign "$T/rsa-inter.csr" "$OUT/rsa-root.pem" "$T/rsa-root.key" 101 "$T/ext-inter" "$OUT/rsa-inter.pem" -days "$DAYS"
|
||||
# gleicher Name, gleicher Schluessel, von einer anderen Wurzel unterschrieben (Cross-Signing)
|
||||
sign "$T/rsa-inter.csr" "$OUT/rsa-root2.pem" "$T/rsa-root2.key" 102 "$T/ext-inter" "$OUT/rsa-inter-cross.pem" -days "$DAYS"
|
||||
# gleicher Name, gleicher Schluessel, abgelaufen
|
||||
sign "$T/rsa-inter.csr" "$OUT/rsa-root.pem" "$T/rsa-root.key" 103 "$T/ext-inter" "$OUT/rsa-inter-expired.pem" \
|
||||
-not_before 20200101000000Z -not_after 20210101000000Z
|
||||
# gleicher Name UND gleiche SubjectKeyIdentifier, aber anderer Schluessel: checkIssued klappt, die Signatur nicht
|
||||
INTER_SKI="$(openssl x509 -in "$OUT/rsa-inter.pem" -noout -ext subjectKeyIdentifier | sed -n 2p | tr -d ' ')"
|
||||
ext_inter "$T/ext-decoy" "http://pki.example.test/rsa-root.cer" "$INTER_SKI"
|
||||
csr "$T/rsa-decoy.key" "/O=Tessera Test/CN=Tessera Test Inter RSA" "$T/rsa-decoy.csr"
|
||||
sign "$T/rsa-decoy.csr" "$OUT/rsa-root.pem" "$T/rsa-root.key" 104 "$T/ext-decoy" "$OUT/rsa-inter-decoy.pem" -days "$DAYS"
|
||||
|
||||
ext_leaf "$T/ext-rsa-leaf" "DNS:www.example.test,DNS:example.test" "caIssuers;URI:http://pki.example.test/rsa-inter.cer"
|
||||
csr "$OUT/rsa-leaf-key.pem" "/O=Tessera Test/CN=www.example.test" "$T/rsa-leaf.csr"
|
||||
sign "$T/rsa-leaf.csr" "$OUT/rsa-inter.pem" "$T/rsa-inter.key" 201 "$T/ext-rsa-leaf" "$OUT/rsa-leaf.pem" -days "$DAYS"
|
||||
openssl x509 -in "$OUT/rsa-leaf.pem" -outform DER -out "$OUT/rsa-leaf.cer"
|
||||
|
||||
# ohne AKI/SKI und ohne AIA
|
||||
cat > "$T/ext-noaki" <<'X'
|
||||
basicConstraints = critical,CA:FALSE
|
||||
keyUsage = critical,digitalSignature,keyEncipherment
|
||||
subjectKeyIdentifier = none
|
||||
authorityKeyIdentifier = none
|
||||
subjectAltName = DNS:noaki.example.test
|
||||
X
|
||||
csr "$T/rsa-leaf-noaki.key" "/O=Tessera Test/CN=noaki.example.test" "$T/noaki.csr"
|
||||
sign "$T/noaki.csr" "$OUT/rsa-inter.pem" "$T/rsa-inter.key" 202 "$T/ext-noaki" "$OUT/rsa-leaf-noaki.pem" -days "$DAYS"
|
||||
|
||||
# AIA-Adressen, die Tessera nie abrufen darf (Task 7)
|
||||
ext_leaf "$T/ext-aia" "DNS:aia-private.example.test" "caIssuers;URI:http://127.0.0.1/inter.cer,caIssuers;URI:http://169.254.169.254/latest,caIssuers;URI:ldap://ldap.example.test/cn=inter"
|
||||
csr "$T/rsa-self.key" "/O=Tessera Test/CN=aia-private.example.test" "$T/aia.csr"
|
||||
sign "$T/aia.csr" "$OUT/rsa-inter.pem" "$T/rsa-inter.key" 203 "$T/ext-aia" "$OUT/aia-private-leaf.pem" -days "$DAYS"
|
||||
|
||||
# selbstsigniert, keine CA
|
||||
cat > "$T/ext-self" <<'X'
|
||||
basicConstraints = critical,CA:FALSE
|
||||
keyUsage = critical,digitalSignature,keyEncipherment
|
||||
subjectKeyIdentifier = hash
|
||||
subjectAltName = DNS:selfsigned.example.test
|
||||
X
|
||||
rsa_key "$T/rsa-self2.key"
|
||||
selfsign "$T/rsa-self2.key" "/O=Tessera Test/CN=selfsigned.example.test" "$T/ext-self" "$OUT/selfsigned-leaf.pem"
|
||||
|
||||
cat "$OUT/rsa-leaf.pem" "$OUT/rsa-inter.pem" "$OUT/rsa-root.pem" > "$OUT/rsa-fullchain.pem"
|
||||
openssl crl2pkcs7 -nocrl -certfile "$OUT/rsa-leaf.pem" -certfile "$OUT/rsa-inter.pem" -certfile "$OUT/rsa-root.pem" -out "$OUT/rsa-chain.p7b"
|
||||
openssl crl2pkcs7 -nocrl -certfile "$OUT/rsa-leaf.pem" -certfile "$OUT/rsa-inter.pem" -certfile "$OUT/rsa-root.pem" -outform DER -out "$OUT/rsa-chain.p7c"
|
||||
openssl x509 -in "$OUT/rsa-leaf.pem" -trustout -addtrust serverAuth -out "$OUT/rsa-trusted.pem"
|
||||
|
||||
# -------------------------------------------------------------------- EC PKI
|
||||
ec_key "$T/ec-root.key" secp384r1
|
||||
ec_key "$T/ec-inter.key" secp384r1
|
||||
ec_key "$OUT/ec-leaf-key.pem" prime256v1
|
||||
|
||||
selfsign "$T/ec-root.key" "/O=Tessera Test/CN=Tessera Test Root EC" "$T/ext-root" "$OUT/ec-root.pem"
|
||||
csr "$T/ec-inter.key" "/O=Tessera Test/CN=Tessera Test Inter EC" "$T/ec-inter.csr"
|
||||
ext_inter "$T/ext-ec-inter" "http://pki.example.test/ec-root.cer"
|
||||
sign "$T/ec-inter.csr" "$OUT/ec-root.pem" "$T/ec-root.key" 301 "$T/ext-ec-inter" "$OUT/ec-inter.pem" -days "$DAYS"
|
||||
ext_leaf "$T/ext-ec-leaf" "DNS:ec.example.test" "caIssuers;URI:http://pki.example.test/ec-inter.cer"
|
||||
csr "$OUT/ec-leaf-key.pem" "/O=Tessera Test/CN=ec.example.test" "$T/ec-leaf.csr"
|
||||
sign "$T/ec-leaf.csr" "$OUT/ec-inter.pem" "$T/ec-inter.key" 401 "$T/ext-ec-leaf" "$OUT/ec-leaf.pem" -days "$DAYS"
|
||||
openssl x509 -in "$OUT/ec-leaf.pem" -outform DER -out "$OUT/ec-leaf.cer"
|
||||
cat "$OUT/ec-leaf.pem" "$OUT/ec-inter.pem" "$OUT/ec-root.pem" > "$OUT/ec-fullchain.pem"
|
||||
openssl crl2pkcs7 -nocrl -certfile "$OUT/ec-leaf.pem" -certfile "$OUT/ec-inter.pem" -certfile "$OUT/ec-root.pem" -out "$OUT/ec-chain.p7b"
|
||||
|
||||
# -------------------------------------------------- Schluessel in allen Formen
|
||||
# RSA (rsa-leaf-key.pem ist PKCS#8, unverschluesselt)
|
||||
openssl rsa -in "$OUT/rsa-leaf-key.pem" -traditional -out "$OUT/rsa-leaf-key-pkcs1.pem" 2>/dev/null
|
||||
openssl pkcs8 -topk8 -in "$OUT/rsa-leaf-key.pem" -v2 aes-256-cbc -passout "pass:$PW" -out "$OUT/rsa-leaf-key-enc-pkcs8.pem"
|
||||
openssl rsa -in "$OUT/rsa-leaf-key.pem" -traditional -aes256 -passout "pass:$PW" -out "$OUT/rsa-leaf-key-enc-trad.pem" 2>/dev/null
|
||||
openssl pkcs8 -topk8 -nocrypt -in "$OUT/rsa-leaf-key.pem" -outform DER -out "$OUT/rsa-leaf-key-pkcs8.der"
|
||||
openssl rsa -in "$OUT/rsa-leaf-key.pem" -traditional -outform DER -out "$OUT/rsa-leaf-key-pkcs1.der" 2>/dev/null
|
||||
# EC (ec-leaf-key.pem ist PKCS#8, unverschluesselt)
|
||||
openssl ec -in "$OUT/ec-leaf-key.pem" -out "$OUT/ec-leaf-key-sec1.pem" 2>/dev/null
|
||||
openssl pkcs8 -topk8 -in "$OUT/ec-leaf-key.pem" -v1 PBE-SHA1-3DES -passout "pass:$PW" -out "$OUT/ec-leaf-key-enc-pkcs8.pem"
|
||||
openssl ec -in "$OUT/ec-leaf-key.pem" -aes256 -passout "pass:$PW" -out "$OUT/ec-leaf-key-enc-trad.pem" 2>/dev/null
|
||||
openssl ec -in "$OUT/ec-leaf-key.pem" -outform DER -out "$OUT/ec-leaf-key-sec1.der" 2>/dev/null
|
||||
openssl pkcs8 -topk8 -in "$OUT/ec-leaf-key.pem" -v1 PBE-SHA1-3DES -passout "pass:$PW" -outform DER -out "$OUT/ec-leaf-key-enc-pkcs8.der"
|
||||
|
||||
# ---------------------------------------------------------------------- CSRs
|
||||
openssl req -new -key "$OUT/rsa-leaf-key.pem" -subj "/O=Tessera Test/CN=www.example.test" \
|
||||
-addext "subjectAltName=DNS:www.example.test,DNS:example.test" -out "$OUT/rsa-leaf.csr"
|
||||
openssl req -in "$OUT/rsa-leaf.csr" -outform DER -out "$OUT/rsa-leaf.csr.der"
|
||||
openssl req -new -key "$OUT/ec-leaf-key.pem" -subj "/O=Tessera Test/CN=ec.example.test" \
|
||||
-addext "subjectAltName=DNS:ec.example.test" -out "$OUT/ec-leaf.csr"
|
||||
openssl req -in "$OUT/ec-leaf.csr" -outform DER -out "$OUT/ec-leaf.csr.der"
|
||||
|
||||
# ---------------------------------------------------------------------- PFX
|
||||
cat "$OUT/rsa-inter.pem" "$OUT/rsa-root.pem" > "$T/rsa-ca.pem"
|
||||
cat "$OUT/ec-inter.pem" "$OUT/ec-root.pem" > "$T/ec-ca.pem"
|
||||
COMPAT=(-certpbe PBE-SHA1-3DES -keypbe PBE-SHA1-3DES -macalg sha1)
|
||||
openssl pkcs12 -export -inkey "$OUT/rsa-leaf-key.pem" -in "$OUT/rsa-leaf.pem" -certfile "$T/rsa-ca.pem" -passout "pass:$PW" -out "$OUT/rsa-modern.pfx"
|
||||
openssl pkcs12 -export -inkey "$OUT/rsa-leaf-key.pem" -in "$OUT/rsa-leaf.pem" -certfile "$T/rsa-ca.pem" -passout "pass:$PW" "${COMPAT[@]}" -out "$OUT/rsa-compat.pfx"
|
||||
openssl pkcs12 -export -legacy -inkey "$OUT/rsa-leaf-key.pem" -in "$OUT/rsa-leaf.pem" -certfile "$T/rsa-ca.pem" -passout "pass:$PW" -out "$OUT/rsa-legacy.pfx"
|
||||
openssl pkcs12 -export -inkey "$OUT/rsa-leaf-key.pem" -in "$OUT/rsa-leaf.pem" -certfile "$T/rsa-ca.pem" -passout "pass:" -out "$OUT/rsa-nopass.pfx"
|
||||
openssl pkcs12 -export -inkey "$OUT/ec-leaf-key.pem" -in "$OUT/ec-leaf.pem" -certfile "$T/ec-ca.pem" -passout "pass:$PW" -out "$OUT/ec-modern.pfx"
|
||||
openssl pkcs12 -export -inkey "$OUT/ec-leaf-key.pem" -in "$OUT/ec-leaf.pem" -certfile "$T/ec-ca.pem" -passout "pass:$PW" "${COMPAT[@]}" -out "$OUT/ec-compat.pfx"
|
||||
cp "$OUT/rsa-modern.pfx" "$OUT/rsa-modern.bin"
|
||||
|
||||
# ----------------------------------------------------- ZIP mit Passwortschutz
|
||||
( cd "$OUT" && zip -q -j -P "$PW" encrypted-entry.zip rsa-leaf.pem )
|
||||
|
||||
echo "fixtures ok: $(find "$OUT" -maxdepth 1 -type f | wc -l) files"
|
||||
@@ -0,0 +1,60 @@
|
||||
-----BEGIN PKCS7-----
|
||||
MIIK2gYJKoZIhvcNAQcCoIIKyzCCCscCAQExADALBgkqhkiG9w0BBwGgggqvMIID
|
||||
zTCCArWgAwIBAgICAMkwDQYJKoZIhvcNAQELBQAwODEVMBMGA1UECgwMVGVzc2Vy
|
||||
YSBUZXN0MR8wHQYDVQQDDBZUZXNzZXJhIFRlc3QgSW50ZXIgUlNBMCAXDTI2MTAw
|
||||
OTEyNDIwMVoYDzIxMjYwOTE1MTI0MjAxWjAyMRUwEwYDVQQKDAxUZXNzZXJhIFRl
|
||||
c3QxGTAXBgNVBAMMEHd3dy5leGFtcGxlLnRlc3QwggEiMA0GCSqGSIb3DQEBAQUA
|
||||
A4IBDwAwggEKAoIBAQChsylCqNQb5oizfylAouxYYV8zg+SyS7tgi+K4BhNxbfKK
|
||||
siX3CU381IdnMxxhbZnjpO5BEsLGxXmFt9tDrleek4Cj+Jsr3bdGBhddAq1D6qyp
|
||||
ovy7jbjGWzwk7RaXLtzLghWJNF+5ZPQxoNs46qFC5f6/CvsP6SWpNaQFr71Wa7sB
|
||||
JTqCs4mTO6x4mhVB5938Ra817KbVBPnCWFkr15gEGr/h6b7Z4JpAUGwPX4nwx97/
|
||||
eVHUuZpuQQv9orPf7gdluGe9WMes+z2C1ffy+OpW2qXj3zeJddSsMZ/Z16gyV7gJ
|
||||
7V6fkvuzIiF4ptbh2putG3VEoQHNch5XwUcjyoGPAgMBAAGjgeQwgeEwDAYDVR0T
|
||||
AQH/BAIwADAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHQYD
|
||||
VR0OBBYEFJZb5QnoYaGiFQ+69CazBuUz4l8eMB8GA1UdIwQYMBaAFOHtu9D1f2Ic
|
||||
pibbPD3+dpxA5G2fMCkGA1UdEQQiMCCCEHd3dy5leGFtcGxlLnRlc3SCDGV4YW1w
|
||||
bGUudGVzdDBBBggrBgEFBQcBAQQ1MDMwMQYIKwYBBQUHMAKGJWh0dHA6Ly9wa2ku
|
||||
ZXhhbXBsZS50ZXN0L3JzYS1pbnRlci5jZXIwDQYJKoZIhvcNAQELBQADggEBAIjy
|
||||
KW27UJvII5osjVF23qEs+3VpxfLFTFGStFcOgfwyo4K7vxkLkHuI2zuKDzgJgJNq
|
||||
VqA0E1iWrvVLYH98+neaHAO19Dhaf7XJeCGIIyHELKl3vunMUhzldF3sgZAptQls
|
||||
9FcTmD0in5Kf+HGztndrLpuCK3Tk9nCH1/N/ijO3duZ21S6ZetgVGBxTmk9GiNaO
|
||||
52ILdP/dfxRejtj+3NwofOCnoUhdu9TKn5Ynhwk//jYTNlmShpytTe3yfqKKF7Hy
|
||||
CSZjsy/sp/4YQp97bOPbfKiz7GrLN11B8cG6E8xNeDkKDzjuiv2qKZ9DfzYxXAo3
|
||||
LtjpjzOkAWpOV/F5v+QwggOWMIICfqADAgECAgFlMA0GCSqGSIb3DQEBCwUAMDcx
|
||||
FTATBgNVBAoMDFRlc3NlcmEgVGVzdDEeMBwGA1UEAwwVVGVzc2VyYSBUZXN0IFJv
|
||||
b3QgUlNBMCAXDTI2MTAwOTEyNDIwMVoYDzIxMjYwOTE1MTI0MjAxWjA4MRUwEwYD
|
||||
VQQKDAxUZXNzZXJhIFRlc3QxHzAdBgNVBAMMFlRlc3NlcmEgVGVzdCBJbnRlciBS
|
||||
U0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDjgDk3CHFghsd3WsY8
|
||||
PVRCEd0jhvgz/AprSNB3hnloSPL6m9JXq+kyG0s/Yd37YiErARPfn3EqGkXsQizZ
|
||||
m/Tf2p5/SJBNephu9cjNhAMvGwpE3svHL8YKrLC88LS/Bwqk4JGEXKSJhSwisoQf
|
||||
AU3m5JacQZH3lzRzTGUnHGA2HwaS7U11oBfi8QA9ODQtT3pMg6cRpZgRzjsmTAuV
|
||||
qKSPECKtO2JITwGR22AyxUeUK1aBrUTq5BPznwAE+kjsZLfe7KK8k4GjJihmEgAx
|
||||
3yDow/xW07jqjMi45q4rg/g8sqV5W5ftSBi+G5WYTZCTSHRdrBCcg74zpOvEiygA
|
||||
R3k9AgMBAAGjgakwgaYwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMC
|
||||
AQYwHQYDVR0OBBYEFOHtu9D1f2IcpibbPD3+dpxA5G2fMB8GA1UdIwQYMBaAFJxB
|
||||
RfrmzUr809FjRO+YO/ZvZSIZMEAGCCsGAQUFBwEBBDQwMjAwBggrBgEFBQcwAoYk
|
||||
aHR0cDovL3BraS5leGFtcGxlLnRlc3QvcnNhLXJvb3QuY2VyMA0GCSqGSIb3DQEB
|
||||
CwUAA4IBAQAtnOFmZdM7sMOX92zrjCSv/oDWpHgu/RX4WsyiYW9u8j4sjvnXtvjb
|
||||
jQ9KDVrTVKMTfTUZ9swW/+bMEfH6r4mQ6/BYPZVPdNN/pCFmNrc2SdgNo+/wUief
|
||||
/8n1vz0NtYySw7wrC4ztuWqtkKL5NOPSIIH78ol/IsuF30UsAVYC+pLe0FbtfZ6M
|
||||
LmbZ1sUCODwWhGNpHfYnBqwlvXkrzLnm8rORTFsWDfh14Poo02x7U2bkv4nRtbF8
|
||||
omPitJdpuxvcfxh8RvuE6AXWxpUfZpjL1sAjuTbS6DG3puvqspQOJLH5kP5zV5QU
|
||||
0zOLUe4dWKR0Oq0RVgZW9+chW4oKbwpaMIIDQDCCAiigAwIBAgIUQS/6gR+OfRDy
|
||||
zTjM/QZTaW68Yt0wDQYJKoZIhvcNAQELBQAwNzEVMBMGA1UECgwMVGVzc2VyYSBU
|
||||
ZXN0MR4wHAYDVQQDDBVUZXNzZXJhIFRlc3QgUm9vdCBSU0EwIBcNMjYxMDA5MTI0
|
||||
MjAxWhgPMjEyNjA5MTUxMjQyMDFaMDcxFTATBgNVBAoMDFRlc3NlcmEgVGVzdDEe
|
||||
MBwGA1UEAwwVVGVzc2VyYSBUZXN0IFJvb3QgUlNBMIIBIjANBgkqhkiG9w0BAQEF
|
||||
AAOCAQ8AMIIBCgKCAQEArZUI8WKuFgKxnUmI9GmwrEXJc49ZnfD9JMhUFTAlMMxo
|
||||
cQNoRFOa9S645qdx1pOPXLGvryoycX65bTwtqERwgNthvr5Mxrx9lVGy/KQQDR1S
|
||||
5kjhaJ4Shvkuf/5G65WwLSl98oKdIjeldvq/ip5VOzvnlixbDCHkHBLV3o/RGQXm
|
||||
jzM2Tp/CmdeCLhFWDQqJEwEZHsb9WMMHorPjR7374nNYnLGk2M2md5hXXpicnfnP
|
||||
sCoQ54XYPc4oQ7Zk8Tf8rgEpHcmGLo54wD0V0WZN+1nT7KUrARbSl1HKC2FNp0f3
|
||||
VPFHSigkzbtsH0IPl+Hp4S9vSv2PUzlNxhLoHeblaQIDAQABo0IwQDAPBgNVHRMB
|
||||
Af8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUnEFF+ubNSvzT0WNE
|
||||
75g79m9lIhkwDQYJKoZIhvcNAQELBQADggEBACacb52tb9EidZl9UKOCgr+03fmc
|
||||
c9UFUv7U6Dh/K/d+BYyg8MW3msOd/oft8VVIA4FeK8UxJG9CMt/V08kcajYZ4rBr
|
||||
tm3A2uM+O2V2KXUtymx17bOsOb4doFHgEPgWanraGXLOfp+1WCljQO4nkyT+peVb
|
||||
+mX8QYh5DionRIviyJdeHUQtZ1lH+4iRS4hcs6qC64ARmE7rNnoPNYS8ANyQO/E0
|
||||
DZJmJiBCnWomMELjIdcZxCEONHvedeq9HgRtiEdd/r1GX/4leMmxAJMFkd8fsljr
|
||||
8Gl9FVmsqw4f7G9Urf9nSOlQMwZGa2//179cs/FuDkz4TxVIfL/0aq8v//MxAA==
|
||||
-----END PKCS7-----
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,65 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDzTCCArWgAwIBAgICAMkwDQYJKoZIhvcNAQELBQAwODEVMBMGA1UECgwMVGVz
|
||||
c2VyYSBUZXN0MR8wHQYDVQQDDBZUZXNzZXJhIFRlc3QgSW50ZXIgUlNBMCAXDTI2
|
||||
MTAwOTEyNDIwMVoYDzIxMjYwOTE1MTI0MjAxWjAyMRUwEwYDVQQKDAxUZXNzZXJh
|
||||
IFRlc3QxGTAXBgNVBAMMEHd3dy5leGFtcGxlLnRlc3QwggEiMA0GCSqGSIb3DQEB
|
||||
AQUAA4IBDwAwggEKAoIBAQChsylCqNQb5oizfylAouxYYV8zg+SyS7tgi+K4BhNx
|
||||
bfKKsiX3CU381IdnMxxhbZnjpO5BEsLGxXmFt9tDrleek4Cj+Jsr3bdGBhddAq1D
|
||||
6qypovy7jbjGWzwk7RaXLtzLghWJNF+5ZPQxoNs46qFC5f6/CvsP6SWpNaQFr71W
|
||||
a7sBJTqCs4mTO6x4mhVB5938Ra817KbVBPnCWFkr15gEGr/h6b7Z4JpAUGwPX4nw
|
||||
x97/eVHUuZpuQQv9orPf7gdluGe9WMes+z2C1ffy+OpW2qXj3zeJddSsMZ/Z16gy
|
||||
V7gJ7V6fkvuzIiF4ptbh2putG3VEoQHNch5XwUcjyoGPAgMBAAGjgeQwgeEwDAYD
|
||||
VR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEw
|
||||
HQYDVR0OBBYEFJZb5QnoYaGiFQ+69CazBuUz4l8eMB8GA1UdIwQYMBaAFOHtu9D1
|
||||
f2IcpibbPD3+dpxA5G2fMCkGA1UdEQQiMCCCEHd3dy5leGFtcGxlLnRlc3SCDGV4
|
||||
YW1wbGUudGVzdDBBBggrBgEFBQcBAQQ1MDMwMQYIKwYBBQUHMAKGJWh0dHA6Ly9w
|
||||
a2kuZXhhbXBsZS50ZXN0L3JzYS1pbnRlci5jZXIwDQYJKoZIhvcNAQELBQADggEB
|
||||
AIjyKW27UJvII5osjVF23qEs+3VpxfLFTFGStFcOgfwyo4K7vxkLkHuI2zuKDzgJ
|
||||
gJNqVqA0E1iWrvVLYH98+neaHAO19Dhaf7XJeCGIIyHELKl3vunMUhzldF3sgZAp
|
||||
tQls9FcTmD0in5Kf+HGztndrLpuCK3Tk9nCH1/N/ijO3duZ21S6ZetgVGBxTmk9G
|
||||
iNaO52ILdP/dfxRejtj+3NwofOCnoUhdu9TKn5Ynhwk//jYTNlmShpytTe3yfqKK
|
||||
F7HyCSZjsy/sp/4YQp97bOPbfKiz7GrLN11B8cG6E8xNeDkKDzjuiv2qKZ9DfzYx
|
||||
XAo3LtjpjzOkAWpOV/F5v+Q=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDljCCAn6gAwIBAgIBZTANBgkqhkiG9w0BAQsFADA3MRUwEwYDVQQKDAxUZXNz
|
||||
ZXJhIFRlc3QxHjAcBgNVBAMMFVRlc3NlcmEgVGVzdCBSb290IFJTQTAgFw0yNjEw
|
||||
MDkxMjQyMDFaGA8yMTI2MDkxNTEyNDIwMVowODEVMBMGA1UECgwMVGVzc2VyYSBU
|
||||
ZXN0MR8wHQYDVQQDDBZUZXNzZXJhIFRlc3QgSW50ZXIgUlNBMIIBIjANBgkqhkiG
|
||||
9w0BAQEFAAOCAQ8AMIIBCgKCAQEA44A5NwhxYIbHd1rGPD1UQhHdI4b4M/wKa0jQ
|
||||
d4Z5aEjy+pvSV6vpMhtLP2Hd+2IhKwET359xKhpF7EIs2Zv039qef0iQTXqYbvXI
|
||||
zYQDLxsKRN7Lxy/GCqywvPC0vwcKpOCRhFykiYUsIrKEHwFN5uSWnEGR95c0c0xl
|
||||
JxxgNh8Gku1NdaAX4vEAPTg0LU96TIOnEaWYEc47JkwLlaikjxAirTtiSE8Bkdtg
|
||||
MsVHlCtWga1E6uQT858ABPpI7GS33uyivJOBoyYoZhIAMd8g6MP8VtO46ozIuOau
|
||||
K4P4PLKleVuX7UgYvhuVmE2Qk0h0XawQnIO+M6TrxIsoAEd5PQIDAQABo4GpMIGm
|
||||
MBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTh
|
||||
7bvQ9X9iHKYm2zw9/nacQORtnzAfBgNVHSMEGDAWgBScQUX65s1K/NPRY0TvmDv2
|
||||
b2UiGTBABggrBgEFBQcBAQQ0MDIwMAYIKwYBBQUHMAKGJGh0dHA6Ly9wa2kuZXhh
|
||||
bXBsZS50ZXN0L3JzYS1yb290LmNlcjANBgkqhkiG9w0BAQsFAAOCAQEALZzhZmXT
|
||||
O7DDl/ds64wkr/6A1qR4Lv0V+FrMomFvbvI+LI7517b4240PSg1a01SjE301GfbM
|
||||
Fv/mzBHx+q+JkOvwWD2VT3TTf6QhZja3NknYDaPv8FInn//J9b89DbWMksO8KwuM
|
||||
7blqrZCi+TTj0iCB+/KJfyLLhd9FLAFWAvqS3tBW7X2ejC5m2dbFAjg8FoRjaR32
|
||||
JwasJb15K8y55vKzkUxbFg34deD6KNNse1Nm5L+J0bWxfKJj4rSXabsb3H8YfEb7
|
||||
hOgF1saVH2aYy9bAI7k20ugxt6br6rKUDiSx+ZD+c1eUFNMzi1HuHVikdDqtEVYG
|
||||
VvfnIVuKCm8KWg==
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDQDCCAiigAwIBAgIUQS/6gR+OfRDyzTjM/QZTaW68Yt0wDQYJKoZIhvcNAQEL
|
||||
BQAwNzEVMBMGA1UECgwMVGVzc2VyYSBUZXN0MR4wHAYDVQQDDBVUZXNzZXJhIFRl
|
||||
c3QgUm9vdCBSU0EwIBcNMjYxMDA5MTI0MjAxWhgPMjEyNjA5MTUxMjQyMDFaMDcx
|
||||
FTATBgNVBAoMDFRlc3NlcmEgVGVzdDEeMBwGA1UEAwwVVGVzc2VyYSBUZXN0IFJv
|
||||
b3QgUlNBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArZUI8WKuFgKx
|
||||
nUmI9GmwrEXJc49ZnfD9JMhUFTAlMMxocQNoRFOa9S645qdx1pOPXLGvryoycX65
|
||||
bTwtqERwgNthvr5Mxrx9lVGy/KQQDR1S5kjhaJ4Shvkuf/5G65WwLSl98oKdIjel
|
||||
dvq/ip5VOzvnlixbDCHkHBLV3o/RGQXmjzM2Tp/CmdeCLhFWDQqJEwEZHsb9WMMH
|
||||
orPjR7374nNYnLGk2M2md5hXXpicnfnPsCoQ54XYPc4oQ7Zk8Tf8rgEpHcmGLo54
|
||||
wD0V0WZN+1nT7KUrARbSl1HKC2FNp0f3VPFHSigkzbtsH0IPl+Hp4S9vSv2PUzlN
|
||||
xhLoHeblaQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB
|
||||
BjAdBgNVHQ4EFgQUnEFF+ubNSvzT0WNE75g79m9lIhkwDQYJKoZIhvcNAQELBQAD
|
||||
ggEBACacb52tb9EidZl9UKOCgr+03fmcc9UFUv7U6Dh/K/d+BYyg8MW3msOd/oft
|
||||
8VVIA4FeK8UxJG9CMt/V08kcajYZ4rBrtm3A2uM+O2V2KXUtymx17bOsOb4doFHg
|
||||
EPgWanraGXLOfp+1WCljQO4nkyT+peVb+mX8QYh5DionRIviyJdeHUQtZ1lH+4iR
|
||||
S4hcs6qC64ARmE7rNnoPNYS8ANyQO/E0DZJmJiBCnWomMELjIdcZxCEONHvedeq9
|
||||
HgRtiEdd/r1GX/4leMmxAJMFkd8fsljr8Gl9FVmsqw4f7G9Urf9nSOlQMwZGa2//
|
||||
179cs/FuDkz4TxVIfL/0aq8v//M=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,22 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDmDCCAoCgAwIBAgIBZjANBgkqhkiG9w0BAQsFADA5MRUwEwYDVQQKDAxUZXNz
|
||||
ZXJhIFRlc3QxIDAeBgNVBAMMF1Rlc3NlcmEgVGVzdCBSb290IFJTQSAyMCAXDTI2
|
||||
MTAwOTEyNDIwMVoYDzIxMjYwOTE1MTI0MjAxWjA4MRUwEwYDVQQKDAxUZXNzZXJh
|
||||
IFRlc3QxHzAdBgNVBAMMFlRlc3NlcmEgVGVzdCBJbnRlciBSU0EwggEiMA0GCSqG
|
||||
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDjgDk3CHFghsd3WsY8PVRCEd0jhvgz/Apr
|
||||
SNB3hnloSPL6m9JXq+kyG0s/Yd37YiErARPfn3EqGkXsQizZm/Tf2p5/SJBNephu
|
||||
9cjNhAMvGwpE3svHL8YKrLC88LS/Bwqk4JGEXKSJhSwisoQfAU3m5JacQZH3lzRz
|
||||
TGUnHGA2HwaS7U11oBfi8QA9ODQtT3pMg6cRpZgRzjsmTAuVqKSPECKtO2JITwGR
|
||||
22AyxUeUK1aBrUTq5BPznwAE+kjsZLfe7KK8k4GjJihmEgAx3yDow/xW07jqjMi4
|
||||
5q4rg/g8sqV5W5ftSBi+G5WYTZCTSHRdrBCcg74zpOvEiygAR3k9AgMBAAGjgakw
|
||||
gaYwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE
|
||||
FOHtu9D1f2IcpibbPD3+dpxA5G2fMB8GA1UdIwQYMBaAFAWS7tXYkaYfZby55IDF
|
||||
jWqB9JYsMEAGCCsGAQUFBwEBBDQwMjAwBggrBgEFBQcwAoYkaHR0cDovL3BraS5l
|
||||
eGFtcGxlLnRlc3QvcnNhLXJvb3QuY2VyMA0GCSqGSIb3DQEBCwUAA4IBAQAgYACL
|
||||
ZoSkNHNY6qb1DBF7x4tmiElh7gFQb1SMlNhRz26uk8nkuLyv/iN76WMMURb2iZ9D
|
||||
LacWOFlJ6HVR5lmgJ9to10c3U8pWETbewV0Y4Y0/onw52xkdNOj18cOaNNSTYcUa
|
||||
bVZf6aYiRVqzve2s2Eizk1/ERghiFYBDmibqmyoiunsWeQQlLJ3KpPoc4Os8+CRG
|
||||
mDcxB5pPTzWDf4KT/O47SlSOLegRkyW0FOFRRInAPU6Xj9eATrw/p7oiApe57+Lb
|
||||
95rJU7K1T4uLczv0AHkTmHdpOy5C0nkZ1+nvc6KOutStWu9h+9EPbV/iu4E1BYJC
|
||||
JI5rO38ryDKo2/Lq
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,22 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDljCCAn6gAwIBAgIBaDANBgkqhkiG9w0BAQsFADA3MRUwEwYDVQQKDAxUZXNz
|
||||
ZXJhIFRlc3QxHjAcBgNVBAMMFVRlc3NlcmEgVGVzdCBSb290IFJTQTAgFw0yNjEw
|
||||
MDkxMjQyMDFaGA8yMTI2MDkxNTEyNDIwMVowODEVMBMGA1UECgwMVGVzc2VyYSBU
|
||||
ZXN0MR8wHQYDVQQDDBZUZXNzZXJhIFRlc3QgSW50ZXIgUlNBMIIBIjANBgkqhkiG
|
||||
9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0GLXEIhbzwlL4jFZiPoG9gGlZt/wTltUoS8A
|
||||
GGlqIznA5IHoOON+YOkcMw8DkU+UJHFbuQZwHAzDxiMf0hPPv7johdvyrsRrQAMG
|
||||
fJ/xa3vdDVKE2O0w1g1q3ivJY7d9arfqxVqNhKpgKV3SoHqI9RUh1Li6wulxNWxv
|
||||
f7OIp+NqfSab9/ocfuRkKDhZFsG11jDhUdO3hmMcqU0WgHbm04/tX1eXOU9l7cMM
|
||||
E1S6m3yUYkC2qt75cdf4TE0UCaxGkiw+RKkTZ3vUzheFOFQXzI0SXcclKsaKckNf
|
||||
iYkQLrUfBeXf5adGqZdRUO6kpIjPrYaSnaBayDu80Hp0DoZwXQIDAQABo4GpMIGm
|
||||
MBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTh
|
||||
7bvQ9X9iHKYm2zw9/nacQORtnzAfBgNVHSMEGDAWgBScQUX65s1K/NPRY0TvmDv2
|
||||
b2UiGTBABggrBgEFBQcBAQQ0MDIwMAYIKwYBBQUHMAKGJGh0dHA6Ly9wa2kuZXhh
|
||||
bXBsZS50ZXN0L3JzYS1yb290LmNlcjANBgkqhkiG9w0BAQsFAAOCAQEAUiEAAGEQ
|
||||
08Uzdb8L4uB/llHWPe4/PuyXzNBtoO0HmG4XHhSDzXNkZIdwZDl1wqnkKGR+Nj0j
|
||||
zvAu1c6DIvtN8ylsdAwgccyc1ge3edvjE11pgUimrKOblvA+l3ZjqqDvFeKoQkTn
|
||||
IWHgdGa5EZY88IA96UYsMkud6QiBAZziUqqSv3a9uVvezfrvSyx3a02yEFTMXc1u
|
||||
7Bxu+Sfc3/bLs/Yspobf2CqR6S9EPvQ41+WvNHv35OjU3ANtThxeLATJ4+F180Or
|
||||
q3Vt2YdVighbxFId8A9/Nzh3ujRawOdV6J3P0w5LhHSpECYUyyOiwCk77uqNo4X2
|
||||
tDj3p1UEZoHeCg==
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,22 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDlDCCAnygAwIBAgIBZzANBgkqhkiG9w0BAQsFADA3MRUwEwYDVQQKDAxUZXNz
|
||||
ZXJhIFRlc3QxHjAcBgNVBAMMFVRlc3NlcmEgVGVzdCBSb290IFJTQTAeFw0yMDAx
|
||||
MDEwMDAwMDBaFw0yMTAxMDEwMDAwMDBaMDgxFTATBgNVBAoMDFRlc3NlcmEgVGVz
|
||||
dDEfMB0GA1UEAwwWVGVzc2VyYSBUZXN0IEludGVyIFJTQTCCASIwDQYJKoZIhvcN
|
||||
AQEBBQADggEPADCCAQoCggEBAOOAOTcIcWCGx3daxjw9VEIR3SOG+DP8CmtI0HeG
|
||||
eWhI8vqb0ler6TIbSz9h3ftiISsBE9+fcSoaRexCLNmb9N/ann9IkE16mG71yM2E
|
||||
Ay8bCkTey8cvxgqssLzwtL8HCqTgkYRcpImFLCKyhB8BTebklpxBkfeXNHNMZScc
|
||||
YDYfBpLtTXWgF+LxAD04NC1PekyDpxGlmBHOOyZMC5WopI8QIq07YkhPAZHbYDLF
|
||||
R5QrVoGtROrkE/OfAAT6SOxkt97soryTgaMmKGYSADHfIOjD/FbTuOqMyLjmriuD
|
||||
+DyypXlbl+1IGL4blZhNkJNIdF2sEJyDvjOk68SLKABHeT0CAwEAAaOBqTCBpjAS
|
||||
BgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQU4e27
|
||||
0PV/YhymJts8Pf52nEDkbZ8wHwYDVR0jBBgwFoAUnEFF+ubNSvzT0WNE75g79m9l
|
||||
IhkwQAYIKwYBBQUHAQEENDAyMDAGCCsGAQUFBzAChiRodHRwOi8vcGtpLmV4YW1w
|
||||
bGUudGVzdC9yc2Etcm9vdC5jZXIwDQYJKoZIhvcNAQELBQADggEBAIxVn/C/QXPn
|
||||
4KJ0to2Zp1RyCF6A63+eMufEwN1E4IVkULfGn4I0hoIr3mfu8bXb+DLi2h6YZhUK
|
||||
TBY1mzAYXrgAp909hOlUjwAvsQ5UIxlJJC3Ksb56JbjDvngDerdwxvahPwWfi/ct
|
||||
t30fTfJ+T6acclubq8MFx0rmzxMsMv0ZyvKkwm7GZ1vJNmMq/oqLIvQlcDTeFzQw
|
||||
TtrU78QlMwIoZ2fzU9H15z6ZIS+R/hzkCXCgwVUDVTlmR+tYUG45COJDgmf6VP7P
|
||||
ayU6qx3yVYNku1k4fbTEiVKHnxtVUakOr4LQDTDVT32Dy4+NHH9wC0lXPqxSnuSm
|
||||
Xa+kt19+8Jo=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,22 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDljCCAn6gAwIBAgIBZTANBgkqhkiG9w0BAQsFADA3MRUwEwYDVQQKDAxUZXNz
|
||||
ZXJhIFRlc3QxHjAcBgNVBAMMFVRlc3NlcmEgVGVzdCBSb290IFJTQTAgFw0yNjEw
|
||||
MDkxMjQyMDFaGA8yMTI2MDkxNTEyNDIwMVowODEVMBMGA1UECgwMVGVzc2VyYSBU
|
||||
ZXN0MR8wHQYDVQQDDBZUZXNzZXJhIFRlc3QgSW50ZXIgUlNBMIIBIjANBgkqhkiG
|
||||
9w0BAQEFAAOCAQ8AMIIBCgKCAQEA44A5NwhxYIbHd1rGPD1UQhHdI4b4M/wKa0jQ
|
||||
d4Z5aEjy+pvSV6vpMhtLP2Hd+2IhKwET359xKhpF7EIs2Zv039qef0iQTXqYbvXI
|
||||
zYQDLxsKRN7Lxy/GCqywvPC0vwcKpOCRhFykiYUsIrKEHwFN5uSWnEGR95c0c0xl
|
||||
JxxgNh8Gku1NdaAX4vEAPTg0LU96TIOnEaWYEc47JkwLlaikjxAirTtiSE8Bkdtg
|
||||
MsVHlCtWga1E6uQT858ABPpI7GS33uyivJOBoyYoZhIAMd8g6MP8VtO46ozIuOau
|
||||
K4P4PLKleVuX7UgYvhuVmE2Qk0h0XawQnIO+M6TrxIsoAEd5PQIDAQABo4GpMIGm
|
||||
MBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTh
|
||||
7bvQ9X9iHKYm2zw9/nacQORtnzAfBgNVHSMEGDAWgBScQUX65s1K/NPRY0TvmDv2
|
||||
b2UiGTBABggrBgEFBQcBAQQ0MDIwMAYIKwYBBQUHMAKGJGh0dHA6Ly9wa2kuZXhh
|
||||
bXBsZS50ZXN0L3JzYS1yb290LmNlcjANBgkqhkiG9w0BAQsFAAOCAQEALZzhZmXT
|
||||
O7DDl/ds64wkr/6A1qR4Lv0V+FrMomFvbvI+LI7517b4240PSg1a01SjE301GfbM
|
||||
Fv/mzBHx+q+JkOvwWD2VT3TTf6QhZja3NknYDaPv8FInn//J9b89DbWMksO8KwuM
|
||||
7blqrZCi+TTj0iCB+/KJfyLLhd9FLAFWAvqS3tBW7X2ejC5m2dbFAjg8FoRjaR32
|
||||
JwasJb15K8y55vKzkUxbFg34deD6KNNse1Nm5L+J0bWxfKJj4rSXabsb3H8YfEb7
|
||||
hOgF1saVH2aYy9bAI7k20ugxt6br6rKUDiSx+ZD+c1eUFNMzi1HuHVikdDqtEVYG
|
||||
VvfnIVuKCm8KWg==
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,30 @@
|
||||
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||
MIIFNTBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQg5fXQz/YSEVEX28A
|
||||
AdpmwgICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEOKJk93MoScWJyo4
|
||||
N+rjX0sEggTQmNcQPcv0NB050XsfUuol1MtoaYl4w6/KfPib1U/AN8lUbcZy0E/5
|
||||
M22jc4bAj1MyodSSMzcUzzYpfzFlVXOBN2oy3fcuoUxn2b2qv4GUiNmUSudHnZIm
|
||||
KFRswKxXjHrESgq/qoJdz9eZAx5zDeLX+eB7WhUB5RLYiv+g7edmarNump1gNpLb
|
||||
8VSNLmh4b4dfk9snoktkP+gLZZ7oRw4FiQw33WJE0MF3oB+WSd1fvtpK2Vv+bND4
|
||||
VngNPIjM71n/jfBkIRDTNGWKkqILJekjcjDvaf9ICHbIoBKORWBnd9VbVdJM/DAW
|
||||
u8hG+ynCl9HsAwkW3KH/nCrfKbmiVSA+gmzyRuEupQeVrYTET2zszZkQhLQFrCgO
|
||||
FfiF38oW3+BQ0uwkEEctv+AT/qsBqBTg+9LvXQKIVuvLF0i0mhjRyIYc8I53FPyK
|
||||
vycuMMEktmXP7kslY7ooYbH8E09DnzZU+V51rrEruox0WAmm/s1kF0LOPMDuB3cY
|
||||
HqhsYBat8kNWDdnsMDTuwPottWj0JeB2nUALxrgYEMQUgPLkwiZgzr9ka5sS97gM
|
||||
dzXfuaaFjvZU7jG9nQPriDftNtTXsspSiwyUFEgpiJF5E4QS/6bxupGwA6kTtU4k
|
||||
i3q93ABKEMC7EuAO98RbeXPQe9pGY0n4F6K7fv2ORdPCUV1QvqDWbDHjxXxg/jLn
|
||||
nc4VZ8OHdhcNj4oY0l3Bsttur/fzNzNBgwb1fW8vFTimJcyYSuB3eaLn3NYZi9Ff
|
||||
8zM2qkmEuHz/TmzveOO3LSu5sTQ7opXPEjkXzFCVy0kqYD6KhAR7EnBHhufAzuyw
|
||||
j1r1J23HUYoZBGpIPpyErm8Om+1EQGk0jmjPX8zM4jODgzpp1wyzQVnJcCJTBDfA
|
||||
wHKZX1Q9twKtmFbzwvqPCZfgDWcah4aH3QvFhiV8l6fA36Bqv+cwzFPEAjW+WdbE
|
||||
mQfzdgZN9LOzwNoru2zh4I3oOktRpolO0ljB+B84ZmmsKtWj2QZqv+KFnacZ1zku
|
||||
KzHxiPzjZA+70xqMEtdo0aZ2KpqLR9BFXsXYWkEUaoKoIEeR71yfzzY2bHn8ghPn
|
||||
e54aj2IJdaHldKku5udo88/xms9F8z8hfQ2xu3z64k7y4gl+NyMUsRz0G22D0tHe
|
||||
PHFiauXP1ChsaMTVDmYuQV/QebV6/xyXmWvHYUbBJUcSdsXxVKu6ps4e0Pdr7Ax6
|
||||
DFOdqhXYjv2Ee1+9Fr0HoFLRIcdFbms2R63akZuh1PYIaomhM2enIWUA4vwIPKOz
|
||||
FHweDGIbckC4uFLxrI96I7QMdwJeYwVN8JQkZWKYZSWz/ImBr4xfI3v/Ykaxn/Ho
|
||||
rQVL3RPBr6UxvXesUPxqNJ0KuFml59lO3LS0X1o6JOBQMLeNoaxkxbJStzQXTovR
|
||||
QNhHrcjXe/uzR6D+7d+b1x/+07YVXu35ngHhykLoFJKHNyLcsY+5x+JV5GC0nwu7
|
||||
tMv0IEQ3ghYb46QumEwZXZcSeOdR/PtKB1Z8T/hq1LwfFrdfjB9El6lykmR8cmY8
|
||||
MOj20JWjzpeT49TL8gNyoIzcgXoTx2N7N7Spk0JjXp2H9lRM9QhVtH4D+cH7ta/x
|
||||
Ck91ivg9KD8yA6/ZyzXhaEWWJk/zQPpXCi05Cmi7qFgQP0N+N+X2WcM=
|
||||
-----END ENCRYPTED PRIVATE KEY-----
|
||||
@@ -0,0 +1,30 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
Proc-Type: 4,ENCRYPTED
|
||||
DEK-Info: AES-256-CBC,442BE8C527B808926A2DF826DAF11BAC
|
||||
|
||||
AKxmS2ayYesaUoL4UCf3uNtttP9epZklQaNSqC6Vvq6FeL4qxOLxw3tvWvDmCIKF
|
||||
4lL+xSrmrIeZiXqraWR0Frtioy04yxNxDzZAMCA4hLkoLdR2mupmOq4OoSeP82Vn
|
||||
NO4yNWAI1/qh3QvqilTLsUHINdyIgVae7r43WlfbUTrdI1iKnNe7GdKRMnUK1NDz
|
||||
wFCLSeZ3Dd7lDh81BwM57u5qJ5tmY0JHj3rZYY8IHJDZNCQAKrNqqgrZiAiq7trs
|
||||
t4PoaFiv4ShVuSpwP6twaGIQEqMkN56coy6h9ytGTSv7AesAORtY7fV7CQDo6BaQ
|
||||
K9gaGAk8w7ehGsTyIZrz7CQX+P1UYUMX8iC+VdnFhMr0rdvgtp3T5/wlgTDn4S1g
|
||||
kLqQqbfp3Ke+bjpaAiISh79Q7wRW04Q+uwL4eSqp0QLNDjsQXo0i5x9LzxN1NPhy
|
||||
u5QcDXg6/TETgzDsrHC187uh/HoF2OlNRn0tX1Cft/vadq62ZZUX0k8OsuOBEGWb
|
||||
sy3VDOOvirNkBeBGBpLsO8IQU4gQaOli5GsKMmrbV1tMfvzucScMUral3oHwOgoG
|
||||
6Oae6U+R0Nr3a7dEPFl5+ahUGtJ8ebuzv2hoTq0eYz9PB13M2sOAOTN+DYUUmob0
|
||||
9ukPX5JgVWAfObK0KlppMPp3ZOl8n+rchU7CzW4hMVqpszkEUMJA+pipL3dl1jgD
|
||||
aI5O5px3RxZA0zJYWn/hiX1d0FZ+/f/zUbXzcBm2kTjAOXxoh61d/8xiZZs47ft3
|
||||
0+VatRPqLH108tmWF/Pu2PYEqj01qlz0vVWdnhPA70J1QAh/KAEhZIFl2PYqGVaB
|
||||
MKzQFdfTtec+vrtBb/V15l1oukgfdr2PUQpmPxHfp/PgGp8dta47uyOpwBFT11Ic
|
||||
Ih5mik2UFZcS7jd4hVDqUb6Xt+GjUqFG7t+moWXKEsD+ACIC4gMQRKkMqSK76G/v
|
||||
g1bikEOJkpdnZyoDtui24eF+5lcV1Bk0iKWoOUrQbmsYkWnPqDocdQBx4Iz3J0cZ
|
||||
4+0nTeemZrSu4Rs6UoIogZkvuGFYGeM2Ao5mXH0uYbF5ZBBYzksAzxInz5fDA2I2
|
||||
Uv1Qx0Hq0SpqCSc03WZQoyo0TY43MufnYafJytgUsiaKh6W3IyQFETebKLvt7ehG
|
||||
enQN22nxpLEUZJdvUuTomjC+VvoPsjGlfrgeX5qpkXHZG3Jc3meTIcSAORkHXFkf
|
||||
OJcSXQqBv3cGq3vu8qKyfIR5aTVftaRXD5tXYmVzxQm6GFJST9zhuj48ptnNrEvC
|
||||
3BudQK3nRXFsUEC4Bx6ZrcLdIZc0wkYyySI6h3ASZx5g/+nbT1RVi0joH//MyoYa
|
||||
SOqhPtZexPxPHYmfcX8g0iDdfmumbcdHIiOG81ugBK4IDKa4I8+PTO/rJn0pV+Ky
|
||||
EPKtcBFln54/HZf4sH8MMfIfqt6maREKJZpRLX1tGtFX+VQrEIUOMhILBVYIh+Sn
|
||||
V6hmqSRfcUUhdpnOEYz747lNU0yOKpOguUwsg6VoKBH8rvsNU6V6RYzccU9BZSDX
|
||||
aUawD9xq4bZ5DyW7TJQtKSTsUDeyZx14RZkIcMFYyAwQ9flmA438yiuqfozdatoT
|
||||
-----END RSA PRIVATE KEY-----
|
||||
Binary file not shown.
@@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEAobMpQqjUG+aIs38pQKLsWGFfM4Pksku7YIviuAYTcW3yirIl
|
||||
9wlN/NSHZzMcYW2Z46TuQRLCxsV5hbfbQ65XnpOAo/ibK923RgYXXQKtQ+qsqaL8
|
||||
u424xls8JO0Wly7cy4IViTRfuWT0MaDbOOqhQuX+vwr7D+klqTWkBa+9Vmu7ASU6
|
||||
grOJkzuseJoVQefd/EWvNeym1QT5wlhZK9eYBBq/4em+2eCaQFBsD1+J8Mfe/3lR
|
||||
1LmabkEL/aKz3+4HZbhnvVjHrPs9gtX38vjqVtql4983iXXUrDGf2deoMle4Ce1e
|
||||
n5L7syIheKbW4dqbrRt1RKEBzXIeV8FHI8qBjwIDAQABAoIBABL00Pp6llsq/I2m
|
||||
c6ybLP/zcli0tqPcUvwvigWu2KqsjAmMdvzt/1GLjNAhYstdMPRKCpBgZastzWBK
|
||||
vI7DqovxmNwgMfjDhk/UOPrzbGKQ6Wr9q4lFRQsj+PoPPV295a6J8oJiPHKcjjAf
|
||||
PnP+tacFFYdJ/0LPP1YF7tOn1ZeC29hkhpsnrarrjLs6MJiSsIE+ffnlXZIx5sLv
|
||||
OmrTQDKQthmc6TNB5nUjz+5mALpQ20eQ2/vMI+mBDhglbm2zOzMmnwRo5mLg49Rt
|
||||
G6MX13a+CA2vEgid3BR9OYxb2KWmoLI5vWZ6fig+I6RYNTqaJaeGoq3nWSQTqaYf
|
||||
JejtQlECgYEA2i7cr3Ppa2EpJbIClXXXgDdtJGO1HWTgWwpb17VKKkYEb5/Fuz3X
|
||||
2G5uHBFIDvHqFFIsLiYgHnivU+sqcdk1AG77a862QTp0EOoT2A3ycGYGSFsc5Gmj
|
||||
wm4jgWwEs/VkLMZmNL9tPxZDzZXJLYI7ipNPLCObErWJsv5epJvUzakCgYEAvboM
|
||||
ER21Ic68vP7OSKBnoS2a5HCcP6XwirLa+Uf7UX43FBq47cdyffHt30vUiSVmBMYF
|
||||
N0CjbGCP1UmQZV/uvi30uTDHSMdiu0OG6YrgvTpzcYCoekC2F6Dwj/+e38vR2HjK
|
||||
6o9IVdyZsREnHSJ3Tp74OnM5NmJ8PMZIaSaoqHcCgYAUtspA5hJNHYZYi9Pu+Kcj
|
||||
ByAXilfRmJgsTIl3q0CFTuxw5IxTQIXTiHhvtbxPnuwITdzf7MKz4eVTZEo9UM/V
|
||||
YLQCAjgPbUtDp9WGd5yCy0zhZbUSgm0Hsokwal7v7uY50gQ2Ec5/ZM+/Hu9goLKi
|
||||
b2Xe3OqGFjIiij/WBO1pMQKBgDF8g4U7YHFvAR+yvo+G90C9KneHP9oCKFmlCSSY
|
||||
N98rsqYVykKLETv0jHz/xHH5RUDdfaLJw1aTYTDavUUT3qvQRNLA/FnpV8FMfbYQ
|
||||
uUniIxZp3of0tQo4ukZ1ADWTbiquZw8DTHTI03Wx6afZw5DIOirUvwmGaq1Xcq+q
|
||||
m7dFAoGBAMjSpKii/ShdmscrvQmdKxsKoxpN7LPnA10Z5fHe4EgDYH8ATugJXfMo
|
||||
y4NEfb30DTYF6PqVn0Fry284kcyjQZEEq73mPdSOQK3olMK7AAi+04dbnNT8l6xX
|
||||
0pzZoycBAsoAsW96PaWq0rHMyD09gKh56DeXEjGRCSRIB2TOG0AD
|
||||
-----END RSA PRIVATE KEY-----
|
||||
Binary file not shown.
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQChsylCqNQb5oiz
|
||||
fylAouxYYV8zg+SyS7tgi+K4BhNxbfKKsiX3CU381IdnMxxhbZnjpO5BEsLGxXmF
|
||||
t9tDrleek4Cj+Jsr3bdGBhddAq1D6qypovy7jbjGWzwk7RaXLtzLghWJNF+5ZPQx
|
||||
oNs46qFC5f6/CvsP6SWpNaQFr71Wa7sBJTqCs4mTO6x4mhVB5938Ra817KbVBPnC
|
||||
WFkr15gEGr/h6b7Z4JpAUGwPX4nwx97/eVHUuZpuQQv9orPf7gdluGe9WMes+z2C
|
||||
1ffy+OpW2qXj3zeJddSsMZ/Z16gyV7gJ7V6fkvuzIiF4ptbh2putG3VEoQHNch5X
|
||||
wUcjyoGPAgMBAAECggEAEvTQ+nqWWyr8jaZzrJss//NyWLS2o9xS/C+KBa7YqqyM
|
||||
CYx2/O3/UYuM0CFiy10w9EoKkGBlqy3NYEq8jsOqi/GY3CAx+MOGT9Q4+vNsYpDp
|
||||
av2riUVFCyP4+g89Xb3lronygmI8cpyOMB8+c/61pwUVh0n/Qs8/VgXu06fVl4Lb
|
||||
2GSGmyetquuMuzowmJKwgT59+eVdkjHmwu86atNAMpC2GZzpM0HmdSPP7mYAulDb
|
||||
R5Db+8wj6YEOGCVubbM7MyafBGjmYuDj1G0boxfXdr4IDa8SCJ3cFH05jFvYpaag
|
||||
sjm9Znp+KD4jpFg1Opolp4airedZJBOpph8l6O1CUQKBgQDaLtyvc+lrYSklsgKV
|
||||
ddeAN20kY7UdZOBbClvXtUoqRgRvn8W7PdfYbm4cEUgO8eoUUiwuJiAeeK9T6ypx
|
||||
2TUAbvtrzrZBOnQQ6hPYDfJwZgZIWxzkaaPCbiOBbASz9WQsxmY0v20/FkPNlckt
|
||||
gjuKk08sI5sStYmy/l6km9TNqQKBgQC9ugwRHbUhzry8/s5IoGehLZrkcJw/pfCK
|
||||
str5R/tRfjcUGrjtx3J98e3fS9SJJWYExgU3QKNsYI/VSZBlX+6+LfS5MMdIx2K7
|
||||
Q4bpiuC9OnNxgKh6QLYXoPCP/57fy9HYeMrqj0hV3JmxEScdIndOnvg6czk2Ynw8
|
||||
xkhpJqiodwKBgBS2ykDmEk0dhliL0+74pyMHIBeKV9GYmCxMiXerQIVO7HDkjFNA
|
||||
hdOIeG+1vE+e7AhN3N/swrPh5VNkSj1Qz9VgtAICOA9tS0On1YZ3nILLTOFltRKC
|
||||
bQeyiTBqXu/u5jnSBDYRzn9kz78e72CgsqJvZd7c6oYWMiKKP9YE7WkxAoGAMXyD
|
||||
hTtgcW8BH7K+j4b3QL0qd4c/2gIoWaUJJJg33yuyphXKQosRO/SMfP/EcflFQN19
|
||||
osnDVpNhMNq9RRPeq9BE0sD8WelXwUx9thC5SeIjFmneh/S1Cji6RnUANZNuKq5n
|
||||
DwNMdMjTdbHpp9nDkMg6KtS/CYZqrVdyr6qbt0UCgYEAyNKkqKL9KF2axyu9CZ0r
|
||||
GwqjGk3ss+cDXRnl8d7gSANgfwBO6Ald8yjLg0R9vfQNNgXo+pWfQWvLbziRzKNB
|
||||
kQSrveY91I5AreiUwrsACL7Th1uc1PyXrFfSnNmjJwECygCxb3o9parSsczIPT2A
|
||||
qHnoN5cSMZEJJEgHZM4bQAM=
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -0,0 +1,19 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDKTCCAhGgAwIBAgICAMowDQYJKoZIhvcNAQELBQAwODEVMBMGA1UECgwMVGVz
|
||||
c2VyYSBUZXN0MR8wHQYDVQQDDBZUZXNzZXJhIFRlc3QgSW50ZXIgUlNBMCAXDTI2
|
||||
MTAwOTEyNDIwMloYDzIxMjYwOTE1MTI0MjAyWjA0MRUwEwYDVQQKDAxUZXNzZXJh
|
||||
IFRlc3QxGzAZBgNVBAMMEm5vYWtpLmV4YW1wbGUudGVzdDCCASIwDQYJKoZIhvcN
|
||||
AQEBBQADggEPADCCAQoCggEBAKw9J4MOPalTZs02fV5vOYgmrXQe26Cqteidu222
|
||||
R+j9xXr9jT0wc6psHQK+mNy4bxX3vjp9d7YbGdwBYl30vU6Lu4hDr9499xR3sPAt
|
||||
1iO4adHwnvWZ/wiMkZ2PqCoWc7F0m9cfg2o1o4Qr7OtVzgpFU3HeHA3Ny+iR4YoD
|
||||
ObBfjFe/OVyBm/BFPXGmrlHCgGku7evMcXEm7xRv5q2c/EThxNidsqFOUptsSZM2
|
||||
e4TqblsuskZVchJMVQ3vp+tNomAwnRq7tf+LRjNN1fYsr7nXHmbxI0bkc0Xl/rGy
|
||||
W4qZNXVUQ9X6dE1fMKD0aELsip9zKVl4LLAZTtGvwDEuIXcCAwEAAaM/MD0wDAYD
|
||||
VR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0RBBYwFIISbm9ha2kuZXhh
|
||||
bXBsZS50ZXN0MA0GCSqGSIb3DQEBCwUAA4IBAQCaA/bLiHvN6SH7iRrS6qsn1R1y
|
||||
EBTvluor+Mtzh/IluoFw8mpgBxdLWN0dlTrTKtk2QmrBQx7ci4iJ31DVlXsZBE7a
|
||||
Vr+OvPsiqfPz9alocxJHBeHeDOVBTup91vatq113bmMwGt5GLUdBcOtIznbMrmgj
|
||||
Uj3ILK7FO6iipudVEDh6tJknGdSUa8g71RWymHQIfIaQTfu1zbP8R5PdSpdFEHpm
|
||||
mUJLqTLKfYlf3FYirbPgT4XP761TAXgXgGnqJN7PElE+wTlCrPeHeA0Hd9Eep2dq
|
||||
UDG+Nq6CeHz1nnx5V929BfO8x14YZvSCBRZW0oFCpQE6btL1IU5kPfd1YbPZ
|
||||
-----END CERTIFICATE-----
|
||||
Binary file not shown.
@@ -0,0 +1,17 @@
|
||||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIICszCCAZsCAQAwMjEVMBMGA1UECgwMVGVzc2VyYSBUZXN0MRkwFwYDVQQDDBB3
|
||||
d3cuZXhhbXBsZS50ZXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
|
||||
obMpQqjUG+aIs38pQKLsWGFfM4Pksku7YIviuAYTcW3yirIl9wlN/NSHZzMcYW2Z
|
||||
46TuQRLCxsV5hbfbQ65XnpOAo/ibK923RgYXXQKtQ+qsqaL8u424xls8JO0Wly7c
|
||||
y4IViTRfuWT0MaDbOOqhQuX+vwr7D+klqTWkBa+9Vmu7ASU6grOJkzuseJoVQefd
|
||||
/EWvNeym1QT5wlhZK9eYBBq/4em+2eCaQFBsD1+J8Mfe/3lR1LmabkEL/aKz3+4H
|
||||
ZbhnvVjHrPs9gtX38vjqVtql4983iXXUrDGf2deoMle4Ce1en5L7syIheKbW4dqb
|
||||
rRt1RKEBzXIeV8FHI8qBjwIDAQABoDwwOgYJKoZIhvcNAQkOMS0wKzApBgNVHREE
|
||||
IjAgghB3d3cuZXhhbXBsZS50ZXN0ggxleGFtcGxlLnRlc3QwDQYJKoZIhvcNAQEL
|
||||
BQADggEBAHIZISAzaPkw4XSO3KEg2Ody+RJW9+OMytWhpI5ntWqg9j/7vFHYt/eY
|
||||
TgA9cIHzlTyKwTdm9WH2nr0hLwucFo7h5ospdnbsn9XTeYSTNvvls8U2Fx+IcIsh
|
||||
nV2oj0M6KTC86vT7xBp4i3W+WFtrBIG8Ptdq8wiNBgaIPyUce2HO4UC1rjVmhEzH
|
||||
o8Ccd+7GXkmQWP3HFqPUn5B9LjAiqg5LDwf12dEc0vIyi5MMtRDgHX5YzcAdbWgf
|
||||
TXGXZaIzbHPsJveC9yRBl3rD8Fr0ydMUFu8OhrSUiCeTvEijlzOhHv2i1jA81+1Y
|
||||
ajhWgXtxKNPaKM2MWRo9CNQMlGJTTWk=
|
||||
-----END CERTIFICATE REQUEST-----
|
||||
Binary file not shown.
@@ -0,0 +1,23 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDzTCCArWgAwIBAgICAMkwDQYJKoZIhvcNAQELBQAwODEVMBMGA1UECgwMVGVz
|
||||
c2VyYSBUZXN0MR8wHQYDVQQDDBZUZXNzZXJhIFRlc3QgSW50ZXIgUlNBMCAXDTI2
|
||||
MTAwOTEyNDIwMVoYDzIxMjYwOTE1MTI0MjAxWjAyMRUwEwYDVQQKDAxUZXNzZXJh
|
||||
IFRlc3QxGTAXBgNVBAMMEHd3dy5leGFtcGxlLnRlc3QwggEiMA0GCSqGSIb3DQEB
|
||||
AQUAA4IBDwAwggEKAoIBAQChsylCqNQb5oizfylAouxYYV8zg+SyS7tgi+K4BhNx
|
||||
bfKKsiX3CU381IdnMxxhbZnjpO5BEsLGxXmFt9tDrleek4Cj+Jsr3bdGBhddAq1D
|
||||
6qypovy7jbjGWzwk7RaXLtzLghWJNF+5ZPQxoNs46qFC5f6/CvsP6SWpNaQFr71W
|
||||
a7sBJTqCs4mTO6x4mhVB5938Ra817KbVBPnCWFkr15gEGr/h6b7Z4JpAUGwPX4nw
|
||||
x97/eVHUuZpuQQv9orPf7gdluGe9WMes+z2C1ffy+OpW2qXj3zeJddSsMZ/Z16gy
|
||||
V7gJ7V6fkvuzIiF4ptbh2putG3VEoQHNch5XwUcjyoGPAgMBAAGjgeQwgeEwDAYD
|
||||
VR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEw
|
||||
HQYDVR0OBBYEFJZb5QnoYaGiFQ+69CazBuUz4l8eMB8GA1UdIwQYMBaAFOHtu9D1
|
||||
f2IcpibbPD3+dpxA5G2fMCkGA1UdEQQiMCCCEHd3dy5leGFtcGxlLnRlc3SCDGV4
|
||||
YW1wbGUudGVzdDBBBggrBgEFBQcBAQQ1MDMwMQYIKwYBBQUHMAKGJWh0dHA6Ly9w
|
||||
a2kuZXhhbXBsZS50ZXN0L3JzYS1pbnRlci5jZXIwDQYJKoZIhvcNAQELBQADggEB
|
||||
AIjyKW27UJvII5osjVF23qEs+3VpxfLFTFGStFcOgfwyo4K7vxkLkHuI2zuKDzgJ
|
||||
gJNqVqA0E1iWrvVLYH98+neaHAO19Dhaf7XJeCGIIyHELKl3vunMUhzldF3sgZAp
|
||||
tQls9FcTmD0in5Kf+HGztndrLpuCK3Tk9nCH1/N/ijO3duZ21S6ZetgVGBxTmk9G
|
||||
iNaO52ILdP/dfxRejtj+3NwofOCnoUhdu9TKn5Ynhwk//jYTNlmShpytTe3yfqKK
|
||||
F7HyCSZjsy/sp/4YQp97bOPbfKiz7GrLN11B8cG6E8xNeDkKDzjuiv2qKZ9DfzYx
|
||||
XAo3LtjpjzOkAWpOV/F5v+Q=
|
||||
-----END CERTIFICATE-----
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,20 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDQDCCAiigAwIBAgIUQS/6gR+OfRDyzTjM/QZTaW68Yt0wDQYJKoZIhvcNAQEL
|
||||
BQAwNzEVMBMGA1UECgwMVGVzc2VyYSBUZXN0MR4wHAYDVQQDDBVUZXNzZXJhIFRl
|
||||
c3QgUm9vdCBSU0EwIBcNMjYxMDA5MTI0MjAxWhgPMjEyNjA5MTUxMjQyMDFaMDcx
|
||||
FTATBgNVBAoMDFRlc3NlcmEgVGVzdDEeMBwGA1UEAwwVVGVzc2VyYSBUZXN0IFJv
|
||||
b3QgUlNBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArZUI8WKuFgKx
|
||||
nUmI9GmwrEXJc49ZnfD9JMhUFTAlMMxocQNoRFOa9S645qdx1pOPXLGvryoycX65
|
||||
bTwtqERwgNthvr5Mxrx9lVGy/KQQDR1S5kjhaJ4Shvkuf/5G65WwLSl98oKdIjel
|
||||
dvq/ip5VOzvnlixbDCHkHBLV3o/RGQXmjzM2Tp/CmdeCLhFWDQqJEwEZHsb9WMMH
|
||||
orPjR7374nNYnLGk2M2md5hXXpicnfnPsCoQ54XYPc4oQ7Zk8Tf8rgEpHcmGLo54
|
||||
wD0V0WZN+1nT7KUrARbSl1HKC2FNp0f3VPFHSigkzbtsH0IPl+Hp4S9vSv2PUzlN
|
||||
xhLoHeblaQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB
|
||||
BjAdBgNVHQ4EFgQUnEFF+ubNSvzT0WNE75g79m9lIhkwDQYJKoZIhvcNAQELBQAD
|
||||
ggEBACacb52tb9EidZl9UKOCgr+03fmcc9UFUv7U6Dh/K/d+BYyg8MW3msOd/oft
|
||||
8VVIA4FeK8UxJG9CMt/V08kcajYZ4rBrtm3A2uM+O2V2KXUtymx17bOsOb4doFHg
|
||||
EPgWanraGXLOfp+1WCljQO4nkyT+peVb+mX8QYh5DionRIviyJdeHUQtZ1lH+4iR
|
||||
S4hcs6qC64ARmE7rNnoPNYS8ANyQO/E0DZJmJiBCnWomMELjIdcZxCEONHvedeq9
|
||||
HgRtiEdd/r1GX/4leMmxAJMFkd8fsljr8Gl9FVmsqw4f7G9Urf9nSOlQMwZGa2//
|
||||
179cs/FuDkz4TxVIfL/0aq8v//M=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,20 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDRDCCAiygAwIBAgIUdyC8QeOCAMFyciFeju4zpGlnkNQwDQYJKoZIhvcNAQEL
|
||||
BQAwOTEVMBMGA1UECgwMVGVzc2VyYSBUZXN0MSAwHgYDVQQDDBdUZXNzZXJhIFRl
|
||||
c3QgUm9vdCBSU0EgMjAgFw0yNjEwMDkxMjQyMDFaGA8yMTI2MDkxNTEyNDIwMVow
|
||||
OTEVMBMGA1UECgwMVGVzc2VyYSBUZXN0MSAwHgYDVQQDDBdUZXNzZXJhIFRlc3Qg
|
||||
Um9vdCBSU0EgMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOKsZ0yL
|
||||
jIeKFN0WDvf0JMzCwaCKIMw6QB7sUImBKycDtitODNo9Wyq0q3FwiUgC7RTxfBDK
|
||||
y8T9ZyXGSIR86lL+foKrMZAChbTQSm85b1AAKoR2ejcMUjlX9pky7h82yuFdmNj0
|
||||
m2R5iX1DRvMkRUwh187jkrWBOoN9j6E+3DXXWxEofgji7PNyPhV641bSim7haf6o
|
||||
lLvTBSachZzrD5clSVznYuaxtETyVJjWdDq1+QpNS+wuDBgcgm4Hvy6XqhQRLfV9
|
||||
Msv0MntnLb/Fjsw18AVpDtf37QjyZUkPBONZb6ePSrEhT7OIbit+zf22m4EcQS5N
|
||||
ct277z/Z27cVfCsCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E
|
||||
BAMCAQYwHQYDVR0OBBYEFAWS7tXYkaYfZby55IDFjWqB9JYsMA0GCSqGSIb3DQEB
|
||||
CwUAA4IBAQCz2BSC7UW2vD8/ZGGv9uduU9LfzyF4IaPB+HZLoYhpoDqdW/PMAQqY
|
||||
AAFnadKTnVkXr6oeJDxU3jKhfEsvSDyzWS6XIsd48H8VCwItkspUhbX4wZzBPdLl
|
||||
8jSr7wMg9GGIEChhtzZSyhwlfFrQRetDIOF7x6lKvOWsErIaWGHadC/6qBOEKRYV
|
||||
tEdhxQGUt2q14vMTPfhgx7f9YZAgYSBnQhXNrrTkDlDwprmZiqdQxujdnPPG/FBU
|
||||
+0TV6PG2EKp2+lZGoEeSGp4WlVAhGLJ/1FCKt/p36/6vfXSJYxi+dQRnb12QiMD7
|
||||
+AeksQrRrPSIR57eSTsUcNJ3321Mduv7
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,23 @@
|
||||
-----BEGIN TRUSTED CERTIFICATE-----
|
||||
MIIDzTCCArWgAwIBAgICAMkwDQYJKoZIhvcNAQELBQAwODEVMBMGA1UECgwMVGVz
|
||||
c2VyYSBUZXN0MR8wHQYDVQQDDBZUZXNzZXJhIFRlc3QgSW50ZXIgUlNBMCAXDTI2
|
||||
MTAwOTEyNDIwMVoYDzIxMjYwOTE1MTI0MjAxWjAyMRUwEwYDVQQKDAxUZXNzZXJh
|
||||
IFRlc3QxGTAXBgNVBAMMEHd3dy5leGFtcGxlLnRlc3QwggEiMA0GCSqGSIb3DQEB
|
||||
AQUAA4IBDwAwggEKAoIBAQChsylCqNQb5oizfylAouxYYV8zg+SyS7tgi+K4BhNx
|
||||
bfKKsiX3CU381IdnMxxhbZnjpO5BEsLGxXmFt9tDrleek4Cj+Jsr3bdGBhddAq1D
|
||||
6qypovy7jbjGWzwk7RaXLtzLghWJNF+5ZPQxoNs46qFC5f6/CvsP6SWpNaQFr71W
|
||||
a7sBJTqCs4mTO6x4mhVB5938Ra817KbVBPnCWFkr15gEGr/h6b7Z4JpAUGwPX4nw
|
||||
x97/eVHUuZpuQQv9orPf7gdluGe9WMes+z2C1ffy+OpW2qXj3zeJddSsMZ/Z16gy
|
||||
V7gJ7V6fkvuzIiF4ptbh2putG3VEoQHNch5XwUcjyoGPAgMBAAGjgeQwgeEwDAYD
|
||||
VR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEw
|
||||
HQYDVR0OBBYEFJZb5QnoYaGiFQ+69CazBuUz4l8eMB8GA1UdIwQYMBaAFOHtu9D1
|
||||
f2IcpibbPD3+dpxA5G2fMCkGA1UdEQQiMCCCEHd3dy5leGFtcGxlLnRlc3SCDGV4
|
||||
YW1wbGUudGVzdDBBBggrBgEFBQcBAQQ1MDMwMQYIKwYBBQUHMAKGJWh0dHA6Ly9w
|
||||
a2kuZXhhbXBsZS50ZXN0L3JzYS1pbnRlci5jZXIwDQYJKoZIhvcNAQELBQADggEB
|
||||
AIjyKW27UJvII5osjVF23qEs+3VpxfLFTFGStFcOgfwyo4K7vxkLkHuI2zuKDzgJ
|
||||
gJNqVqA0E1iWrvVLYH98+neaHAO19Dhaf7XJeCGIIyHELKl3vunMUhzldF3sgZAp
|
||||
tQls9FcTmD0in5Kf+HGztndrLpuCK3Tk9nCH1/N/ijO3duZ21S6ZetgVGBxTmk9G
|
||||
iNaO52ILdP/dfxRejtj+3NwofOCnoUhdu9TKn5Ynhwk//jYTNlmShpytTe3yfqKK
|
||||
F7HyCSZjsy/sp/4YQp97bOPbfKiz7GrLN11B8cG6E8xNeDkKDzjuiv2qKZ9DfzYx
|
||||
XAo3LtjpjzOkAWpOV/F5v+QwDDAKBggrBgEFBQcDAQ==
|
||||
-----END TRUSTED CERTIFICATE-----
|
||||
@@ -0,0 +1,21 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDZTCCAk2gAwIBAgIUVZCxv3SnAxqUosl2K2FJSr6mZ68wDQYJKoZIhvcNAQEL
|
||||
BQAwOTEVMBMGA1UECgwMVGVzc2VyYSBUZXN0MSAwHgYDVQQDDBdzZWxmc2lnbmVk
|
||||
LmV4YW1wbGUudGVzdDAgFw0yNjEwMDkxMjQyMDJaGA8yMTI2MDkxNTEyNDIwMlow
|
||||
OTEVMBMGA1UECgwMVGVzc2VyYSBUZXN0MSAwHgYDVQQDDBdzZWxmc2lnbmVkLmV4
|
||||
YW1wbGUudGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANxM3x+C
|
||||
PXuoxgA4W51JhO0usVyiZUdNya0/nDeiyPTMiAyQ0B5DDRosLVjDzVfG1+akOgxV
|
||||
yOOABg+Jq9NwXGqLcShK1/EYcCcKSpR4CUP5QsSyDqHfs/iA4VT00RSB6bUjbmwN
|
||||
XnLltBMa4U3kUBCzm6kszRRxiSN7ajE/X1JpNpXJuwQDCHkBkWwSDVHOZOdxy6tg
|
||||
4xOEsO4wuuseVfQ+iiV33x9+tcnC/09mfR6FH7/fUHlklbLhtUJ5HMlwp+99JlIC
|
||||
cjPbjDtDeJSRa0mTVXslwBeCvLTveSUAmOKqQ1JUJrmGGtKF6qxYnSkc4XgHq98s
|
||||
T5RoNGabU1JXfLsCAwEAAaNjMGEwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMC
|
||||
BaAwHQYDVR0OBBYEFIQZ3IbjgP382bn1kEe9P6/AeJPpMCIGA1UdEQQbMBmCF3Nl
|
||||
bGZzaWduZWQuZXhhbXBsZS50ZXN0MA0GCSqGSIb3DQEBCwUAA4IBAQCER/EnWGV0
|
||||
AnC2/6bYZyR0jlNlH2ZbNfOPCEx+UsAJAr1y54gXjsgNi8lil5vh906qyAldnfxg
|
||||
UyDM0Z6UkG0KJIgu4Oh1VQFRKeRrXi/4pmTJSZIef6R8pn+9ZjcBzxILaAPYz5we
|
||||
PilSTrpaBN/VJbSg8MRjdHnZKOXSvB6ZTMDbsO/mx6zV0lPskA6tG4Q7K6zaTL4p
|
||||
ZGXpvnsL2OSHy2ksNLJhKRD3RGIFSaK9eim1R0WjGgiJijtuFJqcPmCIKmZYQk8C
|
||||
cLS1hI38LsWKbW+b3J4mJZsIp+B5BePDlTAGKZsprE9rDE9+lxWES7WIMNLx+L3N
|
||||
jY9ghAjkRQzl
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,67 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { analyzeWorkingSet, cleanSourcePath } from './cert-analyze';
|
||||
import type { CertItem } from './cert-types';
|
||||
|
||||
const fx = (name: string) => readFileSync(join(__dirname, '__fixtures__', name));
|
||||
const file = (name: string, as = name) => ({ originalname: as, buffer: fx(name) });
|
||||
|
||||
describe('analyzeWorkingSet', () => {
|
||||
const result = analyzeWorkingSet([
|
||||
file('rsa-leaf.pem'),
|
||||
file('rsa-inter.pem'),
|
||||
file('ec-leaf.cer'),
|
||||
file('ec-inter.pem'),
|
||||
file('ec-root.pem'),
|
||||
file('rsa-leaf.cer'),
|
||||
{ originalname: 'readme.txt', buffer: Buffer.from('Bitte lesen') },
|
||||
]);
|
||||
|
||||
it('fasst dasselbe Zertifikat aus zwei Dateien zu einem Eintrag zusammen', () => {
|
||||
const certs = result.items.filter((i): i is CertItem => i.kind === 'certificate');
|
||||
expect(certs).toHaveLength(5);
|
||||
const leaf = certs.find((c) => c.cn === 'www.example.test');
|
||||
expect(leaf?.sources).toEqual([
|
||||
{ file: 0, path: 'rsa-leaf.pem' },
|
||||
{ file: 5, path: 'rsa-leaf.cer' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('ordnet Serverzertifikat, Zwischenzertifikat, Wurzel, dann Name', () => {
|
||||
const certs = result.items.filter((i): i is CertItem => i.kind === 'certificate');
|
||||
expect(certs.map((c) => `${c.role}:${c.cn}`)).toEqual([
|
||||
'end-entity:ec.example.test',
|
||||
'end-entity:www.example.test',
|
||||
'intermediate:Tessera Test Inter EC',
|
||||
'intermediate:Tessera Test Inter RSA',
|
||||
'root:Tessera Test Root EC',
|
||||
]);
|
||||
});
|
||||
|
||||
it('meldet unbekannte Dateien mit ihrem Index', () => {
|
||||
expect(result.ignored).toEqual([{ file: 6, path: 'readme.txt', reason: 'unknown' }]);
|
||||
});
|
||||
|
||||
it('Ketten und gesperrte Eintraege sind in diesem Stand leer', () => {
|
||||
expect(result.chains).toEqual([]);
|
||||
expect(result.locked).toEqual([]);
|
||||
});
|
||||
|
||||
it('JSON enthaelt weder undefined noch NaN', () => {
|
||||
const json = JSON.stringify(result);
|
||||
expect(json).not.toContain('undefined');
|
||||
expect(json).not.toContain('NaN');
|
||||
});
|
||||
|
||||
it('leere Anfrage ergibt leere Listen', () => {
|
||||
expect(analyzeWorkingSet([])).toEqual({ items: [], chains: [], locked: [], ignored: [] });
|
||||
});
|
||||
});
|
||||
|
||||
describe('cleanSourcePath', () => {
|
||||
it('entfernt Steuerzeichen und kuerzt auf 255', () => {
|
||||
expect(cleanSourcePath('a\u0000b\u001fc.pem')).toBe('abc.pem');
|
||||
expect(cleanSourcePath('x'.repeat(400))).toHaveLength(255);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import { detectBlob } from './cert-model';
|
||||
import type {
|
||||
AnalysisResult,
|
||||
AnyItem,
|
||||
CertItem,
|
||||
IgnoredEntry,
|
||||
ItemSource,
|
||||
LockedEntry,
|
||||
} from './cert-types';
|
||||
|
||||
/**
|
||||
* Fassade der Analyse (quick-261009-ikt, D-15): alle hochgeladenen Dateien erkennen,
|
||||
* gleiche Teile zusammenfassen und ordnen. Zustandslos; nichts wird gespeichert.
|
||||
* Ketten (Task 2), Schluessel/CSR-Zuordnung und gesperrte Container (Task 4) folgen.
|
||||
*/
|
||||
|
||||
export interface AnalyzeFile {
|
||||
originalname: string;
|
||||
buffer: Buffer;
|
||||
}
|
||||
|
||||
/** Anzeigename einer Quelle: ohne Steuerzeichen, hoechstens 255 Zeichen. Nur Anzeige, nie ein Dateipfad. */
|
||||
export function cleanSourcePath(raw: string): string {
|
||||
let out = '';
|
||||
for (const ch of raw) {
|
||||
const code = ch.codePointAt(0) ?? 0;
|
||||
if (code < 0x20 || code === 0x7f) continue;
|
||||
out += ch;
|
||||
}
|
||||
return out.slice(0, 255);
|
||||
}
|
||||
|
||||
const ROLE_RANK: Record<CertItem['role'], number> = { 'end-entity': 0, intermediate: 1, root: 2 };
|
||||
|
||||
function sameSource(a: ItemSource, b: ItemSource): boolean {
|
||||
return a.file === b.file && a.path === b.path;
|
||||
}
|
||||
|
||||
function orderItems(items: AnyItem[]): AnyItem[] {
|
||||
const certs = items.filter((i): i is CertItem => i.kind === 'certificate');
|
||||
const keys = items.filter((i) => i.kind === 'privateKey');
|
||||
const csrs = items.filter((i) => i.kind === 'csr');
|
||||
certs.sort(
|
||||
(a, b) =>
|
||||
ROLE_RANK[a.role] - ROLE_RANK[b.role] ||
|
||||
a.cn.localeCompare(b.cn) ||
|
||||
Date.parse(b.notAfter) - Date.parse(a.notAfter),
|
||||
);
|
||||
return [...certs, ...keys, ...csrs];
|
||||
}
|
||||
|
||||
export function analyzeWorkingSet(files: AnalyzeFile[], passwords: string[] = []): AnalysisResult {
|
||||
const byId = new Map<string, AnyItem>();
|
||||
const ignored: IgnoredEntry[] = [];
|
||||
const locked: LockedEntry[] = [];
|
||||
|
||||
files.forEach((f, index) => {
|
||||
const path = cleanSourcePath(f.originalname);
|
||||
const result = detectBlob(f.buffer, { file: index, path, passwords });
|
||||
for (const item of result.items) {
|
||||
const known = byId.get(item.id);
|
||||
if (!known) {
|
||||
byId.set(item.id, item);
|
||||
continue;
|
||||
}
|
||||
for (const source of item.sources) {
|
||||
if (!known.sources.some((s) => sameSource(s, source))) known.sources.push(source);
|
||||
}
|
||||
}
|
||||
ignored.push(...result.ignored);
|
||||
locked.push(...result.locked);
|
||||
});
|
||||
|
||||
return { items: orderItems([...byId.values()]), chains: [], locked, ignored };
|
||||
}
|
||||
@@ -1,268 +0,0 @@
|
||||
import AdmZip from 'adm-zip';
|
||||
import * as forge from 'node-forge';
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
import { analyzeBundle, exportBundleItem, safeBaseName } from './cert-bundle';
|
||||
|
||||
/**
|
||||
* cert-bundle.spec (quick-261001-l4q) — Zertifikatspaket wie vom Aussteller:
|
||||
* Stamm -> Zwischen -> Server, dazu Schluessel, CSR und PFX, als ZIP.
|
||||
* Alles hier erzeugt (keine echten Kundendaten im Repo).
|
||||
*/
|
||||
|
||||
interface Pki {
|
||||
rootPem: string;
|
||||
interPem: string;
|
||||
leafPem: string;
|
||||
keyPem: string;
|
||||
csrPem: string;
|
||||
pfx: Buffer;
|
||||
leafModulus: string;
|
||||
}
|
||||
|
||||
let pki: Pki;
|
||||
|
||||
function makeCert(
|
||||
subjectCn: string,
|
||||
pub: forge.pki.PublicKey,
|
||||
signer: forge.pki.PrivateKey,
|
||||
issuer: forge.pki.CertificateField[] | null,
|
||||
ca: boolean,
|
||||
serial: string,
|
||||
): forge.pki.Certificate {
|
||||
const cert = forge.pki.createCertificate();
|
||||
cert.publicKey = pub;
|
||||
cert.serialNumber = serial;
|
||||
cert.validity.notBefore = new Date(Date.now() - 86_400_000);
|
||||
cert.validity.notAfter = new Date(Date.now() + 90 * 86_400_000);
|
||||
const subject = [{ name: 'commonName', value: subjectCn }];
|
||||
cert.setSubject(subject);
|
||||
cert.setIssuer(issuer ?? subject);
|
||||
const ext: object[] = [{ name: 'basicConstraints', cA: ca }];
|
||||
if (!ca) ext.push({ name: 'subjectAltName', altNames: [{ type: 2, value: subjectCn }] });
|
||||
cert.setExtensions(ext);
|
||||
cert.sign(signer as forge.pki.rsa.PrivateKey, forge.md.sha256.create());
|
||||
return cert;
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
const rootKeys = forge.pki.rsa.generateKeyPair(1024);
|
||||
const interKeys = forge.pki.rsa.generateKeyPair(1024);
|
||||
const leafKeys = forge.pki.rsa.generateKeyPair(1024);
|
||||
const root = makeCert('Test Root CA', rootKeys.publicKey, rootKeys.privateKey, null, true, '01');
|
||||
const inter = makeCert(
|
||||
'Test Intermediate CA',
|
||||
interKeys.publicKey,
|
||||
rootKeys.privateKey,
|
||||
root.subject.attributes,
|
||||
true,
|
||||
'02',
|
||||
);
|
||||
const leaf = makeCert(
|
||||
'www.example.test',
|
||||
leafKeys.publicKey,
|
||||
interKeys.privateKey,
|
||||
inter.subject.attributes,
|
||||
false,
|
||||
'03',
|
||||
);
|
||||
|
||||
const csr = forge.pki.createCertificationRequest();
|
||||
csr.publicKey = leafKeys.publicKey;
|
||||
csr.setSubject([{ name: 'commonName', value: 'www.example.test' }]);
|
||||
csr.sign(leafKeys.privateKey, forge.md.sha256.create());
|
||||
|
||||
const p12 = forge.pkcs12.toPkcs12Asn1(leafKeys.privateKey, [leaf, inter], 'geheim', {
|
||||
algorithm: '3des',
|
||||
});
|
||||
|
||||
pki = {
|
||||
rootPem: forge.pki.certificateToPem(root),
|
||||
interPem: forge.pki.certificateToPem(inter),
|
||||
leafPem: forge.pki.certificateToPem(leaf),
|
||||
keyPem: forge.pki.privateKeyInfoToPem(
|
||||
forge.pki.wrapRsaPrivateKey(forge.pki.privateKeyToAsn1(leafKeys.privateKey)),
|
||||
),
|
||||
csrPem: forge.pki.certificationRequestToPem(csr),
|
||||
pfx: Buffer.from(forge.asn1.toDer(p12).getBytes(), 'binary'),
|
||||
leafModulus: leafKeys.publicKey.n.toString(16),
|
||||
};
|
||||
}, 60_000);
|
||||
|
||||
function issuerZip(): Buffer {
|
||||
const zip = new AdmZip();
|
||||
zip.addFile('www.example.test/www.example.test.pem', Buffer.from(pki.leafPem + pki.interPem));
|
||||
zip.addFile('www.example.test/www.example.test.key', Buffer.from(pki.keyPem));
|
||||
zip.addFile('www.example.test/www.example.test.csr', Buffer.from(pki.csrPem));
|
||||
zip.addFile('www.example.test/www.example.test.pfx', pki.pfx);
|
||||
zip.addFile('www.example.test/.dnstxtrecord', Buffer.from('_dnsauth abc123'));
|
||||
return zip.toBuffer();
|
||||
}
|
||||
|
||||
describe('analyzeBundle', () => {
|
||||
it('ZIP vom Aussteller: erkennt jedes Teil, fasst PEM/PFX zusammen, ordnet Schluessel und Kette zu', () => {
|
||||
const r = analyzeBundle([{ originalname: 'paket.zip', buffer: issuerZip() }], 'geheim');
|
||||
|
||||
const kinds = r.items.map((i) => (i.kind === 'certificate' ? i.role : i.kind));
|
||||
expect(kinds).toEqual(['end-entity', 'intermediate', 'privateKey', 'csr']);
|
||||
|
||||
const [leaf, inter, key, csr] = r.items;
|
||||
expect(leaf.cn).toBe('www.example.test');
|
||||
expect(leaf.san).toEqual(['www.example.test']);
|
||||
// in PEM UND PFX enthalten -> ein Eintrag mit beiden Quellen
|
||||
expect(leaf.sources.sort()).toEqual(['www.example.test.pem', 'www.example.test.pfx']);
|
||||
expect(leaf.chainIds).toEqual([inter.id]);
|
||||
expect(leaf.matchId).toBe(key.id);
|
||||
expect(key.matchId).toBe(leaf.id);
|
||||
expect(key.sources.sort()).toEqual(['www.example.test.key', 'www.example.test.pfx']);
|
||||
expect(csr.matchId).toBe(leaf.id);
|
||||
expect(csr.cn).toBe('www.example.test');
|
||||
expect(leaf.baseName).toBe('www.example.test');
|
||||
|
||||
expect(r.locked).toEqual([]);
|
||||
expect(r.ignored).toEqual(['.dnstxtrecord']);
|
||||
});
|
||||
|
||||
it('PFX ohne passendes Passwort wird als gesperrt gemeldet, der Rest trotzdem erkannt', () => {
|
||||
const r = analyzeBundle([{ originalname: 'paket.zip', buffer: issuerZip() }], 'falsch');
|
||||
expect(r.locked).toEqual(['www.example.test.pfx']);
|
||||
expect(r.items.filter((i) => i.kind === 'certificate')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('Stammzertifikat wird als root erkannt und an die Kette gehaengt', () => {
|
||||
const r = analyzeBundle(
|
||||
[
|
||||
{
|
||||
originalname: 'chain.pem',
|
||||
buffer: Buffer.from(pki.leafPem + pki.interPem + pki.rootPem),
|
||||
},
|
||||
],
|
||||
'',
|
||||
);
|
||||
expect(r.items.map((i) => i.role)).toEqual(['end-entity', 'intermediate', 'root']);
|
||||
expect(r.items[0].chainIds).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('ohne Dateien -> 400', () => {
|
||||
expect(() => analyzeBundle([], '')).toThrow(/No files/);
|
||||
});
|
||||
|
||||
it('ZIP mit zu vielen Dateien -> 400', () => {
|
||||
const zip = new AdmZip();
|
||||
for (let i = 0; i < 101; i++) zip.addFile(`f${i}.txt`, Buffer.from('x'));
|
||||
expect(() =>
|
||||
analyzeBundle([{ originalname: 'gross.zip', buffer: zip.toBuffer() }], ''),
|
||||
).toThrow(/too many files/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('exportBundleItem', () => {
|
||||
function bundle() {
|
||||
const r = analyzeBundle([{ originalname: 'paket.zip', buffer: issuerZip() }], 'geheim');
|
||||
const byId = Object.fromEntries(r.items.map((i) => [i.id, i]));
|
||||
return { items: r.items, byId };
|
||||
}
|
||||
const decode = (b64: string) => Buffer.from(b64, 'base64');
|
||||
|
||||
it('Zertifikat in jedem Format liest sich wieder ein', () => {
|
||||
const { items, byId } = bundle();
|
||||
const leaf = items[0];
|
||||
const chain = leaf.chainIds.map((id) => byId[id].pem);
|
||||
const keyPem = byId[leaf.matchId!].pem;
|
||||
const base = { kind: leaf.kind, pem: leaf.pem, baseName: leaf.baseName, chain, keyPem };
|
||||
|
||||
const crt = exportBundleItem({ ...base, format: 'crt' });
|
||||
expect(crt.filename).toBe('www.example.test.crt');
|
||||
expect(
|
||||
forge.pki.certificateFromPem(decode(crt.content).toString()).subject.getField('CN').value,
|
||||
).toBe('www.example.test');
|
||||
|
||||
const cer = exportBundleItem({ ...base, format: 'cer' });
|
||||
expect(cer.filename).toBe('www.example.test.cer');
|
||||
forge.pki.certificateFromAsn1(forge.asn1.fromDer(decode(cer.content).toString('binary')));
|
||||
|
||||
const full = exportBundleItem({ ...base, format: 'fullchain' });
|
||||
expect(
|
||||
decode(full.content)
|
||||
.toString()
|
||||
.match(/BEGIN CERTIFICATE/g),
|
||||
).toHaveLength(2);
|
||||
|
||||
const p7b = exportBundleItem({ ...base, format: 'p7b' });
|
||||
const p7 = forge.pkcs7.messageFromPem(decode(p7b.content).toString());
|
||||
expect('certificates' in p7 ? p7.certificates : []).toHaveLength(2);
|
||||
|
||||
const pfx = exportBundleItem({ ...base, format: 'pfx', password: 'neu' });
|
||||
expect(pfx.filename).toBe('www.example.test.pfx');
|
||||
const p12 = forge.pkcs12.pkcs12FromAsn1(
|
||||
forge.asn1.fromDer(decode(pfx.content).toString('binary')),
|
||||
'neu',
|
||||
);
|
||||
expect(p12.getBags({ bagType: forge.pki.oids.certBag })[forge.pki.oids.certBag]).toHaveLength(
|
||||
2,
|
||||
);
|
||||
const keyBag = p12.getBags({ bagType: forge.pki.oids.pkcs8ShroudedKeyBag })[
|
||||
forge.pki.oids.pkcs8ShroudedKeyBag
|
||||
]![0];
|
||||
expect((keyBag.key as forge.pki.rsa.PrivateKey).n.toString(16)).toBe(pki.leafModulus);
|
||||
});
|
||||
|
||||
it('PFX ohne Passwort -> 400', () => {
|
||||
const { items } = bundle();
|
||||
expect(() =>
|
||||
exportBundleItem({ kind: 'certificate', pem: items[0].pem, format: 'pfx' }),
|
||||
).toThrow(/password is required/);
|
||||
});
|
||||
|
||||
it('Schluessel als PKCS#8, PKCS#1 und DER', () => {
|
||||
const { items } = bundle();
|
||||
const key = items.find((i) => i.kind === 'privateKey')!;
|
||||
const base = { kind: key.kind, pem: key.pem, baseName: key.baseName };
|
||||
expect(decode(exportBundleItem({ ...base, format: 'key' }).content).toString()).toContain(
|
||||
'BEGIN PRIVATE KEY',
|
||||
);
|
||||
const rsa = exportBundleItem({ ...base, format: 'key-rsa' });
|
||||
expect(rsa.filename).toBe('www.example.test.rsa.key');
|
||||
expect(decode(rsa.content).toString()).toContain('BEGIN RSA PRIVATE KEY');
|
||||
const der = exportBundleItem({ ...base, format: 'key-der' });
|
||||
const info = forge.asn1.fromDer(decode(der.content).toString('binary'));
|
||||
expect((forge.pki.privateKeyFromAsn1(info) as forge.pki.rsa.PrivateKey).n.toString(16)).toBe(
|
||||
pki.leafModulus,
|
||||
);
|
||||
});
|
||||
|
||||
it('CSR als PEM und DER', () => {
|
||||
const { items } = bundle();
|
||||
const csr = items.find((i) => i.kind === 'csr')!;
|
||||
const der = exportBundleItem({
|
||||
kind: 'csr',
|
||||
pem: csr.pem,
|
||||
baseName: csr.baseName,
|
||||
format: 'csr-der',
|
||||
});
|
||||
expect(der.filename).toBe('www.example.test.csr.der');
|
||||
forge.pki.certificationRequestFromAsn1(
|
||||
forge.asn1.fromDer(decode(der.content).toString('binary')),
|
||||
);
|
||||
});
|
||||
|
||||
it('unpassendes Format -> 400', () => {
|
||||
const { items } = bundle();
|
||||
expect(() =>
|
||||
exportBundleItem({ kind: 'csr', pem: items[3].pem, format: 'pfx', password: 'x' }),
|
||||
).toThrow();
|
||||
expect(() =>
|
||||
exportBundleItem({ kind: 'privateKey', pem: 'kein pem', format: 'key-rsa' }),
|
||||
).toThrow(/Failed to export/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('safeBaseName', () => {
|
||||
it('Platzhalter, Leerzeichen und Pfadteile werden entschaerft', () => {
|
||||
expect(safeBaseName('*.example.de', 'x')).toBe('wildcard.example.de');
|
||||
expect(safeBaseName('Encryption Everywhere DV TLS CA - G1', 'x')).toBe(
|
||||
'Encryption_Everywhere_DV_TLS_CA_-_G1',
|
||||
);
|
||||
expect(safeBaseName('../../etc/passwd', 'x')).toBe('etc_passwd');
|
||||
expect(safeBaseName('', 'fallback')).toBe('fallback');
|
||||
});
|
||||
});
|
||||
@@ -1,655 +0,0 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import AdmZip from 'adm-zip';
|
||||
import * as forge from 'node-forge';
|
||||
import type { UploadedFileLike } from '../auth/types/auth-user';
|
||||
|
||||
/**
|
||||
* Zertifikatspaket (quick-261001-l4q): alles, was ein Aussteller liefert —
|
||||
* Zertifikat mit Kette (.pem/.crt/.cer/.p7b), privater Schluessel (.key),
|
||||
* Zertifikatsanfrage (.csr), PFX/P12, gern als ZIP — auf einmal hochladen,
|
||||
* erkennen, was was ist, und jedes Teil in jedem passenden Format
|
||||
* herunterladen.
|
||||
*
|
||||
* Zustandslos wie der Rest des Moduls: `analyzeBundle` gibt je Teil den
|
||||
* kanonischen PEM-Text zurueck, `exportBundleItem` baut daraus die Datei.
|
||||
* Nichts wird gespeichert, Passwoerter werden nie protokolliert.
|
||||
*/
|
||||
|
||||
type BundleFile = Pick<UploadedFileLike, 'buffer' | 'originalname'>;
|
||||
|
||||
export type BundleCertRole = 'end-entity' | 'intermediate' | 'root';
|
||||
export type BundleItemKind = 'certificate' | 'privateKey' | 'csr';
|
||||
|
||||
export interface BundleItem {
|
||||
id: string;
|
||||
kind: BundleItemKind;
|
||||
/** Nur bei Zertifikaten. */
|
||||
role?: BundleCertRole;
|
||||
/** Dateien, in denen dieses Teil gefunden wurde (Duplikate zusammengefasst). */
|
||||
sources: string[];
|
||||
/** Kanonischer PEM-Text — Grundlage fuer jeden Export. */
|
||||
pem: string;
|
||||
/** Vorschlag fuer den Dateinamen ohne Endung, aus dem CN abgeleitet. */
|
||||
baseName: string;
|
||||
cn: string;
|
||||
organization: string;
|
||||
issuerCn: string;
|
||||
notBefore: string | null;
|
||||
notAfter: string | null;
|
||||
isExpired: boolean | null;
|
||||
daysLeft: number | null;
|
||||
san: string[];
|
||||
keyType: string;
|
||||
keyBits: number;
|
||||
serialNumber: string;
|
||||
sha256: string;
|
||||
/** Zertifikat: id des passenden Schluessels; Schluessel/CSR: id des passenden Zertifikats. */
|
||||
matchId: string | null;
|
||||
/** Zertifikat: ids der Kette darueber (Aussteller, dessen Aussteller ...). */
|
||||
chainIds: string[];
|
||||
/** Formate, die `exportBundleItem` fuer dieses Teil liefern kann. */
|
||||
formats: BundleExportFormat[];
|
||||
}
|
||||
|
||||
export interface BundleAnalysis {
|
||||
items: BundleItem[];
|
||||
/** PFX/P12 oder verschluesselte Schluessel, die ohne (richtiges) Passwort nicht lesbar sind. */
|
||||
locked: string[];
|
||||
/** Dateien ohne erkennbares Zertifikat, Schluessel oder CSR. */
|
||||
ignored: string[];
|
||||
}
|
||||
|
||||
export type BundleExportFormat =
|
||||
| 'crt'
|
||||
| 'cer'
|
||||
| 'fullchain'
|
||||
| 'p7b'
|
||||
| 'pfx'
|
||||
| 'key'
|
||||
| 'key-rsa'
|
||||
| 'key-der'
|
||||
| 'csr'
|
||||
| 'csr-der';
|
||||
|
||||
export interface BundleExportInput {
|
||||
kind: BundleItemKind;
|
||||
pem: string;
|
||||
format: BundleExportFormat;
|
||||
baseName?: string;
|
||||
/** Zertifikat: PEMs der Kette darueber (fuer Fullchain/P7B/PFX). */
|
||||
chain?: string[];
|
||||
/** Zertifikat: PEM des passenden privaten Schluessels (fuer PFX). */
|
||||
keyPem?: string;
|
||||
/** PFX: Passwort fuer die neue Datei. */
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export interface BundleExportFile {
|
||||
filename: string;
|
||||
/** Base64 */
|
||||
content: string;
|
||||
mimeType: string;
|
||||
}
|
||||
|
||||
const MAX_ZIP_ENTRIES = 100;
|
||||
const MAX_ENTRY_BYTES = 5 * 1024 * 1024;
|
||||
|
||||
const PEM_BLOCK = /-----BEGIN ([A-Z0-9 ]+)-----[\s\S]+?-----END \1-----/g;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hilfen
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function binary(buffer: Buffer): forge.util.ByteStringBuffer {
|
||||
return forge.util.createBuffer(buffer.toString('binary'));
|
||||
}
|
||||
|
||||
function bytesToBase64(bytes: string): string {
|
||||
return Buffer.from(forge.util.bytesToHex(bytes), 'hex').toString('base64');
|
||||
}
|
||||
|
||||
function textToBase64(text: string): string {
|
||||
return Buffer.from(text, 'utf-8').toString('base64');
|
||||
}
|
||||
|
||||
function sha256Of(cert: forge.pki.Certificate): string {
|
||||
const md = forge.md.sha256.create();
|
||||
md.update(forge.asn1.toDer(forge.pki.certificateToAsn1(cert)).getBytes());
|
||||
return (md.digest().toHex().match(/.{2}/g) ?? []).join(':').toUpperCase();
|
||||
}
|
||||
|
||||
function field(name: forge.pki.Certificate['subject'], short: string): string {
|
||||
return (name.getField(short)?.value as string | undefined) ?? '';
|
||||
}
|
||||
|
||||
/** Dateiname ohne Pfad und ohne gefaehrliche Zeichen, z. B. „*.example.de“ -> „wildcard.example.de“. */
|
||||
export function safeBaseName(raw: string, fallback: string): string {
|
||||
const cleaned = raw
|
||||
.replace(/^\*\./, 'wildcard.')
|
||||
.replace(/[^A-Za-z0-9._-]+/g, '_')
|
||||
.replace(/^[._]+/, '')
|
||||
.slice(0, 80);
|
||||
return cleaned || fallback;
|
||||
}
|
||||
|
||||
function certRole(cert: forge.pki.Certificate): BundleCertRole {
|
||||
const bc = cert.getExtension('basicConstraints') as { cA?: boolean } | null;
|
||||
if (!bc?.cA) return 'end-entity';
|
||||
return cert.subject.hash === cert.issuer.hash ? 'root' : 'intermediate';
|
||||
}
|
||||
|
||||
function publicKeyInfo(key: unknown): { keyType: string; keyBits: number; modulus: string } {
|
||||
// node-forge liefert RSA-Schluessel mit `n`; EC-Schluessel kennt es nur
|
||||
// eingeschraenkt (siehe Kommentar in CertManagerService.parseCert).
|
||||
const k = key as { n?: forge.jsbn.BigInteger };
|
||||
if (k?.n) return { keyType: 'RSA', keyBits: k.n.bitLength(), modulus: k.n.toString(16) };
|
||||
return { keyType: 'EC', keyBits: 0, modulus: '' };
|
||||
}
|
||||
|
||||
function sanOf(extensions: unknown[] | undefined): string[] {
|
||||
const ext = (extensions ?? []).find((e) => (e as { name?: string }).name === 'subjectAltName') as
|
||||
| { altNames?: { type: number; value?: string; ip?: string }[] }
|
||||
| undefined;
|
||||
return (ext?.altNames ?? []).map((n) =>
|
||||
n.type === 2 ? (n.value ?? '') : `IP:${n.ip ?? n.value ?? ''}`,
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Einsammeln: Dateien (inkl. ZIP) -> rohe Teile
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface RawKey {
|
||||
source: string;
|
||||
pem: string;
|
||||
modulus: string;
|
||||
keyType: string;
|
||||
keyBits: number;
|
||||
}
|
||||
|
||||
interface RawCsr {
|
||||
source: string;
|
||||
pem: string;
|
||||
}
|
||||
|
||||
interface Collected {
|
||||
certs: { source: string; cert: forge.pki.Certificate }[];
|
||||
keys: RawKey[];
|
||||
csrs: RawCsr[];
|
||||
locked: string[];
|
||||
ignored: string[];
|
||||
}
|
||||
|
||||
function expandZips(files: BundleFile[]): { name: string; buffer: Buffer }[] {
|
||||
const out: { name: string; buffer: Buffer }[] = [];
|
||||
for (const file of files) {
|
||||
if (!file.originalname.toLowerCase().endsWith('.zip')) {
|
||||
out.push({ name: file.originalname, buffer: file.buffer });
|
||||
continue;
|
||||
}
|
||||
let zip: AdmZip;
|
||||
try {
|
||||
zip = new AdmZip(file.buffer);
|
||||
} catch {
|
||||
throw new BadRequestException(`"${file.originalname}" is not a readable ZIP archive`);
|
||||
}
|
||||
const entries = zip
|
||||
.getEntries()
|
||||
.filter((e) => !e.isDirectory && !e.entryName.startsWith('__MACOSX/'));
|
||||
if (entries.length > MAX_ZIP_ENTRIES) {
|
||||
throw new BadRequestException(`"${file.originalname}" contains too many files`);
|
||||
}
|
||||
for (const entry of entries) {
|
||||
// Groesse aus dem Kopf pruefen, BEVOR entpackt wird (Zip-Bombe).
|
||||
if (entry.header.size > MAX_ENTRY_BYTES) {
|
||||
throw new BadRequestException(
|
||||
`"${entry.entryName}" in "${file.originalname}" is too large`,
|
||||
);
|
||||
}
|
||||
const name = entry.entryName.split('/').pop() ?? entry.entryName;
|
||||
out.push({ name, buffer: entry.getData() });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function addKey(c: Collected, source: string, privateKey: forge.pki.rsa.PrivateKey): void {
|
||||
const info = publicKeyInfo(privateKey);
|
||||
const pem = forge.pki.privateKeyInfoToPem(
|
||||
forge.pki.wrapRsaPrivateKey(forge.pki.privateKeyToAsn1(privateKey)),
|
||||
);
|
||||
c.keys.push({ source, pem, ...info });
|
||||
}
|
||||
|
||||
function collectPemText(c: Collected, source: string, text: string, password: string): boolean {
|
||||
let found = false;
|
||||
for (const match of text.matchAll(PEM_BLOCK)) {
|
||||
const [block, type] = match;
|
||||
try {
|
||||
if (type === 'CERTIFICATE' || type === 'TRUSTED CERTIFICATE') {
|
||||
c.certs.push({ source, cert: forge.pki.certificateFromPem(block) });
|
||||
found = true;
|
||||
} else if (type === 'PRIVATE KEY' || type === 'RSA PRIVATE KEY') {
|
||||
const key = forge.pki.privateKeyFromPem(block) as forge.pki.rsa.PrivateKey;
|
||||
addKey(c, source, key);
|
||||
found = true;
|
||||
} else if (type === 'ENCRYPTED PRIVATE KEY') {
|
||||
const key = password ? forge.pki.decryptRsaPrivateKey(block, password) : null;
|
||||
if (key) addKey(c, source, key as forge.pki.rsa.PrivateKey);
|
||||
else c.locked.push(source);
|
||||
found = true;
|
||||
} else if (type === 'EC PRIVATE KEY') {
|
||||
// node-forge kann EC nicht umrechnen — Teil bleibt im Original erhalten.
|
||||
c.keys.push({ source, pem: block, modulus: '', keyType: 'EC', keyBits: 0 });
|
||||
found = true;
|
||||
} else if (type === 'CERTIFICATE REQUEST' || type === 'NEW CERTIFICATE REQUEST') {
|
||||
c.csrs.push({
|
||||
source,
|
||||
pem: block.replace(/NEW CERTIFICATE REQUEST/g, 'CERTIFICATE REQUEST'),
|
||||
});
|
||||
found = true;
|
||||
} else if (type === 'PKCS7') {
|
||||
const p7 = forge.pkcs7.messageFromPem(block);
|
||||
for (const cert of 'certificates' in p7 ? p7.certificates : [])
|
||||
c.certs.push({ source, cert });
|
||||
found = true;
|
||||
}
|
||||
} catch {
|
||||
// PKCS#8 mit EC-Schluessel o. ae.: node-forge kann ihn nicht lesen —
|
||||
// im Original behalten statt zu verwerfen.
|
||||
if (type === 'PRIVATE KEY') {
|
||||
c.keys.push({ source, pem: block, modulus: '', keyType: 'EC', keyBits: 0 });
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
function collectPfx(c: Collected, source: string, buffer: Buffer, password: string): void {
|
||||
let p12: forge.pkcs12.Pkcs12Pfx | null = null;
|
||||
for (const candidate of password ? [password, ''] : ['']) {
|
||||
try {
|
||||
p12 = forge.pkcs12.pkcs12FromAsn1(forge.asn1.fromDer(binary(buffer)), candidate);
|
||||
break;
|
||||
} catch {
|
||||
// naechstes Passwort versuchen
|
||||
}
|
||||
}
|
||||
if (!p12) {
|
||||
c.locked.push(source);
|
||||
return;
|
||||
}
|
||||
for (const bag of p12.getBags({ bagType: forge.pki.oids.certBag })[forge.pki.oids.certBag] ??
|
||||
[]) {
|
||||
if (bag.cert) c.certs.push({ source, cert: bag.cert });
|
||||
}
|
||||
for (const oid of [forge.pki.oids.pkcs8ShroudedKeyBag, forge.pki.oids.keyBag]) {
|
||||
for (const bag of p12.getBags({ bagType: oid })[oid] ?? []) {
|
||||
if (bag.key) addKey(c, source, bag.key as forge.pki.rsa.PrivateKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function collectDer(c: Collected, source: string, buffer: Buffer): boolean {
|
||||
try {
|
||||
const asn1 = forge.asn1.fromDer(binary(buffer));
|
||||
try {
|
||||
c.certs.push({ source, cert: forge.pki.certificateFromAsn1(asn1) });
|
||||
return true;
|
||||
} catch {
|
||||
/* kein einzelnes Zertifikat */
|
||||
}
|
||||
try {
|
||||
const p7 = forge.pkcs7.messageFromAsn1(asn1);
|
||||
const certs = 'certificates' in p7 ? p7.certificates : [];
|
||||
for (const cert of certs) c.certs.push({ source, cert });
|
||||
if (certs.length > 0) return true;
|
||||
} catch {
|
||||
/* kein PKCS#7 */
|
||||
}
|
||||
try {
|
||||
forge.pki.certificationRequestFromAsn1(asn1);
|
||||
const body = forge.asn1.toDer(asn1).getBytes();
|
||||
c.csrs.push({ source, pem: forge.pem.encode({ type: 'CERTIFICATE REQUEST', body }) });
|
||||
return true;
|
||||
} catch {
|
||||
/* keine CSR */
|
||||
}
|
||||
} catch {
|
||||
/* kein DER */
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function collect(files: BundleFile[], password: string): Collected {
|
||||
const c: Collected = { certs: [], keys: [], csrs: [], locked: [], ignored: [] };
|
||||
for (const { name, buffer } of expandZips(files)) {
|
||||
const ext = name.split('.').pop()?.toLowerCase() ?? '';
|
||||
if (ext === 'pfx' || ext === 'p12') {
|
||||
collectPfx(c, name, buffer, password);
|
||||
continue;
|
||||
}
|
||||
const head = buffer.subarray(0, 4096).toString('latin1');
|
||||
const found = head.includes('-----BEGIN')
|
||||
? collectPemText(c, name, buffer.toString('utf-8'), password)
|
||||
: collectDer(c, name, buffer);
|
||||
if (!found) c.ignored.push(name);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// analyzeBundle
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const CERT_FORMATS: BundleExportFormat[] = ['crt', 'cer', 'fullchain', 'p7b', 'pfx'];
|
||||
|
||||
export function analyzeBundle(files: BundleFile[], password = ''): BundleAnalysis {
|
||||
if (files.length === 0) throw new BadRequestException('No files provided');
|
||||
const c = collect(files, password);
|
||||
|
||||
// Zertifikate nach Fingerabdruck zusammenfassen (PEM und PFX enthalten oft dieselben).
|
||||
const certMap = new Map<string, { cert: forge.pki.Certificate; sources: Set<string> }>();
|
||||
for (const { source, cert } of c.certs) {
|
||||
const fp = sha256Of(cert);
|
||||
const entry = certMap.get(fp) ?? { cert, sources: new Set<string>() };
|
||||
entry.sources.add(source);
|
||||
certMap.set(fp, entry);
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const certItems: BundleItem[] = [...certMap.entries()].map(([fp, { cert, sources }]) => {
|
||||
const info = publicKeyInfo(cert.publicKey);
|
||||
const cn = field(cert.subject, 'CN');
|
||||
const notAfter = cert.validity.notAfter;
|
||||
const role = certRole(cert);
|
||||
return {
|
||||
id: `cert-${fp.replace(/:/g, '').slice(0, 16).toLowerCase()}`,
|
||||
kind: 'certificate',
|
||||
role,
|
||||
sources: [...sources],
|
||||
pem: forge.pki.certificateToPem(cert),
|
||||
baseName: safeBaseName(cn, role === 'end-entity' ? 'zertifikat' : 'ca'),
|
||||
cn,
|
||||
organization: field(cert.subject, 'O'),
|
||||
issuerCn: field(cert.issuer, 'CN'),
|
||||
notBefore: cert.validity.notBefore.toISOString(),
|
||||
notAfter: notAfter.toISOString(),
|
||||
isExpired: notAfter.getTime() < now,
|
||||
daysLeft: Math.ceil((notAfter.getTime() - now) / 86_400_000),
|
||||
san: sanOf(cert.extensions),
|
||||
keyType: info.keyType,
|
||||
keyBits: info.keyBits,
|
||||
serialNumber: cert.serialNumber,
|
||||
sha256: fp,
|
||||
matchId: null,
|
||||
chainIds: [],
|
||||
formats: CERT_FORMATS,
|
||||
// nur intern fuer Kette/Zuordnung, wird unten entfernt
|
||||
_cert: cert,
|
||||
_modulus: info.modulus,
|
||||
} as BundleItem & { _cert: forge.pki.Certificate; _modulus: string };
|
||||
});
|
||||
|
||||
// Kette: zu jedem Zertifikat den Aussteller im Paket suchen.
|
||||
type Internal = BundleItem & { _cert: forge.pki.Certificate; _modulus: string };
|
||||
const internals = certItems as Internal[];
|
||||
for (const item of internals) {
|
||||
let current = item._cert;
|
||||
const seen = new Set<string>([item.id]);
|
||||
for (let depth = 0; depth < 10; depth++) {
|
||||
if (current.subject.hash === current.issuer.hash) break;
|
||||
const issuer = internals.find(
|
||||
(o) => !seen.has(o.id) && o._cert.subject.hash === current.issuer.hash,
|
||||
);
|
||||
if (!issuer) break;
|
||||
item.chainIds.push(issuer.id);
|
||||
seen.add(issuer.id);
|
||||
current = issuer._cert;
|
||||
}
|
||||
}
|
||||
|
||||
// Schluessel: Duplikate zusammenfassen, dem Zertifikat zuordnen.
|
||||
const keyMap = new Map<string, { key: RawKey; sources: Set<string> }>();
|
||||
for (const key of c.keys) {
|
||||
const id = key.modulus || key.pem;
|
||||
const entry = keyMap.get(id) ?? { key, sources: new Set<string>() };
|
||||
entry.sources.add(key.source);
|
||||
keyMap.set(id, entry);
|
||||
}
|
||||
const keyItems: BundleItem[] = [...keyMap.values()].map(({ key, sources }, i) => {
|
||||
const cert = key.modulus ? internals.find((o) => o._modulus === key.modulus) : undefined;
|
||||
const id = `key-${i + 1}`;
|
||||
if (cert) cert.matchId = id;
|
||||
const isRsa = key.keyType === 'RSA';
|
||||
return {
|
||||
id,
|
||||
kind: 'privateKey',
|
||||
sources: [...sources],
|
||||
pem: key.pem,
|
||||
baseName:
|
||||
cert?.baseName ??
|
||||
safeBaseName(
|
||||
sources
|
||||
.values()
|
||||
.next()
|
||||
.value?.replace(/\.[^.]+$/, '') ?? '',
|
||||
'schluessel',
|
||||
),
|
||||
cn: cert?.cn ?? '',
|
||||
organization: '',
|
||||
issuerCn: '',
|
||||
notBefore: null,
|
||||
notAfter: null,
|
||||
isExpired: null,
|
||||
daysLeft: null,
|
||||
san: [],
|
||||
keyType: key.keyType,
|
||||
keyBits: key.keyBits,
|
||||
serialNumber: '',
|
||||
sha256: '',
|
||||
matchId: cert?.id ?? null,
|
||||
chainIds: [],
|
||||
formats: isRsa ? ['key', 'key-rsa', 'key-der'] : ['key'],
|
||||
};
|
||||
});
|
||||
|
||||
// CSRs: Duplikate zusammenfassen, Details lesen, dem Zertifikat zuordnen.
|
||||
const csrMap = new Map<string, { pem: string; sources: Set<string> }>();
|
||||
for (const csr of c.csrs) {
|
||||
const norm = csr.pem.replace(/\s+/g, '');
|
||||
const entry = csrMap.get(norm) ?? { pem: csr.pem, sources: new Set<string>() };
|
||||
entry.sources.add(csr.source);
|
||||
csrMap.set(norm, entry);
|
||||
}
|
||||
const csrItems: BundleItem[] = [...csrMap.values()].map(({ pem, sources }, i) => {
|
||||
let cn = '';
|
||||
let organization = '';
|
||||
let info = { keyType: '', keyBits: 0, modulus: '' };
|
||||
let san: string[] = [];
|
||||
try {
|
||||
const csr = forge.pki.certificationRequestFromPem(pem);
|
||||
cn = field(csr.subject as forge.pki.Certificate['subject'], 'CN');
|
||||
organization = field(csr.subject as forge.pki.Certificate['subject'], 'O');
|
||||
info = publicKeyInfo(csr.publicKey);
|
||||
const ext = csr.getAttribute({ name: 'extensionRequest' }) as {
|
||||
extensions?: unknown[];
|
||||
} | null;
|
||||
san = sanOf(ext?.extensions);
|
||||
} catch {
|
||||
// EC-CSR: node-forge liest sie nicht — Teil bleibt trotzdem herunterladbar.
|
||||
}
|
||||
const cert = info.modulus ? internals.find((o) => o._modulus === info.modulus) : undefined;
|
||||
return {
|
||||
id: `csr-${i + 1}`,
|
||||
kind: 'csr',
|
||||
sources: [...sources],
|
||||
pem,
|
||||
baseName: cert?.baseName ?? safeBaseName(cn, 'anfrage'),
|
||||
cn,
|
||||
organization,
|
||||
issuerCn: '',
|
||||
notBefore: null,
|
||||
notAfter: null,
|
||||
isExpired: null,
|
||||
daysLeft: null,
|
||||
san,
|
||||
keyType: info.keyType,
|
||||
keyBits: info.keyBits,
|
||||
serialNumber: '',
|
||||
sha256: '',
|
||||
matchId: cert?.id ?? null,
|
||||
chainIds: [],
|
||||
formats: ['csr', 'csr-der'],
|
||||
};
|
||||
});
|
||||
|
||||
// Reihenfolge: Serverzertifikat(e), Zwischen-, Stammzertifikate, Schluessel, CSR.
|
||||
const roleOrder: Record<BundleCertRole, number> = { 'end-entity': 0, intermediate: 1, root: 2 };
|
||||
internals.sort(
|
||||
(a, b) =>
|
||||
roleOrder[a.role ?? 'end-entity'] - roleOrder[b.role ?? 'end-entity'] ||
|
||||
b.chainIds.length - a.chainIds.length,
|
||||
);
|
||||
const certsClean: BundleItem[] = internals.map(({ _cert, _modulus, ...rest }) => rest);
|
||||
|
||||
return {
|
||||
items: [...certsClean, ...keyItems, ...csrItems],
|
||||
locked: [...new Set(c.locked)],
|
||||
ignored: [...new Set(c.ignored)],
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// exportBundleItem
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const MIME = {
|
||||
pem: 'application/x-pem-file',
|
||||
der: 'application/x-x509-ca-cert',
|
||||
p7b: 'application/x-pkcs7-certificates',
|
||||
pfx: 'application/x-pkcs12',
|
||||
key: 'application/x-pem-file',
|
||||
octet: 'application/octet-stream',
|
||||
} as const;
|
||||
|
||||
function pemBody(pem: string): string {
|
||||
const [msg] = forge.pem.decode(pem);
|
||||
if (!msg) throw new Error('no PEM block');
|
||||
return msg.body;
|
||||
}
|
||||
|
||||
export function exportBundleItem(input: BundleExportInput): BundleExportFile {
|
||||
const { kind, pem, format, chain = [], keyPem, password } = input;
|
||||
const base = safeBaseName(
|
||||
input.baseName ?? '',
|
||||
kind === 'csr' ? 'anfrage' : kind === 'privateKey' ? 'schluessel' : 'zertifikat',
|
||||
);
|
||||
|
||||
if (!pem || typeof pem !== 'string') throw new BadRequestException('No PEM provided');
|
||||
if (format === 'pfx' && (!password || password.trim() === '')) {
|
||||
throw new BadRequestException('A password is required for PFX output');
|
||||
}
|
||||
|
||||
try {
|
||||
if (kind === 'certificate') {
|
||||
const cert = forge.pki.certificateFromPem(pem);
|
||||
const chainCerts = chain.map((p) => forge.pki.certificateFromPem(p));
|
||||
switch (format) {
|
||||
case 'crt':
|
||||
return {
|
||||
filename: `${base}.crt`,
|
||||
content: textToBase64(forge.pki.certificateToPem(cert)),
|
||||
mimeType: MIME.pem,
|
||||
};
|
||||
case 'cer':
|
||||
return {
|
||||
filename: `${base}.cer`,
|
||||
content: bytesToBase64(forge.asn1.toDer(forge.pki.certificateToAsn1(cert)).getBytes()),
|
||||
mimeType: MIME.der,
|
||||
};
|
||||
case 'fullchain': {
|
||||
const text = [cert, ...chainCerts].map((x) => forge.pki.certificateToPem(x)).join('');
|
||||
return {
|
||||
filename: `${base}-fullchain.pem`,
|
||||
content: textToBase64(text),
|
||||
mimeType: MIME.pem,
|
||||
};
|
||||
}
|
||||
case 'p7b': {
|
||||
const p7 = forge.pkcs7.createSignedData();
|
||||
for (const x of [cert, ...chainCerts]) p7.addCertificate(x);
|
||||
const text = forge.pem.encode({
|
||||
type: 'PKCS7',
|
||||
body: forge.asn1.toDer(p7.toAsn1()).getBytes(),
|
||||
});
|
||||
return { filename: `${base}.p7b`, content: textToBase64(text), mimeType: MIME.p7b };
|
||||
}
|
||||
case 'pfx': {
|
||||
const key = keyPem
|
||||
? (forge.pki.privateKeyFromPem(keyPem) as forge.pki.rsa.PrivateKey)
|
||||
: null;
|
||||
const p12 = forge.pkcs12.toPkcs12Asn1(
|
||||
// null = reines Zertifikatsbuendel ohne Schluessel (siehe
|
||||
// CertManagerService.mergeCerts).
|
||||
key,
|
||||
[cert, ...chainCerts],
|
||||
password as string, // oben geprueft: PFX verlangt ein Passwort
|
||||
{ algorithm: '3des', friendlyName: input.baseName || undefined },
|
||||
);
|
||||
return {
|
||||
filename: `${base}.pfx`,
|
||||
content: bytesToBase64(forge.asn1.toDer(p12).getBytes()),
|
||||
mimeType: MIME.pfx,
|
||||
};
|
||||
}
|
||||
}
|
||||
} else if (kind === 'privateKey') {
|
||||
switch (format) {
|
||||
case 'key':
|
||||
return {
|
||||
filename: `${base}.key`,
|
||||
content: textToBase64(`${pem.trim()}\n`),
|
||||
mimeType: MIME.key,
|
||||
};
|
||||
case 'key-rsa': {
|
||||
const key = forge.pki.privateKeyFromPem(pem);
|
||||
return {
|
||||
filename: `${base}.rsa.key`,
|
||||
content: textToBase64(forge.pki.privateKeyToPem(key)),
|
||||
mimeType: MIME.key,
|
||||
};
|
||||
}
|
||||
case 'key-der': {
|
||||
const key = forge.pki.privateKeyFromPem(pem);
|
||||
const info = forge.pki.wrapRsaPrivateKey(forge.pki.privateKeyToAsn1(key));
|
||||
return {
|
||||
filename: `${base}.key.der`,
|
||||
content: bytesToBase64(forge.asn1.toDer(info).getBytes()),
|
||||
mimeType: MIME.octet,
|
||||
};
|
||||
}
|
||||
}
|
||||
} else if (kind === 'csr') {
|
||||
switch (format) {
|
||||
case 'csr':
|
||||
return {
|
||||
filename: `${base}.csr`,
|
||||
content: textToBase64(`${pem.trim()}\n`),
|
||||
mimeType: MIME.pem,
|
||||
};
|
||||
case 'csr-der':
|
||||
return {
|
||||
filename: `${base}.csr.der`,
|
||||
content: bytesToBase64(pemBody(pem)),
|
||||
mimeType: MIME.octet,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Passwort und Schluessel nie protokollieren oder zurueckgeben.
|
||||
throw new BadRequestException(`Failed to export ${kind} as ${format}`);
|
||||
}
|
||||
throw new BadRequestException(`Unsupported format "${format}" for ${kind}`);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import 'reflect-metadata';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { RequestMethod } from '@nestjs/common';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { MODULE_SLUG_KEY } from '../module-registry/module.guard';
|
||||
import { CertManagerController, repairFileName } from './cert-manager.controller';
|
||||
|
||||
const fx = (name: string) => readFileSync(join(__dirname, '__fixtures__', name));
|
||||
const upload = (name: string, buffer: Buffer = fx(name)) => ({
|
||||
originalname: name,
|
||||
mimetype: 'application/octet-stream',
|
||||
size: buffer.length,
|
||||
buffer,
|
||||
});
|
||||
|
||||
function codeOf(fn: () => unknown): { status: number; code: string } {
|
||||
try {
|
||||
fn();
|
||||
} catch (error) {
|
||||
const e = error as { getStatus(): number; getResponse(): { code: string } };
|
||||
return { status: e.getStatus(), code: e.getResponse().code };
|
||||
}
|
||||
throw new Error('expected a throw');
|
||||
}
|
||||
|
||||
describe('CertManagerController', () => {
|
||||
const controller = new CertManagerController();
|
||||
|
||||
it('liegt unter modules/cert-manager und gehoert zum Modul cert-manager', () => {
|
||||
expect(Reflect.getMetadata('path', CertManagerController)).toBe('modules/cert-manager');
|
||||
expect(Reflect.getMetadata(MODULE_SLUG_KEY, CertManagerController)).toBe('cert-manager');
|
||||
});
|
||||
|
||||
it('bietet in diesem Stand genau den Handler analyze (POST analyze, Code 200)', () => {
|
||||
const handlers = Object.getOwnPropertyNames(CertManagerController.prototype).filter(
|
||||
(n) => n !== 'constructor',
|
||||
);
|
||||
expect(handlers).toEqual(['analyze']);
|
||||
const handler = CertManagerController.prototype.analyze;
|
||||
expect(Reflect.getMetadata('path', handler)).toBe('analyze');
|
||||
expect(Reflect.getMetadata('method', handler)).toBe(RequestMethod.POST);
|
||||
expect(Reflect.getMetadata('__httpCode__', handler)).toBe(200);
|
||||
});
|
||||
|
||||
it('ohne Dateien: 400 invalidInput', () => {
|
||||
expect(codeOf(() => controller.analyze(undefined))).toEqual({
|
||||
status: 400,
|
||||
code: 'invalidInput',
|
||||
});
|
||||
expect(codeOf(() => controller.analyze([]))).toEqual({ status: 400, code: 'invalidInput' });
|
||||
});
|
||||
|
||||
it('Dateien ueber 20 MiB zusammen: 413 tooLarge, ohne Analyse', () => {
|
||||
const big = Buffer.alloc(5 * 1024 * 1024, 1);
|
||||
const files = [1, 2, 3, 4, 5].map((i) => upload(`big-${i}.bin`, big));
|
||||
expect(codeOf(() => controller.analyze(files))).toEqual({ status: 413, code: 'tooLarge' });
|
||||
});
|
||||
|
||||
it('reicht die Dateien in Anfrage-Reihenfolge an die Analyse', () => {
|
||||
const result = controller.analyze([
|
||||
upload('ec-leaf.pem'),
|
||||
upload('rsa-leaf.pem'),
|
||||
upload('x.txt', Buffer.from('x')),
|
||||
]);
|
||||
const sources = result.items.flatMap((i) => i.sources);
|
||||
expect(sources).toEqual([
|
||||
{ file: 0, path: 'ec-leaf.pem' },
|
||||
{ file: 1, path: 'rsa-leaf.pem' },
|
||||
]);
|
||||
expect(result.ignored).toEqual([{ file: 2, path: 'x.txt', reason: 'unknown' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('repairFileName', () => {
|
||||
it('gewinnt UTF-8-Namen zurueck, die als Latin-1 ankamen', () => {
|
||||
const asLatin1 = Buffer.from('Zertifikat-Müller.pem', 'utf8').toString('latin1');
|
||||
expect(repairFileName(asLatin1)).toBe('Zertifikat-Müller.pem');
|
||||
});
|
||||
it('lässt Namen ohne Umlaute und echtes Latin-1 unverändert', () => {
|
||||
expect(repairFileName('a.pem')).toBe('a.pem');
|
||||
expect(repairFileName('M\xfcller.pem')).toBe('M\xfcller.pem');
|
||||
});
|
||||
});
|
||||
@@ -1,173 +1,54 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Body,
|
||||
Controller,
|
||||
Post,
|
||||
UploadedFile,
|
||||
UploadedFiles,
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express';
|
||||
import { UseModule } from '../module-registry/module.guard';
|
||||
import { Controller, HttpCode, Post, UploadedFiles, UseInterceptors } from '@nestjs/common';
|
||||
import { FilesInterceptor } from '@nestjs/platform-express';
|
||||
import type { UploadedFileLike } from '../auth/types/auth-user';
|
||||
import {
|
||||
analyzeBundle,
|
||||
type BundleExportFormat,
|
||||
type BundleItemKind,
|
||||
exportBundleItem,
|
||||
} from './cert-bundle';
|
||||
import { CertManagerService } from './cert-manager.service';
|
||||
import { UseModule } from '../module-registry/module.guard';
|
||||
import { analyzeWorkingSet } from './cert-analyze';
|
||||
import { type AnalysisResult, certError } from './cert-types';
|
||||
|
||||
/** Obergrenzen (D-17): je Datei 5 MiB, alle Dateien zusammen 20 MiB, hoechstens 30 Dateien. */
|
||||
export const CERT_MAX_FILES = 30;
|
||||
export const CERT_MAX_FILE_BYTES = 5 * 1024 * 1024;
|
||||
export const CERT_MAX_TOTAL_BYTES = 20 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* CertManagerController — 4 POST endpoints for certificate operations.
|
||||
* multer liest Dateinamen als Latin-1. Waren es UTF-8-Bytes (Umlaute), den Namen zurueckgewinnen;
|
||||
* ist das Ergebnis kein gueltiges UTF-8, bleibt der Name wie er ist.
|
||||
*/
|
||||
export function repairFileName(name: string): string {
|
||||
const utf8 = Buffer.from(name, 'latin1').toString('utf8');
|
||||
return utf8.includes('�') ? name : utf8;
|
||||
}
|
||||
|
||||
/**
|
||||
* CertManagerController: Zertifikat-Manager (quick-261009-ikt, D-14).
|
||||
*
|
||||
* All routes are protected by:
|
||||
* - Global JwtAuthGuard (authentication)
|
||||
* - Global TenantGuard (tenant context)
|
||||
* - @UseModule('cert-manager') ModuleGuard (module activation check)
|
||||
* Zustandslos: nichts wird gespeichert, Passwoerter und Schluessel kommen nie in ein Log
|
||||
* (kein Logger-Aufruf mit Anfrageinhalt; das Request-Log kennt nur Pfad und Status).
|
||||
* Geschuetzt durch den globalen JwtAuthGuard, den TenantGuard und @UseModule('cert-manager').
|
||||
*
|
||||
* File size limit: 5 MB per file (T-09-03 — DoS mitigation).
|
||||
* Password parameter is never passed to a logger (T-09-02 — InfoDisc mitigation).
|
||||
* Routen (alle POST, 200):
|
||||
* - analyze Task 1 mehrere Dateien (multipart) erkennen und zusammenfassen
|
||||
* - build Task 2 Ausgabe bauen (JSON), ab Task 5/6 erweitert
|
||||
* - fetch-issuer Task 7 fehlendes Zwischenzertifikat nur auf Knopfdruck holen
|
||||
*/
|
||||
@Controller('modules/cert-manager')
|
||||
@UseModule('cert-manager')
|
||||
export class CertManagerController {
|
||||
constructor(private readonly certManagerService: CertManagerService) {}
|
||||
|
||||
/**
|
||||
* POST /modules/cert-manager/parse
|
||||
* Inspect a single certificate: subject, issuer, validity, SANs, fingerprints.
|
||||
* Accepts multipart file upload OR JSON body with pemText.
|
||||
*/
|
||||
@Post('parse')
|
||||
@UseInterceptors(
|
||||
FileInterceptor('file', {
|
||||
limits: { fileSize: 5 * 1024 * 1024 },
|
||||
}),
|
||||
)
|
||||
async parseCert(
|
||||
@UploadedFile() file: UploadedFileLike | undefined,
|
||||
@Body('password') password?: string,
|
||||
@Body('pemText') pemText?: string,
|
||||
) {
|
||||
if (!file && !pemText) {
|
||||
throw new BadRequestException('No file or PEM text provided');
|
||||
}
|
||||
return this.certManagerService.parseCert({ file, pemText, password });
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /modules/cert-manager/split
|
||||
* Split a fullchain.pem or P7B bundle into individual certificates.
|
||||
*/
|
||||
@Post('split')
|
||||
@UseInterceptors(
|
||||
FileInterceptor('file', {
|
||||
limits: { fileSize: 5 * 1024 * 1024 },
|
||||
}),
|
||||
)
|
||||
async splitCerts(
|
||||
@UploadedFile() file: UploadedFileLike | undefined,
|
||||
@Body('password') password?: string,
|
||||
) {
|
||||
if (!file) {
|
||||
throw new BadRequestException('No file provided');
|
||||
}
|
||||
return this.certManagerService.splitCerts({ file, password });
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /modules/cert-manager/merge
|
||||
* Merge multiple certificates into a PEM chain or PFX bundle.
|
||||
* Uses FilesInterceptor (plural) to accept multiple files with field name "files".
|
||||
*/
|
||||
@Post('merge')
|
||||
@UseInterceptors(
|
||||
FilesInterceptor('files', 20, {
|
||||
limits: { fileSize: 5 * 1024 * 1024 },
|
||||
}),
|
||||
)
|
||||
async mergeCerts(
|
||||
@UploadedFiles() files: UploadedFileLike[],
|
||||
@Body('outputFormat') outputFormat: string,
|
||||
@Body('password') password?: string,
|
||||
) {
|
||||
if (!files || files.length < 2) {
|
||||
throw new BadRequestException('At least 2 files required for merge');
|
||||
}
|
||||
return this.certManagerService.mergeCerts({ files, outputFormat, password });
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /modules/cert-manager/convert
|
||||
* Convert a certificate between PEM, DER, and P7B formats.
|
||||
* Accepts a multipart file upload OR a pemText body field.
|
||||
*
|
||||
* T-09-03: fileSize limit 5 MB (DoS mitigation)
|
||||
* T-09-04: global JwtAuthGuard + @UseModule('cert-manager') ModuleGuard
|
||||
* T-09-02: password is never passed to the logger
|
||||
*/
|
||||
@Post('convert')
|
||||
@UseInterceptors(
|
||||
FileInterceptor('file', {
|
||||
limits: { fileSize: 5 * 1024 * 1024 },
|
||||
}),
|
||||
)
|
||||
async convertCert(
|
||||
@UploadedFile() file: UploadedFileLike | undefined,
|
||||
@Body('targetFormat') targetFormat: string,
|
||||
@Body('password') password?: string,
|
||||
@Body('pemText') pemText?: string,
|
||||
) {
|
||||
if (!file && !pemText) {
|
||||
throw new BadRequestException('No file or PEM text provided');
|
||||
}
|
||||
return this.certManagerService.convertCert({ file, pemText, targetFormat, password });
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /modules/cert-manager/analyze (quick-261001-l4q)
|
||||
* Zertifikatspaket: mehrere Dateien oder ZIP hochladen, jedes Teil erkennen
|
||||
* (Server-/Zwischen-/Stammzertifikat, privater Schluessel, CSR), Duplikate
|
||||
* zusammenfassen, Schluessel und Kette zuordnen.
|
||||
*
|
||||
* T-09-03: 20 Dateien, je 5 MB; ZIP-Inhalt zusaetzlich begrenzt (cert-bundle.ts).
|
||||
* T-09-02: password is never passed to the logger
|
||||
*/
|
||||
@Post('analyze')
|
||||
@HttpCode(200)
|
||||
@UseInterceptors(
|
||||
FilesInterceptor('files', 20, {
|
||||
limits: { fileSize: 5 * 1024 * 1024 },
|
||||
}),
|
||||
FilesInterceptor('files', CERT_MAX_FILES, { limits: { fileSize: CERT_MAX_FILE_BYTES } }),
|
||||
)
|
||||
async analyze(
|
||||
@UploadedFiles() files: UploadedFileLike[] | undefined,
|
||||
@Body('password') password?: string,
|
||||
) {
|
||||
return analyzeBundle(files ?? [], password ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /modules/cert-manager/export (quick-261001-l4q)
|
||||
* Ein Teil aus `analyze` (PEM) in das gewuenschte Format bringen.
|
||||
* JSON-Body; PFX verlangt ein Passwort fuer die neue Datei.
|
||||
*/
|
||||
@Post('export')
|
||||
async export(
|
||||
@Body('kind') kind: BundleItemKind,
|
||||
@Body('pem') pem: string,
|
||||
@Body('format') format: BundleExportFormat,
|
||||
@Body('baseName') baseName?: string,
|
||||
@Body('chain') chain?: string[],
|
||||
@Body('keyPem') keyPem?: string,
|
||||
@Body('password') password?: string,
|
||||
) {
|
||||
if (
|
||||
chain !== undefined &&
|
||||
(!Array.isArray(chain) || chain.some((c) => typeof c !== 'string'))
|
||||
) {
|
||||
throw new BadRequestException('chain must be a list of PEM strings');
|
||||
analyze(@UploadedFiles() files: UploadedFileLike[] | undefined): AnalysisResult {
|
||||
if (!files || files.length === 0) {
|
||||
certError('invalidInput', 400, 'No files provided');
|
||||
}
|
||||
return exportBundleItem({ kind, pem, format, baseName, chain, keyPem, password });
|
||||
const total = files.reduce((sum, f) => sum + f.buffer.length, 0);
|
||||
if (total > CERT_MAX_TOTAL_BYTES) {
|
||||
certError('tooLarge', 413, 'Files together exceed 20 MiB');
|
||||
}
|
||||
return analyzeWorkingSet(
|
||||
files.map((f) => ({ originalname: repairFileName(f.originalname), buffer: f.buffer })),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,12 @@ import { ModuleRegistryModule } from '../module-registry/module-registry.module'
|
||||
import { ModuleRegistryService } from '../module-registry/module-registry.service';
|
||||
import { CertManagerController } from './cert-manager.controller';
|
||||
import { seedCertManagerModule } from './cert-manager.seed';
|
||||
import { CertManagerService } from './cert-manager.service';
|
||||
|
||||
/**
|
||||
* NestJS module for the Cert Manager feature.
|
||||
*
|
||||
* Provides server-side certificate inspection, splitting, merging,
|
||||
* and format conversion using node-forge (pure JS, no native bindings).
|
||||
* Zustandslose Zertifikat-Verarbeitung (node:crypto, node-forge nur fuer PKCS#12 und ASN.1);
|
||||
* keine Provider, die Logik steht in reinen Funktionen (cert-model, cert-analyze, ...).
|
||||
*
|
||||
* Seeds itself into the module registry on application startup via
|
||||
* OnModuleInit lifecycle hook (CERT-06).
|
||||
@@ -21,14 +20,11 @@ import { CertManagerService } from './cert-manager.service';
|
||||
@Module({
|
||||
imports: [ModuleRegistryModule],
|
||||
controllers: [CertManagerController],
|
||||
providers: [CertManagerService],
|
||||
})
|
||||
export class CertManagerModule implements OnModuleInit {
|
||||
private readonly logger = new Logger(CertManagerModule.name);
|
||||
|
||||
constructor(
|
||||
private readonly moduleRegistryService: ModuleRegistryService,
|
||||
) {}
|
||||
constructor(private readonly moduleRegistryService: ModuleRegistryService) {}
|
||||
|
||||
async onModuleInit(): Promise<void> {
|
||||
try {
|
||||
|
||||
@@ -1,778 +0,0 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import * as forge from 'node-forge';
|
||||
import { beforeAll, describe, expect, it, vi } from 'vitest';
|
||||
import { seedCertManagerModule } from './cert-manager.seed';
|
||||
import { CertManagerService } from './cert-manager.service';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Generate a self-signed X.509 cert via node-forge (RSA 1024 — fast for tests) */
|
||||
function generateSelfSignedCert(): forge.pki.Certificate {
|
||||
const keys = forge.pki.rsa.generateKeyPair(1024);
|
||||
const cert = forge.pki.createCertificate();
|
||||
cert.publicKey = keys.publicKey;
|
||||
cert.serialNumber = '01';
|
||||
cert.validity.notBefore = new Date();
|
||||
cert.validity.notAfter = new Date();
|
||||
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
|
||||
|
||||
const attrs = [{ name: 'commonName', value: 'test.example.com' }];
|
||||
cert.setSubject(attrs);
|
||||
cert.setIssuer(attrs);
|
||||
cert.sign(keys.privateKey, forge.md.sha256.create());
|
||||
return cert;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('seedCertManagerModule', () => {
|
||||
it('calls moduleRegistryService.seedModule once with cert-manager slug', async () => {
|
||||
const mockSeedModule = vi.fn().mockResolvedValue(undefined);
|
||||
const mockModuleRegistryService = { seedModule: mockSeedModule } as any;
|
||||
|
||||
await seedCertManagerModule(mockModuleRegistryService);
|
||||
|
||||
expect(mockSeedModule).toHaveBeenCalledTimes(1);
|
||||
const arg = mockSeedModule.mock.calls[0][0];
|
||||
expect(arg.slug).toBe('cert-manager');
|
||||
expect(arg.category).toBe('security-tools');
|
||||
expect(arg.isSystem).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CertManagerService helpers', () => {
|
||||
let service: CertManagerService;
|
||||
let testCert: forge.pki.Certificate;
|
||||
|
||||
beforeAll(() => {
|
||||
service = new CertManagerService();
|
||||
testCert = generateSelfSignedCert();
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// detectFormat
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
describe('detectFormat', () => {
|
||||
it('returns "pfx" for .pfx extension', () => {
|
||||
const buf = Buffer.from([0x30, 0x82]); // arbitrary binary
|
||||
expect(service.detectFormat('cert.pfx', buf)).toBe('pfx');
|
||||
});
|
||||
|
||||
it('returns "p7b" for .p7b extension', () => {
|
||||
const buf = Buffer.from([0x30, 0x82]);
|
||||
expect(service.detectFormat('cert.p7b', buf)).toBe('p7b');
|
||||
});
|
||||
|
||||
it('returns "der" for .der extension', () => {
|
||||
const buf = Buffer.from([0x30, 0x82]);
|
||||
expect(service.detectFormat('cert.der', buf)).toBe('der');
|
||||
});
|
||||
|
||||
it('returns "pem" for .pem extension', () => {
|
||||
const buf = Buffer.from('-----BEGIN CERTIFICATE-----\nfake\n-----END CERTIFICATE-----');
|
||||
expect(service.detectFormat('cert.pem', buf)).toBe('pem');
|
||||
});
|
||||
|
||||
it('returns "pem" for .cer extension with PEM content', () => {
|
||||
const buf = Buffer.from('-----BEGIN CERTIFICATE-----\nfake\n-----END CERTIFICATE-----');
|
||||
expect(service.detectFormat('cert.cer', buf)).toBe('pem');
|
||||
});
|
||||
|
||||
it('returns "der" for .cer extension with binary (non-PEM) content', () => {
|
||||
const buf = Buffer.from([0x30, 0x82, 0x01, 0x00]);
|
||||
expect(service.detectFormat('cert.cer', buf)).toBe('der');
|
||||
});
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// getFingerprint
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
describe('getFingerprint', () => {
|
||||
it('returns uppercase colon-separated hex for sha256', () => {
|
||||
const fp = service.getFingerprint(testCert, 'sha256');
|
||||
// e.g. "AA:BB:CC:..."
|
||||
expect(fp).toMatch(/^[0-9A-F]{2}(:[0-9A-F]{2})+$/);
|
||||
});
|
||||
|
||||
it('returns uppercase colon-separated hex for sha1', () => {
|
||||
const fp = service.getFingerprint(testCert, 'sha1');
|
||||
expect(fp).toMatch(/^[0-9A-F]{2}(:[0-9A-F]{2})+$/);
|
||||
});
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// parsePemChain
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
describe('parsePemChain', () => {
|
||||
it('returns array of length 2 for two concatenated cert PEMs', () => {
|
||||
const cert1 = generateSelfSignedCert();
|
||||
const cert2 = generateSelfSignedCert();
|
||||
const pem1 = forge.pki.certificateToPem(cert1);
|
||||
const pem2 = forge.pki.certificateToPem(cert2);
|
||||
const chain = `${pem1}\n${pem2}`;
|
||||
|
||||
const parsed = service.parsePemChain(chain);
|
||||
expect(parsed).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('returns array of length 1 for a single PEM', () => {
|
||||
const pem = forge.pki.certificateToPem(testCert);
|
||||
const parsed = service.parsePemChain(pem);
|
||||
expect(parsed).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// parseCert — RED tests (CERT-01, CERT-05 read half)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('parseCert', () => {
|
||||
let service: CertManagerService;
|
||||
let certPem: string;
|
||||
let certDerBuffer: Buffer;
|
||||
let pfxBuffer: Buffer;
|
||||
const CN = 'parsecert.example.com';
|
||||
|
||||
beforeAll(() => {
|
||||
service = new CertManagerService();
|
||||
|
||||
// Create a cert+key pair for all fixtures (RSA-1024 for speed)
|
||||
const keys = forge.pki.rsa.generateKeyPair(1024);
|
||||
const cert = forge.pki.createCertificate();
|
||||
cert.publicKey = keys.publicKey;
|
||||
cert.serialNumber = '01';
|
||||
cert.validity.notBefore = new Date();
|
||||
cert.validity.notAfter = new Date();
|
||||
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
|
||||
|
||||
const attrs = [{ name: 'commonName', value: CN }];
|
||||
cert.setSubject(attrs);
|
||||
cert.setIssuer(attrs);
|
||||
cert.sign(keys.privateKey, forge.md.sha256.create());
|
||||
|
||||
// PEM fixture
|
||||
certPem = forge.pki.certificateToPem(cert);
|
||||
|
||||
// DER fixture — binary encoding required (Pitfall 1)
|
||||
const derBytes = forge.asn1.toDer(forge.pki.certificateToAsn1(cert)).getBytes();
|
||||
certDerBuffer = Buffer.from(derBytes, 'binary');
|
||||
|
||||
// PFX fixture with password 'secret'
|
||||
const p12Asn1 = forge.pkcs12.toPkcs12Asn1(
|
||||
keys.privateKey,
|
||||
[cert],
|
||||
'secret',
|
||||
{ algorithm: '3des' },
|
||||
);
|
||||
const p12DerBytes = forge.asn1.toDer(p12Asn1).getBytes();
|
||||
pfxBuffer = Buffer.from(p12DerBytes, 'binary');
|
||||
}, 15000); // 15s timeout — RSA keygen can be slow in pure JS
|
||||
|
||||
it('returns CertDetails with subject.cn, fingerprint.sha256, keyType RSA, keyBits, isExpired false for PEM input', async () => {
|
||||
// RED: parseCert currently throws NotImplementedException — this test will FAIL
|
||||
const result = await (service.parseCert({ pemText: certPem }) as Promise<any>);
|
||||
expect(result.subject.cn).toBe(CN);
|
||||
expect(result.fingerprint.sha256).toMatch(/^[0-9A-F]{2}(:[0-9A-F]{2})+$/);
|
||||
expect(result.keyType).toBe('RSA');
|
||||
expect(result.keyBits).toBe(1024);
|
||||
expect(result.validity.isExpired).toBe(false);
|
||||
});
|
||||
|
||||
it('returns CertDetails with matching subject.cn for DER input', async () => {
|
||||
// RED: parseCert throws NotImplementedException — this test will FAIL
|
||||
const result = await (service.parseCert({
|
||||
file: { originalname: 'c.der', buffer: certDerBuffer },
|
||||
}) as Promise<any>);
|
||||
expect(result.subject.cn).toBe(CN);
|
||||
});
|
||||
|
||||
it('returns CertDetails for PFX with correct password', async () => {
|
||||
// RED: parseCert throws NotImplementedException — this test will FAIL
|
||||
const result = await (service.parseCert({
|
||||
file: { originalname: 'c.pfx', buffer: pfxBuffer },
|
||||
password: 'secret',
|
||||
}) as Promise<any>);
|
||||
expect(result.subject.cn).toBe(CN);
|
||||
});
|
||||
|
||||
it('throws BadRequestException for PFX with wrong password', async () => {
|
||||
// RED: currently throws NotImplementedException (not BadRequestException) — FAIL
|
||||
await expect(
|
||||
service.parseCert({
|
||||
file: { originalname: 'c.pfx', buffer: pfxBuffer },
|
||||
password: 'wrong',
|
||||
}),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it('throws BadRequestException for malformed PEM input', async () => {
|
||||
// RED: currently throws NotImplementedException (not BadRequestException) — FAIL
|
||||
await expect(
|
||||
service.parseCert({ pemText: 'not a cert' }),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// splitCerts — RED tests (CERT-02)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('splitCerts', () => {
|
||||
let service: CertManagerService;
|
||||
let cert1Pem: string;
|
||||
let cert2Pem: string;
|
||||
let fullchainBuffer: Buffer;
|
||||
let p7bBuffer: Buffer;
|
||||
const CN1 = 'split1.example.com';
|
||||
const CN2 = 'split2.example.com';
|
||||
|
||||
beforeAll(() => {
|
||||
service = new CertManagerService();
|
||||
|
||||
// Build cert 1 (RSA-1024 for speed)
|
||||
const keys1 = forge.pki.rsa.generateKeyPair(1024);
|
||||
const c1 = forge.pki.createCertificate();
|
||||
c1.publicKey = keys1.publicKey;
|
||||
c1.serialNumber = '01';
|
||||
c1.validity.notBefore = new Date();
|
||||
c1.validity.notAfter = new Date();
|
||||
c1.validity.notAfter.setFullYear(c1.validity.notBefore.getFullYear() + 1);
|
||||
const attrs1 = [{ name: 'commonName', value: CN1 }];
|
||||
c1.setSubject(attrs1);
|
||||
c1.setIssuer(attrs1);
|
||||
c1.sign(keys1.privateKey, forge.md.sha256.create());
|
||||
cert1Pem = forge.pki.certificateToPem(c1);
|
||||
|
||||
// Build cert 2
|
||||
const keys2 = forge.pki.rsa.generateKeyPair(1024);
|
||||
const c2 = forge.pki.createCertificate();
|
||||
c2.publicKey = keys2.publicKey;
|
||||
c2.serialNumber = '02';
|
||||
c2.validity.notBefore = new Date();
|
||||
c2.validity.notAfter = new Date();
|
||||
c2.validity.notAfter.setFullYear(c2.validity.notBefore.getFullYear() + 1);
|
||||
const attrs2 = [{ name: 'commonName', value: CN2 }];
|
||||
c2.setSubject(attrs2);
|
||||
c2.setIssuer(attrs2);
|
||||
c2.sign(keys2.privateKey, forge.md.sha256.create());
|
||||
cert2Pem = forge.pki.certificateToPem(c2);
|
||||
|
||||
// Fullchain fixture: two PEMs concatenated
|
||||
const fullchainPem = `${cert1Pem}\n${cert2Pem}`;
|
||||
fullchainBuffer = Buffer.from(fullchainPem, 'utf-8');
|
||||
|
||||
// P7B fixture: PEM-wrapped PKCS7 SignedData bundle with both certs
|
||||
const p7 = forge.pkcs7.createSignedData();
|
||||
p7.addCertificate(c1);
|
||||
p7.addCertificate(c2);
|
||||
const p7DerBytes = forge.asn1.toDer(p7.toAsn1()).getBytes();
|
||||
const p7PemStr = forge.pem.encode({ type: 'PKCS7', body: p7DerBytes });
|
||||
p7bBuffer = Buffer.from(p7PemStr, 'utf-8');
|
||||
}, 30000); // 30s — two RSA-1024 keygens
|
||||
|
||||
it('returns count 2 and two certs each with a single PEM block and correct CN for fullchain PEM', async () => {
|
||||
// RED: splitCerts throws NotImplementedException — FAIL
|
||||
const result = await (service.splitCerts({
|
||||
file: { originalname: 'fullchain.pem', buffer: fullchainBuffer },
|
||||
}) as Promise<any>);
|
||||
expect(result.count).toBe(2);
|
||||
expect(result.certs).toHaveLength(2);
|
||||
|
||||
// Each cert content decodes to exactly one BEGIN CERTIFICATE block
|
||||
for (const entry of result.certs) {
|
||||
const decoded = Buffer.from(entry.content, 'base64').toString('utf-8');
|
||||
const matches = decoded.match(/-----BEGIN CERTIFICATE-----/g);
|
||||
expect(matches).toHaveLength(1);
|
||||
}
|
||||
|
||||
// CN values are present
|
||||
const cns = result.certs.map((c: any) => c.subject.cn);
|
||||
expect(cns).toContain(CN1);
|
||||
expect(cns).toContain(CN2);
|
||||
|
||||
// validity.notAfter is present and looks like ISO 8601
|
||||
for (const entry of result.certs) {
|
||||
expect(entry.validity.notAfter).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
||||
}
|
||||
});
|
||||
|
||||
it('returns enclosed certs from a P7B PEM bundle', async () => {
|
||||
// RED: splitCerts throws NotImplementedException — FAIL
|
||||
const result = await (service.splitCerts({
|
||||
file: { originalname: 'bundle.p7b', buffer: p7bBuffer },
|
||||
}) as Promise<any>);
|
||||
expect(result.count).toBeGreaterThanOrEqual(1);
|
||||
expect(result.certs.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('throws BadRequestException for malformed input', async () => {
|
||||
// RED: splitCerts throws NotImplementedException (not BadRequestException) — FAIL
|
||||
await expect(
|
||||
service.splitCerts({
|
||||
file: { originalname: 'bad.pem', buffer: Buffer.from('this is garbage') },
|
||||
}),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// mergeCerts — RED tests (CERT-03, CERT-05 write half)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('mergeCerts', () => {
|
||||
let service: CertManagerService;
|
||||
let certAPem: string;
|
||||
let certBPem: string;
|
||||
const CNA = 'merge-a.example.com';
|
||||
const CNB = 'merge-b.example.com';
|
||||
|
||||
beforeAll(() => {
|
||||
service = new CertManagerService();
|
||||
|
||||
// Cert A (RSA-1024 for speed)
|
||||
const keysA = forge.pki.rsa.generateKeyPair(1024);
|
||||
const certA = forge.pki.createCertificate();
|
||||
certA.publicKey = keysA.publicKey;
|
||||
certA.serialNumber = '01';
|
||||
certA.validity.notBefore = new Date();
|
||||
certA.validity.notAfter = new Date();
|
||||
certA.validity.notAfter.setFullYear(certA.validity.notBefore.getFullYear() + 1);
|
||||
const attrsA = [{ name: 'commonName', value: CNA }];
|
||||
certA.setSubject(attrsA);
|
||||
certA.setIssuer(attrsA);
|
||||
certA.sign(keysA.privateKey, forge.md.sha256.create());
|
||||
certAPem = forge.pki.certificateToPem(certA);
|
||||
|
||||
// Cert B
|
||||
const keysB = forge.pki.rsa.generateKeyPair(1024);
|
||||
const certB = forge.pki.createCertificate();
|
||||
certB.publicKey = keysB.publicKey;
|
||||
certB.serialNumber = '02';
|
||||
certB.validity.notBefore = new Date();
|
||||
certB.validity.notAfter = new Date();
|
||||
certB.validity.notAfter.setFullYear(certB.validity.notBefore.getFullYear() + 1);
|
||||
const attrsB = [{ name: 'commonName', value: CNB }];
|
||||
certB.setSubject(attrsB);
|
||||
certB.setIssuer(attrsB);
|
||||
certB.sign(keysB.privateKey, forge.md.sha256.create());
|
||||
certBPem = forge.pki.certificateToPem(certB);
|
||||
}, 30000); // 30s — two RSA-1024 keygens
|
||||
|
||||
it('returns PEM chain with 2 BEGIN CERTIFICATE blocks when merging 2 PEM files', async () => {
|
||||
// RED: mergeCerts throws NotImplementedException — FAIL
|
||||
const result = await (service.mergeCerts({
|
||||
files: [
|
||||
{ originalname: 'certA.pem', buffer: Buffer.from(certAPem, 'utf-8') },
|
||||
{ originalname: 'certB.pem', buffer: Buffer.from(certBPem, 'utf-8') },
|
||||
],
|
||||
outputFormat: 'pem',
|
||||
}) as Promise<any>);
|
||||
|
||||
expect(result.mimeType).toBe('application/x-pem-file');
|
||||
expect(result.filename).toContain('chain');
|
||||
|
||||
// Decoded content must have exactly 2 cert blocks
|
||||
const decoded = Buffer.from(result.content as string, 'base64').toString('utf-8');
|
||||
const blocks = decoded.match(/-----BEGIN CERTIFICATE-----/g);
|
||||
expect(blocks).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('returns password-protected PFX that round-trips with the correct password', async () => {
|
||||
// RED: mergeCerts throws NotImplementedException — FAIL
|
||||
// Note: 2-file minimum is enforced at the controller level; service accepts 1 file for PFX
|
||||
const result = await (service.mergeCerts({
|
||||
files: [
|
||||
{ originalname: 'certA.pem', buffer: Buffer.from(certAPem, 'utf-8') },
|
||||
],
|
||||
outputFormat: 'pfx',
|
||||
password: 'secret',
|
||||
}) as Promise<any>);
|
||||
|
||||
expect(result.mimeType).toBe('application/x-pkcs12');
|
||||
expect(result.filename).toBe('bundle.pfx');
|
||||
|
||||
// Round-trip: decode base64 PFX → re-parse with password 'secret' → verify cert bag present
|
||||
const pfxBuf = Buffer.from(result.content as string, 'base64');
|
||||
const p12Asn1 = forge.asn1.fromDer(
|
||||
forge.util.createBuffer(pfxBuf.toString('binary')),
|
||||
);
|
||||
// Should NOT throw with the correct password (Open Question 1 resolution)
|
||||
const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, 'secret');
|
||||
const certBags = p12.getBags({ bagType: forge.pki.oids.certBag });
|
||||
const bags = certBags[forge.pki.oids.certBag] ?? [];
|
||||
expect(bags.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('throws BadRequestException when PFX output is requested without a password', async () => {
|
||||
// RED: mergeCerts throws NotImplementedException (not BadRequestException) — FAIL
|
||||
await expect(
|
||||
service.mergeCerts({
|
||||
files: [
|
||||
{ originalname: 'certA.pem', buffer: Buffer.from(certAPem, 'utf-8') },
|
||||
{ originalname: 'certB.pem', buffer: Buffer.from(certBPem, 'utf-8') },
|
||||
],
|
||||
outputFormat: 'pfx',
|
||||
// no password — should be rejected
|
||||
}),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it('throws BadRequestException for malformed (garbage) input files', async () => {
|
||||
// RED: mergeCerts throws NotImplementedException (not BadRequestException) — FAIL
|
||||
await expect(
|
||||
service.mergeCerts({
|
||||
files: [
|
||||
{ originalname: 'bad.pem', buffer: Buffer.from('this is garbage') },
|
||||
{ originalname: 'bad2.pem', buffer: Buffer.from('also garbage') },
|
||||
],
|
||||
outputFormat: 'pem',
|
||||
}),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// convertCert — RED tests (CERT-04)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('convertCert', () => {
|
||||
let service: CertManagerService;
|
||||
let certPem: string;
|
||||
let certDerBuffer: Buffer;
|
||||
const CN = 'convert.example.com';
|
||||
|
||||
beforeAll(() => {
|
||||
service = new CertManagerService();
|
||||
|
||||
// RSA-1024 self-signed cert (fast for tests)
|
||||
const keys = forge.pki.rsa.generateKeyPair(1024);
|
||||
const cert = forge.pki.createCertificate();
|
||||
cert.publicKey = keys.publicKey;
|
||||
cert.serialNumber = '01';
|
||||
cert.validity.notBefore = new Date();
|
||||
cert.validity.notAfter = new Date();
|
||||
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
|
||||
|
||||
const attrs = [{ name: 'commonName', value: CN }];
|
||||
cert.setSubject(attrs);
|
||||
cert.setIssuer(attrs);
|
||||
cert.sign(keys.privateKey, forge.md.sha256.create());
|
||||
|
||||
// PEM fixture
|
||||
certPem = forge.pki.certificateToPem(cert);
|
||||
|
||||
// DER fixture — binary encoding required (Pitfall 1)
|
||||
const derBytes = forge.asn1.toDer(forge.pki.certificateToAsn1(cert)).getBytes();
|
||||
certDerBuffer = Buffer.from(derBytes, 'binary');
|
||||
}, 15000);
|
||||
|
||||
it('PEM→DER: returns FileResponse with correct mimeType and round-trip identity', async () => {
|
||||
// RED: convertCert throws NotImplementedException — this test will FAIL
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const result = await (service.convertCert({ pemText: certPem, targetFormat: 'der' } as any) as Promise<any>);
|
||||
expect(result.mimeType).toBe('application/x-x509-ca-cert');
|
||||
expect(result.filename).toContain('converted');
|
||||
expect(result.content).toBeTruthy();
|
||||
|
||||
// Round-trip identity: decode base64 DER → re-parse → verify CN matches original
|
||||
const derBuf = Buffer.from(result.content as string, 'base64');
|
||||
const asn1 = forge.asn1.fromDer(forge.util.createBuffer(derBuf.toString('binary')));
|
||||
const reparsed = forge.pki.certificateFromAsn1(asn1);
|
||||
expect(reparsed.subject.getField('CN')?.value).toBe(CN);
|
||||
});
|
||||
|
||||
it('DER→PEM: returned PEM parses to cert with same CN (round-trip identity)', async () => {
|
||||
// RED: convertCert throws NotImplementedException — this test will FAIL
|
||||
const result = await (service.convertCert({
|
||||
file: { originalname: 'c.der', buffer: certDerBuffer },
|
||||
targetFormat: 'pem',
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any) as Promise<any>);
|
||||
expect(result.mimeType).toBe('application/x-pem-file');
|
||||
|
||||
// Decode base64 PEM content → parse → verify CN matches original
|
||||
const pemStr = Buffer.from(result.content as string, 'base64').toString('utf-8');
|
||||
const reparsed = forge.pki.certificateFromPem(pemStr);
|
||||
expect(reparsed.subject.getField('CN')?.value).toBe(CN);
|
||||
});
|
||||
|
||||
it('PEM→P7B: returned P7B contains the original certificate', async () => {
|
||||
// RED: convertCert throws NotImplementedException — this test will FAIL
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const result = await (service.convertCert({ pemText: certPem, targetFormat: 'p7b' } as any) as Promise<any>);
|
||||
expect(result.mimeType).toBe('application/x-pkcs7-certificates');
|
||||
expect(result.filename).toContain('converted');
|
||||
|
||||
// Decode base64 P7B (PEM-wrapped PKCS7) and verify at least 1 cert enclosed
|
||||
const p7bContent = Buffer.from(result.content as string, 'base64').toString('utf-8');
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const p7 = forge.pkcs7.messageFromPem(p7bContent) as any;
|
||||
expect((p7.certificates as forge.pki.Certificate[]).length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('throws BadRequestException for malformed PEM input', async () => {
|
||||
// RED: convertCert throws NotImplementedException (not BadRequestException) — FAIL
|
||||
await expect(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
service.convertCert({ pemText: 'garbage input not a cert', targetFormat: 'der' } as any),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PFX mit unlesbarem Zertifikats-Bag — bag.cert = null (quick-260921-iwr, D-01/D-04)
|
||||
//
|
||||
// node-forge setzt bag.cert bei einem wohlgeformten, aber nicht als X.509
|
||||
// lesbaren Zertifikats-Bag auf null (lib/pkcs12.js Zeile 703-709: der Fehler
|
||||
// aus certificateFromAsn1 wird abgefangen und durch bag.cert = null ersetzt).
|
||||
// Die drei betroffenen Zusicherungen (parseCert Zeile 207, mergeCerts Zeile
|
||||
// 469, convertCert Zeile 602) behaupten also etwas, das node-forge selbst
|
||||
// widerlegt. Alle vier Pfade antworteten schon vorher mit 400 statt 500 — die
|
||||
// hier gepruefte Aenderung ist die Meldung, nicht der Statuscode (D-02).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('PFX mit unlesbarem Zertifikats-Bag (bag.cert = null)', () => {
|
||||
let malformedPfxBuffer: Buffer;
|
||||
|
||||
beforeAll(() => {
|
||||
// Handgebaute ~83-Byte-PFX-Datei mit forge.asn1: wohlgeformte DER-Struktur
|
||||
// von aussen nach innen (PFX -> ContentInfo -> AuthenticatedSafe ->
|
||||
// ContentInfo -> SafeContents -> SafeBag -> CertBag), deren
|
||||
// Zertifikats-Bag-Inhalt aber nur "SEQUENCE { INTEGER 1 }" ist — kein
|
||||
// X.509-Zertifikat. Passwort ist die leere Zeichenkette.
|
||||
const fakeCertAsn1 = forge.asn1.create(
|
||||
forge.asn1.Class.UNIVERSAL,
|
||||
forge.asn1.Type.SEQUENCE,
|
||||
true,
|
||||
[forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.INTEGER, false, String.fromCharCode(1))],
|
||||
);
|
||||
const fakeCertDer = forge.asn1.toDer(fakeCertAsn1).getBytes();
|
||||
|
||||
const certBagAsn1 = forge.asn1.create(
|
||||
forge.asn1.Class.UNIVERSAL,
|
||||
forge.asn1.Type.SEQUENCE,
|
||||
true,
|
||||
[
|
||||
forge.asn1.create(
|
||||
forge.asn1.Class.UNIVERSAL,
|
||||
forge.asn1.Type.OID,
|
||||
false,
|
||||
forge.asn1.oidToDer(forge.pki.oids.x509Certificate).getBytes(),
|
||||
),
|
||||
forge.asn1.create(forge.asn1.Class.CONTEXT_SPECIFIC, 0, true, [
|
||||
forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.OCTETSTRING, false, fakeCertDer),
|
||||
]),
|
||||
],
|
||||
);
|
||||
|
||||
const safeBagAsn1 = forge.asn1.create(
|
||||
forge.asn1.Class.UNIVERSAL,
|
||||
forge.asn1.Type.SEQUENCE,
|
||||
true,
|
||||
[
|
||||
forge.asn1.create(
|
||||
forge.asn1.Class.UNIVERSAL,
|
||||
forge.asn1.Type.OID,
|
||||
false,
|
||||
forge.asn1.oidToDer(forge.pki.oids.certBag).getBytes(),
|
||||
),
|
||||
forge.asn1.create(forge.asn1.Class.CONTEXT_SPECIFIC, 0, true, [certBagAsn1]),
|
||||
],
|
||||
);
|
||||
|
||||
const safeContentsAsn1 = forge.asn1.create(
|
||||
forge.asn1.Class.UNIVERSAL,
|
||||
forge.asn1.Type.SEQUENCE,
|
||||
true,
|
||||
[safeBagAsn1],
|
||||
);
|
||||
const safeContentsDer = forge.asn1.toDer(safeContentsAsn1).getBytes();
|
||||
|
||||
const innerContentInfoAsn1 = forge.asn1.create(
|
||||
forge.asn1.Class.UNIVERSAL,
|
||||
forge.asn1.Type.SEQUENCE,
|
||||
true,
|
||||
[
|
||||
forge.asn1.create(
|
||||
forge.asn1.Class.UNIVERSAL,
|
||||
forge.asn1.Type.OID,
|
||||
false,
|
||||
forge.asn1.oidToDer(forge.pki.oids.data).getBytes(),
|
||||
),
|
||||
forge.asn1.create(forge.asn1.Class.CONTEXT_SPECIFIC, 0, true, [
|
||||
forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.OCTETSTRING, false, safeContentsDer),
|
||||
]),
|
||||
],
|
||||
);
|
||||
|
||||
const authSafeAsn1 = forge.asn1.create(
|
||||
forge.asn1.Class.UNIVERSAL,
|
||||
forge.asn1.Type.SEQUENCE,
|
||||
true,
|
||||
[innerContentInfoAsn1],
|
||||
);
|
||||
const authSafeDer = forge.asn1.toDer(authSafeAsn1).getBytes();
|
||||
|
||||
const outerContentInfoAsn1 = forge.asn1.create(
|
||||
forge.asn1.Class.UNIVERSAL,
|
||||
forge.asn1.Type.SEQUENCE,
|
||||
true,
|
||||
[
|
||||
forge.asn1.create(
|
||||
forge.asn1.Class.UNIVERSAL,
|
||||
forge.asn1.Type.OID,
|
||||
false,
|
||||
forge.asn1.oidToDer(forge.pki.oids.data).getBytes(),
|
||||
),
|
||||
forge.asn1.create(forge.asn1.Class.CONTEXT_SPECIFIC, 0, true, [
|
||||
forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.OCTETSTRING, false, authSafeDer),
|
||||
]),
|
||||
],
|
||||
);
|
||||
|
||||
const pfxAsn1 = forge.asn1.create(
|
||||
forge.asn1.Class.UNIVERSAL,
|
||||
forge.asn1.Type.SEQUENCE,
|
||||
true,
|
||||
[
|
||||
forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.INTEGER, false, String.fromCharCode(3)),
|
||||
outerContentInfoAsn1,
|
||||
],
|
||||
);
|
||||
|
||||
const pfxDer = forge.asn1.toDer(pfxAsn1).getBytes();
|
||||
malformedPfxBuffer = Buffer.from(pfxDer, 'binary');
|
||||
});
|
||||
|
||||
it('Vorbedingung: forge liefert genau einen Bag mit cert=null fuer diese Datei (belegt, dass der Waechter den gemeinten Fall trifft)', () => {
|
||||
expect(malformedPfxBuffer.length).toBe(83);
|
||||
|
||||
const p12Asn1 = forge.asn1.fromDer(
|
||||
forge.util.createBuffer(malformedPfxBuffer.toString('binary')),
|
||||
);
|
||||
const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, '');
|
||||
const certBags = p12.getBags({ bagType: forge.pki.oids.certBag });
|
||||
const bags = certBags[forge.pki.oids.certBag] ?? [];
|
||||
|
||||
expect(bags).toHaveLength(1);
|
||||
expect(bags[0].cert).toBeNull();
|
||||
});
|
||||
|
||||
it('parseCert: wirft 400 mit einer Meldung, die den Zertifikats-Bag benennt, statt der irrefuehrenden "Failed to extract certificate details"', async () => {
|
||||
const service = new CertManagerService();
|
||||
|
||||
await expect(
|
||||
service.parseCert({
|
||||
file: { originalname: 'bad.pfx', buffer: malformedPfxBuffer },
|
||||
password: '',
|
||||
}),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
|
||||
await expect(
|
||||
service.parseCert({
|
||||
file: { originalname: 'bad.pfx', buffer: malformedPfxBuffer },
|
||||
password: '',
|
||||
}),
|
||||
).rejects.toThrow(/certificate bag/i);
|
||||
});
|
||||
|
||||
it('mergeCerts (outputFormat pem): wirft 400 mit derselben praezisen Aussage statt "Failed to create merged certificate output"', async () => {
|
||||
const service = new CertManagerService();
|
||||
|
||||
await expect(
|
||||
service.mergeCerts({
|
||||
files: [{ originalname: 'bad.pfx', buffer: malformedPfxBuffer }],
|
||||
outputFormat: 'pem',
|
||||
password: '',
|
||||
}),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
|
||||
await expect(
|
||||
service.mergeCerts({
|
||||
files: [{ originalname: 'bad.pfx', buffer: malformedPfxBuffer }],
|
||||
outputFormat: 'pem',
|
||||
password: '',
|
||||
}),
|
||||
).rejects.toThrow(/certificate bag/i);
|
||||
});
|
||||
|
||||
it('mergeCerts (outputFormat pfx): wirft 400 mit derselben praezisen Aussage statt "Failed to create merged certificate output"', async () => {
|
||||
const service = new CertManagerService();
|
||||
|
||||
await expect(
|
||||
service.mergeCerts({
|
||||
files: [{ originalname: 'bad.pfx', buffer: malformedPfxBuffer }],
|
||||
outputFormat: 'pfx',
|
||||
password: 'secret',
|
||||
}),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
|
||||
await expect(
|
||||
service.mergeCerts({
|
||||
files: [{ originalname: 'bad.pfx', buffer: malformedPfxBuffer }],
|
||||
outputFormat: 'pfx',
|
||||
password: 'secret',
|
||||
}),
|
||||
).rejects.toThrow(/certificate bag/i);
|
||||
});
|
||||
|
||||
it('convertCert: wirft 400 mit einer Meldung, die den Zertifikats-Bag benennt, statt der irrefuehrenden "Failed to convert certificate to pem: serialization error"', async () => {
|
||||
const service = new CertManagerService();
|
||||
|
||||
await expect(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
service.convertCert({
|
||||
file: { originalname: 'bad.pfx', buffer: malformedPfxBuffer },
|
||||
password: '',
|
||||
targetFormat: 'pem',
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
|
||||
await expect(
|
||||
service.convertCert({
|
||||
file: { originalname: 'bad.pfx', buffer: malformedPfxBuffer },
|
||||
password: '',
|
||||
targetFormat: 'pem',
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any),
|
||||
).rejects.toThrow(/certificate bag/i);
|
||||
});
|
||||
|
||||
it('eine gueltige PFX-Datei verhaelt sich unveraendert: parseCert liefert weiterhin die CertDetails', async () => {
|
||||
const service = new CertManagerService();
|
||||
|
||||
const keys = forge.pki.rsa.generateKeyPair(1024);
|
||||
const cert = forge.pki.createCertificate();
|
||||
cert.publicKey = keys.publicKey;
|
||||
cert.serialNumber = '01';
|
||||
cert.validity.notBefore = new Date();
|
||||
cert.validity.notAfter = new Date();
|
||||
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
|
||||
const attrs = [{ name: 'commonName', value: 'valid-pfx.example.com' }];
|
||||
cert.setSubject(attrs);
|
||||
cert.setIssuer(attrs);
|
||||
cert.sign(keys.privateKey, forge.md.sha256.create());
|
||||
|
||||
const p12Asn1 = forge.pkcs12.toPkcs12Asn1(keys.privateKey, [cert], 'secret', {
|
||||
algorithm: '3des',
|
||||
});
|
||||
const validPfxBuffer = Buffer.from(forge.asn1.toDer(p12Asn1).getBytes(), 'binary');
|
||||
|
||||
const result = await (service.parseCert({
|
||||
file: { originalname: 'valid.pfx', buffer: validPfxBuffer },
|
||||
password: 'secret',
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
}) as Promise<any>);
|
||||
|
||||
expect(result.subject.cn).toBe('valid-pfx.example.com');
|
||||
}, 15000);
|
||||
});
|
||||
@@ -1,793 +0,0 @@
|
||||
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
||||
import * as forge from 'node-forge';
|
||||
|
||||
/**
|
||||
* Was `forge.pkcs7.messageFromPem()` bzw. `messageFromAsn1()` zurueckgeben —
|
||||
* der mitgelieferte Typ aus `@types/node-forge`, nicht ein eigener.
|
||||
*
|
||||
* Nur die signierte Form traegt `certificates`; die Lesestellen grenzen
|
||||
* deshalb mit `'certificates' in p7` ein. Das ist verhaltensgleich zum
|
||||
* bisherigen `p7.certificates ?? []`: bei einer enveloped-Nachricht fehlt
|
||||
* das Feld, und beide Schreibweisen liefern dann die leere Liste.
|
||||
*/
|
||||
type P7Message = forge.pkcs7.Captured<
|
||||
forge.pkcs7.PkcsEnvelopedData | forge.pkcs7.PkcsSignedData
|
||||
>;
|
||||
import type { UploadedFileLike } from '../auth/types/auth-user';
|
||||
|
||||
/**
|
||||
* Eine hochgeladene Zertifikatsdatei, so weit dieser Dienst sie liest:
|
||||
* Inhalt und eingereichter Name (der Name geht ausschliesslich in
|
||||
* `detectFormat` und in Fehlermeldungen). Abgeleitet aus `UploadedFileLike`
|
||||
* statt daneben erfunden (quick-260921-m34); `mimetype` und `size` bleiben
|
||||
* bewusst draussen, weil kein Zweig dieses Dienstes sie liest.
|
||||
*/
|
||||
type CertFileLike = Pick<UploadedFileLike, 'buffer' | 'originalname'>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CertDetails — the structured result returned by parseCert
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface CertDetails {
|
||||
subject: { cn: string; o: string; ou: string; c: string };
|
||||
issuer: { cn: string; o: string; c: string };
|
||||
validity: { notBefore: string; notAfter: string; isExpired: boolean; daysLeft: number };
|
||||
san: string[];
|
||||
keyType: string; // "RSA" | "EC"
|
||||
keyBits: number; // 2048, 4096, 256, ...
|
||||
serialNumber: string;
|
||||
signatureAlgorithm: string; // "sha256WithRSAEncryption", etc.
|
||||
fingerprint: { sha1: string; sha256: string };
|
||||
pemPreview: string;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SplitResponse — the structured result returned by splitCerts
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type CertRole = 'root' | 'intermediate' | 'end-entity';
|
||||
|
||||
export interface SplitEntry {
|
||||
index: number;
|
||||
filename: string;
|
||||
/** PEM content base64-encoded (one BEGIN CERTIFICATE block per entry) */
|
||||
content: string;
|
||||
subject: { cn: string };
|
||||
validity: { notAfter: string };
|
||||
certRole: CertRole;
|
||||
}
|
||||
|
||||
export interface SplitResponse {
|
||||
count: number;
|
||||
certs: SplitEntry[];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FileResponse — the structured result returned by convertCert / mergeCerts
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface FileResponse {
|
||||
/** Suggested download filename, e.g. "converted.der" */
|
||||
filename: string;
|
||||
/** Base64-encoded file content */
|
||||
content: string;
|
||||
/** MIME type for the download */
|
||||
mimeType: string;
|
||||
}
|
||||
|
||||
/** Map from target format key to MIME type */
|
||||
const FORMAT_MIME: Record<string, string> = {
|
||||
pem: 'application/x-pem-file',
|
||||
der: 'application/x-x509-ca-cert',
|
||||
p7b: 'application/x-pkcs7-certificates',
|
||||
pfx: 'application/x-pkcs12',
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Reverse OID map (OID string -> human-readable algorithm name)
|
||||
// Built once at module load — node-forge's pki.oids is name->OID
|
||||
// ---------------------------------------------------------------------------
|
||||
function buildReverseOids(): Record<string, string> {
|
||||
const result: Record<string, string> = {};
|
||||
for (const [name, oid] of Object.entries(forge.pki.oids as Record<string, string>)) {
|
||||
result[oid] = name;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
const REVERSE_OIDS = buildReverseOids();
|
||||
|
||||
/**
|
||||
* CertManagerService — server-side certificate operations.
|
||||
*
|
||||
* All cryptographic processing is ephemeral (upload → process → return).
|
||||
* No data is persisted to disk or database.
|
||||
*
|
||||
* SECURITY NOTES:
|
||||
* - Binary buffers MUST use toString('binary') for forge (never 'utf-8' — Pitfall 1)
|
||||
* - Password parameters are never passed to the logger
|
||||
* - All forge operations wrapped in try/catch → BadRequestException
|
||||
*/
|
||||
@Injectable()
|
||||
export class CertManagerService {
|
||||
private readonly logger = new Logger(CertManagerService.name);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared helpers (used by all operation methods)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Detect the format of a certificate file from extension + content sniff.
|
||||
* .cer is ambiguous — resolved by inspecting the first bytes of the buffer.
|
||||
*/
|
||||
detectFormat(
|
||||
filename: string,
|
||||
buffer: Buffer,
|
||||
): 'pem' | 'der' | 'pfx' | 'p7b' {
|
||||
const ext = filename.split('.').pop()?.toLowerCase() ?? '';
|
||||
const isPemContent = buffer.slice(0, 27).toString('ascii').includes('-----BEGIN');
|
||||
|
||||
if (ext === 'pfx' || ext === 'p12') return 'pfx';
|
||||
if (ext === 'p7b' || ext === 'p7c') return 'p7b';
|
||||
if (ext === 'der') return 'der';
|
||||
if (ext === 'pem' || ext === 'crt') return 'pem';
|
||||
if (ext === 'cer') return isPemContent ? 'pem' : 'der'; // .cer is ambiguous
|
||||
// Fallback: sniff content
|
||||
return isPemContent ? 'pem' : 'der';
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a Node.js Buffer to a forge ByteStringBuffer using 'binary' encoding.
|
||||
*
|
||||
* CRITICAL: Always use 'binary' encoding — UTF-8 corrupts DER/PFX/P7B binary data.
|
||||
* See RESEARCH.md Pitfall 1.
|
||||
*/
|
||||
toForgeBuffer(buffer: Buffer): forge.util.ByteStringBuffer {
|
||||
return forge.util.createBuffer(buffer.toString('binary'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute SHA-1 or SHA-256 fingerprint of a certificate.
|
||||
* Hash is computed over the DER-encoded bytes, returned as uppercase colon-joined hex.
|
||||
*/
|
||||
getFingerprint(cert: forge.pki.Certificate, algorithm: 'sha1' | 'sha256'): string {
|
||||
const md = algorithm === 'sha1' ? forge.md.sha1.create() : forge.md.sha256.create();
|
||||
const der = forge.asn1.toDer(forge.pki.certificateToAsn1(cert)).getBytes();
|
||||
md.update(der);
|
||||
return md.digest().toHex().match(/.{2}/g)!.join(':').toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a PEM string containing one or more concatenated certificates.
|
||||
* Returns an array of parsed forge Certificate objects.
|
||||
*/
|
||||
parsePemChain(pem: string): forge.pki.Certificate[] {
|
||||
const blocks =
|
||||
pem.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g) ?? [];
|
||||
return blocks.map((b) => forge.pki.certificateFromPem(b));
|
||||
}
|
||||
|
||||
private detectCertRole(cert: forge.pki.Certificate): CertRole {
|
||||
const bc = cert.getExtension('basicConstraints') as { cA?: boolean } | null;
|
||||
if (!bc?.cA) return 'end-entity';
|
||||
// Self-signed = subject hash matches issuer hash → Root CA
|
||||
return cert.subject.hash === cert.issuer.hash ? 'root' : 'intermediate';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// parseCert — CERT-01 + CERT-05 (read half)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Parse a certificate from PEM text or an uploaded file (PEM/DER/PFX/P7B).
|
||||
*
|
||||
* Security contract (T-09-01, T-09-02):
|
||||
* - All forge calls wrapped in try/catch → BadRequestException (never an unhandled 500)
|
||||
* - Wrong PFX password → generic 400 message (password value never logged or echoed)
|
||||
*/
|
||||
async parseCert(input: {
|
||||
file?: CertFileLike;
|
||||
pemText?: string;
|
||||
password?: string;
|
||||
}): Promise<CertDetails> {
|
||||
const { file, pemText, password } = input;
|
||||
|
||||
let cert: forge.pki.Certificate;
|
||||
|
||||
try {
|
||||
if (pemText) {
|
||||
// ── PEM text input ──────────────────────────────────────────────────
|
||||
const certs = this.parsePemChain(pemText);
|
||||
if (certs.length === 0) {
|
||||
throw new Error('No certificate block found in PEM text');
|
||||
}
|
||||
cert = certs[0];
|
||||
} else if (file) {
|
||||
const format = this.detectFormat(file.originalname, file.buffer);
|
||||
|
||||
if (format === 'pem') {
|
||||
// ── PEM file ───────────────────────────────────────────────────────
|
||||
const pemStr = file.buffer.toString('utf-8');
|
||||
const certs = this.parsePemChain(pemStr);
|
||||
if (certs.length === 0) {
|
||||
throw new Error('No certificate block found in PEM file');
|
||||
}
|
||||
cert = certs[0];
|
||||
} else if (format === 'der') {
|
||||
// ── DER binary file ────────────────────────────────────────────────
|
||||
// CRITICAL: binary encoding, never utf-8 (RESEARCH Pitfall 1)
|
||||
const asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer));
|
||||
cert = forge.pki.certificateFromAsn1(asn1);
|
||||
} else if (format === 'pfx') {
|
||||
// ── PFX/PKCS12 file ───────────────────────────────────────────────
|
||||
// wrong password → forge throws → caught below → BadRequestException (T-09-02)
|
||||
const p12Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer));
|
||||
const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, password ?? '');
|
||||
const certBags = p12.getBags({ bagType: forge.pki.oids.certBag });
|
||||
const bags = certBags[forge.pki.oids.certBag] ?? [];
|
||||
if (bags.length === 0) {
|
||||
throw new Error('No certificate bag found in PFX/PKCS12');
|
||||
}
|
||||
// node-forge sets bag.cert to null when the bag's content parses as
|
||||
// valid DER but is not a readable X.509 certificate (lib/pkcs12.js
|
||||
// certBag decoder). An explicit guard here — not an assertion — so
|
||||
// the 400 names the real cause (quick-260921-iwr, D-01/D-04).
|
||||
const parsedCert = bags[0].cert;
|
||||
if (!parsedCert) {
|
||||
throw new BadRequestException(
|
||||
'Certificate bag in PFX/PKCS12 does not contain a readable X.509 certificate',
|
||||
);
|
||||
}
|
||||
cert = parsedCert;
|
||||
} else {
|
||||
// ── P7B/PKCS7 file — PEM-wrapped or binary DER (Pitfall 4) ────────
|
||||
const isPemP7b = file.buffer
|
||||
.slice(0, 27)
|
||||
.toString('ascii')
|
||||
.includes('-----BEGIN');
|
||||
// siehe P7Message oben — mitgelieferter Typ, keine Behauptung.
|
||||
let p7: P7Message;
|
||||
if (isPemP7b) {
|
||||
p7 = forge.pkcs7.messageFromPem(file.buffer.toString('utf-8'));
|
||||
} else {
|
||||
const p7Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer));
|
||||
p7 = forge.pkcs7.messageFromAsn1(p7Asn1);
|
||||
}
|
||||
const p7Certs: forge.pki.Certificate[] =
|
||||
'certificates' in p7 ? p7.certificates : [];
|
||||
if (p7Certs.length === 0) {
|
||||
throw new Error('No certificate found in P7B/PKCS7');
|
||||
}
|
||||
cert = p7Certs[0];
|
||||
}
|
||||
} else {
|
||||
// Neither file nor pemText — controller should have rejected this already,
|
||||
// but guard here too (BadRequestException is NOT caught by the outer try/catch below)
|
||||
throw new BadRequestException('No file or PEM text provided');
|
||||
}
|
||||
} catch (err) {
|
||||
// Re-throw BadRequestException as-is; convert everything else to 400
|
||||
if (err instanceof BadRequestException) throw err;
|
||||
// Do NOT log the password (T-09-02)
|
||||
this.logger.warn('parseCert: failed to parse certificate (format/password error)');
|
||||
throw new BadRequestException(
|
||||
'Failed to parse certificate: invalid format or wrong password',
|
||||
);
|
||||
}
|
||||
|
||||
// ── Build CertDetails ──────────────────────────────────────────────────
|
||||
try {
|
||||
const notBefore = cert.validity.notBefore;
|
||||
const notAfter = cert.validity.notAfter;
|
||||
const now = new Date();
|
||||
const isExpired = notAfter < now;
|
||||
const daysLeft = Math.ceil(
|
||||
(notAfter.getTime() - now.getTime()) / (1000 * 60 * 60 * 24),
|
||||
);
|
||||
|
||||
// Key type and size
|
||||
// BLEIBT als any, mit Begruendung (260921-m34, Aufgabe 3c, D-01/D-02):
|
||||
// @types/node-forge kennt nur `PublicKey = rsa.PublicKey | ed25519.Key`
|
||||
// (index.d.ts:232). Der EC-Zweig unten liest `curve` und
|
||||
// `params.curve.q.bitLength()` — Felder, die node-forge zur Laufzeit
|
||||
// liefert, die der mitgelieferte Typ aber GAR NICHT kennt. Eine
|
||||
// Umdeutung ueber zwei Stufen wuerde dieselbe Luecke verdecken und
|
||||
// zusaetzlich so aussehen, als sei sie geprueft. Ein ehrliches any mit
|
||||
// dieser Zeile ist hier das bessere Ergebnis.
|
||||
const pubKey = cert.publicKey as any;
|
||||
let keyType = 'RSA';
|
||||
let keyBits = 0;
|
||||
if (pubKey.n) {
|
||||
keyType = 'RSA';
|
||||
keyBits = pubKey.n.bitLength();
|
||||
} else if (pubKey.curve) {
|
||||
keyType = 'EC';
|
||||
// EC key size from curve params — estimate from key length
|
||||
keyBits = pubKey.params?.curve?.q?.bitLength() ?? 0;
|
||||
}
|
||||
|
||||
// Subject Alternative Names
|
||||
//
|
||||
// BLEIBEN als any, mit Begruendung (260921-m34, Aufgabe 3c, D-01/D-02):
|
||||
// @types/node-forge deklariert `Certificate.extensions` als `any[]`
|
||||
// (index.d.ts:435) und sagt damit ueber den Inhalt einer Erweiterung
|
||||
// NICHTS aus. Jede Schnittstelle, die wir hier selbst fuer `altNames`
|
||||
// schrieben, waere unbelegt — der Compiler koennte sie an keiner
|
||||
// Stelle gegen etwas pruefen, sie saehe aber geprueft aus. Die drei
|
||||
// any-Stellen dieses Blocks bleiben deshalb sichtbar stehen, statt
|
||||
// gegen eine Behauptung getauscht zu werden.
|
||||
const sanExt = cert.extensions?.find((e: any) => e.name === 'subjectAltName');
|
||||
const san: string[] = ((sanExt as any)?.altNames ?? []).map((n: any) =>
|
||||
n.type === 2 ? (n.value as string) : `IP:${(n.ip ?? n.value) as string}`,
|
||||
);
|
||||
|
||||
// Signature algorithm — OID → human-readable name
|
||||
// @types/node-forge deklariert siginfo.algorithmOid als string — die
|
||||
// Zusicherung war ueberfluessig. Das ?. bleibt woertlich erhalten.
|
||||
const sigOid = cert.siginfo?.algorithmOid ?? '';
|
||||
const signatureAlgorithm = REVERSE_OIDS[sigOid] ?? sigOid;
|
||||
|
||||
// Fingerprints
|
||||
const sha1 = this.getFingerprint(cert, 'sha1');
|
||||
const sha256 = this.getFingerprint(cert, 'sha256');
|
||||
|
||||
return {
|
||||
subject: {
|
||||
cn: cert.subject.getField('CN')?.value ?? '',
|
||||
o: cert.subject.getField('O')?.value ?? '',
|
||||
ou: cert.subject.getField('OU')?.value ?? '',
|
||||
c: cert.subject.getField('C')?.value ?? '',
|
||||
},
|
||||
issuer: {
|
||||
cn: cert.issuer.getField('CN')?.value ?? '',
|
||||
o: cert.issuer.getField('O')?.value ?? '',
|
||||
c: cert.issuer.getField('C')?.value ?? '',
|
||||
},
|
||||
validity: {
|
||||
notBefore: notBefore.toISOString(),
|
||||
notAfter: notAfter.toISOString(),
|
||||
isExpired,
|
||||
daysLeft,
|
||||
},
|
||||
san,
|
||||
keyType,
|
||||
keyBits,
|
||||
serialNumber: cert.serialNumber,
|
||||
signatureAlgorithm,
|
||||
fingerprint: { sha1, sha256 },
|
||||
pemPreview: forge.pki.certificateToPem(cert),
|
||||
};
|
||||
} catch {
|
||||
this.logger.warn('parseCert: failed to extract CertDetails fields');
|
||||
throw new BadRequestException('Failed to extract certificate details');
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Remaining operation stubs (implemented in later plan slices)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Split a fullchain PEM or P7B/PKCS7 bundle into individual certificates.
|
||||
*
|
||||
* Security contract (T-09-01):
|
||||
* - All forge calls wrapped in try/catch → BadRequestException on malformed input
|
||||
*
|
||||
* Security contract (T-09-03):
|
||||
* - File size limit 5 MB enforced by FileInterceptor in the controller
|
||||
*/
|
||||
async splitCerts(input: {
|
||||
file?: CertFileLike;
|
||||
password?: string;
|
||||
}): Promise<SplitResponse> {
|
||||
const { file } = input;
|
||||
|
||||
if (!file) {
|
||||
throw new BadRequestException('No file provided');
|
||||
}
|
||||
|
||||
let certs: forge.pki.Certificate[];
|
||||
|
||||
try {
|
||||
const format = this.detectFormat(file.originalname, file.buffer);
|
||||
|
||||
if (format === 'pem') {
|
||||
// ── PEM chain (fullchain.pem, .crt — both map to 'pem' in detectFormat) ─
|
||||
const pemStr = file.buffer.toString('utf-8');
|
||||
certs = this.parsePemChain(pemStr);
|
||||
if (certs.length === 0) {
|
||||
throw new Error('No certificate blocks found in PEM file');
|
||||
}
|
||||
} else if (format === 'p7b') {
|
||||
// ── P7B/PKCS7 bundle — PEM-wrapped or binary DER (Pitfall 4) ─────────
|
||||
const isPemP7b = file.buffer
|
||||
.slice(0, 27)
|
||||
.toString('ascii')
|
||||
.includes('-----BEGIN');
|
||||
// Der mitgelieferte Typ traegt hier: messageFromPem/messageFromAsn1
|
||||
// liefern beide Captured<PkcsEnvelopedData | PkcsSignedData>.
|
||||
let p7: P7Message;
|
||||
if (isPemP7b) {
|
||||
// PEM-wrapped PKCS7 (e.g. -----BEGIN PKCS7-----)
|
||||
p7 = forge.pkcs7.messageFromPem(file.buffer.toString('utf-8'));
|
||||
} else {
|
||||
// Binary DER PKCS7
|
||||
const p7Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer));
|
||||
p7 = forge.pkcs7.messageFromAsn1(p7Asn1);
|
||||
}
|
||||
certs = 'certificates' in p7 ? p7.certificates : [];
|
||||
if (certs.length === 0) {
|
||||
throw new Error('No certificates found in P7B/PKCS7 bundle');
|
||||
}
|
||||
} else {
|
||||
// DER / PFX — not a valid chain/bundle format for splitting
|
||||
throw new BadRequestException(
|
||||
'Only PEM chains (.pem, .crt) and P7B bundles (.p7b) can be split',
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof BadRequestException) throw err;
|
||||
this.logger.warn('splitCerts: failed to parse bundle');
|
||||
throw new BadRequestException('Failed to split certificates: invalid format or corrupted file');
|
||||
}
|
||||
|
||||
// ── Determine cert roles ───────────────────────────────────────────────
|
||||
const roles: CertRole[] = certs.map((cert) => this.detectCertRole(cert));
|
||||
|
||||
// Build counters for filename disambiguation
|
||||
const roleCounters: Record<CertRole, number> = { root: 0, intermediate: 0, 'end-entity': 0 };
|
||||
const roleFilename = (role: CertRole): string => {
|
||||
roleCounters[role]++;
|
||||
const n = roleCounters[role];
|
||||
if (role === 'root') return n === 1 ? 'root-ca.pem' : `root-ca-${n}.pem`;
|
||||
if (role === 'intermediate') return `intermediate-${n}.pem`;
|
||||
return n === 1 ? 'cert.pem' : `cert-${n}.pem`;
|
||||
};
|
||||
|
||||
// ── Build SplitResponse ────────────────────────────────────────────────
|
||||
const certEntries: SplitEntry[] = certs.map((cert, index) => {
|
||||
const pemStr = forge.pki.certificateToPem(cert);
|
||||
const content = Buffer.from(pemStr, 'utf-8').toString('base64');
|
||||
const cn: string = cert.subject.getField('CN')?.value ?? '';
|
||||
const notAfter: string = cert.validity.notAfter.toISOString();
|
||||
const certRole = roles[index];
|
||||
|
||||
return {
|
||||
index,
|
||||
filename: roleFilename(certRole),
|
||||
content,
|
||||
subject: { cn },
|
||||
validity: { notAfter },
|
||||
certRole,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
count: certEntries.length,
|
||||
certs: certEntries,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge multiple certificate files into a PEM chain or a password-protected PFX/PKCS12 bundle.
|
||||
*
|
||||
* Security contract (T-09-01, T-09-02, T-09-03):
|
||||
* - All forge calls wrapped in try/catch → BadRequestException on malformed input
|
||||
* - Password required for PFX output; never logged or echoed
|
||||
* - File size limit enforced by FilesInterceptor (controller level)
|
||||
*
|
||||
* Open Question 1 resolution: `forge.pkcs12.toPkcs12Asn1(null, certs, password)` was tested
|
||||
* at implementation time — node-forge 1.4.0 accepts null as the private key for cert-only PFX.
|
||||
* No fallback to lower-level certBag construction was needed.
|
||||
*/
|
||||
async mergeCerts(input: {
|
||||
files?: CertFileLike[];
|
||||
outputFormat: string;
|
||||
password?: string;
|
||||
}): Promise<FileResponse> {
|
||||
const { files, outputFormat, password } = input;
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
throw new BadRequestException('No files provided');
|
||||
}
|
||||
|
||||
// PFX output requires a non-empty password (T-09-02)
|
||||
if (outputFormat === 'pfx' && (!password || password.trim() === '')) {
|
||||
throw new BadRequestException('A password is required for PFX output');
|
||||
}
|
||||
|
||||
// ── Parse all input files to forge Certificate objects ────────────────────
|
||||
let certs: forge.pki.Certificate[];
|
||||
|
||||
try {
|
||||
certs = files.flatMap((file) => {
|
||||
const format = this.detectFormat(file.originalname, file.buffer);
|
||||
|
||||
if (format === 'pem') {
|
||||
const pemStr = file.buffer.toString('utf-8');
|
||||
const parsed = this.parsePemChain(pemStr);
|
||||
if (parsed.length === 0) {
|
||||
throw new Error(`No certificate block found in ${file.originalname}`);
|
||||
}
|
||||
return parsed;
|
||||
} else if (format === 'der') {
|
||||
// CRITICAL: binary encoding, never utf-8 (RESEARCH Pitfall 1)
|
||||
const asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer));
|
||||
return [forge.pki.certificateFromAsn1(asn1)];
|
||||
} else if (format === 'pfx') {
|
||||
// Extract all certs from the PFX bag
|
||||
const p12Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer));
|
||||
const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, password ?? '');
|
||||
const certBags = p12.getBags({ bagType: forge.pki.oids.certBag });
|
||||
const bags = certBags[forge.pki.oids.certBag] ?? [];
|
||||
// node-forge sets bag.cert to null when a bag's content parses as
|
||||
// valid DER but is not a readable X.509 certificate (lib/pkcs12.js
|
||||
// certBag decoder). No entry may be silently dropped (D-04) — check
|
||||
// every bag and reject the whole file, naming it, before returning.
|
||||
const bagCerts = bags.map((bag) => bag.cert);
|
||||
// @types/node-forge declares Bag.cert as `Certificate | undefined`,
|
||||
// but node-forge's own runtime sets it to `null` for an unreadable
|
||||
// bag (lib/pkcs12.js certBag decoder) — check both, not just `===
|
||||
// undefined`, so the guard actually catches what the library does.
|
||||
const missingCertIndex = bagCerts.findIndex((c) => c === undefined || c === null);
|
||||
if (missingCertIndex !== -1) {
|
||||
throw new BadRequestException(
|
||||
`Certificate bag in "${file.originalname}" does not contain a readable X.509 certificate`,
|
||||
);
|
||||
}
|
||||
return bagCerts.filter((c): c is forge.pki.Certificate => c !== undefined && c !== null);
|
||||
} else {
|
||||
// P7B/PKCS7 — PEM-wrapped or binary DER (Pitfall 4)
|
||||
const isPemP7b = file.buffer
|
||||
.slice(0, 27)
|
||||
.toString('ascii')
|
||||
.includes('-----BEGIN');
|
||||
// siehe P7Message oben — mitgelieferter Typ, keine Behauptung.
|
||||
let p7: P7Message;
|
||||
if (isPemP7b) {
|
||||
p7 = forge.pkcs7.messageFromPem(file.buffer.toString('utf-8'));
|
||||
} else {
|
||||
const p7Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer));
|
||||
p7 = forge.pkcs7.messageFromAsn1(p7Asn1);
|
||||
}
|
||||
return 'certificates' in p7 ? p7.certificates : [];
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof BadRequestException) throw err;
|
||||
// Password never logged (T-09-02)
|
||||
this.logger.warn('mergeCerts: failed to parse one or more input files');
|
||||
throw new BadRequestException(
|
||||
'Failed to parse certificate files: invalid format or corrupted input',
|
||||
);
|
||||
}
|
||||
|
||||
if (certs.length === 0) {
|
||||
throw new BadRequestException('No valid certificates found in uploaded files');
|
||||
}
|
||||
|
||||
// ── Serialize to requested output format ──────────────────────────────────
|
||||
try {
|
||||
if (outputFormat === 'pem') {
|
||||
// PEM chain: concatenate all certs
|
||||
const chain = certs.map((cert) => forge.pki.certificateToPem(cert)).join('\n');
|
||||
const content = Buffer.from(chain, 'utf-8').toString('base64');
|
||||
return {
|
||||
filename: 'chain.pem',
|
||||
content,
|
||||
mimeType: FORMAT_MIME.pem,
|
||||
};
|
||||
} else if (outputFormat === 'pfx') {
|
||||
// Open Question 1 resolution: toPkcs12Asn1(null, certs, password) works in node-forge 1.4.0
|
||||
// null as the private key produces a cert-only PKCS12 bundle (no key bag — cert bag only)
|
||||
const p12Asn1 = forge.pkcs12.toPkcs12Asn1(
|
||||
// BLEIBT (260921-m34, Aufgabe 3c): node-forge 1.4.0 nimmt hier einen
|
||||
// fehlenden Schluessel an und erzeugt ein reines
|
||||
// Zertifikatsbuendel; @types/node-forge schliesst null aus. Die
|
||||
// mitgelieferten Typen beschreiben die Bibliothek an dieser Stelle
|
||||
// also nachweislich falsch — ein erzwungener Typ waere eine
|
||||
// Behauptung ueber etwas, das nicht stimmt.
|
||||
null as any, // cert-only PFX — null key accepted by node-forge 1.4.0
|
||||
certs,
|
||||
password!,
|
||||
{ algorithm: '3des' },
|
||||
);
|
||||
// CRITICAL: bytesToHex → Buffer.from(hex,'hex') — avoids utf-8 corruption (Pitfall 1)
|
||||
const p12Hex = forge.util.bytesToHex(forge.asn1.toDer(p12Asn1).getBytes());
|
||||
const pfxBuffer = Buffer.from(p12Hex, 'hex');
|
||||
const content = pfxBuffer.toString('base64');
|
||||
return {
|
||||
filename: 'bundle.pfx',
|
||||
content,
|
||||
mimeType: FORMAT_MIME.pfx,
|
||||
};
|
||||
} else {
|
||||
throw new BadRequestException(
|
||||
`Unsupported output format: "${outputFormat}". Supported: pem, pfx`,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof BadRequestException) throw err;
|
||||
this.logger.warn('mergeCerts: failed to serialize merged output');
|
||||
throw new BadRequestException('Failed to create merged certificate output');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a certificate between PEM, DER, and P7B formats.
|
||||
*
|
||||
* Security contract (T-09-01, T-09-06):
|
||||
* - All forge calls wrapped in try/catch → BadRequestException on malformed input
|
||||
* - DER output built via bytesToHex → Buffer.from(hex, 'hex') → base64 (never utf-8 round-trip)
|
||||
* - Password is never passed to the logger (T-09-02)
|
||||
*/
|
||||
async convertCert(input: {
|
||||
file?: CertFileLike;
|
||||
pemText?: string;
|
||||
targetFormat: string;
|
||||
password?: string;
|
||||
}): Promise<FileResponse> {
|
||||
const { file, pemText, targetFormat, password } = input;
|
||||
|
||||
// ── Validate targetFormat ──────────────────────────────────────────────
|
||||
if (!FORMAT_MIME[targetFormat]) {
|
||||
throw new BadRequestException(
|
||||
`Unsupported target format: "${targetFormat}". Supported: pem, der, p7b, pfx`,
|
||||
);
|
||||
}
|
||||
|
||||
// PFX output requires a non-empty password (T-09-02)
|
||||
if (targetFormat === 'pfx' && (!password || password.trim() === '')) {
|
||||
throw new BadRequestException('A password is required for PFX output');
|
||||
}
|
||||
|
||||
let cert: forge.pki.Certificate;
|
||||
|
||||
try {
|
||||
// ── Resolve input to a forge Certificate ────────────────────────────
|
||||
if (pemText) {
|
||||
// PEM text pasted by the user
|
||||
const certs = this.parsePemChain(pemText);
|
||||
if (certs.length === 0) {
|
||||
throw new Error('No certificate block found in PEM text');
|
||||
}
|
||||
cert = certs[0];
|
||||
} else if (file) {
|
||||
const format = this.detectFormat(file.originalname, file.buffer);
|
||||
|
||||
if (format === 'pem') {
|
||||
const pemStr = file.buffer.toString('utf-8');
|
||||
const certs = this.parsePemChain(pemStr);
|
||||
if (certs.length === 0) {
|
||||
throw new Error('No certificate block found in PEM file');
|
||||
}
|
||||
cert = certs[0];
|
||||
} else if (format === 'der') {
|
||||
// CRITICAL: binary encoding, never utf-8 (RESEARCH Pitfall 1)
|
||||
const asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer));
|
||||
cert = forge.pki.certificateFromAsn1(asn1);
|
||||
} else if (format === 'pfx') {
|
||||
// PFX/PKCS12 — extract first cert bag (wrong password → BadRequestException)
|
||||
const p12Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer));
|
||||
const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, password ?? '');
|
||||
const certBags = p12.getBags({ bagType: forge.pki.oids.certBag });
|
||||
const bags = certBags[forge.pki.oids.certBag] ?? [];
|
||||
if (bags.length === 0) {
|
||||
throw new Error('No certificate bag found in PFX/PKCS12');
|
||||
}
|
||||
// node-forge sets bag.cert to null when the bag's content parses as
|
||||
// valid DER but is not a readable X.509 certificate (lib/pkcs12.js
|
||||
// certBag decoder). An explicit guard here — not an assertion — so
|
||||
// the 400 names the real cause (quick-260921-iwr, D-01/D-04).
|
||||
const parsedCert = bags[0].cert;
|
||||
if (!parsedCert) {
|
||||
throw new BadRequestException(
|
||||
'Certificate bag in PFX/PKCS12 does not contain a readable X.509 certificate',
|
||||
);
|
||||
}
|
||||
cert = parsedCert;
|
||||
} else {
|
||||
// P7B — extract first cert
|
||||
const isPemP7b = file.buffer
|
||||
.slice(0, 27)
|
||||
.toString('ascii')
|
||||
.includes('-----BEGIN');
|
||||
// siehe P7Message oben — mitgelieferter Typ, keine Behauptung.
|
||||
let p7: P7Message;
|
||||
if (isPemP7b) {
|
||||
p7 = forge.pkcs7.messageFromPem(file.buffer.toString('utf-8'));
|
||||
} else {
|
||||
const p7Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer));
|
||||
p7 = forge.pkcs7.messageFromAsn1(p7Asn1);
|
||||
}
|
||||
const p7Certs: forge.pki.Certificate[] =
|
||||
'certificates' in p7 ? p7.certificates : [];
|
||||
if (p7Certs.length === 0) {
|
||||
throw new Error('No certificate found in P7B/PKCS7');
|
||||
}
|
||||
cert = p7Certs[0];
|
||||
}
|
||||
} else {
|
||||
throw new BadRequestException('No file or PEM text provided');
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof BadRequestException) throw err;
|
||||
// Password never logged (T-09-02)
|
||||
this.logger.warn('convertCert: failed to parse input certificate');
|
||||
throw new BadRequestException(
|
||||
'Failed to parse certificate: invalid format or wrong password',
|
||||
);
|
||||
}
|
||||
|
||||
// ── Serialize to targetFormat ─────────────────────────────────────────
|
||||
try {
|
||||
let content: string;
|
||||
|
||||
if (targetFormat === 'pem') {
|
||||
// PEM text → base64 via utf-8
|
||||
const pemOut = forge.pki.certificateToPem(cert);
|
||||
content = Buffer.from(pemOut, 'utf-8').toString('base64');
|
||||
} else if (targetFormat === 'der') {
|
||||
// DER binary — CRITICAL: bytesToHex → Buffer.from(hex, 'hex') → base64
|
||||
// Avoids utf-8 round-trip corruption (RESEARCH Pitfall 1 / T-09-06)
|
||||
const derHex = forge.util.bytesToHex(
|
||||
forge.asn1.toDer(forge.pki.certificateToAsn1(cert)).getBytes(),
|
||||
);
|
||||
content = Buffer.from(derHex, 'hex').toString('base64');
|
||||
} else if (targetFormat === 'p7b') {
|
||||
// P7B — PEM-wrapped PKCS7 SignedData containing the certificate
|
||||
const p7 = forge.pkcs7.createSignedData();
|
||||
p7.addCertificate(cert);
|
||||
const p7DerBytes = forge.asn1.toDer(p7.toAsn1()).getBytes();
|
||||
const p7PemStr = forge.pem.encode({ type: 'PKCS7', body: p7DerBytes });
|
||||
content = Buffer.from(p7PemStr, 'utf-8').toString('base64');
|
||||
} else {
|
||||
// PFX — cert-only PKCS12 bundle (Open Question 1: null key works in node-forge 1.4.0)
|
||||
const p12Asn1 = forge.pkcs12.toPkcs12Asn1(
|
||||
// BLEIBT (260921-m34, Aufgabe 3c): node-forge 1.4.0 nimmt hier einen
|
||||
// fehlenden Schluessel an und erzeugt ein reines
|
||||
// Zertifikatsbuendel; @types/node-forge schliesst null aus. Die
|
||||
// mitgelieferten Typen beschreiben die Bibliothek an dieser Stelle
|
||||
// also nachweislich falsch — ein erzwungener Typ waere eine
|
||||
// Behauptung ueber etwas, das nicht stimmt.
|
||||
null as any, // cert-only PFX — null key accepted by node-forge 1.4.0
|
||||
[cert],
|
||||
password!,
|
||||
{ algorithm: '3des' },
|
||||
);
|
||||
// CRITICAL: bytesToHex → Buffer.from(hex,'hex') — avoids utf-8 corruption (Pitfall 1)
|
||||
const p12Hex = forge.util.bytesToHex(forge.asn1.toDer(p12Asn1).getBytes());
|
||||
content = Buffer.from(p12Hex, 'hex').toString('base64');
|
||||
return {
|
||||
filename: 'converted.pfx',
|
||||
content,
|
||||
mimeType: FORMAT_MIME.pfx,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
filename: `converted.${targetFormat}`,
|
||||
content,
|
||||
mimeType: FORMAT_MIME[targetFormat],
|
||||
};
|
||||
} catch {
|
||||
this.logger.warn('convertCert: failed to serialize to target format');
|
||||
throw new BadRequestException(
|
||||
`Failed to convert certificate to ${targetFormat}: serialization error`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal helpers for later slices
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Wrap a node-forge operation and re-throw as BadRequestException on failure */
|
||||
protected _parseOrThrow<T>(fn: () => T, errorMsg: string): T {
|
||||
try {
|
||||
return fn();
|
||||
} catch (_err) {
|
||||
this.logger.warn(`Cert parse failed: ${errorMsg}`);
|
||||
throw new BadRequestException(errorMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import { X509Certificate } from 'node:crypto';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { certItemFromDer, detectBlob } from './cert-model';
|
||||
import type { CertItem } from './cert-types';
|
||||
|
||||
const fx = (name: string) => readFileSync(join(__dirname, '__fixtures__', name));
|
||||
const ctx = (path: string) => ({ file: 0, path, passwords: [] as string[] });
|
||||
|
||||
function certs(name: string): CertItem[] {
|
||||
const r = detectBlob(fx(name), ctx(name));
|
||||
return r.items.filter((i): i is CertItem => i.kind === 'certificate');
|
||||
}
|
||||
|
||||
describe('detectBlob: Zertifikate', () => {
|
||||
it('RSA-Serverzertifikat: alle Felder aus node:crypto', () => {
|
||||
const [c] = certs('rsa-leaf.pem');
|
||||
const x = new X509Certificate(fx('rsa-leaf.pem'));
|
||||
expect(c.cn).toBe('www.example.test');
|
||||
expect(c.san).toEqual(['www.example.test', 'example.test']);
|
||||
expect(c.issuerCn).toBe('Tessera Test Inter RSA');
|
||||
expect(c.role).toBe('end-entity');
|
||||
expect(c.keyType).toBe('RSA');
|
||||
expect(c.keyBits).toBe(2048);
|
||||
expect(c.curve).toBeNull();
|
||||
expect(c.isCa).toBe(false);
|
||||
expect(c.selfSigned).toBe(false);
|
||||
expect(c.aiaIssuerUrls).toEqual(['http://pki.example.test/rsa-inter.cer']);
|
||||
expect(c.sha256).toBe(x.fingerprint256);
|
||||
expect(c.sha1).toBe(x.fingerprint);
|
||||
expect(c.id).toBe(`c-${x.fingerprint256.replace(/:/g, '').slice(0, 16).toLowerCase()}`);
|
||||
expect(c.sources).toEqual([{ file: 0, path: 'rsa-leaf.pem' }]);
|
||||
expect(c.pem.startsWith('-----BEGIN CERTIFICATE-----')).toBe(true);
|
||||
expect(c.baseName).toBe('www.example.test');
|
||||
expect(c.keyId).toMatch(/^k-[0-9a-f]{16}$/);
|
||||
expect(c.isExpired).toBe(false);
|
||||
expect(c.daysLeft).toBeGreaterThan(30000);
|
||||
});
|
||||
|
||||
it('Zwischenzertifikat und Stammzertifikat bekommen ihre Rolle', () => {
|
||||
expect(certs('rsa-inter.pem')[0].role).toBe('intermediate');
|
||||
expect(certs('rsa-inter.pem')[0].baseName).toBe('Tessera_Test_Inter_RSA');
|
||||
const root = certs('rsa-root.pem')[0];
|
||||
expect(root.role).toBe('root');
|
||||
expect(root.selfSigned).toBe(true);
|
||||
expect(root.isCa).toBe(true);
|
||||
});
|
||||
|
||||
it('EC-Zertifikate: Schluesseltyp und Kurve', () => {
|
||||
const leaf = certs('ec-leaf.pem')[0];
|
||||
expect(leaf.keyType).toBe('EC');
|
||||
expect(leaf.curve).toBe('P-256');
|
||||
expect(leaf.keyBits).toBe(256);
|
||||
const root = certs('ec-root.pem')[0];
|
||||
expect(root.curve).toBe('P-384');
|
||||
expect(root.keyBits).toBe(384);
|
||||
expect(root.isCa).toBe(true);
|
||||
expect(root.selfSigned).toBe(true);
|
||||
expect(root.role).toBe('root');
|
||||
});
|
||||
|
||||
it('selbstsigniert, aber keine CA: bleibt Serverzertifikat', () => {
|
||||
const c = certs('selfsigned-leaf.pem')[0];
|
||||
expect(c.selfSigned).toBe(true);
|
||||
expect(c.isCa).toBe(false);
|
||||
expect(c.role).toBe('end-entity');
|
||||
});
|
||||
|
||||
it('abgelaufenes Zwischenzertifikat wird als abgelaufen gemeldet', () => {
|
||||
const c = certs('rsa-inter-expired.pem')[0];
|
||||
expect(c.isExpired).toBe(true);
|
||||
expect(c.daysLeft).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it('DER ergibt dieselbe Kennung wie PEM', () => {
|
||||
expect(certs('ec-leaf.cer')[0].id).toBe(certs('ec-leaf.pem')[0].id);
|
||||
expect(certs('rsa-leaf.cer')[0].id).toBe(certs('rsa-leaf.pem')[0].id);
|
||||
});
|
||||
|
||||
it('Fullchain mit BOM, CRLF und Text drumherum: drei Zertifikate', () => {
|
||||
const body = fx('ec-fullchain.pem').toString('utf8').replace(/\n/g, '\r\n');
|
||||
const messy = Buffer.concat([
|
||||
Buffer.from([0xef, 0xbb, 0xbf]),
|
||||
Buffer.from(`Bag Attributes\r\n friendlyName: x\r\n${body}\r\nEnde der Datei\r\n`, 'utf8'),
|
||||
]);
|
||||
const r = detectBlob(messy, ctx('messy.pem'));
|
||||
expect(r.items).toHaveLength(3);
|
||||
expect(r.ignored).toEqual([]);
|
||||
expect(r.items.map((i) => (i as CertItem).role).sort()).toEqual([
|
||||
'end-entity',
|
||||
'intermediate',
|
||||
'root',
|
||||
]);
|
||||
});
|
||||
|
||||
it('TRUSTED CERTIFICATE liefert das Zertifikat', () => {
|
||||
const [c] = certs('rsa-trusted.pem');
|
||||
expect(c.id).toBe(certs('rsa-leaf.pem')[0].id);
|
||||
});
|
||||
|
||||
it('kaputte Eingaben werden gemeldet, ohne zu werfen', () => {
|
||||
const half = fx('rsa-leaf.cer').subarray(0, 300);
|
||||
const brokenBase64 = Buffer.from(
|
||||
'-----BEGIN CERTIFICATE-----\nMIIDzTCC@@@@@@@@@!!!\n-----END CERTIFICATE-----\n',
|
||||
);
|
||||
const random = Buffer.from(Array.from({ length: 512 }, (_, i) => (i * 37 + 11) % 256));
|
||||
for (const blob of [random, Buffer.alloc(0), half, brokenBase64, Buffer.from('hallo welt')]) {
|
||||
const r = detectBlob(blob, ctx('x.bin'));
|
||||
expect(r.items).toEqual([]);
|
||||
expect(r.ignored).toEqual([{ file: 0, path: 'x.bin', reason: 'unknown' }]);
|
||||
}
|
||||
});
|
||||
|
||||
it('ein kaputter Block neben einem guten verhindert das Zertifikat nicht', () => {
|
||||
const mixed = Buffer.concat([
|
||||
Buffer.from('-----BEGIN CERTIFICATE-----\n@@@@\n-----END CERTIFICATE-----\n'),
|
||||
fx('rsa-leaf.pem'),
|
||||
]);
|
||||
const r = detectBlob(mixed, ctx('mixed.pem'));
|
||||
expect(r.items).toHaveLength(1);
|
||||
expect(r.ignored).toEqual([]);
|
||||
});
|
||||
|
||||
it('certItemFromDer erzeugt Kennung und Quelle', () => {
|
||||
const der = fx('rsa-leaf.cer');
|
||||
const item = certItemFromDer(der, { file: 3, path: 'a/b.cer' });
|
||||
expect(item.sources).toEqual([{ file: 3, path: 'a/b.cer' }]);
|
||||
expect(item.id).toMatch(/^c-[0-9a-f]{16}$/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,324 @@
|
||||
import { createHash, type KeyObject, X509Certificate } from 'node:crypto';
|
||||
import { safeBaseName } from './cert-output';
|
||||
import type {
|
||||
AnyItem,
|
||||
CertItem,
|
||||
CertRole,
|
||||
IgnoredEntry,
|
||||
ItemSource,
|
||||
LockedEntry,
|
||||
} from './cert-types';
|
||||
|
||||
/**
|
||||
* Der eine Parser des Zertifikat-Managers (quick-261009-ikt, D-08, D-16).
|
||||
*
|
||||
* Alles ueber Zertifikate und Schluessel entscheidet node:crypto (RSA und EC, PEM und DER).
|
||||
* node-forge wird nur fuer PKCS#12 und als allgemeiner ASN.1-Leser/-Schreiber genutzt; die
|
||||
* Zertifikat-, CSR- und PKCS#7-Parser von forge sind nur fuer RSA gebaut und kommen hier nie vor.
|
||||
*
|
||||
* Erkennung nach Inhalt, nie nach Dateiendung. Jede Stufe steht in try/catch: eine kaputte
|
||||
* Datei ergibt hoechstens einen Eintrag „unbekannt“, nie einen Fehler fuer die ganze Anfrage.
|
||||
* Stand Task 1: Zertifikate als PEM (auch TRUSTED CERTIFICATE) und als DER. ZIP und PKCS#7
|
||||
* (Task 3) sowie Schluessel, PKCS#12 und CSR (Task 4) sind benannte, noch leere Stufen.
|
||||
*/
|
||||
|
||||
export interface DetectContext {
|
||||
/** Index der hochgeladenen Datei in Anfrage-Reihenfolge */
|
||||
file: number;
|
||||
/** Dateiname, bei ZIP-Inhalt "zip/eintrag" */
|
||||
path: string;
|
||||
/** Passwoerter fuer geschuetzte Container (ab Task 4) */
|
||||
passwords: string[];
|
||||
}
|
||||
|
||||
export interface DetectResult {
|
||||
items: AnyItem[];
|
||||
ignored: IgnoredEntry[];
|
||||
locked: LockedEntry[];
|
||||
}
|
||||
|
||||
const CERT_LABELS = new Set(['CERTIFICATE', 'X509 CERTIFICATE', 'TRUSTED CERTIFICATE']);
|
||||
const PEM_BLOCK = /-----BEGIN ([A-Z0-9 ]+)-----([\s\S]*?)-----END \1-----/g;
|
||||
const BASE64_BODY = /^[A-Za-z0-9+/]+={0,2}$/;
|
||||
|
||||
const CURVE_NAMES: Record<string, { name: string; bits: number }> = {
|
||||
prime256v1: { name: 'P-256', bits: 256 },
|
||||
secp256r1: { name: 'P-256', bits: 256 },
|
||||
secp384r1: { name: 'P-384', bits: 384 },
|
||||
secp521r1: { name: 'P-521', bits: 521 },
|
||||
};
|
||||
|
||||
export function sha256Hex(data: Buffer | string): string {
|
||||
return createHash('sha256').update(data).digest('hex');
|
||||
}
|
||||
|
||||
/** Beschreibung eines oeffentlichen oder privaten Schluessels, fuer Zertifikat, Schluessel und CSR gleich. */
|
||||
export function describeKey(key: KeyObject): {
|
||||
keyType: string;
|
||||
keyBits: number | null;
|
||||
curve: string | null;
|
||||
} {
|
||||
const type = key.asymmetricKeyType ?? 'unknown';
|
||||
const details = key.asymmetricKeyDetails ?? {};
|
||||
if (type === 'rsa') {
|
||||
return { keyType: 'RSA', keyBits: details.modulusLength ?? null, curve: null };
|
||||
}
|
||||
if (type === 'ec') {
|
||||
const raw = details.namedCurve ?? '';
|
||||
const known = CURVE_NAMES[raw];
|
||||
return {
|
||||
keyType: 'EC',
|
||||
keyBits: known?.bits ?? null,
|
||||
curve: known?.name ?? (raw || null),
|
||||
};
|
||||
}
|
||||
if (type === 'ed25519') return { keyType: 'ED25519', keyBits: 256, curve: null };
|
||||
return { keyType: type.toUpperCase(), keyBits: null, curve: null };
|
||||
}
|
||||
|
||||
/** 'k-' + 16 Hex von sha256 des SPKI-DER: gleiche Kennung fuer Zertifikat, Schluessel und CSR. */
|
||||
export function keyIdOf(publicKey: KeyObject): string {
|
||||
const spki = publicKey.export({ type: 'spki', format: 'der' });
|
||||
return `k-${sha256Hex(spki).slice(0, 16)}`;
|
||||
}
|
||||
|
||||
function firstValue(v: unknown): string {
|
||||
if (Array.isArray(v)) return typeof v[0] === 'string' ? v[0] : '';
|
||||
return typeof v === 'string' ? v : '';
|
||||
}
|
||||
|
||||
const SAN_SPLIT = /,\s(?=(?:DNS|IP Address|email|URI|Registered ID|othername|DirName):)/;
|
||||
|
||||
function sanList(subjectAltName: string | undefined): string[] {
|
||||
if (!subjectAltName) return [];
|
||||
return subjectAltName
|
||||
.split(SAN_SPLIT)
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean)
|
||||
.map((part) => {
|
||||
if (part.startsWith('DNS:')) return part.slice(4);
|
||||
if (part.startsWith('IP Address:')) return `IP:${part.slice('IP Address:'.length)}`;
|
||||
return part;
|
||||
});
|
||||
}
|
||||
|
||||
function aiaUrls(legacyInfoAccess: unknown): string[] {
|
||||
const raw = (legacyInfoAccess as Record<string, unknown> | undefined)?.['CA Issuers - URI'];
|
||||
const list = Array.isArray(raw) ? raw : typeof raw === 'string' ? [raw] : [];
|
||||
const urls: string[] = [];
|
||||
for (const entry of list) {
|
||||
if (typeof entry !== 'string') continue;
|
||||
try {
|
||||
const u = new URL(entry);
|
||||
if (u.protocol === 'http:' || u.protocol === 'https:') urls.push(entry);
|
||||
} catch {
|
||||
// keine gueltige Adresse: ueberspringen
|
||||
}
|
||||
if (urls.length >= 5) break;
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selbstsigniert = vom eigenen Schluessel unterschrieben. OpenSSLs checkIssued lehnt ein Zertifikat
|
||||
* ab, dessen Schluesselverwendung kein Zertifikatsignieren erlaubt (typisch bei selbstsignierten
|
||||
* Serverzertifikaten); darum zaehlt auch „Aussteller gleich Inhaber“ plus echte Signaturpruefung.
|
||||
*/
|
||||
function isSelfSigned(x: X509Certificate): boolean {
|
||||
try {
|
||||
if (!x.verify(x.publicKey)) return false;
|
||||
return x.checkIssued(x) || x.subject === x.issuer;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function roleOf(isCa: boolean, selfSigned: boolean): CertRole {
|
||||
if (!isCa) return 'end-entity';
|
||||
return selfSigned ? 'root' : 'intermediate';
|
||||
}
|
||||
|
||||
/** Baut aus einem DER-Zertifikat den Eintrag (alles aus node:crypto). Wirft bei ungueltigem DER. */
|
||||
export function certItemFromDer(der: Buffer, source: ItemSource): CertItem {
|
||||
const x = new X509Certificate(der);
|
||||
const legacy = x.toLegacyObject() as unknown as {
|
||||
subject?: Record<string, unknown>;
|
||||
issuer?: Record<string, unknown>;
|
||||
infoAccess?: unknown;
|
||||
};
|
||||
const cn = firstValue(legacy.subject?.CN);
|
||||
const selfSigned = isSelfSigned(x);
|
||||
const isCa = x.ca;
|
||||
const role = roleOf(isCa, selfSigned);
|
||||
const notAfter = x.validToDate;
|
||||
const key = describeKey(x.publicKey);
|
||||
return {
|
||||
id: `c-${sha256Hex(x.raw).slice(0, 16)}`,
|
||||
kind: 'certificate',
|
||||
role,
|
||||
sources: [{ ...source }],
|
||||
pem: x.toString(),
|
||||
baseName: safeBaseName(cn, role === 'end-entity' ? 'zertifikat' : 'ca'),
|
||||
cn,
|
||||
organization: firstValue(legacy.subject?.O),
|
||||
issuerCn: firstValue(legacy.issuer?.CN),
|
||||
issuerOrganization: firstValue(legacy.issuer?.O),
|
||||
notBefore: x.validFromDate.toISOString(),
|
||||
notAfter: notAfter.toISOString(),
|
||||
isExpired: notAfter.getTime() < Date.now(),
|
||||
daysLeft: Math.ceil((notAfter.getTime() - Date.now()) / 86_400_000),
|
||||
san: sanList(x.subjectAltName),
|
||||
keyType: key.keyType,
|
||||
keyBits: key.keyBits,
|
||||
curve: key.curve,
|
||||
serialNumber: x.serialNumber,
|
||||
sha256: x.fingerprint256,
|
||||
sha1: x.fingerprint,
|
||||
isCa,
|
||||
selfSigned,
|
||||
aiaIssuerUrls: aiaUrls(legacy.infoAccess),
|
||||
keyId: keyIdOf(x.publicKey),
|
||||
csrIds: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Das fuehrende ASN.1-SEQUENCE-Element eines DER-Blocks (Tag 0x30 samt Laenge).
|
||||
* Nach hinten angehaengte Daten (z. B. Vertrauensangaben bei TRUSTED CERTIFICATE) fallen weg.
|
||||
* Ein unvollstaendiger Block ergibt null.
|
||||
*/
|
||||
export function leadingDerSequence(buf: Buffer): Buffer | null {
|
||||
if (buf.length < 2 || buf[0] !== 0x30) return null;
|
||||
let length = buf[1];
|
||||
let headerLength = 2;
|
||||
if (length & 0x80) {
|
||||
const lengthBytes = length & 0x7f;
|
||||
if (lengthBytes === 0 || lengthBytes > 4 || buf.length < 2 + lengthBytes) return null;
|
||||
length = 0;
|
||||
for (let i = 0; i < lengthBytes; i++) length = length * 256 + buf[2 + i];
|
||||
headerLength = 2 + lengthBytes;
|
||||
}
|
||||
const total = headerLength + length;
|
||||
if (total > buf.length) return null;
|
||||
return buf.subarray(0, total);
|
||||
}
|
||||
|
||||
function emptyResult(): DetectResult {
|
||||
return { items: [], ignored: [], locked: [] };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stufen. Jede liefert null, wenn sie den Inhalt nicht als „ihren“ erkennt.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** ZIP (Task 3): erkannt an den Anfangsbytes, nicht an der Endung. */
|
||||
function detectZip(_blob: Buffer, _ctx: DetectContext): DetectResult | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
interface PemBlock {
|
||||
label: string;
|
||||
der: Buffer | null;
|
||||
encrypted: boolean;
|
||||
}
|
||||
|
||||
function pemBlocks(text: string): PemBlock[] {
|
||||
const blocks: PemBlock[] = [];
|
||||
for (const m of text.matchAll(PEM_BLOCK)) {
|
||||
const label = m[1];
|
||||
const rawBody = m[2];
|
||||
const encrypted = /Proc-Type:\s*4,\s*ENCRYPTED/i.test(rawBody);
|
||||
const body = rawBody
|
||||
.split(/\r?\n/)
|
||||
.filter((line) => !line.includes(':'))
|
||||
.join('')
|
||||
.replace(/\s+/g, '');
|
||||
const valid = BASE64_BODY.test(body) && body.length % 4 === 0;
|
||||
blocks.push({ label, der: valid ? Buffer.from(body, 'base64') : null, encrypted });
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
function certFromDer(der: Buffer, ctx: DetectContext): CertItem | null {
|
||||
const seq = leadingDerSequence(der);
|
||||
if (!seq) return null;
|
||||
try {
|
||||
return certItemFromDer(seq, { file: ctx.file, path: ctx.path });
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Text mit -----BEGIN-Bloecken: jeder Block nach seinem Etikett. */
|
||||
function detectPem(blob: Buffer, ctx: DetectContext): DetectResult | null {
|
||||
const text = blob.toString('utf8').replace(/^/, '');
|
||||
if (!text.includes('-----BEGIN ')) return null;
|
||||
const result = emptyResult();
|
||||
for (const block of pemBlocks(text)) {
|
||||
if (!block.der) continue;
|
||||
if (CERT_LABELS.has(block.label)) {
|
||||
const item = certFromDer(block.der, ctx);
|
||||
if (item) result.items.push(item);
|
||||
}
|
||||
// PKCS7/CMS (Task 3); PRIVATE KEY, RSA/EC PRIVATE KEY, ENCRYPTED PRIVATE KEY und
|
||||
// CERTIFICATE REQUEST (Task 4) folgen in dieser Schleife.
|
||||
}
|
||||
return result.items.length > 0 ? result : null;
|
||||
}
|
||||
|
||||
/** Einzelnes DER-Zertifikat (.cer/.crt/.der). */
|
||||
function detectDerCertificate(blob: Buffer, ctx: DetectContext): DetectResult | null {
|
||||
const item = certFromDer(blob, ctx);
|
||||
if (!item) return null;
|
||||
return { items: [item], ignored: [], locked: [] };
|
||||
}
|
||||
|
||||
/** PKCS#12 (Task 4). */
|
||||
function detectPkcs12(_blob: Buffer, _ctx: DetectContext): DetectResult | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** PKCS#7 signedData als DER (Task 3). */
|
||||
function detectPkcs7(_blob: Buffer, _ctx: DetectContext): DetectResult | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Privater Schluessel als DER (Task 4). */
|
||||
function detectPrivateKey(_blob: Buffer, _ctx: DetectContext): DetectResult | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Zertifikatsanfrage als DER (Task 4). */
|
||||
function detectCsr(_blob: Buffer, _ctx: DetectContext): DetectResult | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
const STAGES: ((blob: Buffer, ctx: DetectContext) => DetectResult | null)[] = [
|
||||
detectZip,
|
||||
detectPem,
|
||||
detectDerCertificate,
|
||||
detectPkcs12,
|
||||
detectPkcs7,
|
||||
detectPrivateKey,
|
||||
detectCsr,
|
||||
];
|
||||
|
||||
/**
|
||||
* Erkennt, was in einer Datei steckt (D-16). Wirft nie: eine Datei, die keine Stufe
|
||||
* erkennt, ergibt einen Eintrag „unbekannt“.
|
||||
*/
|
||||
export function detectBlob(blob: Buffer, ctx: DetectContext): DetectResult {
|
||||
for (const stage of STAGES) {
|
||||
try {
|
||||
const result = stage(blob, ctx);
|
||||
if (result) return result;
|
||||
} catch {
|
||||
// diese Stufe kann den Inhalt nicht lesen: naechste Stufe versuchen
|
||||
}
|
||||
}
|
||||
return {
|
||||
items: [],
|
||||
ignored: [{ file: ctx.file, path: ctx.path, reason: 'unknown' }],
|
||||
locked: [],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { safeBaseName } from './cert-output';
|
||||
|
||||
describe('safeBaseName', () => {
|
||||
it('Platzhalter, Leerzeichen und Pfadteile werden entschaerft', () => {
|
||||
expect(safeBaseName('*.example.de', 'x')).toBe('wildcard.example.de');
|
||||
expect(safeBaseName('Encryption Everywhere DV TLS CA - G1', 'x')).toBe(
|
||||
'Encryption_Everywhere_DV_TLS_CA_-_G1',
|
||||
);
|
||||
expect(safeBaseName('../../etc/passwd', 'x')).toBe('etc_passwd');
|
||||
expect(safeBaseName('', 'fallback')).toBe('fallback');
|
||||
});
|
||||
|
||||
it('kuerzt auf 80 Zeichen und behaelt den Ersatz bei leerem Ergebnis', () => {
|
||||
expect(safeBaseName('a'.repeat(200), 'x')).toHaveLength(80);
|
||||
expect(safeBaseName('...', 'ersatz')).toBe('ersatz');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Ausgabe-Bausteine des Zertifikat-Managers (quick-261009-ikt).
|
||||
* Task 1: nur der Dateiname-Helfer; die Ausgabeformate (build) kommen in den folgenden Tasks.
|
||||
*/
|
||||
|
||||
/** Dateiname ohne Pfad und ohne gefaehrliche Zeichen, z. B. „*.example.de“ -> „wildcard.example.de“. */
|
||||
export function safeBaseName(raw: string, fallback: string): string {
|
||||
const cleaned = raw
|
||||
.replace(/^\*\./, 'wildcard.')
|
||||
.replace(/[^A-Za-z0-9._-]+/g, '_')
|
||||
.replace(/^[._]+/, '')
|
||||
.slice(0, 80);
|
||||
return cleaned || fallback;
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import { HttpException } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* Vertrag des Zertifikat-Managers (quick-261009-ikt, D-15, D-19, D-24).
|
||||
* Die Typen werden 1:1 in apps/web/.../cert-manager/actions.ts gespiegelt.
|
||||
* Alles hier ist zustandslos: Tessera speichert weder Dateien noch Schluessel noch Passwoerter.
|
||||
*/
|
||||
|
||||
/** Wo ein Teil herkam: Index der hochgeladenen Datei (Reihenfolge der Anfrage) und Pfad (bei ZIP: "zipname/eintrag"). */
|
||||
export interface ItemSource {
|
||||
file: number;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export type CertRole = 'end-entity' | 'intermediate' | 'root';
|
||||
|
||||
export interface CertItem {
|
||||
/** 'c-' + die ersten 16 Hex-Zeichen (klein) von sha256(DER) */
|
||||
id: string;
|
||||
kind: 'certificate';
|
||||
role: CertRole;
|
||||
sources: ItemSource[];
|
||||
pem: string;
|
||||
baseName: string;
|
||||
cn: string;
|
||||
organization: string;
|
||||
issuerCn: string;
|
||||
issuerOrganization: string;
|
||||
notBefore: string;
|
||||
notAfter: string;
|
||||
isExpired: boolean;
|
||||
daysLeft: number;
|
||||
/** DNS-Namen unveraendert, andere Arten mit Praefix wie 'IP:' */
|
||||
san: string[];
|
||||
keyType: string;
|
||||
keyBits: number | null;
|
||||
curve: string | null;
|
||||
serialNumber: string;
|
||||
sha256: string;
|
||||
sha1: string;
|
||||
isCa: boolean;
|
||||
selfSigned: boolean;
|
||||
/** nur http/https, hoechstens 5 */
|
||||
aiaIssuerUrls: string[];
|
||||
/** Kennung des oeffentlichen Schluessels: 'k-' + 16 Hex von sha256(SPKI-DER), passt zu KeyItem.id und CsrItem.keyId */
|
||||
keyId: string | null;
|
||||
csrIds: string[];
|
||||
}
|
||||
|
||||
export interface KeyItem {
|
||||
/** 'k-' + 16 Hex von sha256(SPKI-DER) */
|
||||
id: string;
|
||||
kind: 'privateKey';
|
||||
sources: ItemSource[];
|
||||
/** unverschluesseltes PKCS#8 */
|
||||
pem: string;
|
||||
baseName: string;
|
||||
keyType: string;
|
||||
keyBits: number | null;
|
||||
curve: string | null;
|
||||
wasEncrypted: boolean;
|
||||
certIds: string[];
|
||||
}
|
||||
|
||||
export interface CsrItem {
|
||||
/** 'r-' + 16 Hex von sha256(DER) */
|
||||
id: string;
|
||||
kind: 'csr';
|
||||
sources: ItemSource[];
|
||||
pem: string;
|
||||
baseName: string;
|
||||
cn: string;
|
||||
organization: string;
|
||||
san: string[];
|
||||
keyType: string;
|
||||
keyBits: number | null;
|
||||
curve: string | null;
|
||||
keyId: string | null;
|
||||
certIds: string[];
|
||||
}
|
||||
|
||||
export type AnyItem = CertItem | KeyItem | CsrItem;
|
||||
|
||||
export interface ChainGap {
|
||||
certId: string;
|
||||
kind: 'afterLeaf' | 'afterCa';
|
||||
missingIssuerCn: string;
|
||||
aiaUrls: string[];
|
||||
}
|
||||
|
||||
export interface ChainInfo {
|
||||
headId: string;
|
||||
/** Kopf zuerst, dann jeder Aussteller, Wurzel zuletzt (falls vorhanden) */
|
||||
path: string[];
|
||||
rootId: string | null;
|
||||
complete: boolean;
|
||||
gap: ChainGap | null;
|
||||
alternatives: number;
|
||||
}
|
||||
|
||||
export interface LockedEntry {
|
||||
file: number;
|
||||
path: string;
|
||||
container: 'pkcs12' | 'privateKey';
|
||||
reason: 'passwordNeeded' | 'passwordWrong';
|
||||
}
|
||||
|
||||
export type IgnoredReason =
|
||||
| 'unknown'
|
||||
| 'nestedZip'
|
||||
| 'encryptedZip'
|
||||
| 'brokenZip'
|
||||
| 'tooLarge'
|
||||
| 'suspicious'
|
||||
| 'zipTooLarge'
|
||||
| 'tooManyEntries'
|
||||
| 'unsupportedKey';
|
||||
|
||||
export interface IgnoredEntry {
|
||||
file: number;
|
||||
path: string;
|
||||
reason: IgnoredReason;
|
||||
}
|
||||
|
||||
export interface AnalysisResult {
|
||||
items: AnyItem[];
|
||||
chains: ChainInfo[];
|
||||
locked: LockedEntry[];
|
||||
ignored: IgnoredEntry[];
|
||||
}
|
||||
|
||||
export type BuildContent = 'leaf' | 'fullchain' | 'chain' | 'leafKey' | 'pfx' | 'key' | 'csr';
|
||||
|
||||
export interface BuildInput {
|
||||
content: BuildContent;
|
||||
format?: string;
|
||||
certPem?: string;
|
||||
poolPems?: string[];
|
||||
keyPem?: string;
|
||||
csrPem?: string;
|
||||
includeRoot?: boolean;
|
||||
includeChain?: boolean;
|
||||
password?: string;
|
||||
pfxEncryption?: 'compat' | 'modern';
|
||||
template?: string;
|
||||
baseName?: string;
|
||||
}
|
||||
|
||||
export interface BuildFile {
|
||||
filename: string;
|
||||
/** Base64 */
|
||||
content: string;
|
||||
mimeType: string;
|
||||
}
|
||||
|
||||
export interface BuildResult {
|
||||
files: BuildFile[];
|
||||
chainComplete: boolean;
|
||||
missingIssuerCn: string | null;
|
||||
snippet?: string | null;
|
||||
}
|
||||
|
||||
/** Fehlercodes (D-24); die Meldung ist Englisch fuer Entwickler, die Oberflaeche zeigt eigene Texte zum Code. */
|
||||
export type CertErrorCode =
|
||||
| 'invalidInput'
|
||||
| 'notACertificate'
|
||||
| 'noChain'
|
||||
| 'keyMissing'
|
||||
| 'keyMismatch'
|
||||
| 'passwordRequired'
|
||||
| 'formatNotPossible'
|
||||
| 'templateNeedsKey'
|
||||
| 'tooLarge'
|
||||
| 'aiaMissing'
|
||||
| 'aiaInternal'
|
||||
| 'aiaNotIssuer'
|
||||
| 'aiaUnreachable'
|
||||
| 'aiaTooLarge';
|
||||
|
||||
/** Wirft eine Nest-Ausnahme mit dem Koerper `{ code, message }` (nie mit Passwort oder Schluessel im Text). */
|
||||
export function certError(code: CertErrorCode, status: number, message: string): never {
|
||||
throw new HttpException({ code, message }, status);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* DTO for the cert-manager convert endpoint body fields.
|
||||
* Used alongside FileInterceptor for single-file upload.
|
||||
*/
|
||||
export class ConvertCertDto {
|
||||
targetFormat!: 'pem' | 'der' | 'pfx' | 'p7b';
|
||||
password?: string;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* DTO for the cert-manager merge endpoint body fields.
|
||||
* Used alongside FilesInterceptor for multi-file upload.
|
||||
*/
|
||||
export class MergeCertsDto {
|
||||
outputFormat!: 'pem' | 'pfx';
|
||||
password?: string;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* DTO for the cert-manager parse endpoint (text paste / JSON body path).
|
||||
* For file uploads the body fields are extracted via @Body() in the controller.
|
||||
*/
|
||||
export class ParseCertDto {
|
||||
pemText!: string;
|
||||
password?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user