| Current Path : /home/rtorresani/www/vendor/spomky-labs/pki-framework/src/X509/Certificate/ |
| Current File : //home/rtorresani/www/vendor/spomky-labs/pki-framework/src/X509/Certificate/TBSCertificate.php |
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X509\Certificate;
use Brick\Math\BigInteger;
use function count;
use LogicException;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Type\Constructed\Sequence;
use SpomkyLabs\Pki\ASN1\Type\Primitive\Integer;
use SpomkyLabs\Pki\ASN1\Type\Tagged\ExplicitlyTaggedType;
use SpomkyLabs\Pki\ASN1\Type\Tagged\ImplicitlyTaggedType;
use SpomkyLabs\Pki\CryptoBridge\Crypto;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\AlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Feature\SignatureAlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\Asymmetric\PrivateKeyInfo;
use SpomkyLabs\Pki\CryptoTypes\Asymmetric\PublicKeyInfo;
use SpomkyLabs\Pki\X501\ASN1\Name;
use SpomkyLabs\Pki\X509\Certificate\Extension\AuthorityKeyIdentifierExtension;
use SpomkyLabs\Pki\X509\Certificate\Extension\Extension;
use SpomkyLabs\Pki\X509\Certificate\Extension\SubjectKeyIdentifierExtension;
use SpomkyLabs\Pki\X509\CertificationRequest\CertificationRequest;
use function strval;
use UnexpectedValueException;
/**
* Implements *TBSCertificate* ASN.1 type.
*
* @see https://tools.ietf.org/html/rfc5280#section-4.1.2
*/
final class TBSCertificate
{
// Certificate version enumerations
final public const VERSION_1 = 0;
final public const VERSION_2 = 1;
final public const VERSION_3 = 2;
/**
* Certificate version.
*/
private ?int $version = null;
/**
* Serial number.
*/
private ?string $serialNumber = null;
/**
* Signature algorithm.
*/
private ?SignatureAlgorithmIdentifier $signature = null;
/**
* Issuer unique identifier.
*/
private ?UniqueIdentifier $issuerUniqueID = null;
/**
* Subject unique identifier.
*/
private ?UniqueIdentifier $subjectUniqueID = null;
/**
* Extensions.
*/
private Extensions $extensions;
/**
* @param Name $subject Certificate subject
* @param PublicKeyInfo $subjectPublicKeyInfo Subject public key
* @param Name $issuer Certificate issuer
* @param Validity $validity Validity period
*/
private function __construct(
private Name $subject,
private PublicKeyInfo $subjectPublicKeyInfo,
private Name $issuer,
private Validity $validity
) {
$this->extensions = Extensions::create();
}
public static function create(
Name $subject,
PublicKeyInfo $subjectPublicKeyInfo,
Name $issuer,
Validity $validity
): self {
return new self($subject, $subjectPublicKeyInfo, $issuer, $validity);
}
/**
* Initialize from ASN.1.
*/
public static function fromASN1(Sequence $seq): self
{
$idx = 0;
if ($seq->hasTagged(0)) {
++$idx;
$version = $seq->getTagged(0)
->asExplicit()
->asInteger()
->intNumber();
} else {
$version = self::VERSION_1;
}
$serial = $seq->at($idx++)
->asInteger()
->number();
$algo = AlgorithmIdentifier::fromASN1($seq->at($idx++)->asSequence());
if (! $algo instanceof SignatureAlgorithmIdentifier) {
throw new UnexpectedValueException('Unsupported signature algorithm ' . $algo->name() . '.');
}
$issuer = Name::fromASN1($seq->at($idx++)->asSequence());
$validity = Validity::fromASN1($seq->at($idx++)->asSequence());
$subject = Name::fromASN1($seq->at($idx++)->asSequence());
$pki = PublicKeyInfo::fromASN1($seq->at($idx++)->asSequence());
$tbs_cert = self::create($subject, $pki, $issuer, $validity)
->withVersion($version)
->withSerialNumber($serial)
->withSignature($algo)
;
if ($seq->hasTagged(1)) {
$tbs_cert = $tbs_cert->withIssuerUniqueID(UniqueIdentifier::fromASN1(
$seq->getTagged(1)
->asImplicit(Element::TYPE_BIT_STRING)
->asBitString()
));
}
if ($seq->hasTagged(2)) {
$tbs_cert = $tbs_cert->withSubjectUniqueID(UniqueIdentifier::fromASN1(
$seq->getTagged(2)
->asImplicit(Element::TYPE_BIT_STRING)
->asBitString()
));
}
if ($seq->hasTagged(3)) {
$tbs_cert = $tbs_cert->withExtensions(Extensions::fromASN1($seq->getTagged(3)->asExplicit()->asSequence()));
}
return $tbs_cert;
}
/**
* Initialize from certification request.
*
* Note that signature is not verified and must be done by the caller.
*/
public static function fromCSR(CertificationRequest $cr): self
{
$cri = $cr->certificationRequestInfo();
$tbs_cert = self::create(
$cri->subject(),
$cri->subjectPKInfo(),
Name::create(),
Validity::fromStrings(null, null)
);
// if CSR has Extension Request attribute
if ($cri->hasAttributes()) {
$attribs = $cri->attributes();
if ($attribs->hasExtensionRequest()) {
$tbs_cert = $tbs_cert->withExtensions($attribs->extensionRequest()->extensions());
}
}
// add Subject Key Identifier extension
return $tbs_cert->withAdditionalExtensions(
SubjectKeyIdentifierExtension::create(false, $cri->subjectPKInfo()->keyIdentifier())
);
}
/**
* Get self with fields set from the issuer's certificate.
*
* Issuer shall be set to issuing certificate's subject. Authority key identifier extensions shall be added with a
* key identifier set to issuing certificate's public key identifier.
*
* @param Certificate $cert Issuing party's certificate
*/
public function withIssuerCertificate(Certificate $cert): self
{
$obj = clone $this;
// set issuer DN from cert's subject
$obj->issuer = $cert->tbsCertificate()
->subject();
// add authority key identifier extension
$key_id = $cert->tbsCertificate()
->subjectPublicKeyInfo()
->keyIdentifier();
$obj->extensions = $obj->extensions->withExtensions(AuthorityKeyIdentifierExtension::create(false, $key_id));
return $obj;
}
/**
* Get self with given version.
*
* If version is not set, appropriate version is automatically determined during signing.
*/
public function withVersion(int $version): self
{
$obj = clone $this;
$obj->version = $version;
return $obj;
}
/**
* Get self with given serial number.
*
* @param int|string $serial Base 10 number
*/
public function withSerialNumber(int|string $serial): self
{
$obj = clone $this;
$obj->serialNumber = strval($serial);
return $obj;
}
/**
* Get self with random positive serial number.
*
* @param int $size Number of random bytes
*/
public function withRandomSerialNumber(int $size): self
{
// ensure that first byte is always non-zero and having first bit unset
$num = BigInteger::of(random_int(1, 0x7f));
for ($i = 1; $i < $size; ++$i) {
$num = $num->shiftedLeft(8);
$num = $num->plus(random_int(0, 0xff));
}
return $this->withSerialNumber($num->toBase(10));
}
/**
* Get self with given signature algorithm.
*/
public function withSignature(SignatureAlgorithmIdentifier $algo): self
{
$obj = clone $this;
$obj->signature = $algo;
return $obj;
}
/**
* Get self with given issuer.
*/
public function withIssuer(Name $issuer): self
{
$obj = clone $this;
$obj->issuer = $issuer;
return $obj;
}
/**
* Get self with given validity.
*/
public function withValidity(Validity $validity): self
{
$obj = clone $this;
$obj->validity = $validity;
return $obj;
}
/**
* Get self with given subject.
*/
public function withSubject(Name $subject): self
{
$obj = clone $this;
$obj->subject = $subject;
return $obj;
}
/**
* Get self with given subject public key info.
*/
public function withSubjectPublicKeyInfo(PublicKeyInfo $pub_key_info): self
{
$obj = clone $this;
$obj->subjectPublicKeyInfo = $pub_key_info;
return $obj;
}
/**
* Get self with issuer unique ID.
*/
public function withIssuerUniqueID(UniqueIdentifier $id): self
{
$obj = clone $this;
$obj->issuerUniqueID = $id;
return $obj;
}
/**
* Get self with subject unique ID.
*/
public function withSubjectUniqueID(UniqueIdentifier $id): self
{
$obj = clone $this;
$obj->subjectUniqueID = $id;
return $obj;
}
/**
* Get self with given extensions.
*/
public function withExtensions(Extensions $extensions): self
{
$obj = clone $this;
$obj->extensions = $extensions;
return $obj;
}
/**
* Get self with extensions added.
*
* @param Extension ...$exts One or more Extension objects
*/
public function withAdditionalExtensions(Extension ...$exts): self
{
$obj = clone $this;
$obj->extensions = $obj->extensions->withExtensions(...$exts);
return $obj;
}
/**
* Check whether version is set.
*/
public function hasVersion(): bool
{
return isset($this->version);
}
/**
* Get certificate version.
*/
public function version(): int
{
if (! $this->hasVersion()) {
throw new LogicException('version not set.');
}
return $this->version;
}
/**
* Check whether serial number is set.
*/
public function hasSerialNumber(): bool
{
return isset($this->serialNumber);
}
/**
* Get serial number.
*
* @return string Base 10 integer
*/
public function serialNumber(): string
{
if (! $this->hasSerialNumber()) {
throw new LogicException('serialNumber not set.');
}
return $this->serialNumber;
}
/**
* Check whether signature algorithm is set.
*/
public function hasSignature(): bool
{
return isset($this->signature);
}
/**
* Get signature algorithm.
*/
public function signature(): SignatureAlgorithmIdentifier
{
if (! $this->hasSignature()) {
throw new LogicException('signature not set.');
}
return $this->signature;
}
public function issuer(): Name
{
return $this->issuer;
}
/**
* Get validity period.
*/
public function validity(): Validity
{
return $this->validity;
}
public function subject(): Name
{
return $this->subject;
}
/**
* Get subject public key.
*/
public function subjectPublicKeyInfo(): PublicKeyInfo
{
return $this->subjectPublicKeyInfo;
}
/**
* Whether issuer unique identifier is present.
*/
public function hasIssuerUniqueID(): bool
{
return isset($this->issuerUniqueID);
}
public function issuerUniqueID(): UniqueIdentifier
{
if (! $this->hasIssuerUniqueID()) {
throw new LogicException('issuerUniqueID not set.');
}
return $this->issuerUniqueID;
}
/**
* Whether subject unique identifier is present.
*/
public function hasSubjectUniqueID(): bool
{
return isset($this->subjectUniqueID);
}
public function subjectUniqueID(): UniqueIdentifier
{
if (! $this->hasSubjectUniqueID()) {
throw new LogicException('subjectUniqueID not set.');
}
return $this->subjectUniqueID;
}
public function extensions(): Extensions
{
return $this->extensions;
}
/**
* Generate ASN.1 structure.
*/
public function toASN1(): Sequence
{
$elements = [];
$version = $this->version();
// if version is not default
if ($version !== self::VERSION_1) {
$elements[] = ExplicitlyTaggedType::create(0, Integer::create($version));
}
$serial = $this->serialNumber();
$signature = $this->signature();
// add required elements
array_push(
$elements,
Integer::create($serial),
$signature->toASN1(),
$this->issuer->toASN1(),
$this->validity->toASN1(),
$this->subject->toASN1(),
$this->subjectPublicKeyInfo->toASN1()
);
if (isset($this->issuerUniqueID)) {
$elements[] = ImplicitlyTaggedType::create(1, $this->issuerUniqueID->toASN1());
}
if (isset($this->subjectUniqueID)) {
$elements[] = ImplicitlyTaggedType::create(2, $this->subjectUniqueID->toASN1());
}
if (count($this->extensions) !== 0) {
$elements[] = ExplicitlyTaggedType::create(3, $this->extensions->toASN1());
}
return Sequence::create(...$elements);
}
/**
* Create signed certificate.
*
* @param SignatureAlgorithmIdentifier $algo Algorithm used for signing
* @param PrivateKeyInfo $privkey_info Private key used for signing
* @param null|Crypto $crypto Crypto engine, use default if not set
*/
public function sign(
SignatureAlgorithmIdentifier $algo,
PrivateKeyInfo $privkey_info,
?Crypto $crypto = null
): Certificate {
$crypto ??= Crypto::getDefault();
$tbs_cert = clone $this;
if (! isset($tbs_cert->version)) {
$tbs_cert->version = $tbs_cert->_determineVersion();
}
if (! isset($tbs_cert->serialNumber)) {
$tbs_cert->serialNumber = '0';
}
$tbs_cert->signature = $algo;
$data = $tbs_cert->toASN1()
->toDER();
$signature = $crypto->sign($data, $privkey_info, $algo);
return Certificate::create($tbs_cert, $algo, $signature);
}
/**
* Determine minimum version for the certificate.
*/
private function _determineVersion(): int
{
// if extensions are present
if (count($this->extensions) !== 0) {
return self::VERSION_3;
}
// if UniqueIdentifier is present
if (isset($this->issuerUniqueID) || isset($this->subjectUniqueID)) {
return self::VERSION_2;
}
return self::VERSION_1;
}
}