Packages · Digital identity & credentials
k2gl/sd-jwt-vc
SD-JWT Verifiable Credentials (dc+sd-jwt): issue and verify.
The credential layer over SD-JWT — vct rules and issuer key discovery for EUDI-style relying parties.
Install
composer require k2gl/sd-jwt-vc Reach for it when
- You verify dc+sd-jwt credentials as a relying party (issuer metadata, x5c, or pinned keys).
- You issue SD-JWT VCs and want the vct/protected-claims rules enforced.
Look elsewhere when
- You need the raw SD-JWT format without the credential rules — use sd-jwt directly.
k2gl/sd-jwt-vc
SD-JWT-based Verifiable Credentials
(draft-ietf-oauth-sd-jwt-vc)
in pure PHP: issue and verify dc+sd-jwt credentials — the format HAIP and the EU Digital
Identity Wallet build on. RFC 9901 processing is provided by
k2gl/sd-jwt; this package adds the credential layer:
the typ/vct rules, the protected-claims rules, and Issuer key discovery.
Tracks draft -19 (in IETF Last Call). The test suite verifies the draft's own worked
examples byte for byte. The typ is dc+sd-jwt; the pre-2024 vc+sd-jwt is refused unless
you opt in (acceptLegacyType: true) — draft -19 ended the transition period.
Install
composer require k2gl/sd-jwt-vc
Requires PHP 8.1+. The JWT VC Issuer Metadata resolver speaks PSR-18/PSR-17, so bring any HTTP client (or none, if you pin keys or use x5c).
Usage
Verify a presentation (Verifier / relying party)
use K2gl\SdJwt\KeyBinding;
use K2gl\SdJwtVc\JwtVcIssuerMetadata;
use K2gl\SdJwtVc\SdJwtVcVerifier;
$verifier = new SdJwtVcVerifier;
$credential = $verifier->verifyPresentation(
$compact,
new JwtVcIssuerMetadata($psr18Client, $psr17RequestFactory), // resolves /.well-known/jwt-vc-issuer
KeyBinding::required(audience: 'https://verifier.example.org', nonce: $nonce),
);
$credential->vct(); // "https://credentials.example.com/identity_credential"
$credential->claims(); // the Processed SD-JWT Payload
$credential->status(); // the status claim, if any (e.g. Token Status List reference)
Key discovery mechanisms (Section 2.5), all pluggable via IssuerKeyResolver:
JwtVcIssuerMetadata— web-based resolution from/.well-known/jwt-vc-issuer,jwksorjwks_uri, kid-aware,issuer⇔issidentity enforced.X5cIssuerKeys— thex5cheader chain, validated against your trust anchors (signatures, validity windows, anchor membership; revocation is out of scope).StaticIssuerKeys— keys pinned in configuration.- Any k2gl/dsse
Verifierdirectly, when there is exactly one trusted key.
Issue a credential
use K2gl\SdJwt\Jws\JwsSigner;
use K2gl\SdJwt\Sd;
use K2gl\SdJwtVc\SdJwtVcIssuer;
$issuer = new SdJwtVcIssuer(JwsSigner::es256FromPem($pem));
$credential = $issuer->issue([
'vct' => 'https://credentials.example.com/identity_credential',
'iss' => 'https://issuer.example.com',
'iat' => time(),
'cnf' => ['jwk' => $holderJwk],
'given_name' => Sd::hide('John'),
'birthdate' => Sd::hide('1983-07-29'),
]);
$compact = $credential->toCompact();
The typ header is always dc+sd-jwt; vct is required; the claims that the draft
forbids disclosing selectively (iss, nbf, exp, cnf, vct, vct#integrity,
aka_vcts, status) are rejected if wrapped in Sd::hide(), sub-claims included — and
rejected again at verification time if a foreign issuer tried it. aka_vcts, the additional
types a credential is also known as, is validated on both sides and read back with
$credential->alsoKnownAsTypes().
Presentation building (selecting disclosures, Key Binding) is the Holder side and lives in
k2gl/sd-jwt's Presentation.
Fetching metadata safely
JwtVcIssuerMetadata dereferences URLs derived from the credential's iss claim, so it
follows the draft's Section 3 retrieval rules rather than trusting the network: a document
counts only when the response is 2xx and application/json; redirects are followed by
hand (three at most) so that every hop, like the original URL, passes the UrlPolicy —
HTTPS only, no loopback, link-local or private address, and DNS names resolved and checked
the same way; the body is read up to a size limit (1 MiB by default). Time limits are the
PSR-18 client's job — configure its timeout. Tune the policy per deployment:
new JwtVcIssuerMetadata(
$psr18Client,
$psr17RequestFactory,
urlPolicy: new UrlPolicy(resolveHosts: false), // e.g. behind an egress proxy that enforces this itself
maxRedirects: 1,
maxBytes: 256 * 1024,
);
Type Metadata
A credential type can publish Type Metadata (Section 5): a human-readable name, display
and rendering hints for wallets, and per-claim rules — which claims must or must not be
selectively disclosable (sd), which are mandatory. Give the verifier a
TypeMetadataResolver and it processes the metadata for every credential: the extends
chain is resolved (extended types first, circular chains refused), vct#integrity and
extends#integrity are checked as W3C SRI digests, and the presentation is validated
against the effective claim metadata. A credential whose metadata cannot be obtained is
rejected, as Section 5.7 requires once metadata is part of the policy.
use K2gl\SdJwtVc\HttpTypeMetadata;
use K2gl\SdJwtVc\StaticTypeMetadata;
// from the vct URL itself, under the same Section 3 retrieval rules and UrlPolicy
$verifier = new SdJwtVcVerifier(typeMetadata: new HttpTypeMetadata($psr18Client, $psr17RequestFactory));
// or from documents you hold — a registry, a cache filled ahead of time, non-URL types
$verifier = new SdJwtVcVerifier(typeMetadata: (new StaticTypeMetadata)->add($vct, $typeMetadataJson));
$credential = $verifier->verifyPresentation($compact, $issuerKeys, $keyBinding);
$metadata = $credential->typeMetadata(); // ResolvedTypeMetadata
$metadata->claims(); // ClaimMetadata after extension: path, sd(), mandatory(), display, svgId
$metadata->display(); // the display entries of the nearest type that defines any
$metadata->chain(); // the documents, the type itself first
What is validated: every claim path must evaluate against the credential (a string
component on a non-object, or an index on a non-array, is an error), and every claim a
path selects must be selectively disclosable exactly as its sd says — "disclosable"
meaning directly represented as a Disclosure, not merely inside one; array positions
count the elements as issued, so a withheld element still satisfies "sd": "always".
mandatory is not checked by a Verifier: the draft forbids reading the absence of a
selectively disclosable claim from a presentation as the Issuer having left it out.
Display and rendering metadata are handed out as data; rendering is the wallet's job.
The draft's own example type (Appendix A.2) is a fixture of the test suite.
Check revocation
A verified credential can still have been revoked. The draft's status mechanism is the Token Status List, and it requires the Status List Token to be a JWT — which is what k2gl/token-status-list implements:
use K2gl\TokenStatusList\StatusListResolver;
use K2gl\TokenStatusList\StatusReference;
$status = $credential->status();
if ($status !== null) {
$resolver = new StatusListResolver($psr18Client, $psr17RequestFactory, $statusIssuerKey, cache: $psr16Cache);
if (! $resolver->check(StatusReference::fromClaim($status))->isValid()) {
// revoked or suspended
}
}
Scope
dc+sd-jwtissuance and verification per draft -19, compact and JWS JSON serialization (via k2gl/sd-jwt 1.1).- Key discovery: JWT VC Issuer Metadata (PSR-18, Section 3 retrieval rules), x5c chains, pinned keys.
statusclaim surfaced for the check; the Token Status List itself is k2gl/token-status-list.- Type Metadata (Section 5): retrieval from the
vctURL or a local registry, integrity metadata,extendschains, claim metadata validation (sd); display and rendering metadata as data. Not covered:mandatory(a Verifier cannot check it) and rendering.
License
MIT © Nick Harin
API
Public classes and methods, generated from the source.
K2gl\SdJwtVc\ClaimMetadata class
fromArray(array $object): selfsd(): stringmandatory(): boolpathKey(): stringoverriddenBy(self $child): self
K2gl\SdJwtVc\Exception\InvalidSdJwtVcException class
K2gl\SdJwtVc\Exception\IssuerKeyResolutionFailed class
K2gl\SdJwtVc\Exception\SdJwtVcException class
K2gl\SdJwtVc\Exception\TypeMetadataException class
K2gl\SdJwtVc\HttpTypeMetadata class
__construct( private readonly ClientInterface $httpClient, private readonly RequestFactoryInterface $requestFactory, private readonly UrlPolicy $urlPolicy = new UrlPolicy, private readonly int $maxRedirects = 3, private readonly int $maxBytes = 1024 * 1024, )resolve(string $vct, ?string $integrity): TypeMetadata
K2gl\SdJwtVc\IssuerKeyResolver interface
resolve(?string $issuer, stdClass $header): Verifier
K2gl\SdJwtVc\JwtVcIssuerMetadata class
__construct( private readonly ClientInterface $httpClient, private readonly RequestFactoryInterface $requestFactory, private readonly UrlPolicy $urlPolicy = new UrlPolicy, private readonly int $maxRedirects = 3, private readonly int $maxBytes = 1024 * 1024, )resolve(?string $issuer, stdClass $header): VerifierwellKnownUrl(string $issuer): string
K2gl\SdJwtVc\ResolvedTypeMetadata class
resolve(TypeMetadataResolver $resolver, string $vct, ?string $integrity): selfvct(): stringtype(): TypeMetadatachain(): arrayclaims(): arraydisplay(): ?array
K2gl\SdJwtVc\SdJwtVcIssuer class
__construct( JwsSigner $signer, string $hashAlgorithm = HashAlgorithm::DEFAULT, ?Closure $saltGenerator = null, )issue(array|stdClass $claims, array $header = []): SdJwt
K2gl\SdJwtVc\SdJwtVcVerifier class
__construct( array $allowedAlgorithms = SdJwtVerifier::DEFAULT_ALGORITHMS, array $allowedHashAlgorithms = SdJwtVerifier::DEFAULT_HASH_ALGORITHMS, ?int $clock = null, int $clockLeewaySeconds = 0, private readonly bool $acceptLegacyType = false, private readonly ?TypeMetadataResolver $typeMetadata = null, )verify(SdJwt|string $sdJwtVc, IssuerKeyResolver|Verifier $issuerKeys): VerifiedSdJwtVcverifyPresentation( SdJwt|string $presentation, IssuerKeyResolver|Verifier $issuerKeys, KeyBinding $keyBinding, ): VerifiedSdJwtVcassertTypeList(mixed $value): void
K2gl\SdJwtVc\StaticIssuerKeys class
add(string $issuer, Verifier $verifier): selfaddJwks(string $issuer, array $jwks): selfresolve(?string $issuer, stdClass $header): Verifier
K2gl\SdJwtVc\StaticTypeMetadata class
add(string $vct, string $json): selfresolve(string $vct, ?string $integrity): TypeMetadata
K2gl\SdJwtVc\TypeMetadata class
fromJson(string $json): selffromArray(array $document): selfraw(): array
K2gl\SdJwtVc\TypeMetadataResolver interface
resolve(string $vct, ?string $integrity): TypeMetadata
K2gl\SdJwtVc\UrlPolicy class
__construct(private readonly bool $resolveHosts = true)assertAllowed(string $url): void
K2gl\SdJwtVc\VerifiedSdJwtVc class
__construct( private readonly VerifiedSdJwt $inner, private readonly ?ResolvedTypeMetadata $typeMetadata = null, )typeMetadata(): ?ResolvedTypeMetadatavct(): stringalsoKnownAsTypes(): arrayissuer(): ?stringstatus(): ?stdClasspayload(): stdClassclaims(): arraykeyBindingPayload(): ?stdClassdisclosedPaths(): arrayundisclosedPaths(): array
K2gl\SdJwtVc\X5cIssuerKeys class
__construct(array $trustedCertificates, private readonly ?int $clock = null)resolve(?string $issuer, stdClass $header): Verifier