OID4VCI: Unique ID for each element in credentials

Issue #1922 resolved
Takahiko Kawasaki created an issue

Abstract

  1. Each element in the credentials array in a credential offer or in an authorization_details element whose type is openid_credential should have a unique identifier.
  2. A credential request should use the identifier.

This makes a credential request simpler and logically prevents the wallet from requesting more than what the end-user has approved. This proposal contains breaking changes.

1. Proposed Changes

1.1. Credential Offer / authorization_details element of type openid_credential

Current:

{
  "credential_issuer": "https://credential-issuer.example.com",
  "credentials": [
    "UniversityDegree_JWT",
    {
      "format": "mso_mdoc",
      "doctype": "org.iso.18013.5.1.mDL"
    },
    {
      "format": "jwt_vc_json",
      "credential_definition": {
        "type": [
          "VerifiableCredential",
          "UniversityDegreeCredential"
        ]
      }
    }
  ]
}

Proposed Change: Insert "id" into each JSON object. (Note that “string” elements already represent IDs in credentials_supported.)

{
  "credential_issuer": "https://credential-issuer.example.com",
  "credentials": [
    "UniversityDegree_JWT",
    {
      "id": "a_unique_identifier_1",
      "format": "mso_mdoc",
      "doctype": "org.iso.18013.5.1.mDL"
    },
    {
      "id": "a_unique_identifier_2",
      "format": "jwt_vc_json",
      "credential_definition": {
        "type": [
          "VerifiableCredential",
          "UniversityDegreeCredential"
        ]
      }
    }
  ]
}

1.2. Credential Request

Current:

{
  "format": "jwt_vc_json",
  "type": [
    "VerifiableCredential",
    "UniversityDegreeCredential"
  ],
  "proof": {
    "proof_type": "jwt",
    "jwt": "eyJ..."
  }
}

Proposed Change: Insert "id" and remove properties that were in the credential offer or the authorization_details element.

{
  "id": "a_unique_identifier_2",
  "proof": {
    "proof_type": "jwt",
    "jwt": "eyJ..."
  }
}

1.3. Credential Response

Current:

{
  "format": "jwt_vc_json",
  "credential": "...",
  "c_nonce": "...",
  "c_nonce_expires_in": 86400
}

Proposed Change: Insert "id" and remove properties that were in the credential offer or the authorization_details element.

{
  "id": "a_unique_identifier_2",
  "credential": "...",
  "c_nonce": "...",
  "c_nonce_expires_in": 86400
}

1.4. Batch Credential Request

Current:

{
  "credential_requests": [
    {
      "format": "jwt_vc_json",
      "type": [
        "VerifiableCredential",
        "UniversityDegreeCredential"
      ],
      "proof": {
        "proof_type": "jwt",
        "jwt": "eyJ..."
      }
    },
    {
      "format": "mso_mdoc",
      "doctype": "org.iso.18013.5.1.mDL",
      "proof": {
        "proof_type": "jwt",
        "jwt": "eyJ..."
      }
    }
  ]
}

Proposed Change: Insert "id" into each JSON object in the credential_requests array and remove properties that were in the credential offer or the authorization_details element.

{
  "credential_requests": [
    {
      "id": "a_unique_identifier_2",
      "proof": {
        "proof_type": "jwt",
        "jwt": "eyJ..."
      }
    },
    {
      "id": "a_unique_identifier_1",
      "proof": {
        "proof_type": "jwt",
        "jwt": "eyJ..."
      }
    }
  ]
}

1.5. Batch Credential Response

Current:

{
  "credential_responses": [
    {
      "format": "jwt_vc_json",
      "credential": "..."
    },
    {
      "format": "mso_mdoc",
      "credential": "..."
    }
  ],
  "c_nonce": "...",
  "c_nonce_expires_in": 86400
}

Proposed Change: Insert "id" into each JSON object in the credential_responses array and remove properties that were in the credential offer or the authorization_details element. Note that inserting "id" can make the order of array elements less important.

