-
-
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.
feat: implement ca generate support #519
- Loading branch information
1 parent
8994273
commit 739f345
Showing
3 changed files
with
511 additions
and
0 deletions.
There are no files selected for viewing
26 changes: 26 additions & 0 deletions
26
...TAPI/files/usr/local/pkg/RESTAPI/Endpoints/SystemCertificateAuthorityGenerateEndpoint.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,26 @@ | ||
<?php | ||
|
||
namespace RESTAPI\Endpoints; | ||
|
||
require_once 'RESTAPI/autoloader.inc'; | ||
|
||
use RESTAPI\Core\Endpoint; | ||
|
||
/** | ||
* Defines an Endpoint for interacting with a singular CertificateAuthority CertificateAuthorityGenerate object at | ||
* /api/v2/system/certificate_authority/generate. | ||
*/ | ||
class SystemCertificateAuthorityGenerateEndpoint extends Endpoint { | ||
public function __construct() { | ||
# Set Endpoint attributes | ||
$this->url = '/api/v2/system/certificate_authority/generate'; | ||
$this->model_name = 'CertificateAuthorityGenerate'; | ||
$this->request_method_options = ['POST']; | ||
|
||
# Set help text | ||
$this->post_help_text = 'Generate a new internal or intermediate certificate.'; | ||
|
||
# Construct the parent Endpoint object | ||
parent::__construct(); | ||
} | ||
} |
305 changes: 305 additions & 0 deletions
305
pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/CertificateAuthorityGenerate.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,305 @@ | ||
<?php | ||
|
||
namespace RESTAPI\Models; | ||
|
||
use RESTAPI\Core\Model; | ||
use RESTAPI\Fields\Base64Field; | ||
use RESTAPI\Fields\BooleanField; | ||
use RESTAPI\Fields\ForeignModelField; | ||
use RESTAPI\Fields\IntegerField; | ||
use RESTAPI\Fields\StringField; | ||
use RESTAPI\Fields\UIDField; | ||
use RESTAPI\Responses\ForbiddenError; | ||
use RESTAPI\Responses\NotFoundError; | ||
use RESTAPI\Responses\ServerError; | ||
use RESTAPI\Responses\ValidationError; | ||
use RESTAPI\Validators\RegexValidator; | ||
use RESTAPI\Validators\X509Validator; | ||
|
||
/** | ||
* Defines a Model for generating internal and intermediate certificate authorities. | ||
*/ | ||
class CertificateAuthorityGenerate extends Model { | ||
public StringField $descr; | ||
public UIDField $refid; | ||
public BooleanField $trust; | ||
public BooleanField $randomserial; | ||
public IntegerField $serial; | ||
public BooleanField $is_intermediate; | ||
public ForeignModelField $caref; | ||
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 Base64Field $crt; | ||
public Base64Field $prv; | ||
|
||
public function __construct(mixed $id = null, mixed $parent_id = null, mixed $data = [], mixed ...$options) { | ||
# Set model attributes | ||
$this->config_path = 'ca'; | ||
$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 authority.', | ||
); | ||
$this->refid = new UIDField( | ||
help_text: 'The unique ID assigned to this certificate authority for internal system use. This value is ' . | ||
'generated by this system and cannot be changed.', | ||
); | ||
$this->trust = new BooleanField( | ||
default: false, | ||
indicates_true: 'enabled', | ||
help_text: "Adds or removes this certificate authority from the operating system's trust stored.", | ||
); | ||
$this->randomserial = new BooleanField( | ||
default: false, | ||
indicates_true: 'enabled', | ||
help_text: 'Enables or disables the randomization of serial numbers for certificates signed by this CA.', | ||
); | ||
$this->serial = new IntegerField( | ||
default: 1, | ||
read_only: true, | ||
minimum: 0, | ||
help_text: 'The decimal number to be used as a sequential serial number for the next certificate to be ' . | ||
'signed by this CA. This value is ignored when Randomize Serial is checked.', | ||
); | ||
$this->is_intermediate = new BooleanField( | ||
default: false, | ||
write_only: true, | ||
help_text: 'Indicates if this certificate authority is an intermediate certificate authority.', | ||
); | ||
$this->caref = new ForeignModelField( | ||
model_name: 'CertificateAuthority', | ||
model_field: 'refid', | ||
required: true, | ||
write_only: true, | ||
conditions: ['is_intermediate' => true], | ||
help_text: 'The certificate authority to use as the parent for this intermediate certificate authority.', | ||
); | ||
$this->keytype = new StringField( | ||
required: true, | ||
choices: ['RSA', 'ECDSA'], | ||
write_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], | ||
write_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', | ||
write_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', | ||
write_only: true, | ||
help_text: 'The digest algorithm to use when signing certificates.', | ||
); | ||
$this->lifetime = new IntegerField( | ||
default: 3650, | ||
write_only: true, | ||
minimum: 1, | ||
maximum: 12000, | ||
help_text: 'The number of days the certificate authority is valid for.', | ||
); | ||
$this->dn_commonname = new StringField( | ||
default: 'internal-ca', | ||
write_only: true, | ||
help_text: 'The common name for the certificate authority.', | ||
); | ||
$this->dn_country = new StringField( | ||
default: '', | ||
choices_callable: 'get_country_choices', | ||
allow_empty: true, | ||
write_only: true, | ||
help_text: 'The country for the certificate authority.', | ||
); | ||
$this->dn_state = new StringField( | ||
default: '', | ||
allow_empty: true, | ||
write_only: true, | ||
help_text: 'The state for the certificate authority.', | ||
); | ||
$this->dn_city = new StringField( | ||
default: '', | ||
allow_empty: true, | ||
help_text: 'The city for the certificate authority.', | ||
); | ||
$this->dn_organization = new StringField( | ||
default: '', | ||
allow_empty: true, | ||
write_only: true, | ||
help_text: 'The organization for the certificate authority.', | ||
); | ||
$this->dn_organizationalunit = new StringField( | ||
default: '', | ||
allow_empty: true, | ||
write_only: true, | ||
help_text: 'The organizational unit for the certificate authority.', | ||
); | ||
$this->crt = new Base64Field( | ||
default: null, | ||
allow_null: true, | ||
read_only: true, | ||
help_text: 'The X509 certificate string. This field is required when `method` is set to `existing`.', | ||
); | ||
$this->prv = new Base64Field( | ||
default: null, | ||
allow_null: true, | ||
read_only: true, | ||
sensitive: true, | ||
help_text: 'The X509 private key string. This field is required when `method` is set to `existing`.', | ||
); | ||
|
||
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 authority. | ||
* @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 authority | ||
$this->is_intermediate->value ? $this->generate_intermediate_ca() : $this->generate_ca(); | ||
|
||
# Call the parent _create method to write the certificate authority to config | ||
parent::_create(); | ||
} | ||
|
||
/** | ||
* Apply CA changes to the OS trust store. | ||
*/ | ||
public function apply() { | ||
ca_setup_trust_store(); | ||
} | ||
|
||
/** | ||
* Converts this CertificateAuthority object's DN values into a X509 DN array. | ||
* @returns array The X509 DN array. | ||
*/ | ||
private function to_x509_dn(): array { | ||
return [ | ||
'commonName' => $this->dn_commonname->value, | ||
'countryName' => $this->dn_country->value, | ||
'stateOrProvinceName' => $this->dn_state->value, | ||
'localityName' => $this->dn_city->value, | ||
'organizationName' => $this->dn_organization->value, | ||
'organizationalUnitName' => $this->dn_organizationalunit->value, | ||
]; | ||
} | ||
|
||
/** | ||
* Generates a new CA certificate and key pair using the requested parameters. This populate the `crt` and `prv` fields. | ||
* @throws ServerError When the CA certificate and key pair fails to be generated. | ||
*/ | ||
private function generate_ca(): void { | ||
# Define a placeholder for create_ca() to populate | ||
$ca = []; | ||
|
||
# Generate the CA certificate and key pair | ||
$success = ca_create( | ||
ca: $ca, | ||
lifetime: $this->lifetime->value, | ||
dn: $this->to_x509_dn(), | ||
digest_alg: $this->digest_alg->value, | ||
keytype: $this->keytype->value, | ||
keylen: $this->keylen->value, | ||
ecname: $this->ecname->value, | ||
); | ||
|
||
# Throw a server error if the CA certificate and key pair fails to be generated | ||
if (!$success) { | ||
throw new ServerError( | ||
message: 'Failed to generate the certificate authority for unknown reason.', | ||
response_id: 'CERTIFICATE_AUTHORITY_GENERATE_FAILED', | ||
); | ||
} | ||
|
||
# Populate the `crt` and `prv` fields with the generated values | ||
$this->crt->from_internal($ca['crt']); | ||
$this->prv->from_internal($ca['prv']); | ||
$this->serial->value = $ca['serial']; | ||
} | ||
|
||
/** | ||
* Generates a new CA certificate and key pair using the requested parameters. This populate the `crt` and `prv` fields. | ||
* @throws ServerError When the CA certificate and key pair fails to be generated. | ||
*/ | ||
private function generate_intermediate_ca(): void { | ||
# Define a placeholder for ca_inter_create() to populate | ||
$ca = []; | ||
|
||
# Generate the intermediate CA certificate and key pair | ||
# Note: This also bumps the serial number of the parent CA by 1 | ||
$success = ca_inter_create( | ||
ca: $ca, | ||
caref: $this->caref->value, | ||
lifetime: $this->lifetime->value, | ||
dn: $this->to_x509_dn(), | ||
digest_alg: $this->digest_alg->value, | ||
keytype: $this->keytype->value, | ||
keylen: $this->keylen->value, | ||
ecname: $this->ecname->value, | ||
); | ||
|
||
# Throw a server error if the CA certificate and key pair fails to be generated | ||
if (!$success) { | ||
throw new ServerError( | ||
message: 'Failed to generate the intermediate certificate authority for unknown reason.', | ||
response_id: 'CERTIFICATE_AUTHORITY_GENERATE_INTERMEDIATE_FAILED', | ||
); | ||
} | ||
|
||
# Populate the `crt` and `prv` fields with the generated values | ||
$this->crt->from_internal($ca['crt']); | ||
$this->prv->from_internal($ca['prv']); | ||
$this->serial->value = $ca['serial']; | ||
} | ||
} |
Oops, something went wrong.