Key restrictions (alg and key_ops)¶
A COSE key may restrict itself. RFC 9052, section 7.1 gives
it two parameters for that: alg (label 3) pins it to one algorithm ("If the algorithms do not match, then this key
object MUST NOT be used to perform the cryptographic operation") and key_ops (label 4) pins it to a set of
operations, whose values are those of Table 5: sign (1), verify (2), encrypt (3), decrypt (4), wrap key
(5), unwrap key (6), derive key (7), derive bits (8), MAC create (9) and MAC verify (10) for the algorithms
this library implements. RFC 9053 repeats both as a
per-algorithm requirement for ECDSA (§2.1), EdDSA (§2.2), HMAC (§3.1), AES-CBC-MAC (§3.2), the content encryption
algorithms (§4.1 to §4.3) and the key management algorithms (§6).
For the signature and MAC algorithms, enforcing them is opt-in, so that a key which used to work keeps working.
Ask an algorithm, or a whole Manager, to enforce the restrictions, and it refuses the key with an
InvalidArgumentException whenever the key forbids what is being done with it. The
content encryption and
key management algorithms, which have no such history, enforce them from the
start and take withKeyRestrictionsEnforced(false) to stop:
use Cose\Algorithm\Manager;
use Cose\Algorithm\Signature\ECDSA\ES256;
use Cose\Algorithm\Signature\RSA\RS256;
$algorithm = ES256::create()->withKeyRestrictionsEnforced();
// …or for every algorithm of a manager at once
$manager = Manager::create()
->add(ES256::create(), RS256::create())
->withKeyRestrictionsEnforced();
// The key says "alg": -7 and "key_ops": [2], i.e. ES256, verification only
$isValid = $algorithm->verify($data, $key, $signature); // fine
$signature = $algorithm->sign($data, $key); // InvalidArgumentException: the key does not allow "sign"
withKeyRestrictionsEnforced() returns a new algorithm and leaves the one it is called on untouched, so a manager
that enforces the restrictions can live next to one that does not. enforcesKeyRestrictions() says which one you are
holding, and withKeyRestrictionsEnforced(false) turns it off again.
This matters when the algorithm is chosen from the message rather than from the key. A verifier that reads alg from
the protected header and looks it up with Manager::get() has, without enforcement, no reason to refuse an RS1
(RSASSA-PKCS1-v1_5 with SHA-1) signature made under a key that says it is for RS256, nor a 64 bit HMAC tag under a key
that says it is for HMAC 256/256. With enforcement, the key itself rejects the downgrade.
The restrictions can also be read and applied without going through an algorithm:
use Cose\Algorithm\Signature\ECDSA\ES256;
use Cose\Key\Key;
$key->alg(); // -7; throws when "alg" is absent or is not an algorithm identifier
$key->keyOps(); // [2] or ['verify'], null when the key carries no "key_ops"
// Throws an InvalidArgumentException naming the restriction that is not satisfied
$key->assertUsableWith(ES256::ID, Key::OP_VERIFY);
// …or ask without the exception
$isUsable = $key->isUsableWith(ES256::ID, Key::OP_SIGN);
Three details are worth knowing:
- Identifiers are compared as they are. A key with
alg=ES256(-7) is refused byESP256(-9), and one withalg=EdDSA(-8) is refused by the fully-specifiedEd25519(-19), even though the cryptography is the same. RFC 9864, section 7 asks for it: "A cryptographic key MUST be used with only a single algorithm unless the use of the same key with different algorithms is proven secure." A key meant to serve both carries noalgat all. key_opsaccepts both spellings. COSE writes the operations as the integers of Table 5; a key converted from a JWK may carry the text names JOSE uses ("sign","verify","MAC create","MAC verify"). Both are recognised.- An operation may go by two names. RFC 9053 §4 lets a content encryption key list
encryptorwrap keyto encrypt, anddecryptorunwrap keyto decrypt.Key::assertUsableWithAny($alg, Key::OP_ENCRYPT, Key::OP_WRAP_KEY)passes on either and fails naming both;assertUsableWith()is its single-operation form.
Key::alg() is strict about the value it reads: an alg that is not an integer, the text 'RS256' for instance,
throws instead of being cast to 0, an identifier no algorithm is registered under. An integer written as a string
('-7') is accepted, as the key constructors do for kty and crv.