diff --git a/README.md b/README.md index 939e79a57..c82a021ed 100644 --- a/README.md +++ b/README.md @@ -698,7 +698,8 @@ There is no limit to API calls at this time but is important to note that pfSens * [SYSTEM/CERTIFICATE](#systemcertificate) 1. [Read System Certificates](#1-read-system-certificates) 1. [Create System Certificates](#2-create-system-certificates) - 1. [Delete System Certificates](#3-delete-system-certificates) + 1. [Update System Certificates](#3-update-system-certificates) + 1. [Delete System Certificates](#4-delete-system-certificates) * [SYSTEM/HOSTNAME](#systemhostname) 1. [Read System Hostname](#1-read-system-hostname) 1. [Update System Hostname](#2-update-system-hostname) @@ -5196,7 +5197,48 @@ URL: https://{{$hostname}}/api/v1/system/certificate -### 3. Delete System Certificates +### 3. Update System Certificates + + +Update an installed SSL certificate. +Dependent services are NOT restarted.

+ +_Requires at least one of the following privileges:_ [`page-all`, `page-system-certmanager`] + + +***Endpoint:*** + +```bash +Method: PUT +URL: https://{{$hostname}}/api/v1/system/certificate +``` + + + +***Fields:*** + +| Key | Type | Description | +| --- | ------|-------------| +| refid | string | Specify the refid of the certificate to change (required if `descr` is not defined) | +| descr | string | Specify the description of the certificate to delete (required if `refid` is not defined) _Note: if multiple certificates exist with the same name, only the first matching certificate will be updated_ _Note: `descr` can be changed when `refid` is specified_| +| crt | string | Specify the Base64 encoded PEM certificate to import. This field is required when `method` is set to `existing`. _Note: previous releases referred to the `crt` field as `cert`. Both `crt` and `cert` can be used interchangeably._ | +| prv | string | Specify the corresponding Base64 encoded certificate key. This field is required when `method` is set to `existing`. _Note: previous releases referred to the `prv` field as `key`. Both `prv` and `key` can be used interchangeably._ | + + + +***Example Request:*** + +```js +{ + "descr": "my already existing certificate", + "crt": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNZRENDQWNtZ0F3SUJBZ0lVSUt0U1pUYVRZSWhncytNNllsUUxrZFZ0Tm1nd0RRWUpLb1pJaHZjTkFRRUwKQlFBd1FqRUxNQWtHQTFVRUJoTUNXRmd4RlRBVEJnTlZCQWNNREVSbFptRjFiSFFnUTJsMGVURWNNQm9HQTFVRQpDZ3dUUkdWbVlYVnNkQ0JEYjIxd1lXNTVJRXgwWkRBZUZ3MHlNakF6TWpreE5ESTVNRGRhRncweU16QXpNamt4Ck5ESTVNRGRhTUVJeEN6QUpCZ05WQkFZVEFsaFlNUlV3RXdZRFZRUUhEQXhFWldaaGRXeDBJRU5wZEhreEhEQWEKQmdOVkJBb01FMFJsWm1GMWJIUWdRMjl0Y0dGdWVTQk1kR1F3Z1o4d0RRWUpLb1pJaHZjTkFRRUJCUUFEZ1kwQQpNSUdKQW9HQkFLTjlnaGlHT29rakp1aGs3VlZHYnVWdWg4MUxwVUFYNjEzRzRUZWlFMXJOUVl4U0NWSmdCaXpQCkxZK2hidmRjWUxGdGk4Rm1EM1hpM2J1aUxmdmY3UW5VOWljbmxwVTU4bU00VEEvR1orMisyZXpJTUhCdVowek8KZVRQSXhpOEMzYmZtb1VzME9JdEc0SGNlcWlpK09taXIrY3VlNk5xcVNJQUJuRzFoWHpENUFnTUJBQUdqVXpCUgpNQjBHQTFVZERnUVdCQlJMWFRWa01sQUtKbktaNTJRNmVIWmVRb3pNbHpBZkJnTlZIU01FR0RBV2dCUkxYVFZrCk1sQUtKbktaNTJRNmVIWmVRb3pNbHpBUEJnTlZIUk1CQWY4RUJUQURBUUgvTUEwR0NTcUdTSWIzRFFFQkN3VUEKQTRHQkFKRjd6MHBtQlhkQ0xJOWo5c2dFWCs3MWwyRGtYVEttZ2thS05ZN011RFBVQ1RNdUpjZ2MzbDdyd25BbwpRV3FpQ3o4VVZYdENER2xKVzZGYXJUWU5VSTlyaHAxR3hsbDQ3ckt5eXVCVUlLQmkwSFpPa3Y4dnBGM0lqcldECmlqRE5DbFZHQWlDSUU4STFIZDExT3cyY2pNODVCcFpDVFZycXJVaUJFSEswWWtpSwotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==", + "prv": "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUNkZ0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQW1Bd2dnSmNBZ0VBQW9HQkFLTjlnaGlHT29rakp1aGsKN1ZWR2J1VnVoODFMcFVBWDYxM0c0VGVpRTFyTlFZeFNDVkpnQml6UExZK2hidmRjWUxGdGk4Rm1EM1hpM2J1aQpMZnZmN1FuVTlpY25scFU1OG1NNFRBL0daKzIrMmV6SU1IQnVaMHpPZVRQSXhpOEMzYmZtb1VzME9JdEc0SGNlCnFpaStPbWlyK2N1ZTZOcXFTSUFCbkcxaFh6RDVBZ01CQUFFQ2dZQnI1U0dkYzhCZnp0NFhrcnY2Z2pBZnBERmwKY0IzUHpibGNPeXRaSHRKdEkzYTExMUlsbGcrZE5PRlpuKzF1dS8xb091WjNyUlpZODI3b0xLRHlVQmJMVHpEcwpHVzcwU09JZElwTXQxRmtWNmJZbDBpTXllaU1TUFdOeVl3YjA4Yko3ejZobzgxZnR1NGU4Y2FVS2lDdnZOK1lvCmtmVjk3RHEvNUV1NjVkSGZyUUpCQU5GbSt6dzVWNU83QmVhNXdiNXFSc0daODdBUDMxTU1TNWR3d21nM0VzVmoKekc0UnNMMXNTTjZ3SnM3LytaRERhcDVXS2M0T1FEZDA2YjVQTW5tM3dFTUNRUURIM3c3SXhneVRpaWNSUnpQegpBam5oZU5JL0h2NmloR0craFBMMnIxaFRTUFlWTDhRb2xHLzhhM3JGZW5iNm5rMjlyWks0eXdaK2x1R3hqbFRUCm02UVRBa0VBbmxMOW04QkRUZ2cyNHdjSnpMMnY5OHM5NjUxa25mY0s1RnEyTW5PSmRzTUpHeU8yL05GMW15R1cKaGlZVi9IVTBGTGxTN0Yvci84SWV4T3crWHJjbTN3SkFWZDJiSVdBTUtScFIvRmRGbHlHZXNpSFEyVE04bTU4Wgp5dHFjOHFPVDQzdlYxSFpINUZNWTVTMWJlaGxKb2hOK1BIMmtLZVYyN2MxdU9uUjJOczZIcHdKQU8zdyt4ZnJoCk1ETlJ0TlZPQmZDc0p6aVV1ejRTZXY1K1pMVEphRWpmNkJUSUhVVHl2MGJzRUt3SGNtV3FQc0lFcFFvUkZnSlQKbElmelJSQXdBRWpDNVE9PQotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tCg==" +} +``` + + + +### 4. Delete System Certificates Delete an existing certificate.

diff --git a/pfSense-pkg-API/files/etc/inc/api/endpoints/APISystemCertificate.inc b/pfSense-pkg-API/files/etc/inc/api/endpoints/APISystemCertificate.inc index 1231511da..35f88d431 100644 --- a/pfSense-pkg-API/files/etc/inc/api/endpoints/APISystemCertificate.inc +++ b/pfSense-pkg-API/files/etc/inc/api/endpoints/APISystemCertificate.inc @@ -28,6 +28,10 @@ class APISystemCertificate extends APIEndpoint { return (new APISystemCertificateCreate())->call(); } + protected function put() { + return (new APISystemCertificateUpdate())->call(); + } + protected function delete() { return (new APISystemCertificateDelete())->call(); } diff --git a/pfSense-pkg-API/files/etc/inc/api/models/APISystemCertificateUpdate.inc b/pfSense-pkg-API/files/etc/inc/api/models/APISystemCertificateUpdate.inc new file mode 100644 index 000000000..f44645055 --- /dev/null +++ b/pfSense-pkg-API/files/etc/inc/api/models/APISystemCertificateUpdate.inc @@ -0,0 +1,152 @@ +privileges = ["page-all", "page-system-certmanager"]; + $this->change_note = "Modified system certificate address via API"; + } + + public function action() { + $this->config["cert"][$this->id] = $this->validated_data; + $this->write_config(); + mark_subsystem_dirty("cert"); + + # Only reload the firewall filter if a false value was not passed in + # TODO: This condition applies the changes by default to stay backwards compatible with v1.3.0 + # TODO: this should be refactored in a future release to not apply by default + #if ($this->initial_data["apply"] !== false) { + # APIFirewallApplyCreate::apply(); + #} + return APIResponse\get(0, $this->config["cert"][$this->id]); + } + + private function __validate_id() { + # Validate optional 'refid' field containing the refid of an existing certificate. If refid is not passed, find the cert through 'descr' field + if (isset($this->initial_data["refid"])) { + # Loop through each cert and check for a match + foreach ($this->config["cert"] as $id=>$cert) { + # Check if the field 'refid' matches the certificate's refid + if ($this->initial_data["refid"] === $cert["refid"]) { + $this->id = $id; + $this->validated_data = $cert; + break; + } + } + # If we did not find an ID in the loop, return a not found error + if (is_null($this->id)) { + $this->errors[] = APIResponse\get(1009); + } + } + } + + private function __validate_descr() { + # Validate the required 'descr' field + if (isset($this->initial_data['descr'])) { + # Ensure description does not contain invalid characters + if (preg_match("/[\?\>\<\&\/\\\"\']/", $this->initial_data['descr'])) { + $this->errors[] = APIResponse\get(1037); + } else { + # Match certificate by 'descr' field only when refid is not set + if (isset($this->initial_data["refid"])) { + $this->validated_data["descr"] = $this->initial_data['descr']; + } else { + # Loop through each cert and check for a match + foreach ($this->config["cert"] as $id=>$cert) { + # Check if the field 'descr' matches the certificate's description + if ($this->initial_data["descr"] === $cert["descr"]) { + $this->id = $id; + $this->validated_data["descr"] = $this->initial_data['descr']; + break; + } + } + # If we did not find an ID in the loop, return a not found error + if (is_null($this->id)) { + $this->errors[] = APIResponse\get(1009); + } + } + } + } else { + $this->errors[] = APIResponse\get(1002); + } + } + + private function __validate_crt() { + # Check for our required 'crt' field + if (isset($this->initial_data["crt"]) or isset($this->initial_data["cert"])) { + # Convert 'cert' to 'crt' to remain backwards compatible + if (isset($this->initial_data["cert"])) { + $this->initial_data["crt"] = $this->initial_data["cert"]; + } + + # Decode certificate from base64 format + $crt = base64_decode($this->initial_data["crt"]); + + # Check if our certificate is valid + if (!strstr($crt, "BEGIN CERTIFICATE") || !strstr($crt, "END CERTIFICATE")) { + $this->errors[] = APIResponse\get(1003); + } + else { + # Certificate must be stored base64-encoded + $this->validated_data["crt"] = base64_encode($crt); + } + } else { + $this->errors[] = APIResponse\get(1003); + } + } + + private function __validate_prv() { + # Check for our optional 'prv' field + if (isset($this->initial_data["prv"]) or isset($this->initial_data["key"])) { + # Convert 'key' to 'prv' to remain backwards compatible + if (isset($this->initial_data["key"])) { + $this->initial_data["prv"] = $this->initial_data["key"]; + } + + # Decode certificate from base64 format + $crt = base64_decode($this->initial_data["crt"]); + $key = base64_decode($this->initial_data["prv"]); + + # Check if this private key is encrypted + if (strstr($key, "ENCRYPTED")) { + $this->errors[] = APIResponse\get(1036); + } + # Check if our certificate and key matches + elseif (cert_get_publickey($crt, false) != cert_get_publickey($key, false, 'prv')) { + $this->errors[] = APIResponse\get(1049); + } + else { + # key must be stored base64-encoded + $this->validated_data["prv"] = base64_encode($key); + } + } else { + + } + } + + public function validate_payload() { + $this->__validate_id(); + $this->__validate_descr(); + $this->__validate_crt(); + $this->__validate_prv(); + } + +} diff --git a/tests/test_api_v1_system_certificate.py b/tests/test_api_v1_system_certificate.py index 66669fcce..e93910b1c 100644 --- a/tests/test_api_v1_system_certificate.py +++ b/tests/test_api_v1_system_certificate.py @@ -231,6 +231,22 @@ class APIE2ETestSystemCertificate(e2e_test_framework.APIE2ETest): "payload": {"method": "internal", "descr": "TestCA", "keytype": "ECDSA", "ecname": "prime256v1", "digest_alg": "sha256", "lifetime": 365, "dn_commonname": "test.example.com", "dn_country": "US", "type": "user", "altnames": [{"email": "#@!INVALIDEMAIL!@#"}]} } ] + put_tests = [ + { + "name": "Check updating a non-existing certificate", + "status": 400, + "return": 1009, + "payload": {"descr": "INVALID"} + }, + { + "name": "Update an existing certificate", + "payload": { + "descr": "Unit Test", + "crt": crt, + "prv": prv + } + }, + ] delete_tests = [ { "name": "Delete certificate",