@@ -20,10 +20,14 @@ package iam
2020
2121import (
2222 "context"
23+ "crypto/ecdsa"
2324 "crypto/tls"
2425 "encoding/json"
2526 "errors"
2627 "fmt"
28+ "github.com/lestrrat-go/jwx/v2/jwa"
29+ "github.com/lestrrat-go/jwx/v2/jws"
30+ "github.com/lestrrat-go/jwx/v2/jwt"
2731 "github.com/nuts-foundation/nuts-node/http/client"
2832 test2 "github.com/nuts-foundation/nuts-node/test"
2933 "github.com/nuts-foundation/nuts-node/vcr/credential"
@@ -40,6 +44,7 @@ import (
4044 "github.com/nuts-foundation/nuts-node/audit"
4145 "github.com/nuts-foundation/nuts-node/auth/oauth"
4246 "github.com/nuts-foundation/nuts-node/crypto"
47+ cryptoTest "github.com/nuts-foundation/nuts-node/crypto/test"
4348 http2 "github.com/nuts-foundation/nuts-node/test/http"
4449 "github.com/nuts-foundation/nuts-node/vcr/holder"
4550 "github.com/nuts-foundation/nuts-node/vcr/openid4vci"
@@ -486,8 +491,9 @@ func createClientTestContext(t *testing.T, tlsConfig *tls.Config) *clientTestCon
486491 wallet : wallet ,
487492 subjectManager : subjectManager ,
488493 httpClient : HTTPClient {
489- strictMode : false ,
490- httpClient : client .NewWithTLSConfig (10 * time .Second , tlsConfig ),
494+ strictMode : false ,
495+ httpClient : client .NewWithTLSConfig (10 * time .Second , tlsConfig ),
496+ keyResolver : keyResolver ,
491497 },
492498 jwtSigner : jwtSigner ,
493499 keyResolver : keyResolver ,
@@ -697,6 +703,238 @@ func TestIAMClient_OpenIdCredentialIssuerMetadata(t *testing.T) {
697703 assert .Nil (t , response )
698704 assert .EqualError (t , err , "failed to retrieve Openid credential issuer metadata: server returned HTTP 404 (expected: 200)" )
699705 })
706+ t .Run ("ok - signed_metadata is verified" , func (t * testing.T ) {
707+ ctx := createClientServerTestContext (t )
708+ ecKey := cryptoTest .GenerateECKey ()
709+ kid := "did:web:example.com#key-1"
710+ signedJWT := createSignedMetadataJWT (t , ecKey , kid , map [string ]interface {}{
711+ "credential_issuer" : "https://issuer.example.com" ,
712+ "credential_endpoint" : "https://issuer.example.com/credential" ,
713+ })
714+ issuerMetadata := & oauth.OpenIDCredentialIssuerMetadata {
715+ CredentialIssuer : "https://issuer.example.com" ,
716+ CredentialEndpoint : "https://issuer.example.com/credential" ,
717+ SignedMetadata : signedJWT ,
718+ }
719+ ctx .openIDCredentialIssuerMetadata = issuerMetadata
720+ ctx .credentialIssuerMetadata = func (writer http.ResponseWriter ) {
721+ writer .Header ().Add ("Content-Type" , "application/json" )
722+ writer .WriteHeader (http .StatusOK )
723+ bytes , _ := json .Marshal (* issuerMetadata )
724+ _ , _ = writer .Write (bytes )
725+ }
726+ ctx .keyResolver .EXPECT ().ResolveKeyByID (kid , nil , resolver .AssertionMethod ).Return (ecKey .Public (), nil )
727+
728+ metadata , err := ctx .client .OpenIdCredentialIssuerMetadata (context .Background (), ctx .tlsServer .URL + "/issuer" )
729+
730+ require .NoError (t , err )
731+ require .NotNil (t , metadata )
732+ assert .Equal (t , "https://issuer.example.com" , metadata .CredentialIssuer )
733+ })
734+ t .Run ("error - signed_metadata JWT signature invalid" , func (t * testing.T ) {
735+ ctx := createClientServerTestContext (t )
736+ issuerMetadata := & oauth.OpenIDCredentialIssuerMetadata {
737+ CredentialIssuer : "https://issuer.example.com" ,
738+ CredentialEndpoint : "https://issuer.example.com/credential" ,
739+ SignedMetadata : "invalid.jwt.token" ,
740+ }
741+ ctx .credentialIssuerMetadata = func (writer http.ResponseWriter ) {
742+ writer .Header ().Add ("Content-Type" , "application/json" )
743+ writer .WriteHeader (http .StatusOK )
744+ bytes , _ := json .Marshal (* issuerMetadata )
745+ _ , _ = writer .Write (bytes )
746+ }
747+
748+ metadata , err := ctx .client .OpenIdCredentialIssuerMetadata (context .Background (), ctx .tlsServer .URL + "/issuer" )
749+
750+ require .Error (t , err )
751+ assert .Nil (t , metadata )
752+ assert .ErrorContains (t , err , "signed_metadata verification failed" )
753+ })
754+ t .Run ("error - signed_metadata sub mismatch" , func (t * testing.T ) {
755+ ctx := createClientServerTestContext (t )
756+ ecKey := cryptoTest .GenerateECKey ()
757+ kid := "did:web:example.com#key-1"
758+ signedJWT := createSignedMetadataJWT (t , ecKey , kid , map [string ]interface {}{
759+ "credential_issuer" : "https://other-issuer.example.com" ,
760+ "credential_endpoint" : "https://issuer.example.com/credential" ,
761+ })
762+ issuerMetadata := & oauth.OpenIDCredentialIssuerMetadata {
763+ CredentialIssuer : "https://issuer.example.com" ,
764+ CredentialEndpoint : "https://issuer.example.com/credential" ,
765+ SignedMetadata : signedJWT ,
766+ }
767+ ctx .credentialIssuerMetadata = func (writer http.ResponseWriter ) {
768+ writer .Header ().Add ("Content-Type" , "application/json" )
769+ writer .WriteHeader (http .StatusOK )
770+ bytes , _ := json .Marshal (* issuerMetadata )
771+ _ , _ = writer .Write (bytes )
772+ }
773+ ctx .keyResolver .EXPECT ().ResolveKeyByID (kid , nil , resolver .AssertionMethod ).Return (ecKey .Public (), nil )
774+
775+ metadata , err := ctx .client .OpenIdCredentialIssuerMetadata (context .Background (), ctx .tlsServer .URL + "/issuer" )
776+
777+ require .Error (t , err )
778+ assert .Nil (t , metadata )
779+ assert .ErrorContains (t , err , "sub" )
780+ assert .ErrorContains (t , err , "does not match credential_issuer" )
781+ })
782+ t .Run ("error - signed_metadata credential_endpoint mismatch" , func (t * testing.T ) {
783+ ctx := createClientServerTestContext (t )
784+ ecKey := cryptoTest .GenerateECKey ()
785+ kid := "did:web:example.com#key-1"
786+ signedJWT := createSignedMetadataJWT (t , ecKey , kid , map [string ]interface {}{
787+ "credential_issuer" : "https://issuer.example.com" ,
788+ "credential_endpoint" : "https://evil.example.com/credential" ,
789+ })
790+ issuerMetadata := & oauth.OpenIDCredentialIssuerMetadata {
791+ CredentialIssuer : "https://issuer.example.com" ,
792+ CredentialEndpoint : "https://issuer.example.com/credential" ,
793+ SignedMetadata : signedJWT ,
794+ }
795+ ctx .credentialIssuerMetadata = func (writer http.ResponseWriter ) {
796+ writer .Header ().Add ("Content-Type" , "application/json" )
797+ writer .WriteHeader (http .StatusOK )
798+ bytes , _ := json .Marshal (* issuerMetadata )
799+ _ , _ = writer .Write (bytes )
800+ }
801+ ctx .keyResolver .EXPECT ().ResolveKeyByID (kid , nil , resolver .AssertionMethod ).Return (ecKey .Public (), nil )
802+
803+ metadata , err := ctx .client .OpenIdCredentialIssuerMetadata (context .Background (), ctx .tlsServer .URL + "/issuer" )
804+
805+ require .Error (t , err )
806+ assert .Nil (t , metadata )
807+ assert .ErrorContains (t , err , "credential_endpoint claim" )
808+ assert .ErrorContains (t , err , "does not match metadata" )
809+ })
810+ t .Run ("error - signed_metadata wrong typ header" , func (t * testing.T ) {
811+ ctx := createClientServerTestContext (t )
812+ ecKey := cryptoTest .GenerateECKey ()
813+ kid := "did:web:example.com#key-1"
814+ signedJWT := createSignedMetadataJWTCustom (t , ecKey , kid , "jwt" , map [string ]interface {}{
815+ "iss" : "https://issuer.example.com" ,
816+ "sub" : "https://issuer.example.com" ,
817+ "credential_issuer" : "https://issuer.example.com" ,
818+ "credential_endpoint" : "https://issuer.example.com/credential" ,
819+ }, true , true )
820+ issuerMetadata := & oauth.OpenIDCredentialIssuerMetadata {
821+ CredentialIssuer : "https://issuer.example.com" ,
822+ CredentialEndpoint : "https://issuer.example.com/credential" ,
823+ SignedMetadata : signedJWT ,
824+ }
825+ ctx .credentialIssuerMetadata = func (writer http.ResponseWriter ) {
826+ writer .Header ().Add ("Content-Type" , "application/json" )
827+ writer .WriteHeader (http .StatusOK )
828+ bytes , _ := json .Marshal (* issuerMetadata )
829+ _ , _ = writer .Write (bytes )
830+ }
831+
832+ metadata , err := ctx .client .OpenIdCredentialIssuerMetadata (context .Background (), ctx .tlsServer .URL + "/issuer" )
833+
834+ require .Error (t , err )
835+ assert .Nil (t , metadata )
836+ assert .ErrorContains (t , err , "typ header must be openidvci-issuer-metadata+jwt" )
837+ })
838+ t .Run ("error - signed_metadata missing sub" , func (t * testing.T ) {
839+ ctx := createClientServerTestContext (t )
840+ ecKey := cryptoTest .GenerateECKey ()
841+ kid := "did:web:example.com#key-1"
842+ signedJWT := createSignedMetadataJWTCustom (t , ecKey , kid , "openidvci-issuer-metadata+jwt" , map [string ]interface {}{
843+ "iss" : "https://issuer.example.com" ,
844+ "credential_issuer" : "https://issuer.example.com" ,
845+ "credential_endpoint" : "https://issuer.example.com/credential" ,
846+ }, true , true )
847+ issuerMetadata := & oauth.OpenIDCredentialIssuerMetadata {
848+ CredentialIssuer : "https://issuer.example.com" ,
849+ CredentialEndpoint : "https://issuer.example.com/credential" ,
850+ SignedMetadata : signedJWT ,
851+ }
852+ ctx .credentialIssuerMetadata = func (writer http.ResponseWriter ) {
853+ writer .Header ().Add ("Content-Type" , "application/json" )
854+ writer .WriteHeader (http .StatusOK )
855+ bytes , _ := json .Marshal (* issuerMetadata )
856+ _ , _ = writer .Write (bytes )
857+ }
858+ ctx .keyResolver .EXPECT ().ResolveKeyByID (kid , nil , resolver .AssertionMethod ).Return (ecKey .Public (), nil )
859+
860+ metadata , err := ctx .client .OpenIdCredentialIssuerMetadata (context .Background (), ctx .tlsServer .URL + "/issuer" )
861+
862+ require .Error (t , err )
863+ assert .Nil (t , metadata )
864+ assert .ErrorContains (t , err , "sub" )
865+ assert .ErrorContains (t , err , "does not match credential_issuer" )
866+ })
867+ t .Run ("error - signed_metadata missing iat" , func (t * testing.T ) {
868+ ctx := createClientServerTestContext (t )
869+ ecKey := cryptoTest .GenerateECKey ()
870+ kid := "did:web:example.com#key-1"
871+ signedJWT := createSignedMetadataJWTCustom (t , ecKey , kid , "openidvci-issuer-metadata+jwt" , map [string ]interface {}{
872+ "sub" : "https://issuer.example.com" ,
873+ "credential_issuer" : "https://issuer.example.com" ,
874+ "credential_endpoint" : "https://issuer.example.com/credential" ,
875+ }, false , false )
876+ issuerMetadata := & oauth.OpenIDCredentialIssuerMetadata {
877+ CredentialIssuer : "https://issuer.example.com" ,
878+ CredentialEndpoint : "https://issuer.example.com/credential" ,
879+ SignedMetadata : signedJWT ,
880+ }
881+ ctx .credentialIssuerMetadata = func (writer http.ResponseWriter ) {
882+ writer .Header ().Add ("Content-Type" , "application/json" )
883+ writer .WriteHeader (http .StatusOK )
884+ bytes , _ := json .Marshal (* issuerMetadata )
885+ _ , _ = writer .Write (bytes )
886+ }
887+ ctx .keyResolver .EXPECT ().ResolveKeyByID (kid , nil , resolver .AssertionMethod ).Return (ecKey .Public (), nil )
888+
889+ metadata , err := ctx .client .OpenIdCredentialIssuerMetadata (context .Background (), ctx .tlsServer .URL + "/issuer" )
890+
891+ require .Error (t , err )
892+ assert .Nil (t , metadata )
893+ assert .ErrorContains (t , err , "iat claim is required" )
894+ })
895+ }
896+
897+ func createSignedMetadataJWT (t * testing.T , key * ecdsa.PrivateKey , kid string , claims map [string ]interface {}) string {
898+ t .Helper ()
899+ token := jwt .New ()
900+ for k , v := range claims {
901+ require .NoError (t , token .Set (k , v ))
902+ }
903+ if iss , ok := claims ["credential_issuer" ].(string ); ok {
904+ require .NoError (t , token .Set (jwt .IssuerKey , iss ))
905+ require .NoError (t , token .Set (jwt .SubjectKey , iss ))
906+ }
907+ require .NoError (t , token .Set (jwt .IssuedAtKey , time .Now ().Unix ()))
908+ require .NoError (t , token .Set (jwt .ExpirationKey , time .Now ().Add (time .Hour ).Unix ()))
909+ hdrs := jws .NewHeaders ()
910+ require .NoError (t , hdrs .Set (jws .KeyIDKey , kid ))
911+ require .NoError (t , hdrs .Set (jws .TypeKey , "openidvci-issuer-metadata+jwt" ))
912+ signed , err := jwt .Sign (token , jwt .WithKey (jwa .ES256 , key , jws .WithProtectedHeaders (hdrs )))
913+ require .NoError (t , err )
914+ return string (signed )
915+ }
916+
917+ // createSignedMetadataJWTCustom allows overriding specific JWT fields for negative tests.
918+ func createSignedMetadataJWTCustom (t * testing.T , key * ecdsa.PrivateKey , kid string , typ string , claims map [string ]interface {}, setIat bool , setExp bool ) string {
919+ t .Helper ()
920+ token := jwt .New ()
921+ for k , v := range claims {
922+ require .NoError (t , token .Set (k , v ))
923+ }
924+ if setIat {
925+ require .NoError (t , token .Set (jwt .IssuedAtKey , time .Now ().Unix ()))
926+ }
927+ if setExp {
928+ require .NoError (t , token .Set (jwt .ExpirationKey , time .Now ().Add (time .Hour ).Unix ()))
929+ }
930+ hdrs := jws .NewHeaders ()
931+ require .NoError (t , hdrs .Set (jws .KeyIDKey , kid ))
932+ if typ != "" {
933+ require .NoError (t , hdrs .Set (jws .TypeKey , typ ))
934+ }
935+ signed , err := jwt .Sign (token , jwt .WithKey (jwa .ES256 , key , jws .WithProtectedHeaders (hdrs )))
936+ require .NoError (t , err )
937+ return string (signed )
700938}
701939
702940func TestIAMClient_RequestNonce (t * testing.T ) {
0 commit comments