Skip to content
38 changes: 38 additions & 0 deletions docs/pages/deployment/policy.rst
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,44 @@ You can define the following field in the input descriptor constraint, to have t

Only 1 capture group is supported in regular expressions. If multiple capture groups are defined, an error will be returned.

Presentation definition validation
**********************************

The node validates presentation definitions when loading policy files, and refuses to start on a definition
that would fail or misbehave on every request. All problems are reported at once. The checks are:

- **Duplicate ids.** An input descriptor id may be used once per definition, and a field id once per
input descriptor. The same field id on fields of *different* descriptors is allowed and meaningful:
the chosen credentials must agree on that field's value.
- **Same-id consistency.** Filters on fields that share an id must allow at least one common value.
For example, a ``const`` on one descriptor that is not in another descriptor's ``enum`` for the same
id can never be satisfied and fails validation. Two ``pattern`` filters without any ``const`` or
``enum`` are not checked (undecidable); such a conflict surfaces at request time instead.
- **Filters that can never match.** A ``const`` combined with a non-string ``type``, or an empty
``enum``, rejects every credential. A ``pattern`` that does not compile, or that has more than one
capture group, errors on every evaluation.
- **Ignored constraints.** Keywords the matcher does not evaluate are rejected instead of silently
weakening the filter: ``const`` or ``pattern`` next to ``enum`` (``enum`` takes precedence),
``pattern`` on a non-string ``type``, and unsupported JSON Schema keywords such as ``minimum``.
- **Submission requirements.** Every group referenced by an input descriptor must be covered by a
submission requirement, rules must be ``all`` or ``pick``, exactly one of ``from``/``from_nested``
must be set, and bounds must be possible (``min`` not above ``max``, no negative values).

A failing definition is reported with every conflict, for example::

presentation definition 'example-care-pd' is invalid: active: const "true" can never match: it
requires type "string", declared type is "boolean"; org_ura: conflicting filter types: string vs number

Guidance for policy authors:

- Issue claims whose values must be filtered or selected **as strings**. The matcher can require a
value to be a number (``"type": "number"``), but cannot constrain a number's value: ``const``,
``enum`` and ``pattern`` compare as strings, and numeric JSON Schema keywords are not evaluated.
- Give every claim used for logging, authorization, or credential selection a distinct field ``id``;
only declared field ids are returned by token introspection and usable in ``credential_selection``.
- When reusing a field ``id`` across input descriptors, make sure the filters agree: the id binds a
single value across the chosen credentials.

Two-VP flow and cross-VP binding (experimental)
***********************************************

Expand Down
8 changes: 7 additions & 1 deletion vcr/pe/presentation_definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -553,11 +553,17 @@ func matchFilter(filter Filter, value interface{}) (bool, interface{}, error) {
}

if filter.Pattern != nil && filter.Type == "string" {
str, isString := value.(string)
if !isString {
// arrays whose elements all failed the filter fall through the type switch;
// a pattern can only accept strings
return false, nil, nil
}
re, err := regexp2.Compile(*filter.Pattern, regexp2.ECMAScript)
if err != nil {
return false, nil, err
}
match, err := re.FindStringMatch(value.(string))
match, err := re.FindStringMatch(str)
if err != nil {
return false, nil, err
}
Expand Down
31 changes: 31 additions & 0 deletions vcr/pe/presentation_definition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,37 @@ func Test_matchField(t *testing.T) {
}

func Test_matchFilter(t *testing.T) {
t.Run("array values with a string pattern filter do not panic", func(t *testing.T) {
// an array whose elements all fail the filter falls through the type switch; the
// pattern check must not assume the value is a string
pattern := "^Nuts"
filter := Filter{Type: "string", Pattern: &pattern}

for _, value := range [][]interface{}{
{"SomethingElse"},
{"SomethingElse", "AlsoNoMatch"},
{42.0, true},
{},
} {
match, matched, err := matchFilter(filter, value)

require.NoError(t, err)
assert.False(t, match)
assert.Nil(t, matched)
}
})

t.Run("an array element matching the pattern still returns the match", func(t *testing.T) {
pattern := "^Nuts"
filter := Filter{Type: "string", Pattern: &pattern}

match, value, err := matchFilter(filter, []interface{}{"SomethingElse", "NutsOrganizationCredential"})

require.NoError(t, err)
assert.True(t, match)
assert.Equal(t, []interface{}{"SomethingElse", "NutsOrganizationCredential"}, value)
})

// values for pointer fields
stringValue := "test"
boolValue := true
Expand Down
38 changes: 37 additions & 1 deletion vcr/pe/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@
// Package pe stands for Presentation Exchange which includes Presentation Definition and Presentation Submission
package pe

import "errors"
import (
"encoding/json"
"errors"
"sort"
)

var ErrNoCredentials = errors.New("missing credentials")
var ErrMultipleCredentials = errors.New("multiple matching credentials")
Expand Down Expand Up @@ -130,6 +134,38 @@ type Filter struct {
Enum []string `json:"enum,omitempty"`
// Pattern is a pattern to match according to ECMA-262, section 21.2.1
Pattern *string `json:"pattern,omitempty"`
// unsupported records JSON Schema keywords present in the filter JSON that this
// implementation does not evaluate. Parsing stays lenient (remote presentation definitions
// keep working); Validate reports them, because a dropped keyword silently weakens the
// filter below what its author declared.
unsupported []string
}

// UnmarshalJSON parses the filter and records unsupported keywords instead of silently dropping
// them.
func (f *Filter) UnmarshalJSON(data []byte) error {
type alias Filter
var parsed alias
if err := json.Unmarshal(data, &parsed); err != nil {
return err
}
*f = Filter(parsed)
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
for keyword := range raw {
switch keyword {
case "type", "const", "enum", "pattern":
// annotation keywords carry no constraint, so dropping them weakens nothing
case "description", "title", "$comment", "$schema", "$id", "examples", "default",
"readOnly", "writeOnly", "deprecated":
default:
f.unsupported = append(f.unsupported, keyword)
}
}
sort.Strings(f.unsupported)
return nil
}

// SubmissionRequirement
Expand Down
Loading
Loading