web-auth/cose-lib
PHP 8.1+ COSE (RFC 9052/9053) library for CBOR Object Signing and Encryption. Supports COSE_Sign1/Sign, Encrypt0/Encrypt, Mac0/Mac tags plus ECDSA, EdDSA, RSA/PS and HMAC algorithms; useful for WebAuthn/FIDO2 and certificates.
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);
$stream = new StringStream($cborData);
$coseSign1 = $decoder->decode($stream);
// Extract components for verification
$protectedHeader = $coseSign1->getProtectedHeader();
$payload = $coseSign1->getPayload();
$signature = $coseSign1->getSignature();
Where to Look First:
doc/Usage.md: Detailed workflows for all COSE tags.src/Cose/: Core classes (Signature/, Encryption/, Mac/).tests/): Real-world examples (e.g., COVID certificates).use Cose\Signature\CoseSign1Tag;
use CBOR\MapObject;
// 1. Define headers (alg, kid)
$protectedHeader = MapObject::create([
[1, -7], // ES256
[4, 'key-id'] // kid
]);
// 2. Generate signature (using `web-token/jwt-framework` or `paragonie/sodium`)
$signature = generateEcdsaSignature($privateKey, $payload);
// 3. Create COSE_Sign1
$coseSign1 = CoseSign1Tag::create(
$protectedHeader,
MapObject::create(), // unprotected
$payload,
$signature
);
// 1. Decode COSE_Sign1
$coseSign1 = $decoder->decode($cborData);
// 2. Reconstruct Sig_structure (RFC 9052 §3.1)
$sigStructure = Signature1::create(
$coseSign1->getProtectedHeader(),
$coseSign1->getPayload()
);
// 3. Verify with OpenSSL
$isValid = openssl_verify(
(string) $sigStructure,
$signature->getValue(),
$publicKey,
'sha256'
);
use Cose\Encryption\CoseEncrypt0Tag;
// 1. Generate ephemeral key pair and shared secret
$ephemeralKey = generateEcdhKeyPair();
$sharedSecret = deriveSharedSecret($ephemeralKey, $recipientPublicKey);
// 2. Encrypt payload (e.g., using AES-GCM)
$ciphertext = encryptWithAesGcm($sharedSecret, $payload);
// 3. Create COSE_Encrypt0
$protectedHeader = MapObject::create([
[1, -35], // ECDH-ES-AES128-GCM
[5, $ephemeralKey->getPublicKey()] // epk
]);
$coseEncrypt0 = CoseEncrypt0Tag::create(
$protectedHeader,
MapObject::create([[-1, $iv]]), // IV in unprotected header
$ciphertext
);
// 1. Decode COSE_Encrypt0
$coseEncrypt0 = $decoder->decode($cborData);
// 2. Extract ephemeral key and IV
$ephemeralKey = $coseEncrypt0->getProtectedHeaderAsMap()[[5]];
$iv = $coseEncrypt0->getUnprotectedHeader()[[-1]];
// 3. Derive shared secret and decrypt
$sharedSecret = deriveSharedSecret($recipientPrivateKey, $ephemeralKey);
$plaintext = decryptWithAesGcm($sharedSecret, $iv, $ciphertext);
use Cose\Mac\CoseMac0Tag;
// 1. Generate HMAC (e.g., HS256)
$mac = generateHmac($key, $payload);
// 2. Create COSE_Mac0
$protectedHeader = MapObject::create([[1, 5]]); // HS256
$coseMac0 = CoseMac0Tag::create(
$protectedHeader,
MapObject::create(),
$payload,
$mac
);
// 1. Decode COSE_Mac0
$coseMac0 = $decoder->decode($cborData);
// 2. Recompute MAC
$expectedMac = generateHmac($key, $coseMac0->getPayload());
// 3. Compare
$isValid = hash_equals($expectedMac, $coseMac0->getTag());
// app/Providers/CoseServiceProvider.php
public function register()
{
$this->app->singleton(CoseDecoder::class, function () {
$tagManager = TagManager::create()
->add(CoseSign1Tag::class)
->add(CoseEncrypt0Tag::class);
return Decoder::create($tagManager);
});
}
// app/Http/Middleware/ValidateCoseSignature.php
public function handle(Request $request, Closure $next)
{
$coseData = $request->header('X-Cose-Signature');
$decoder = app(CoseDecoder::class);
$coseSign1 = $decoder->decode($coseData);
if (!$this->verifySignature($coseSign1)) {
abort(403, 'Invalid COSE signature');
}
return $next($request);
}
Use Laravel’s Hash facade or web-token/jwt-framework for key storage:
// Store private key (e.g., in config)
config(['cose.keys.private' => $privateKey]);
// Retrieve in a service
$privateKey = config('cose.keys.private');
Protected Header Decoding:
protectedHeader in COSE is a CBOR-encoded map, not a plain PHP array. Use getProtectedHeaderAsMap() to decode it:
$protectedHeaderMap = $coseSign1->getProtectedHeaderAsMap();
$algorithm = $protectedHeaderMap[[1]]; // ES256 (-7)
Signature Structure Reconstruction:
Sig_structure (RFC 9052 §3.1) must include the protected header as a CBOR byte string. Omitting it will cause verification to fail:
// WRONG: Missing protected header
$sigStructure = $payload;
// CORRECT: Include protected header
$sigStructure = Signature1::create(
$coseSign1->getProtectedHeader(), // <-- Critical!
$coseSign1->getPayload()
);
Algorithm Mismatch:
alg field in the protected header must match the actual algorithm used to generate the signature. For example:
alg = -7 (ES256), use openssl_verify with 'sha256'.alg = -8 (EdDSA), use a library like paragonie/sodium:
$isValid = sodium_crypto_sign_verify_detached(
$signature,
(string) $sigStructure,
$publicKey
);
CBOR Tag Registration:
TagManager:
$tagManager = TagManager::create()
->add(CoseSign1Tag::class)
->add(MyCustomTag::class); // Your custom tag
Key ID (kid) Handling:
kid (key ID) in unprotected headers is not validated by default. Ensure it matches your key storage system:
$kid = $coseSign1->getUnprotectedHeader()[[4]];
$publicKey = $this->keyStore->findByKid($kid);
How can I help you explore Laravel packages today?