Skip to content

COSE_Encrypt

The content layer is the same, under the "Encrypt" context, with a content encryption key (CEK) drawn at random and wrapped for each recipient:

use CBOR\ListObject;
use CBOR\Tag\CoseEncryptTag;
use Cose\Encryption\EncryptStructure;
use Cose\Structure\CoseRecipient;

$ciphertext = EncryptStructure::create($protectedHeader)->encrypt($algorithm, $contentEncryptionKey, $plaintext, $nonce);

$recipients = ListObject::create([
    ListObject::create([/* recipient 1: protected, unprotected, wrapped CEK */]),
    ListObject::create([/* recipient 2 */]),
]);

$coseEncrypt = CoseEncryptTag::create(ListObject::create([
    $protectedHeader,
    $unprotectedHeader,
    ByteStringObject::create($ciphertext),
    $recipients,
]));

// Reading them back as checked views; a recipient may carry recipients of its own
foreach (CoseRecipient::all($coseEncrypt->getRecipients()) as $recipient) {
    $kid = $recipient->getUnprotectedHeaderParameter(4);
    $wrappedKey = $recipient->hasDetachedCiphertext() ? null : $recipient->getCiphertext();
    $nested = $recipient->getRecipients(); // list<CoseRecipient>
}

Warning

The additional authenticated data of the content encryption is the Enc_structure of RFC 9052 §5.3, not the protected header on its own. Encrypt0Structure and EncryptStructure differ by their context string only, and that difference is authenticated: a ciphertext produced for a COSE_Encrypt does not open as a COSE_Encrypt0. For a recipient layer, the structure is RecipientStructure::forEncryptRecipient() ("Enc_Recipient"):

use Cose\Encryption\Encrypt0Structure;
use Cose\Encryption\EncryptStructure;
use Cose\Encryption\RecipientStructure;

$aad = (string) EncryptStructure::create($coseEncrypt->getProtectedHeader());               // "Encrypt"
$aad = (string) Encrypt0Structure::create($coseEncrypt0->getProtectedHeader());             // "Encrypt0"
$aad = (string) RecipientStructure::forEncryptRecipient($recipient->getProtectedHeader());  // "Enc_Recipient"

RFC 9052 §5.1 writes the list as [+COSE_recipient]: at least one entry, each a [bstr, map, bstr / nil, ? [+ COSE_recipient]] array. CoseRecipient::all() applies that rule, nested levels included.

The recipient entries are filled by the key management algorithms of RFC 9053 §5 and §6, and EncryptStructure::encryptFor() runs the whole of it (draw the CEK, wrap or derive it for each recipient, encrypt the content, assemble the message) in one call:

use Cose\Algorithm\ContentEncryption\A128GCM;
use Cose\Algorithm\KeyManagement\A256KW;
use Cose\Algorithm\KeyManagement\ECDH_ES_A128KW;
use Cose\Encryption\EncryptStructure;
use Cose\Encryption\Recipient;

$algorithm = A128GCM::create();
$coseEncrypt = EncryptStructure::create($protectedHeader)->encryptFor($algorithm, $plaintext, random_bytes(12), [
    Recipient::create(A256KW::create(), $sharedKek, null, $kidHeader),  // a Symmetric key the parties share
    Recipient::create(ECDH_ES_A128KW::create(), $bobPublicKey),         // Bob's EC2 or OKP public key
]); // CBOR\Tag\CoseEncryptTag: the IV is in its unprotected bucket, each COSE_recipient carries its "alg"

Opening it is the reverse, one recipient at a time: the recipient's own key, the algorithm its headers announce, and a RecipientLayer that says what the recovered key is for:

use Cose\Algorithm\KeyManagement\KeyManagement;
use Cose\Algorithm\KeyManagement\RecipientLayer;
use Cose\Encryption\InitializationVector;
use Cose\Key\SymmetricKey;
use Cose\Structure\CoseHeaders;

$entries = CoseRecipient::all($coseEncrypt->getRecipients());
$mine = $entries[1]; // located by "kid", by the ephemeral key, or however the application names its recipients
$keyManagement = $manager->get((int) $mine->headers()->getHeaderParameter(1)?->normalize());
assert($keyManagement instanceof KeyManagement);

$cek = $keyManagement->recoverKey(RecipientLayer::fromRecipient($mine, $algorithm, null, count($entries)), $bobPrivateKey);
$plaintext = EncryptStructure::create($coseEncrypt->getProtectedHeader())->decrypt(
    $algorithm,
    SymmetricKey::create([SymmetricKey::TYPE => SymmetricKey::TYPE_OCT, SymmetricKey::DATA_K => $cek]),
    $coseEncrypt->getCiphertext()->getValue(),
    InitializationVector::resolve(CoseHeaders::fromMessage($coseEncrypt), $algorithm->nonceLength())
);

See Key management for the eighteen algorithms, the rules each family enforces, and the parameters of a static-static agreement; and examples/05-encrypt-recipients.php for the whole of it, nested recipients included.