Ietf status list#108
Conversation
wistefan
commented
Jun 26, 2026
- adds support for IETF Token Status Lists - https://datatracker.ietf.org/doc/draft-ietf-oauth-status-list/
- enables status checks by default - if a status list entry is present in the credential, it gets evaluated. Needs to be explicitly disabled(per credential type).
| // References: | ||
| // - https://www.w3.org/TR/vc-bitstring-status-list/ | ||
| // - https://www.w3.org/TR/2023/WD-vc-status-list-20230427/ (StatusList2021) | ||
| // - https://www.ietf.org/archive/id/draft-ietf-oauth-status-list-11.html |
There was a problem hiding this comment.
I noticed a link to OAuth Status List 11, but it expired a year ago. The latest version is 21. Is there any reason to use version 11 instead of 21? There are no significant changes, but I think we should reference version 21 instead.
| // parseIETFStatusListJWT extracts the `status_list` payload from an IETF | ||
| // Token Status List JWT without verifying the signature (status lists are | ||
| // public resources). The JWT payload is the second dot-separated segment. | ||
| func parseIETFStatusListJWT(jwtString string) (*common.IETFStatusList, error) { |
There was a problem hiding this comment.
It skips JWT signature verification. The specification (5.1) requires that Relying Parties MUST reject JWTs with invalid signatures (8.3, step 3a). Before using the payload, the signature must be verified using the issuer's public key (resolved via the iss claim together with kid/JWK). Without this verification, any intermediary could forge the status list.
| return nil, fmt.Errorf("%w: JWT payload JSON unmarshal failed: %v", ErrorStatusListUnparseable, err) | ||
| } | ||
|
|
||
| statusListRaw, ok := claims[common.IETFStatusListKey] |
There was a problem hiding this comment.
JWT sub claim must also be validated against the URI used to fetch it (8.3, step 4a). Extract claims["sub"] and compare it with entry.URI. If they do not match, the JWT must be rejected with an error.
| } | ||
|
|
||
| var claims map[string]interface{} | ||
| if err := json.Unmarshal(payload, &claims); err != nil { |
There was a problem hiding this comment.
The exp claim is not validated. The specification (8.3, step 4c) requires checking whether the Status List Token has expired when the exp claim is present. Extract claims["exp"] and reject the token if time.Now().After(exp)
| // Token Status List JWT without verifying the signature (status lists are | ||
| // public resources). The JWT payload is the second dot-separated segment. | ||
| func parseIETFStatusListJWT(jwtString string) (*common.IETFStatusList, error) { | ||
| parts := strings.Split(strings.TrimSpace(jwtString), ".") |
There was a problem hiding this comment.
JWT header (parts[0]) is neither decoded nor validated. The specification (5.1) requires the typ header to be "statuslist+jwt". Decode parts[0] and reject the JWT if the typ value is not statuslist+jwt.
|
|
||
| // parseIETFBits converts the `bits` value from a status list JWT payload | ||
| // to an int. JSON numbers arrive as float64. | ||
| func parseIETFBits(raw interface{}) (int, error) { |
There was a problem hiding this comment.
parseIETFBits currently accepts any positive value, but 4.1 restricts valid values to {1, 2, 4, 8}. Values outside this set produce incorrect results during bit unpacking, so validation should be added.
Additionally, when bits is nil, the function falls back to DefaultStatusSizeBits. However, the bits claim is REQUIRED (4.2), so this case should return an error instead of using a default value.
| return nil, err | ||
| } | ||
|
|
||
| c.cache.Set(uri, statusList, c.expiry) |
There was a problem hiding this comment.
ttl claim of the Status List JWT is currently ignored. The specification (8.3, step 4d) recommends using ttl to determine when the cached Status List should be refreshed.
IETFStatusList struct should include a TTL field, and FetchIETF should use min(ttl, configExpiry) as the cache TTL instead of always using the fixed c.expiry value.
| entry.StatusListCredential = s | ||
| } | ||
| if idx, ok := obj[StatusListEntryKeyStatusListIndex]; ok && idx != nil { | ||
| parsed, err := parseStatusListIndex(idx) |
There was a problem hiding this comment.
I just notice this. statusListIndex is REQUIRED by the W3C Bitstring Status List specification. If it is missing, the current implementation silently defaults to index 0, which may result in accepting a credential without validating the intended status list entry.
Instead, I think the implementation should return ErrorStatusListEntryMalformed when the statusListIndex field is absent.
| // | ||
| // A non-nil error is returned when the credential does not expose an | ||
| // `encodedList` string on at least one subject. | ||
| func extractStatusListFields(statusCred *common.Credential) (encodedList string, statusPurpose string, err error) { |
There was a problem hiding this comment.
extractStatusListFields does not verify that the fetched credential is of type BitstringStatusListCredential or StatusList2021Credential. As a result, any Verifiable Credential containing a credentialSubject.encodedList would be accepted.
The implementation should validate Contents().Types and reject the credential if it is not one of the supported status list credential types.
| if err != nil { | ||
| return nil, fmt.Errorf("%w: %v", ErrorStatusListHttpFailure, err) | ||
| } | ||
| req.Header.Set("Accept", ContentTypeCredentialJson) |
There was a problem hiding this comment.
The Accept header only includes application/vc+ld+json. Although the implementation also supports JWT responses, it does not advertise this capability during content negotiation.
To support issuers with strict content negotiation, the Accept header should also include application/vc+jwt