openid / OpenID4VP

44 stars 11 forks source link

Proposal for new query language #178

Open danielfett opened 1 month ago

danielfett commented 1 month ago

Related to #160, #161, #162 and #157.

This is a proposal for a new query language fulfilling the requirements discussed in the issues linked above. I can create a PR once we are sure that this is the direction we want to go.

Note: This proposal for now does not cover restricting/querying values of claims. See examples 6 and 7 here for a proposal, but that is out of scope for this issue.

Example 1a - Request one Credential (SD-JWT VC)

Just one credential is requested. It must contain the claims last_name, first_name, and address/street_address.

{
  "credentials": {
    "my_cred_1": {
      "format": "vc+sd-jwt",
      "vc+sd-jwt": {  # format-specific requirements
          "vct": "https://credentials.example.com/identity_credential",
          "alg_values": ["ES256", "ES384"],
          "hash_alg_values": ["SHA-256"]
      },
      "claims":[
        { "path": ["last_name"] },
        {
          "path": ["first_name"],
          "intent_to_retain": false  # might be defined as an add-on, not in this PR
        },
        {  "path": ["address", "street_address"]  }
      ]
    }
  }
}

Note the identifier my_cred_1. It will show up in the VP Token, which is restructured to be an object mapping the identifiers in the request to the presentation (string or object depending on a credential format):

vp_token={
    "my_cred_1": "eY..."
}

Example 1b - Request one Credential (mdoc)

The syntax for the claims is slightly different due to the structure of mdocs:

{
  "credentials": {
  {
     "my_mdoc_credential": {
        "format": "mso_mdoc",
        "mso_mdoc": {  # format-specific requirements
            "doctype": "org.iso.7367.1.mVR",
            "alg_values": [ "EdDSA" ],
            "hash_algorithm_values": [ "SHA-384" ],
        },
        "claims": [
          {
            "namespace": "org.iso.7367.1",
            "claim_name": "vehicle_holder",
            "intent_to_retain": false
          },
          {
            "namespace": "org.iso.18013.5.1",
            "claim_name": "first_name",
            "intent_to_retain": false
          }      
        ]
      }
    }
  }
}

Response:

vp_token={
    "my_mdoc_credential": "<mdoc string>"
}

Example 2 - Request multiple Credentials (all of the requested credentials needs to be returned)

All three credentials have to be delivered:

{
  "credentials": {
    "my_cred_1": {
      "format": "vc+sd-jwt",
      "vc+sd-jwt": {
          "vct": "https://credentials.example.com/identity_credential",
          "alg_values": ["ES256", "ES384"],
          "hash_alg_values": ["SHA-256"]
      },
      "purpose": "give me the credential",  # we might pull this to the top
      "claims":[
        { "path": ["last_name"] }
      ]
    },
    "my_cred_2": {
      "format": "vc+sd-jwt",
      "vc+sd-jwt": {
          "vct": "https://credentials.example.com/identity_credential/2",
          "alg_values": ["ES256", "ES384"],
          "hash_alg_values": ["SHA-256"]
      },
      "claims": [
          { "path": ["addresses"] }
      ]
    },
    "my_cred_3": {
      "format": "vc+sd-jwt",
      "vc+sd-jwt": {
          "vct": "https://credentials.example.com/identity_credential/3",
          "alg_values": ["ES256", "ES384"],
          "hash_alg_values": ["SHA-256"]
      },
      "claims": [
        { "path": ["addresses"] }
      ]
    }
  }
}

All three credentials will show up in the VP Token:

vp_token={
    "my_cred_1": "eY...",
    "my_cred_2": "eY...",
    "my_cred_3": "eY..."
}

Note: If there is a requirement on sending multiple credentials fulfilling one request, this should be turned into an array.

Example 3 - Request alternative claims (one rule)

Require age_over_18 or birth_date or age_birth_year by grouping them in an object with the properties require and from. The order of the claims in the from list implies a preference by the verifier.

{
  "credentials": {
    "my_cred_1": {
      "format": "vc+sd-jwt",
      "vc+sd-jwt": {
          "vct": "https://credentials.example.com/identity_credential",
          "alg_values": ["ES256", "ES384"],
          "hash_alg_values": ["SHA-256"]
      },
      "purpose": "give me the credential",
      "claims":[
        { "path": ["last_name"] },
        {
          "required": 1,
          "from": [
            { "path": ["age_over_18"] },
            { "path": ["birth_date"]  },
            { "path": ["age_birth_year"] }
          ]
        },
      ]
    }
  }
}

Note: the expectation is for the wallet to check which of the alternative claims are present and render a user screen based on that - not for the wallet to display to the user all alternative claims (age_over_18 or birth_date or age_birth_year in this case).

Example 4 - Request alternative claims (multiple rules)

By nesting groups, complex queries can be expressed; here, either "zip_code" or both "city" and "state" are required.

{
  "credentials": {
    "my_cred_1": {
      "format": "vc+sd-jwt",
      "vc+sd-jwt": {
          "vct": "https://credentials.example.com/identity_credential",
          "alg_values": ["ES256", "ES384"],
          "hash_alg_values": ["SHA-256"]
      },
      "purpose": "give me the credential",
      "claims":[
        { "path": ["last_name"] }, 
        { 
          "required": 1, 
          "from": [ 
            { "path": [ "zip_code" ] }, 
            {
              "required": 2,
              "from": [
                { "path": [ "city" ] },
                { "path": [ "state" ] }
              ]
            }
          ] 
        } 
      ]
    }
  }
}

