Skip to content

X.509 header parameters

Warning

No chain validation and no network access is performed by this library. RFC 9360 defines how a COSE message carries or references X.509 certificates; what to make of them is the application's. Chain building, path validation (RFC 5280 §6), revocation checking and the choice of trust anchors happen in your code, before anything is acted upon. RFC 9360 §5: "both the signature validation and the certificate validation MUST be completed successfully before acting on any requests." getX5U() returns the URI as a string and nothing else is done with it: this library never fetches a URL.

The four parameters

The four header parameters of RFC 9360 §2 have typed accessors on CoseHeaders, each looking in the protected bucket first, then in the unprotected one, and returning null when the message carries neither:

Name Label Type Reference Accessor
x5bag 32 (CoseHeaders::LABEL_X5BAG) COSE_X509 RFC 9360 §2 getX5Bag(): ?X5Bag
x5chain 33 (CoseHeaders::LABEL_X5CHAIN) COSE_X509 RFC 9360 §2 getX5Chain(): ?X5Chain
x5t 34 (CoseHeaders::LABEL_X5T) COSE_CertHash RFC 9360 §2 getX5T(): ?CoseCertHash
x5u 35 (CoseHeaders::LABEL_X5U) uri RFC 9360 §2 getX5U(): ?string
use Cose\Algorithm\Hash\SHA256;
use Cose\Algorithm\Manager;
use Cose\Algorithm\Signature\CertificateSignatureVerifier;
use Cose\Algorithm\Signature\ECDSA\ES256;
use Cose\Structure\CoseHeaders;
use SpomkyLabs\Pki\X509\CertificationPath\CertificationPath;
use SpomkyLabs\Pki\X509\CertificationPath\PathValidation\PathValidationConfig;

// RFC 9360 §2: "applications that use this header parameter MUST support the hash algorithm 'SHA-256'".
$manager = Manager::create()->add(ES256::create(), SHA256::create());
$verifier = CertificateSignatureVerifier::create($manager);
$headers = CoseHeaders::fromMessage($coseSign1);

// x5chain: the signature verifies with the end-entity certificate, in one call ...
$chain = $headers->getX5Chain();
$isValid = $verifier->verifyWithX5Chain($alg, $chain, $toBeSigned, $signature);

// ... and the proposed path is yours to validate, against your trust anchors, before you rely on the result.
$path = CertificationPath::fromCertificateChain($chain->toCertificateChain());
$path->validate(PathValidationConfig::create(new DateTimeImmutable(), 5)->withTrustAnchor($yourTrustAnchor));

// x5t: the thumbprint names the certificate to use, in the bag that travels with it or in your own store.
$x5t = $headers->getX5T();
$hash = $x5t->hashAlgorithm($manager);              // FilterOnlyHash: SHA1 is accepted here, on purpose
$leaf = $headers->getX5Bag()?->find($x5t, $hash);   // the DER certificate, or null
foreach ($yourStore as $der) {
    if ($x5t->matches($der, $hash)) { /* ... */ }
}

// x5u: a string. Whether to fetch it, over what, and whether to trust the answer is your decision.
$uri = $headers->getX5U();

getX5U() reads a text string, tagged 32 or not, with a scheme; HeaderMapHelper::assertUriValue() is the value check behind it.

The classes

The classes live in Cose\Structure\X509:

  • CoseX509 is the wire structure, COSE_X509 = bstr / [ 2*certs: bstr ]: one certificate travels as a single byte string, two or more as an array of byte strings, one DER certificate per entry. An array of length one is invalid CDDL: fromCBOR() rejects it with a message that says so, and toCBOR() never produces it. The decoder checks the shape only; toCertificates() parses the entries with spomky-labs/pki-framework when asked.
  • X5Bag and X5Chain are the two parameters over it. A bag "is unordered and may contain self-signed certificates", duplicates, and "certificates that are completely extraneous to the message"; toCertificateBundle() hands it to pki-framework for path building. A chain is "ordered starting with the certificate containing the end-entity key followed by the certificate that signed it, and so on", a candidate path that "the relying party is still required" to build and validate; endEntityCertificate() is its first entry and toCertificateChain() the CertificateChain of pki-framework.
  • CoseCertHash is COSE_CertHash = [ hashAlg: (int / tstr), hashValue: bstr ], the thumbprint x5t carries. hashAlgorithm(Manager $manager) resolves the identifier through the hash registry of the application; matches(string $der, FilterOnlyHash $hash) compares with hash_equals(); compute() makes one.

What follows from the RFC

  • Integrity. Each of the four parameters "can be in either the protected or unprotected header bucket", yet "The end-entity certificate MUST be integrity protected by COSE": by the parameter being in the protected bucket, by an x5t in the protected bucket naming the certificate, or by the certificate being in the external_aad. The accessors take the protected value first and do not tell you which bucket answered; getProtectedHeaderParameter(CoseHeaders::LABEL_X5CHAIN) does, when the distinction matters to your policy.
  • Thumbprints are computed over the bytes as carried. compute() and matches() take the DER string, never a parsed certificate: a certificate re-encoded by an ASN.1 library is not always the byte string that was sent (the cose-wg/Examples certificates are a case in point), and only the bytes on the wire give two implementations the same digest. A thumbprint "does not provide any trust" (§5): it selects a candidate, which is then verified for real, so SHA-1 (-14) and SHA-256/64 (-15) are legitimate here and hashAlgorithm() returns FilterOnlyHash.
  • The certificate is untrusted input until validated: "The presence of a self-signed certificate in the parameter MUST NOT cause the update of the set of trust anchors without some out-of-band confirmation." Before using its key, "the key MUST be checked against the algorithm to be used", which the registered Signature instance does, with the policies it was created with, when verifyWithX5Chain() calls it.

The *-sender parameters

The three header algorithm parameters of RFC 9360 §3, x5t-sender (-27), x5u-sender (-28) and x5chain-sender (-29), identify the sender's key exchange certificate of an ECDH-SS recipient, and getX5TSender(), getX5USender() and getX5ChainSender() read them exactly as their §2 counterparts: same structures, same rules, same absence of trust. The key they name is what the application hands to the algorithm through RecipientLayer::withSenderKey(), after validating the chain, out of its own store, or from wherever the URI it chose to fetch led; see What each family enforces. C509 certificates (a draft) are out of scope.

examples/13-x509-header-parameters.php runs the whole of this on the certificates of cose-wg/Examples.