Skip to content

Commit

Permalink
feat: implement ca generate support #519
Browse files Browse the repository at this point in the history
  • Loading branch information
jaredhendrickson13 committed Sep 8, 2024
1 parent 8994273 commit 739f345
Show file tree
Hide file tree
Showing 3 changed files with 511 additions and 0 deletions.
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();
}
}
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'];
}
}
Loading

0 comments on commit 739f345

Please sign in to comment.