web-auth/cose-lib
PHP 8.1+ COSE (RFC 9052/9053) library supporting Sign1/Sign, Encrypt0/Encrypt, Mac0/Mac with full tag support. Implements ECDSA, EdDSA, RSA and HMAC algorithms for signing, encryption and MAC; compatible with WebAuthn/FIDO2.
Installation:
composer require web-auth/cose-lib spomky-labs/cbor-php
Ensure PHP 8.1+ with ext-json and ext-openssl enabled.
First Use Case: Verify a COSE_Sign1 signature (e.g., from a WebAuthn attestation or EU Digital COVID Certificate):
use CBOR\Decoder;
use CBOR\StringStream;
use CBOR\Tag\TagManager;
use Cose\Signature\CoseSign1Tag;
$tagManager = TagManager::create()->add(CoseSign1Tag::class);
$decoder = Decoder::create($tagManager);
$coseSign1 = $decoder->decode(new StringStream($cborData));
// Extract components for verification
$payload = $coseSign1->getPayload();
$signature = $coseSign1->getSignature();
Where to Look First:
doc/Usage.md: Detailed API reference and examples for all COSE tags.src/Cose/: Core classes (CoseSign1Tag, CoseEncrypt0Tag, etc.).tests/Integration/ for COVID certificates).// Generate key (e.g., ECDSA P-256)
$key = openssl_pkey_new(['digest_alg' => 'sha256']);
// Sign payload
openssl_sign($payload, $signature, $key, OPENSSL_ALGO_DSA);
// Create COSE_Sign1
$protectedHeader = MapObject::create([
MapItem::create(UnsignedIntegerObject::create(1), NegativeIntegerObject::create(-7)) // alg: ES256
]);
$coseSign1 = CoseSign1Tag::create(
$protectedHeader,
MapObject::create(), // unprotected header
ByteStringObject::create($payload),
ByteStringObject::create($signature)
);
// Decode COSE_Sign1
$coseSign1 = $decoder->decode(new StringStream($cborData));
// Reconstruct Sig_structure (RFC 9052 §3.1)
$sigStructure = Signature1::create(
$coseSign1->getProtectedHeader(),
$coseSign1->getPayload()
);
// Verify with OpenSSL
$isValid = openssl_verify(
(string) $sigStructure,
$coseSign1->getSignature()->getValue(),
$publicKey,
'sha256'
);
// Encrypt payload (e.g., using libsodium)
$ciphertext = sodium_crypto_box($payload, $nonce, $publicKey);
// Create COSE_Encrypt0
$protectedHeader = MapObject::create([
MapItem::create(UnsignedIntegerObject::create(1), UnsignedIntegerObject::create(25)) // alg: XChaCha20Poly1305
]);
$coseEncrypt0 = CoseEncrypt0Tag::create(
$protectedHeader,
MapObject::create([/* IV, kid */]),
ByteStringObject::create($ciphertext)
);
public function register()
{
$this->app->singleton(CoseDecoder::class, function () {
return Decoder::create(
TagManager::create()
->add(CoseSign1Tag::class)
->add(CoseEncrypt0Tag::class),
OtherObjectManager::create()
);
});
}
public function handle($request, Closure $next)
{
$cborData = $request->header('X-COSE-Signature');
$decoder = app(CoseDecoder::class);
$coseSign1 = $decoder->decode(new StringStream($cborData));
if (!$this->verifySignature($coseSign1)) {
abort(403, 'Invalid signature');
}
return $next($request);
}
Reuse Decoders:
Create a singleton CoseDecoder instance in Laravel’s service container to avoid reinitializing TagManager/OtherObjectManager on every request.
Algorithm Mappings:
Use a helper method to map algorithm IDs (e.g., -7 → ES256) to OpenSSL constants:
function getOpenSslAlgorithm(int $coseAlg): string
{
return match ($coseAlg) {
-7 => 'sha256',
-35 => 'sha384',
-36 => 'sha512',
default => throw new \InvalidArgumentException("Unsupported algorithm: $coseAlg"),
};
}
Error Handling: Wrap COSE operations in try-catch blocks to handle malformed CBOR or unsupported algorithms:
try {
$coseSign1 = $decoder->decode($stream);
} catch (CborException $e) {
Log::error("Invalid COSE data: {$e->getMessage()}");
abort(400);
}
Key Management:
Store public keys in Laravel’s cache or database with a key_id (from the kid header) for quick lookup:
$kid = $coseSign1->getUnprotectedHeaderAsMap()->get(4)?->getValue(); // kid
$publicKey = cache()->get("cose_public_key:$kid");
Protected Header Decoding:
protectedHeader in COSE_Sign1 is a CBOR-encoded map, not a plain map. Use getProtectedHeaderAsMap() to decode it:
$protectedHeaderMap = $coseSign1->getProtectedHeaderAsMap();
$algorithm = $protectedHeaderMap->get(1)?->getValue(); // alg
Decoder to getProtectedHeaderAsMap():
$customDecoder = Decoder::create(
TagManager::create()->add(MyCustomTag::class)
);
$protectedHeaderMap = $coseSign1->getProtectedHeaderAsMap($customDecoder);
Signature Verification:
Sig_structure (RFC 9052 §3.1) must include the protected header and payload. Omitting either will fail verification.openssl_verify:
$derSignature = ECSignature::toAsn1($coseSign1->getSignature()->getValue(), 64);
Algorithm Compatibility:
openssl_sign with OPENSSL_ALGO_RSA_PSS:
openssl_sign($payload, $signature, $key, OPENSSL_ALGO_RSA_PSS, ['salt_length' => 'auto']);
sodium_crypto_sign_detached and convert the signature to COSE format:
$signature = sodium_crypto_sign_detached($payload, $privateKey);
CBOR Encoding/Decoding:
payload in COSE_Sign1 is a ByteStringObject, but it may represent text or binary data. Always encode/decode strings explicitly:
$payload = ByteStringObject::create($stringPayload); // For text
// OR
$payload = ByteStringObject::create($binaryData); // For binary
Unprotected Headers:
kid, exp), not sensitive data.Inspect CBOR Data:
Use spomky-labs/cbor-php’s Encoder to pretty-print COSE objects:
$encoder = Encoder::create();
$prettyCbor = $encoder->encode($coseSign1, EncoderOptions::create()->withPrettyPrint());
Validate COSE Structures:
Check the tag field to ensure you’re handling the correct COSE type:
if ($coseSign1->getTag() !== 18) {
throw new \RuntimeException("Expected COSE_Sign1 (tag
How can I help you explore Laravel packages today?