-
-
Notifications
You must be signed in to change notification settings - Fork 110
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
4ef1476
commit d1701f9
Showing
5 changed files
with
398 additions
and
0 deletions.
There are no files selected for viewing
23 changes: 23 additions & 0 deletions
23
...RESTAPI/files/usr/local/pkg/RESTAPI/Endpoints/SystemCertificateSigningRequestEndpoint.inc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
<?php | ||
|
||
namespace RESTAPI\Endpoints; | ||
|
||
require_once 'RESTAPI/autoloader.inc'; | ||
|
||
use RESTAPI\Core\Endpoint; | ||
|
||
/** | ||
* Defines an Endpoint for interacting with a singular CertificateSigningRequest object at | ||
* /api/v2/system/certificate/signing_request. | ||
*/ | ||
class SystemCertificateSigningRequestEndpoint extends Endpoint { | ||
public function __construct() { | ||
# Set Endpoint attributes | ||
$this->url = '/api/v2/system/certificate/signing_request'; | ||
$this->model_name = 'CertificateSigningRequest'; | ||
$this->request_method_options = ['POST']; | ||
|
||
# Construct the parent Endpoint object | ||
parent::__construct(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
304 changes: 304 additions & 0 deletions
304
pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/CertificateSigningRequest.inc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,304 @@ | ||
<?php | ||
|
||
namespace RESTAPI\Models; | ||
|
||
use RESTAPI\Core\Model; | ||
use RESTAPI\Fields\Base64Field; | ||
use RESTAPI\Fields\ForeignModelField; | ||
use RESTAPI\Fields\IntegerField; | ||
use RESTAPI\Fields\StringField; | ||
use RESTAPI\Fields\UIDField; | ||
use RESTAPI\Responses\ServerError; | ||
use RESTAPI\Validators\EmailAddressValidator; | ||
use RESTAPI\Validators\HostnameValidator; | ||
use RESTAPI\Validators\IPAddressValidator; | ||
use RESTAPI\Validators\RegexValidator; | ||
use RESTAPI\Validators\URLValidator; | ||
|
||
/** | ||
* Defines a Model for generating new CSRs. | ||
*/ | ||
class CertificateSigningRequest extends Model { | ||
public StringField $descr; | ||
public UIDField $refid; | ||
public IntegerField $serial; | ||
public StringField $keytype; | ||
public IntegerField $keylen; | ||
public StringField $ecname; | ||
public StringField $digest_alg; | ||
public IntegerField $lifetime; | ||
public StringField $dn_commonname; | ||
public StringField $dn_country; | ||
public StringField $dn_state; | ||
public StringField $dn_city; | ||
public StringField $dn_organization; | ||
public StringField $dn_organizationalunit; | ||
public StringField $type; | ||
public StringField $dn_dns_sans; | ||
public StringField $dn_email_sans; | ||
public StringField $dn_ip_sans; | ||
public StringField $dn_uri_sans; | ||
public Base64Field $csr; | ||
public Base64Field $prv; | ||
|
||
public function __construct(mixed $id = null, mixed $parent_id = null, mixed $data = [], mixed ...$options) { | ||
# Set model attributes | ||
$this->config_path = 'cert'; | ||
$this->many = true; | ||
$this->always_apply = true; | ||
|
||
# Set model fields | ||
$this->descr = new StringField( | ||
required: true, | ||
validators: [new RegexValidator(pattern: "/[\?\>\<\&\/\\\"\']/", invert: true)], | ||
help_text: 'The descriptive name for this certificate.', | ||
); | ||
$this->refid = new UIDField( | ||
help_text: 'The unique ID assigned to this certificate for internal system use. This value is ' . | ||
'generated by this system and cannot be changed.', | ||
); | ||
$this->keytype = new StringField( | ||
required: true, | ||
choices: ['RSA', 'ECDSA'], | ||
representation_only: true, | ||
help_text: 'The type of key pair to generate.', | ||
); | ||
$this->keylen = new IntegerField( | ||
required: true, | ||
choices: [1024, 2048, 3072, 4096, 6144, 7680, 8192, 15360, 16384], | ||
representation_only: true, | ||
conditions: ['keytype' => 'RSA'], | ||
help_text: 'The length of the RSA key pair to generate.', | ||
); | ||
$this->ecname = new StringField( | ||
required: true, | ||
choices_callable: 'get_ecname_choices', | ||
representation_only: true, | ||
conditions: ['keytype' => 'ECDSA'], | ||
help_text: 'The name of the elliptic curve to use for the ECDSA key pair.', | ||
); | ||
$this->digest_alg = new StringField( | ||
required: true, | ||
choices_callable: 'get_digest_alg_choices', | ||
representation_only: true, | ||
help_text: 'The digest method used when the certificate is signed.', | ||
); | ||
$this->lifetime = new IntegerField( | ||
default: 3650, | ||
representation_only: true, | ||
minimum: 1, | ||
maximum: 12000, | ||
help_text: 'The number of days the certificate is valid for.', | ||
); | ||
$this->dn_commonname = new StringField( | ||
required: true, | ||
representation_only: true, | ||
help_text: 'The common name of the certificate.', | ||
); | ||
$this->dn_country = new StringField( | ||
default: null, | ||
choices_callable: 'get_country_choices', | ||
allow_null: true, | ||
representation_only: true, | ||
help_text: 'The country of the certificate.', | ||
); | ||
$this->dn_state = new StringField( | ||
default: null, | ||
allow_null: true, | ||
representation_only: true, | ||
help_text: 'The state/province of the certificate.', | ||
); | ||
$this->dn_city = new StringField( | ||
default: null, | ||
allow_null: true, | ||
representation_only: true, | ||
help_text: 'The city of the certificate.', | ||
); | ||
$this->dn_organization = new StringField( | ||
default: null, | ||
allow_null: true, | ||
representation_only: true, | ||
help_text: 'The organization of the certificate.', | ||
); | ||
$this->dn_organizationalunit = new StringField( | ||
default: null, | ||
allow_null: true, | ||
representation_only: true, | ||
help_text: 'The organizational unit of the certificate.', | ||
); | ||
$this->type = new StringField( | ||
default: 'user', | ||
choices: ['server', 'user'], | ||
help_text: 'The type of certificate to generate.', | ||
); | ||
$this->dn_dns_sans = new StringField( | ||
default: [], | ||
allow_empty: true, | ||
representation_only: true, | ||
many: true, | ||
validators: [new HostnameValidator(allow_hostname: true, allow_domain: true, allow_fqdn: true)], | ||
help_text: 'The DNS Subject Alternative Names (SANs) for the certificate.', | ||
); | ||
$this->dn_email_sans = new StringField( | ||
default: [], | ||
allow_empty: true, | ||
representation_only: true, | ||
many: true, | ||
validators: [new EmailAddressValidator()], | ||
help_text: 'The Email Subject Alternative Names (SANs) for the certificate.', | ||
); | ||
$this->dn_ip_sans = new StringField( | ||
default: [], | ||
allow_empty: true, | ||
representation_only: true, | ||
many: true, | ||
validators: [new IPAddressValidator(allow_ipv4: true, allow_ipv6: true)], | ||
help_text: 'The IP Subject Alternative Names (SANs) for the certificate.', | ||
); | ||
$this->dn_uri_sans = new StringField( | ||
default: [], | ||
allow_empty: true, | ||
representation_only: true, | ||
many: true, | ||
validators: [new URLValidator()], | ||
help_text: 'The URI Subject Alternative Names (SANs) for the certificate.', | ||
); | ||
$this->csr = new Base64Field( | ||
default: null, | ||
allow_null: true, | ||
read_only: true, | ||
help_text: 'The X509 certificate signing request string. You will need to provide this to a ' . | ||
'certificate authority to sign the certificate.', | ||
); | ||
$this->prv = new Base64Field( | ||
default: null, | ||
allow_null: true, | ||
read_only: true, | ||
write_only: true, | ||
sensitive: true, | ||
help_text: 'The X509 private key string.', | ||
); | ||
|
||
parent::__construct($id, $parent_id, $data, ...$options); | ||
} | ||
|
||
/** | ||
* Returns a list of available elliptic curve names for ECDSA key pairs. | ||
* @returns array The list of available elliptic curve names. | ||
*/ | ||
public static function get_ecname_choices(): array { | ||
# Obtain the available curve list from pfSense's built-in cert_build_curve_list function | ||
return array_keys(cert_build_curve_list()); | ||
} | ||
|
||
/** | ||
* Returns a list of available digest algorithms for signing certificates. | ||
* @returns array The list of available digest algorithms. | ||
*/ | ||
public static function get_digest_alg_choices(): array { | ||
# Obtain the available digest algorithms from pfSense's built-in $openssl_digest_algs global | ||
global $openssl_digest_algs; | ||
return $openssl_digest_algs; | ||
} | ||
|
||
/** | ||
* Returns a list of available country codes for the certificate. | ||
* @returns array The list of available country codes. | ||
*/ | ||
public static function get_country_choices(): array { | ||
# Obtain the available country codes from pfSense's built-in get_cert_country_codes function | ||
return array_keys(get_cert_country_codes()); | ||
} | ||
|
||
/** | ||
* Extends the default _create method to ensure the certificate is generated before it is written to config. | ||
*/ | ||
protected function _create(): void { | ||
# Generate the certificate | ||
$this->generate_cert(); | ||
|
||
# Call the parent _create method to write the certificate to config | ||
parent::_create(); | ||
} | ||
|
||
/** | ||
* Converts this Certificate object's DN values into a X509 DN array. | ||
* @returns array The X509 DN array. | ||
*/ | ||
private function to_x509_dn(): array { | ||
# Define static DN values | ||
$dn = ['commonName' => $this->dn_commonname->value, 'subjectAltName' => []]; | ||
|
||
# Add countryName if it was given | ||
if ($this->dn_country->value) { | ||
$dn['countryName'] = $this->dn_country->value; | ||
} | ||
# Add stateOrProvinceName if it was given | ||
if ($this->dn_state->value) { | ||
$dn['stateOrProvinceName'] = $this->dn_state->value; | ||
} | ||
# Add localityName if it was given | ||
if ($this->dn_city->value) { | ||
$dn['localityName'] = $this->dn_city->value; | ||
} | ||
# Add organizationName if it was given | ||
if ($this->dn_organization->value) { | ||
$dn['organizationName'] = $this->dn_organization->value; | ||
} | ||
# Add organizationalUnitName if it was given | ||
if ($this->dn_organizationalunit->value) { | ||
$dn['organizationalUnitName'] = $this->dn_organizationalunit->value; | ||
} | ||
|
||
# Loop through the SAN fields and add them to the subjectAltName array accordingly | ||
foreach ($this->dn_dns_sans->value as $san) { | ||
$dn['subjectAltName'][] = "DNS:$san"; | ||
} | ||
foreach ($this->dn_email_sans->value as $san) { | ||
$dn['subjectAltName'][] = "email:$san"; | ||
} | ||
foreach ($this->dn_ip_sans->value as $san) { | ||
$dn['subjectAltName'][] = "IP:$san"; | ||
} | ||
foreach ($this->dn_uri_sans->value as $san) { | ||
$dn['subjectAltName'][] = "URI:$san"; | ||
} | ||
|
||
# Piece together the subjectAltName array into a comma-separated string | ||
$dn['subjectAltName'] = implode(',', $dn['subjectAltName']); | ||
|
||
return $dn; | ||
} | ||
|
||
/** | ||
* Generates a new CSR and key using the requested parameters. This populate the `csr` and `prv` fields. | ||
* @throws ServerError When the CSR and key fails to be generated. | ||
*/ | ||
private function generate_cert(): void { | ||
# Define a placeholder for csr_generate() to populate | ||
$csr = []; | ||
|
||
# Generate the CSR and key pair | ||
$success = csr_generate( | ||
cert: $csr, | ||
keylen: $this->keylen->value, | ||
dn: $this->to_x509_dn(), | ||
type: $this->type->value, | ||
digest_alg: $this->digest_alg->value, | ||
keytype: $this->keytype->value, | ||
ecname: $this->ecname->value, | ||
); | ||
|
||
# Throw a server error if the CSR and key fails to be generated | ||
if (!$success) { | ||
throw new ServerError( | ||
message: 'Failed to generate the certificate signing request for unknown reason.', | ||
response_id: 'CERTIFICATE_SIGNING_REQUEST_GENERATE_FAILED', | ||
); | ||
} | ||
|
||
# Populate the `csr` and `prv` fields with the generated values | ||
$this->csr->from_internal($csr['csr']); | ||
$this->prv->from_internal($csr['prv']); | ||
} | ||
} |
Oops, something went wrong.