{
  "credential_responses": [
    {
      "id": "a_unique_identifier_2",
      "credential": "..."
    },
    {
      "id": "a_unique_identifier_1",
      "credential": "..."
    }
  ],
  "c_nonce": "...",
  "c_nonce_expires_in": 86400
}

2. Reason to Propose

The following is an example of an authorization_details element in Appendix E.

[
  {
    "type": "openid_credential",
    "format": "jwt_vc_json",
    "credential_definition": {
      "type": [
        "VerifiableCredential",
        "UniversityDegreeCredential"
      ],
      "credentialSubject": {
        "given_name": {},
        "last_name": {},
        "degree": {}
      }
    }
  }
]

This example implies that the wallet requests three claims, namely, given_name, last_name and degree, only. For data minimization policy (likely to be imposed by regulators), the credential issuer should not issue a Verifiable Credential that includes other claims. At least, an implementer of “OpenID Connect for Identity Assurance 1.0” (me) thinks so.

A cumbersome and error-prone point for implementers is a case where a credential request demands more than the credential offer or the authorization_details element originally intends to offer. Suppose the following credential request comes.

{
  "format": "jwt_jv_json",
  "credential_definition": {
    "type": [
      "VerifiableCredential",
      "UniversityDegreeCredential"
    ],
    "credentialSubject": {
      "given_name": {},
      "birthdate": {},
      "degree": {}
    }
  }
}

This credential request requests the birthdate claim that is not in the original authorization_details element. For data minimization policy, this credential request should be rejected or the birthdate claim should be absent from the Verifiable Credential being issued.

It will be a cumbersome and error-prone task for implementers to write programs to make sure that a credential request is requesting claims within the approved range and it is not requesting more than approved.

Describing almost the same thing in the credential offer / the authorization_details element and in the credential request is redundant. Considering that validation of their complex formats is not so easy, it is unlikely that majority of implementers would support that redundancy.

Inserting an unique identifier into each elements in the credentials array and having the wallet use the identifier when making a credential request would be able to make the OID4VCI specification and its implementations simpler, and as a result, the specification would be more acceptable to implementers and the market.

