Skip to content

Instantly share code, notes, and snippets.

@SagePtr
Created September 24, 2025 00:34
Show Gist options
  • Select an option

  • Save SagePtr/1a0e88473b12a1c05f943466e53f98a8 to your computer and use it in GitHub Desktop.

Select an option

Save SagePtr/1a0e88473b12a1c05f943466e53f98a8 to your computer and use it in GitHub Desktop.
<?php
class DerHelpers
{
public const COSE_ALG_EDDSA = -8;
public const COSE_ALG_ES256 = -7;
public const COSE_ALG_RS256 = -257;
public const COSE_KTY_OKP = 1;
public const COSE_KTY_EC2 = 2;
public const COSE_KTY_RSA = 3;
public const COSE_CRV_P256 = 1;
public const COSE_CRV_ED25519 = 6;
private static function derLength(int $length): string
{
if ($length < 0x80) {
return \chr($length);
}
$bytes = '';
while ($length > 0) {
$bytes .= \chr($length & 0xFF);
$length >>= 8;
}
return \chr(0x80 | \strlen($bytes)) . \strrev($bytes);
}
private static function derTLV(int $tag, string $value): string
{
return \chr($tag) . self::derLength(\strlen($value)) . $value;
}
private static function derSequence(array $items): string
{
return self::derTLV(0x30, implode('', $items));
}
private static function derOID(string $oid): string
{
$oidbytes = \array_map('\intval', \explode('.', $oid));
$bytes = \chr($oidbytes[0] * 40 + $oidbytes[1]);
foreach (\array_slice($oidbytes, 2) as $x) {
$part = \chr($x & 0x7F);
while ($x >>= 7) {
$part .= \chr($x & 0x7F | 0x80);
}
$bytes .= \strrev($part);
}
return self::derTLV(0x06, $bytes);
}
private static function derBitString(string $bytes, int $unusedBits = 0): string
{
return self::derTLV(0x03, \chr($unusedBits) . $bytes);
}
private static function derPositiveInteger(string $bytes): string
{
$lzero = (\ord($bytes[0]) & 0x80) ? "\0" : '';
return self::derTLV(0x02, $lzero . $bytes);
}
public static function derNULL(): string
{
return "\x05\0";
}
private static function derPublicKeyOKP(int $curve, string $bytes): ?string
{
$curveParams = match ($curve) {
self::COSE_CRV_ED25519 => ['oid' => '1.3.101.112', 'length' => 32],
default => null,
};
if (!$curveParams) {
return null;
}
if (\strlen($bytes) !== $curveParams['length']) {
return null;
}
return self::derSequence([
self::derSequence([
self::derOID($curveParams['oid']),
]),
self::derBitString($bytes),
]);
}
public static function derPublicKeyEC2(int $curve, string $x, string $y): ?string
{
$curveParams = match ($curve) {
self::COSE_CRV_P256 => ['oid' => '1.2.840.10045.3.1.7', 'length' => 32],
default => null,
};
if (!$curveParams) {
return null;
}
// @see https://www.rfc-editor.org/rfc/rfc9053.html#name-double-coordinate-curves
// "Leading-zero octets MUST be preserved."
if (\strlen($x) !== $curveParams['length'] || \strlen($y) !== $curveParams['length']) {
return null;
}
return self::derSequence([
self::derSequence([
self::derOID('1.2.840.10045.2.1'),
self::derOID($curveParams['oid']),
]),
self::derBitString("\x04" . $x . $y),
]);
}
public static function derPublicKeyRSA(string $modulus, string $publicExponent): ?string
{
return self::derSequence([
self::derSequence([
self::derOID('1.2.840.113549.1.1.1'),
self::derNULL(),
]),
self::derBitString(
self::derSequence([
self::derPositiveInteger($modulus),
self::derPositiveInteger($publicExponent),
])
),
]);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment