Skip to content

Commit

Permalink
Take into account differences between DIF claim formats and OpenID
Browse files Browse the repository at this point in the history
  • Loading branch information
reinkrul committed Dec 6, 2023
1 parent 38f0db9 commit 1a7d353
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 59 deletions.
6 changes: 3 additions & 3 deletions auth/api/iam/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ func authorizationServerMetadata(identity url.URL) oauth.AuthorizationServerMeta
GrantTypesSupported: grantTypesSupported,
PreAuthorizedGrantAnonymousAccessSupported: true,
PresentationDefinitionEndpoint: identity.JoinPath("presentation_definition").String(),
VPFormats: credential.DefaultSupportedFormats(),
VPFormatsSupported: credential.DefaultSupportedFormats(),
VPFormats: credential.DefaultOpenIDSupportedFormats(),
VPFormatsSupported: credential.DefaultOpenIDSupportedFormats(),
ClientIdSchemesSupported: clientIdSchemesSupported,
}
}
Expand All @@ -76,7 +76,7 @@ func clientMetadata(identity url.URL) OAuthClientMetadata {
SoftwareID: softwareID, // nuts-node-refimpl
SoftwareVersion: softwareVersion, // version tag or "unknown"
//CredentialOfferEndpoint: "",
VPFormats: credential.DefaultSupportedFormats(),
VPFormats: credential.DefaultOpenIDSupportedFormats(),
ClientIdScheme: "did",
}
}
6 changes: 3 additions & 3 deletions auth/services/oauth/relying_party.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,12 @@ func (s *relyingParty) RequestRFC021AccessToken(ctx context.Context, requester d
// - what the local Nuts node supports
// - the presentation definition "claimed format designation" (optional)
// - the verifier's metadata (optional)
formatCandidates := credential.DefaultSupportedFormats()
formatCandidates := credential.OpenIDSupportedFormats(credential.DefaultOpenIDSupportedFormats())
if metadata.VPFormats != nil {
formatCandidates = formatCandidates.Match(metadata.VPFormats)
formatCandidates = formatCandidates.Match(credential.OpenIDSupportedFormats(metadata.VPFormats))
}
if presentationDefinition.Format != nil {
formatCandidates = formatCandidates.Match(credential.SupportedFormats(*presentationDefinition.Format))
formatCandidates = formatCandidates.Match(credential.DIFClaimFormats(*presentationDefinition.Format))
}
format, _ := formatCandidates.First()
if format == "" {
Expand Down
4 changes: 2 additions & 2 deletions auth/services/oauth/relying_party_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ func TestRelyingParty_RequestRFC021AccessToken(t *testing.T) {
})
t.Run("authorization server supported VP formats don't match", func(t *testing.T) {
ctx := createOAuthRPContext(t)
ctx.authzServerMetadata.VPFormats = credential.SupportedFormats{
ctx.authzServerMetadata.VPFormats = map[string]map[string][]string{
"unsupported": nil,
}
ctx.wallet.EXPECT().List(gomock.Any(), walletDID).Return(credentials, nil)
Expand Down Expand Up @@ -426,7 +426,7 @@ func createOAuthRPContext(t *testing.T) *rpOAuthTestContext {
]
}
`
authzServerMetadata := &oauth.AuthorizationServerMetadata{VPFormats: credential.DefaultSupportedFormats()}
authzServerMetadata := &oauth.AuthorizationServerMetadata{VPFormats: credential.DefaultOpenIDSupportedFormats()}
ctx := &rpOAuthTestContext{
rpTestContext: createRPContext(t, nil),
metadata: func(writer http.ResponseWriter) {
Expand Down
97 changes: 71 additions & 26 deletions vcr/credential/formats.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,61 +29,86 @@ var algValuesSupported = []string{"PS256", "PS384", "PS512", "ES256", "ES384", "
// Recommended list of options https://w3c-ccg.github.io/ld-cryptosuite-registry/
var proofTypeValuesSupported = []string{"JsonWebSignature2020"}

// DefaultSupportedFormats returns the supported formats and is used in the
// DefaultOpenIDSupportedFormats returns the OpenID formats supported by the Nuts node and is used in the
// - Authorization Server's metadata field `vp_formats_supported`
// - Client's metadata field `vp_formats`
//
// TODO: spec is very unclear about this part.
// See https://github.com/nuts-foundation/nuts-node/issues/2447
func DefaultSupportedFormats() SupportedFormats {
return SupportedFormats{
func DefaultOpenIDSupportedFormats() map[string]map[string][]string {
return map[string]map[string][]string{
"jwt_vp_json": {"alg_values_supported": algValuesSupported},
"jwt_vc_json": {"alg_values_supported": algValuesSupported},
"ldp_vc": {"proof_type_values_supported": proofTypeValuesSupported},
"ldp_vp": {"proof_type_values_supported": proofTypeValuesSupported},
}
}

// SupportedFormats is a map of supported formats and their parameters.
// E.g., ldp_vp: {proof_type_values_supported: [Ed25519Signature2018, JsonWebSignature2020]}
type SupportedFormats map[string]map[string][]string
// DIFClaimFormats returns the given DIF claim formats as specified by https://identity.foundation/claim-format-registry/
// as Formats.
func DIFClaimFormats(formats map[string]map[string][]string) Formats {
return Formats{
Map: formats,
ParamAliases: map[string]string{
// no aliases for this type
},
}
}

// OpenIDSupportedFormats returns the given OpenID supported formats as specified by the OpenID4VC family of specs.
func OpenIDSupportedFormats(formats map[string]map[string][]string) Formats {
return Formats{
Map: formats,
ParamAliases: map[string]string{
"alg_values_supported": "alg",
"proof_type_values_supported": "proof_type",
},
}
}

// Formats is a map of supported formats and their parameters according to https://identity.foundation/claim-format-registry/
// E.g., ldp_vp: {proof_type: [Ed25519Signature2018, JsonWebSignature2020]}
type Formats struct {
Map map[string]map[string][]string
ParamAliases map[string]string
}

// Match takes the other supports formats and returns the formats that are supported by both sets.
// If a format is supported by both sets, it returns the intersection of the parameters.
// If a format is supported by both sets, but parameters overlap (e.g. supported cryptographic algorithms),
// the format is not included in the result.
func (f SupportedFormats) Match(other SupportedFormats) SupportedFormats {
result := SupportedFormats{}
func (f Formats) Match(other Formats) Formats {
result := Formats{
Map: map[string]map[string][]string{},
ParamAliases: map[string]string{},
}

for thisFormat, thisFormatParams := range f {
otherFormatParams, supported := other[thisFormat]
if !supported {
for thisFormat, thisFormatParams := range f.Map {
otherFormatParams := other.normalizeParameters(other.Map[thisFormat])
if otherFormatParams == nil {
// format not supported by other
continue
}

result[thisFormat] = map[string][]string{}
for thisParam, thisValues := range thisFormatParams {
result.Map[thisFormat] = map[string][]string{}
for thisParam, thisValues := range f.normalizeParameters(thisFormatParams) {
otherValues, supported := otherFormatParams[thisParam]
if !supported {
// param not supported by other
continue
}

result[thisFormat][thisParam] = []string{}
result.Map[thisFormat][thisParam] = []string{}
for _, thisValue := range thisValues {
for _, otherValue := range otherValues {
if thisValue == otherValue {
result[thisFormat][thisParam] = append(result[thisFormat][thisParam], thisValue)
result.Map[thisFormat][thisParam] = append(result.Map[thisFormat][thisParam], thisValue)
}
}
}
if len(result[thisFormat][thisParam]) == 0 {
delete(result[thisFormat], thisParam)
if len(result.Map[thisFormat][thisParam]) == 0 {
delete(result.Map[thisFormat], thisParam)
}
}
if len(result[thisFormat]) == 0 {
delete(result, thisFormat)
if len(result.Map[thisFormat]) == 0 {
delete(result.Map, thisFormat)
}
}

Expand All @@ -92,15 +117,35 @@ func (f SupportedFormats) Match(other SupportedFormats) SupportedFormats {

// First returns the first format and its parameters.
// If there are no formats, it returns an empty string and nil.
func (f SupportedFormats) First() (string, map[string][]string) {
if len(f) == 0 {
func (f Formats) First() (string, map[string][]string) {
if len(f.Map) == 0 {
return "", nil
}
// Sort the keys to get a deterministic result
var formats []string
for format := range f {
for format := range f.Map {
formats = append(formats, format)
}
sort.Strings(formats)
return formats[0], f[formats[0]]
return formats[0], f.normalizeParameters(f.Map[formats[0]])
}

// normalizeParameter normalizes the parameter name to the name used in the DIF spec.
func (f Formats) normalizeParameter(param string) string {
if alias, ok := f.ParamAliases[param]; ok {
return alias
}
return param
}

// normalizeParameters normalizes the parameter map to the names used in the DIF spec.
func (f Formats) normalizeParameters(params map[string][]string) map[string][]string {
if params == nil {
return nil
}
result := map[string][]string{}
for param, values := range params {
result[f.normalizeParameter(param)] = values
}
return result
}
85 changes: 60 additions & 25 deletions vcr/credential/formats_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,74 +23,109 @@ import (
"testing"
)

func TestSupportedFormats_Match(t *testing.T) {
t.Run("set 2 is subset", func(t *testing.T) {
set1 := SupportedFormats{
func TestFormats_Match(t *testing.T) {
t.Run("DIF - set 2 is subset", func(t *testing.T) {
set1 := DIFClaimFormats(map[string]map[string][]string{
"jwt_vp": {
"alg_values_supported": {"ES256", "EdDSA"},
},
"ldp_vp": {
"proof_type_values_supported": {"Ed25519Signature2018", "JsonWebSignature2020"},
},
}
set2 := SupportedFormats{
})
set2 := DIFClaimFormats(map[string]map[string][]string{
"jwt_vp": {
"alg_values_supported": {"ES256"},
},
"ldp_vp": {
"proof_type_values_supported": {"JsonWebSignature2020"},
},
}
expected := SupportedFormats{
})
expected := DIFClaimFormats(map[string]map[string][]string{
"jwt_vp": {
"alg_values_supported": {"ES256"},
},
"ldp_vp": {
"proof_type_values_supported": {"JsonWebSignature2020"},
},
}
})

result := set1.Match(set2)
assert.Equal(t, expected, result)
})
t.Run("set 2 does not match format params for JWT", func(t *testing.T) {
set1 := SupportedFormats{
t.Run("one set PEX style, other set OpenID4VC style", func(t *testing.T) {
set1 := DIFClaimFormats(map[string]map[string][]string{
"jwt_vp": {
"alg": {"ES256", "EdDSA"},
},
"ldp_vp": {
"proof_type": {"Ed25519Signature2018", "JsonWebSignature2020"},
},
})
set2 := OpenIDSupportedFormats(map[string]map[string][]string{
"jwt_vp": {
"alg_values_supported": {"ES256", "EdDSA"},
},
"ldp_vp": {
"proof_type_values_supported": {"Ed25519Signature2018", "JsonWebSignature2020"},
},
}
set2 := SupportedFormats{
})
expected := DIFClaimFormats(map[string]map[string][]string{
"jwt_vp": {
"alg": {"ES256", "EdDSA"},
},
"ldp_vp": {
"proof_type": {"Ed25519Signature2018", "JsonWebSignature2020"},
},
})

t.Run("PEX match OpenID", func(t *testing.T) {
result := set1.Match(set2)
assert.Equal(t, expected, result)
})
t.Run("OpenID match PEX", func(t *testing.T) {
result := set2.Match(set1)
assert.Equal(t, expected, result)
})
})
t.Run("set 2 does not match format params for JWT", func(t *testing.T) {
set1 := DIFClaimFormats(map[string]map[string][]string{
"jwt_vp": {
"alg": {"ES256", "EdDSA"},
},
"ldp_vp": {
"proof_type": {"Ed25519Signature2018", "JsonWebSignature2020"},
},
})
set2 := DIFClaimFormats(map[string]map[string][]string{
"jwt_vp": {
"alg_values_supported": {"ES256K"},
"alg": {"ES256K"},
},
}
expected := SupportedFormats{}
})
expected := DIFClaimFormats(map[string]map[string][]string{})

result := set1.Match(set2)
assert.Equal(t, expected, result)
})
t.Run("set 2 does not support one of the formats", func(t *testing.T) {
set1 := SupportedFormats{
set1 := DIFClaimFormats(map[string]map[string][]string{
"jwt_vp": {
"alg_values_supported": {"ES256", "EdDSA"},
},
"ldp_vp": {
"proof_type_values_supported": {"Ed25519Signature2018", "JsonWebSignature2020"},
},
}
set2 := SupportedFormats{
})
set2 := DIFClaimFormats(map[string]map[string][]string{
"jwt_vp": {
"alg_values_supported": {"ES256"},
},
}
expected := SupportedFormats{
})
expected := DIFClaimFormats(map[string]map[string][]string{
"jwt_vp": {
"alg_values_supported": {"ES256"},
},
}
})

result := set1.Match(set2)
assert.Equal(t, expected, result)
Expand All @@ -99,24 +134,24 @@ func TestSupportedFormats_Match(t *testing.T) {

func TestSupportedFormats_First(t *testing.T) {
t.Run("empty", func(t *testing.T) {
set := SupportedFormats{}
set := Formats{}
format, params := set.First()
assert.Equal(t, "", format)
assert.Nil(t, params)
})
t.Run("non-empty", func(t *testing.T) {
set := SupportedFormats{
set := OpenIDSupportedFormats(map[string]map[string][]string{
"ldp_vp": {
"proof_type_values_supported": {"Ed25519Signature2018", "JsonWebSignature2020"},
},
"jwt_vp": {
"alg_values_supported": {"ES256", "EdDSA"},
},
}
})
format, params := set.First()
assert.Equal(t, "jwt_vp", format)
assert.Equal(t, map[string][]string{
"alg_values_supported": {"ES256", "EdDSA"},
"alg": {"ES256", "EdDSA"},
}, params)
})
}

0 comments on commit 1a7d353

Please sign in to comment.