Example 5 - Credentials Alternatives

The same logic to requesting alternative claims can be applied to requesting alternative credentials as well. The "rules" for credentials are separated from the credentials to make the processing easier and the syntax more clean. The following example requests "mycred_1" and either "my_cred_2" or "my_cred_3":

{
  "rules": {
    "required": 2,
    "from": [
      { "ref": "my_cred_1" },
      {
        "required": 1,
        "from": [
          { "ref": "my_cred_2" },
          { "ref": "my_cred_3" }
        ]
      }
    ]
  },
  "credentials": { # each object follows the syntax described above
    "my_cred_1": {...}, 
    "my_cred_2": {...},
    "my_cred_3": {...}
  }
}

As shown in the examples further up, if rules is omitted, all credentials have to be delivered.

If the verifier just wants the user's claim (like street address/age), it would express it as "give me one of these credentials and outline any number of credentials whose schema includes a desired claim" - the verifier still needs to specify the format/type of the credential to indicate what it can understand.

Only the available/selected credential will show up in the VP Token:

vp_token={
    "my_cred_1": "eY..."
    "my_cred_2": "eY..."
}
tplooker commented 1 month ago

Appreciate you putting this proposal together @danielfett, as I shared in the hackmd version that preceded this, I had the following feedback.

Supporting algorithm negotiation per credential request I'm still not in favour of, as I have outlined here I'm yet to understand a usecase where this is required rather then handling algorithm negotiation at a wallet metadata level.

The syntax around alternative claims here is quite difficult to understand, for the following reasons

  1. IIUC each claim object becomes polymorphic? e.g describing this as a type in a language like typescript would look like
type ClaimRequest = {
   path: string;
} | {
  required: number;
  from: ClaimRequest[]
}

Which could be difficult to parse in other programming languages that don't support union types.

It also creates situations where the syntax becomes ambiguous to a parser, for example how would I parse the following and what would it mean? Am I asking for last_name OR zip_code OR city and state?

{
  "credentials": {
    "my_cred_1": {
      "format": "vc+sd-jwt",
      "vc+sd-jwt": {
          "vct": "https://credentials.example.com/identity_credential",
          "alg_values": ["ES256", "ES384"],
          "hash_alg_values": ["SHA-256"]
      },
      "purpose": "give me the credential",
      "claims": [
        { 
          "path": ["last_name"] 
          "required": 1, 
          "from": [ 
            { "path": [ "zip_code" ] }, 
            {
              "required": 2,
              "from": [
                { "path": [ "city" ] },
                { "path": [ "state" ] }
              ]
            }
          ] 
        }
      ]
    }
  }
}
  1. I would perhaps question how many usecases exist where the value of required > 1. I understand the example you have provided but how many usecases do we expect that require this feature, because if it is very small, then simpler syntaxes, like what were discussed at IIW can be used which can reduce implementation complexity.
danielfett commented 3 weeks ago
  1. IIUC each claim object becomes polymorphic? e.g describing this as a type in a language like typescript would look like
type ClaimRequest = {
   path: string;
} | {
  required: number;
  from: ClaimRequest[]
}

These would be two different types of objects. If that doesn't map well in the language of choice, it could be one object with three properties that are mutually exclusive.

It also creates situations where the syntax becomes ambiguous to a parser, for example how would I parse the following and what would it mean? Am I asking for last_name OR zip_code OR city and state?

The shown example would be illegal. Either path is provided or (required and from).

2. I would perhaps question how many usecases exist where the value of required > 1. I understand the example you have provided but how many usecases do we expect that require this feature, because if it is very small, then simpler syntaxes, like what were discussed at IIW can be used which can reduce implementation complexity.

The point is that "required > 1" comes essentially for free. It's a generalization of "and" and "or" operators which you would need otherwise. After having iterated through a couple of options, I doubt that the syntax becomes simpler when omitting this feature. It might become simpler when removing the functionality of being able to express "and" and "or" with arbitrary nesting.

samuelgoto commented 2 weeks ago

I got this answered out of band, so I figured others would have this question too:

In case you are wondering, the list of requirements that this proposal is constrained by is here: https://github.com/openid/OpenID4VP/issues/157#issuecomment-2093104135

samuelgoto commented 2 weeks ago

In case you are wondering too, here is a list of use cases that this proposal is constrained by.

danielfett commented 2 weeks ago

Note to myself: To cover https://github.com/openid/OpenID4VP/issues/193, the vct in the format-specific parameters for SD-JWT should not mean a perfect match, but that a credential of this type or a type inheriting from this should be deliviered. If required, both exact matching and inheritance matching could be supported.

Sakurann commented 2 weeks ago

I think what has been coming up as a feedback is whether it should be a different syntax between credential formats depending on how they handle types, which is a current design where mdoc have "doctype" and "namespace" claim, while other credential formats only have "type". I think suggestion has been, not to have something specific to mdoc and try have the same syntax.