Comments (23)

  1. Torsten Lodderstedt

    Hi Taka, just to make sure I understand the rationale. Your are saying comparing the authorization details in the access tokens to the parameter values of the credential request is to complex? I’m asking since the birthdate is not present in the access token so the credential endpoint could detect and refuse to issue the requested credential.

  2. Takahiko Kawasaki reporter

    My answer to your question is “yes”.

    The deeper data structure becomes, the more complex comparison becomes.

    For example, if the following two come as an authorization_details element and a credential request,

    authorization_details:

    [
      {
        "type": "openid_credential",
        "format": "jwt_vc_json",
        "credential_definition": {
          "type": [
            "VerifiableCredential",
            "SpecificCredential"
          ],
          "credentialSubject" {
            "address": {
              "country": {},
              "postal_code": {},
              "region": {}
            }
          }
        }
      }
    ]
    

    credential request:

    {
      "format": "jwt_vc_json",
      "credential_definition": {
        "type": [
          "VerifiableCredential",
          "SpecificCredential"
        ],
        "credentialSubject": {
          "address": {
            "country": {},
            "region": {},
            "locality": {}
          }
        }
      }
    }
    

    the credential request should be rejected because address/locality is not included in the authorization_details element. For the validation, implementations have to compare the nested structures. This validation would become hard when complex structures like verified_claims (“OpenID Connect for Identity Assurance 1.0”) are used.

    Also, it’s not so clear what should happen when a credential request like below comes.

    {
      "format": "jwt_vc_json",
      "credential_definition": {
        "type": [
          "VerifiableCredential",
          "SpecificCredential"
        ],
        "credentialSubject": {
          "address": {}
        }
      }
    } 
    

    And, if data structures include requirements and conditions like claims (OIDC Core) and verified_claims (OIDC4IDA), a simple comparison will not work. For example, the following verified_claims request

    {
      "verified_claims": {
        "verification": {
          "trust_framework": null,
          "evidence": [
            {
              "type": {
                "value": "document"
              },
              "method": null,
              "document_details": {
                "type": null,
                "document_number": null
              }
            },
            {
              "type": {
                "value": "electronic_signature"
              },
              "signature_type": null,
              "issuer": null,
              "serial_number": null
            }
          ]
        },
        "claims": {
          "given_name": null
        }
      }
    }
    

    completely subsumes the following verified_claims request.

    {
      "verified_claims": {
        "verification": {
          "trust_framework": null,
          "evidence": [
            {
              "type": {
                "value": "document"
              },
              "method": null,
              "document_details": {
                "type": null,
                "document_number": null
              }
            }
          ]
        },
        "claims": {
          "given_name": null
        }
      }
    }
    

    However, adopting the latter breaks the Logical-OR rule on evidence and may result in different output.

    Another example. This request

    {
      "credentialSubject": {
        "favorites": {
          "values": ["apple", "banana"]
        }
      }
    }
    

    seemingly subsumes the following.

    {
      "credentialSubject": {
        "favorites": {}
      }
    }
    

    However, the meaning is opposite. The condition of the former is more strict than that of the latter.

    If it is ensured that complex things like verified_claims never appear in a credential offer / an authorization_details and a credential request, the current specification won’t be a big hurdle. However, still my proposal will bring benefits.

    As you may notice, I’m thinking how to combine OID4VCI and OIDC4IDA for GAIN (Global Assured Identity Network). My proposal has come from the context.

  3. Torsten Lodderstedt

    Thanks for the explanation.

    As far as I understand you want the wallet to always request a credential using the identifier, which means we would remove the format identifier and all content definition from the credential request?

    The effect of your proposal is that the wallet would always request exactly the payload the user has consented for, no subset. Right?

    I’m not sure what that means regarding subsequent attempts to obtain a credential or to obtain multiple instances of the same credential with the same token.

    Subsequent requests: the wallet wants to refresh a credential because it has a short expiration. Since the wallet has a refresh token, it can obtain a fresh access token and get a new credential. Would the AS create a new identifier? Would the wallet use the pre-existing identifier?

    multiple instances: the user consents to issue (a) mobile drivers license, however since the deployments requires use of ephemeral credentials (to prevent co-relation across Verifiers), the wallet obtains a batch of five credentials, each of it bound to a different key for cryptographic holder binding. Would that mean the same identifier can be used multiple times?

    Note: issue #1923 asks for a similar extension for scenarios where the issuer wants to issue multiple credentials of the same type.

  4. Kristina Yasuda
    • changed status to open

    05182023 SIOP call: Taka is trying to minimize the errors that could come from the range of credential formats available. big breaking change, some outstanding questions, but worth pursuing :)

  5. Kristina Yasuda

    I think one use-case we will not be able to support with this change is if the user gave consent for the issuance of the “given_name", "last_name", "degree", and “birthdate” at the authz endpoint and wallet wants to receive multiple credentials - one that contains only birthdate and the other that contains other three claims. I don’t know if this is a niche scenario or if it is valid or not, but thought I’d point it out.

  6. Kristina Yasuda

    while the access token is valid, the wallet wants to refresh a credential because it has a short expiration. Since the wallet has a refresh token, it can obtain a fresh access token and get a new credential. Would the AS create a new identifier? Would the wallet use the pre-existing identifier?

    How would the wallet get a new identifier? when receiving a fresh access token? I think same identifier would work, but I am not sure how this ties into the scenario that we have been discussing with @Tobias Looker in another issue, where the claim values in the fresh credential have changed and the issuer want to communicate that to the wallet - guess the credential response will contain a new identifier…

  7. Pedro Felix
    • I agree that comparing the credentialSubject that was authorized (i.e. the credentialSubject in the authorization_details parameters) with the credentialSubject on the credential request may become quite complex, namely with nested structures.
    • IINM, a similar problem already exists when using a authorization_details when refreshing an access token - how to ensure the credentialSubject in the refresh request’s authorization_details is a subset of the credentialSubject in the authorization request's authorization_details.
    • When using a credential offer, the ID would be defined by the credential issuer, however when directly using an authorization request with an authorization_details, the IDs would be defined by the wallet, right?

      • BTW, is it possible to use authorization_details on an authorization request triggered by a credential offer? I.e. use both issuer_state and authorization_details? If so, we could potentially have IDs controlled by both parties in the same authorization request.

  8. Torsten Lodderstedt

    I think the IDs should be defined by the AS and conveyed in the token response in any use case. One might argue that the identifiers might already be known when the credential offer is generated. But there is basically no need for the wallet to know them then as it first needs to request the access token before it can request the credentials. So I would opt for simplification of the flow.

    re BTW: of course, esp. in conjunction with the code grant type! It is straight forward to derive the authorization details from the credential types listed in the credential offer.

  9. Kristina Yasuda

    Does this ID need to be unique to the transaction? I don’t think it does, and if so, the suggestion in PR #520 to use scopes value as the identifier stands, no?

  10. Torsten Lodderstedt

    Please see my comment at PR #520. Issue #1923 cannot be solved with static, type-specific identifiers as the issue tries to solve the problem of multiple credentials of the same type. I would be in favor in solving both issues #1922 and #1923 with the same mechanism.

  11. Takahiko Kawasaki reporter

    I’m feeling like retracting this proposal because my proposal makes it impossible for wallets to request a “subset” of claims.

    My concern is that it is unclear whether a mechanical comparison between two complex JSON structures can assure that permissions represented by one JSON structure are all covered by the other. Sometimes a smaller strucutre can represent a broader permission (e.g. {"permissions":["read","write"]} vs {"permissions":["all"]}). However, without a well-defined logic in the spec, what implementers can do is a mechanical comparison at best.

    My proposal can remove the complexity of permission comparison but makes it impossible to request a “subset” of claims, which may be unacceptable in some use cases.

  12. Timo Glastra

    One limitation in not having an unique identifier, is that it can actually be ambiguous for the wallet / receiver of the credential which credential it has received.

    Taking a similar example from #1922, where the following credentials are supported.

    {
      "credential_supported": [
        {
          "format": "jwt_vc_json",
          "id": "AERO-123-FluidMechanics-II-2023",
          "types": ["VerifiableCredential", "AerospaceEngineering"],
          "display": {
            "name": "Fluid Mechanics",
            /* ... other display properties ... */
          }
        },
        {
          "format": "jwt_vc_json",
          "id": "AERO-631-SoldingMechanics-IV-2023",
          "types": ["VerifiableCredential", "AerospaceEngineering"],
          "display": {
            "name": "Solid Mechanics",
            /* ... other display properties ... */
          }
        }
      ]
    }
    

    If a credential offer would then include the following:

    {
      "credentials": ["AERO-123-FluidMechanics-II-2023", "AERO-631-SoldingMechanics-IV-2023"]
    }
    

    The request for both would something like:

    {
     "format": "jwt_vc_json",
     "types": ["VerifiableCredential", "AerospaceEngineering"]
    }
    

    It is now (maybe I’m missing something), not 100% clear to the issuer which credential is being requested (AERO-123-FluidMechanics-II-2023 or AERO-631-SoldingMechanics-IV-2023?), and it’s also not clear for the wallet which credential is being issued. Both could have different display metadata, and thus it’s not really possible to know for the wallet which credential supported display metadata to associate with the issued credential.

  13. Kristina Yasuda

    talked to our engineering team, we are in huge support of having an id parameter (or equivalent) in the Credential Request.

  14. Andres Olave

    We are pro this change. We primarily use batch issuing with many of the same type (such as badges). Matching based on properties that are not necessarily unique can lead to unexpected behaviour.

    • We do not have use cases for users to request a subset of claims
    • In our model, subsequent (refreshed) credentials have a new id as the credential offer is retrieved again once the person is authenticated

  15. Emmanouil Koukoularis

    @Kristina Yasuda
    Hello,
    may I ask when are you planning to introduce the id parameter in the Credential Endpoint on the specification?
    We are really interested in adopting the id parameter to our implementation as we have raised the issue of having more than one credentials available of the same type for a single user.

  16. Michael Jones

    A few clarifying questions…

    Would this proposal still allow credentials to be requested using “format” and “type” or only with “id”?

    Given we’re credential-format agnostic, are we sure that all credential formats will be able to carry an “id” value?

    And while I hate to even ask this, might the use of “id” conflict with JSON-LD’s use of “id”?

  17. Kristina Yasuda

    If the problem raised in this PR is the fact that the subset of claims requested using credentialSubject parameter in authorization request and credential request might not match (credential offer does not allow for credentialSubject), could we solve this problem by removing an option to include credentialSubject in authorization request?

    if the purpose of credentialSubject parameter is data minimization, I think we can achieve the same result as now by allowing the wallet to request subset of claims in credential request. In the end of the day it is up to the issuer to decide which claims to put in the credential.

    the question is really, is there a need for an Access token to be scoped for the issuance of only a subset of claims? if the answer is yes, we cannot remove credentialSubject from the authorization request, but I have not heard that requiremet yet…

    ^ my thinking when I was doing PR #612 trying to address both issues #1922 and #1923, and ultimately ending up addressing only #1923 in the PR..

  18. Alen Horvat

    @Takahiko Kawasaki , the proposal only introduces identifiers that are “local”/issuer-assigned to the actual entry in the authorisation details, or?

    The identifier is not reflected in the credential itself. Or?

    Is my understanding correct?

    If that’s so, this simplifies the request/response syntax and the proposal can be compatible with whatever solution is proposed for #1923.

    1923 is asking about a different thing: I want to request my masters and phd diploma. Both will be of the same type and in the same format. Main difference will be a field expressing the level of education (that will appear in the credentialSubject).

  19. Kristina Yasuda

    SIOP call: taka agreed that changing the authorization details structure as below might work - he will confirm:

    Current structure (using the example here) :

    [
        {
            "type": "openid_credential",
            "format": "jwt_vc_json",
            "credential_definition": {
                "type": [
                    "VerifiableCredential",
                    "UniversityDegreeCredential"
                ],
                "credentialSubject": {
                    "given_name": {},
                    "family_name": {},
                    "degree": {}
                }
            }
        }
    ]
    

    New structure:

    [
        {
            "type": "openid_credential",
            "format": "jwt_vc_json",
            "credential_definition": {
                "type": [
                    "VerifiableCredential",
                    "UniversityDegreeCredential"
                ]
            }
        }
    ]
    

  20. Daniel Fett

    Wait, isn’t the purpose of the claims in the authz_details parameter to show a consent screen to the user in such a way that if only a subset of claims is required, the user can see that only this subset will end up in the credential?

    Or is this not a use case? In that case, why bother requesting subsets at all (i.e., why have the option to request credentials with subsets in the credential endpoint)?

    I guess I’m missing something here, what is it?

  21. Kristina Yasuda

    I don’t think you are missing anything: access token will be scoped to the type (entire claimset), and if necessary, the wallet can scope it down by requesting only the subset at the credential endpoint. Are you saying it is against the philosophy of RAR that enables tightly scoped access tokens (i mentioned this during the call)? in which case, my alternative suggestion would be to keep an option to request a subset using RAR and remove an option to request subset at the credential endpoint. Actually that might work better - it is at the discretion of the issuer to issue whatever claims it decided necessary within the scope of an access token. and we could also potentially use the mechanism in PR #612 where the Issuer returns an identifier in token response that stands for the subset of the claims that the wallet can use in credential endpoint.

  22. Log in to comment