diff --git a/api/spec/openapi.gen.go b/api/spec/openapi.gen.go index 3344886a4..8093059c2 100644 --- a/api/spec/openapi.gen.go +++ b/api/spec/openapi.gen.go @@ -19,152 +19,153 @@ import ( // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+x97XLbOLbgq6C0W5WkSrIz/XHvjvfPuu10t2aS2Nd2nJqapFQQCUnoUAQbAK2oU67a", - "19jX2ye5hQOABEiApGQr3TPtX4lFfB4cnG+c82WUsHXBcpJLMTr5MhLJiqwx/Pc0SYgQN+wTya+IKFgu", - "iPo5JSLhtJCU5aOT0RuWkgwtGEe6OYL2yHY4Go1HBWcF4ZISGBVDs5lUzdrD3awI0i0QtEBUiJKkaL5F", - "Un0q5Ypx+htWzZEg/I5wNYXcFmR0MhKS03w5uh+PklnO8iSw3mtoghKWS0xz9V+MoCmSDM0JKgVJ1X8T", - "TrAkCKOCM7ZAbIEKJgQRQk3MFugT2aI1loRTnKHNiuSIk19LIqQeMuEkJbmkOOta3ox8LignYkYDoJjm", - "kiwJRynJGYyqAJDRBZF0TRBV209Yngq1GvXJjOnMR/UIasKuiW66x3WPIzw4JwtOxKrrTE0TPcoYbVY0", - "WaEE5y7I2VwdCcrJxptTBCEoElYEjvfi8mZ68fb09RjRBaJwBAnO1OhqK9DJHlSNVUlGSS7/N2JyRfiG", - "CjJGV6/+69306tV5cG5Y1kz/HNqs+mKh52JxYDCA3q8l5SQdnfzTvxzeRB/HI0llpvqG7mU1MJv/QhI5", - "Go8+TyReCjUoo2nyXUJHH+/Ho7MKL8+pKDK8VTvwL+gcJ5+WnJV5OktYxrj6rQWCjCU4I5FPS6Y+/E9O", - "FqOT0f84rgnMsaEux69Vm/vxKMfr8CCSfJYd05c8C/x+X8Oovc8IhBSA4HR4Az5TIUqcJ+RnKiTj23Ms", - "cfu0O5sjTgpOhNq4RrOqMeCDao1WujnCnOMtIhlZk1y2iWbddUbTrmWgaRhlnQHUN9Ee42/XF2/NMtjC", - "XSu0V4NSSdYifFr6B+hdURug0bMUy8AteVU1QOdYkuCSLYwiQ1iAdw9AeLgn4YY6LGiEg0iOc4ET2EQI", - "5jf19zDQG1fbP8JqdYGj+RhC4wZ+IT7k5gcR+0qTvy5WfjE9P0MOUhmK2cbLBeNrHBjqR/i94kz1SHOi", - "OFmUHML4bNFHPv72/uYS2hncC6DzqcVk+D5gJUOxu3GsHUdmAb07bR4ibrXPqEviKiRcpQiHPkVw+0VA", - "NtL8S5RzoXaTy2zb5NfYWcURevPu+kYxdUP5tGjkUT6UM4k4kSXPIzgQFd6iqzyABHf2cAkOlksfU4yr", - "AalmYzm5WIxO/tnG2S8NlLtX+BW7rC5UvVUuvFtsJPFOuDQuh5nRW3fkquwtx1xLLEvRyRMFNGlfDFF1", - "jdz5Lz37MwOY5sGdXXtNhlNp3e+iCJzXBfxHABVQfeE2eKfib3PYXvq2oJYycBevPicrnC/JqautnbGU", - "DGA9RPeFO1jKFUpYStCCs7XGP46Y+rm1R1bM1GEM2GfV0tlr74IfvvEYQbdf0PqhIJCfjZzSc87QbNjm", - "dxMxpjmVFEuiONN3Z9MBh217tJhZJdxdxSQPzxIwS4nENAtRgVJItqa/EYE2KyzRJ5qniqAZBdOIghsM", - "MjpDS3oHbOT27DpM9TNM10ogxaGT1ECGnV1yMrEAVURTHeGPGdscqaH1dq8Jv6OJ0q+lQFigi0voucFZ", - "RiTCRZHRBHbXph7VSkieFozmASCfqe/Ifrc03OwXkGmzItyTiGBIpDaHVlgYblory3ghCUeiBMgtyizb", - "IpyoLQOi9irsWsmeUXPks0q8N7qcv/x3V69dvgO4YLoq9u3uC6P3ALIjdIM/EaEkj0TtKSGI3RFutPvZ", - "hmTZp5xtKjaPCszxmkjCj9B0geZMXbWORSKcp+3BMCcg0BSc3dFUSR5akjC32o5U70LtbEOzzAowKAEU", - "jbSkecWFC5LTdGKbTWyzk+PjLnhXKx1iCtO4d7xiWUq4i4IaY/WQqN58wvIFXZZGk3t39bpP9/QW0MGx", - "3Q/dI1r5KyTYKM2wKYAKJFaszFKF2wnLBYWdCqTHUYqZFZNGSu+cKCmtZwnWiBHdDTToUcrJusgA40Kq", - "pvkY0GL0JTWi2WZFM+Lf0ITlSVamWqKjAqRRrrXWo8rIBcYyNXDB2UINQUV1tFqWLhWDKjNJi8yf3qws", - "fOWXHOcyYiczlCjBub069iJAL6O2yRVn5XKl1+7c1xv1d93QoVcg/WtAuHw0963KitD6tmRgsjRHajcc", - "CUkKAWShfbdTssBlJtV8PhNSQwTh4AonwSt4h7OSGIWlsko22KFCU8W7CvxrSaxBU1M+JBVro6JSfeaK", - "y4HBs5xPjOIGi9X2UNiwpYIbKleR+dQOgTyQzxIJIlFZoLSEFRec3FFWCgdStSUVKQpM74hA2GxNwds/", - "wzGiUiuLFDCUqL9pbldtF33qL9qIA3b7ARBpU5uFeD2fXojRT99e3FS4QnPkST6aVy8yttGko+BkgitO", - "PtN4Iqx+GzxvS/0jqH+mCa6ouQTgsDlE2Ab5XBAlFihhwVw/jdMF4Yo+qSMAkuwjsbXToHONo3Apmob7", - "Xht6tT74LoYtzNWd2xdLnX8tXvjr04xtNwNjKQifFTSf1ZLtnuLYD4xlBOcGT0VBErrYAi9cEblSl8Cq", - "vvXmzdnr/YEEotaDLqdvEc6Y6mvvlHWGaawFY4mPTwY8ain1Cc31mtRGNUeuBJK0kkjaG7Y7WWR4qQh9", - "qi4NyL16I0q2zZFj1kTAD8zAiuoYOSqwEMeyHpHy99UR+g1dQ5SEmOmLLRaEzxw+GxQ2zWIiIpjDVwxl", - "rsljgYW6xhm5U6yI5lp2ULBtEGgWGBxOHV2XRcG4FFoA/fnm5hL99OoGaD38cUVSykkij8y0Aq3xtjKv", - "/deVxiBHiLOEHQR5BUCFnHDThOK2IPvLFaEcrdlcXd33lcYRNoR/DgslHlgs+XW0Fn3pGeck0yChC5QT", - "kkaMfvZKt2e69G+MBttPJCccEPfi5hIVWk6uYNtvmgpixritHccQdh98v720XiQfS116ck4WgCks/5Fm", - "knDRZwu/7OwMFvVQg2kaJLRFyQsmSLdzLbSpLnjcEQ5+lhBEXArQoeE7xoQAgk7P++0eweFM54/RvUXP", - "W+1EHbPjaAxaBWo6ZphIl6UOWGTAinFdqSyaQVEltyxQKXzTXyWOBw0Gnuk4qqvQHP2yEc81EF8gxtEv", - "guVZ+lyP9MKooyDw72h/PqgeeHAl7KwNZgSOvIC4r602PRe3gT7GqutftACGDSU84dEfbExOVopb5MsQ", - "sFc4w/kSxGOcploVMWolW8RMA4qGh8MoUkfl1UMoNYOtqVRkX2yFJGsEXhSwpxhu1GOCqN0FXWcTMn7f", - "j0cpW+MQhzqH33fYt6aImlG+IXLFIiB4dzW1EGh30cxXq1YhCC0oFxKR9Jvvv//LX1FRzjOagNeLLdD5", - "9Bw9N0wb5GOt+J9Pz1/0QTOOnxbJBqJo5cVtkf5fNgFrThWSg67pMicp+tv7G6XvVe49tbXaxRf3LkfU", - "snp8cIhdBxxieirV/QidlZxrfygonHm2RUKLciR1GiqkePbLRj7rF0mcxY0BBA5bqmA11EH22kThNHhq", - "JmdKm98vqAbGHBZHc6G0rUurfIsYXwRFSZ2bVr8KTLlwZdlKfdfmnZJmqTFFMk7Cyi96fvXj2X/853d/", - "faG1B43j0MnYcbTkrhVpa24HBc4fD8xLIR6tTcRh+cl8FSThJAzllnEgrpbvGZLgzzB2Vtxcn53LQbTm", - "wQ28y5ecFJgT8DooNnUaEd5iwpHpj7TbAqKnfKvM7o4gQ9+PFH1fs/xoi9dZkNh7E52bARpmu11tPLeA", - "zzYYQmgV8MNI6WofRt3GmEc69ZCfc9ApPc6J9+v1A448GtHinXncCaYv/zPRuP7+Pbfdg6fiz8RrRO6S", - "Hpp3CPQqsSLpLDjc7hu4PL3qXnZMZ/di1iBIxujnBJVFwtZt850bVtSapmWdUrr5TndP2x6s0pX+mLEN", - "SLmd2lt1DuMYJgQU+WH4uiPyd+hUAUQfEFCHy5SSPNHLDMuEH1SjDyNjTzWm9rSy6xgbfPC80hBSnGtM", - "0CHZxpPk6JS1a2XOyjwsXj9+GOAgBA73/J3jAT/PKq1QAIK6BxrAxxqF9sW9KyLKTO6MgTHiepAYsxoT", - "WhgWdpnRNJnFBtMSYL2XKswswKUl3wbQ6OrdK0QXbsSDiUvcEonwHaYZnmfE+qOM5ebi0r440f5H0JOs", - "nb2O65BMd0DNuEtEcyEJThvx1ZUX6Pk5WRDOvRg7sHa+iJjkw+HFtQvbB6OFRhcOGlQajonddstGsDAl", - "WSp2FHGcpXbMNdj6eFmKVUjeGyKilmLVkFBM5y5y/jsIp7GAt3FkOS5C9IBnKGKAtLO7RAjdBkuBXaG3", - "JqI5L9dz8OFh2XyNUYXgGlZg1cd3V1M3KhcLhFHBBJX0jthgXkUA/B51QK9AWMKAKRVKyTI+wtjrNTQv", - "paYkclvQBGfZVoddZVjNmG2RWDEu0XNytDwaozmRG0Jy9D04iP7j5Uu70Bexp1laxCw5jT3MqjcBwqCC", - "to4XYYFFV7FTTEiSGkIIIFNwEjRfZmRSCnjwRTgxUdkavqIgCUDR81C1ff5hn3av+cTdqvfgrYHfMcQc", - "alm5IksqJOEg5Z+BsPWKc8bjGA4t0TdHL+v4CDWECZsiqnMHP4bvAZ8AwBqdXp9Np2YM8ARq6ASZKrTq", - "tr3/XK5xPuEEp8AA9egQ/+G0s/isZ62skCmZl8tlePLGWek9OQfTC9QHnE6UtnefS5SoGxtK2OPQAKAJ", - "q1dtK5lTz6VlakOSapsxydMJGKNMoI13GboC/YI3/N3Va7sEiFPYkDkq8JIYVRIkXsdpjeeslH1KBJjn", - "EtklY+vGoia5OrhwK7TKCf1RQViREYv4VEGrChPS048dmkjWmGYIpyknQuz6Hq0OROtadY0OfggadkEC", - "hC7L2KYKjKtc+CTVZkxxEggMG6OS5yeUyMUJGDXFCURxn8BUEzXVSSDSaLdt/rL5FAqBhnU/E5ojvidz", - "9HeyRddEopQl5VrtCZZdvcq14T/1pp8Jx33gxvrV7kU1dy8OWqZg7elJcGnP//b+7y+8Be6zNP9Bau/S", - "jIhgmJZiZqpb5V3puA8Fy2iyHTYBWCeEDqxb+ZSi4PQOJ1ukh6vPBvrpUedEoBXbaOmCFBnbQgvGlziv", - "w62yjCRSjBVqijHiBCA2BnlBiSQZE0SggnDBcpzpeKyw6qTjTtTGum6NvQy2vY4EnlY0oAFBVMVlgf4F", - "V0rYEJj2tXGu4m53wTOHDrv1Xjhe++InOId4N/NrxIgYIAa7X+RIYF4od4EocEImQulxEA2TUQFqtn7q", - "rpcQ3Urr1Vv/s3u2kBvMwy7yU1Tm9NeSOE9rLfaD+IrevZuev0BYCO3A857fo5TckUzxWcQ4svPoyy1W", - "hFehRr7wZOAOd8pM641aDaT5bbrN8dqwFG5EhYgJqtrqHeEiKCydIvMpsGEf7etlVC1hLx9cgEYcAzoJ", - "gN0omJBn64jP+Kp6rWHmbQT96n7V4rRZogt3c5aTMfK8RjMl+zd/m2NBkyP0luWkCkRWsxjarBsL9DwH", - "rQbhohBjG3+m/njhpITImUQrfEeQHltU4aInwUnDMBMPJsiS8DUYCoV5qFOR5MbZNii0DpnmOJElWHd0", - "9JtY0aLS3jxBD5tYbXc0vwHYkYS+rZbs+Cy0203fIRM/SKzufdMG7t36min0w1VYog13b0rhPS7XkHDT", - "95a/GmCmrY/B1yI3Sn3H0iCiK/HVl3uDRdtq7b7P/UOqBrU3Ogg8/dno8toRb6+wpdAsX9D6mZhdpENc", - "KjW/QVJ6V9X5gCd6JLqvtpvoARTTeAlJX8zPioroT51H9aQ2PalNT2rTk9r0pDY9qU1PatOT2vSkNv3p", - "1SbPrd6OnfS0iE488yWojz0K2Y6OjmvJ+F4ZVoRkfNfcIqpZkAR3xkh+vfAwx8cNS3Vg3Q2ngQ7t2CA7", - "ZHXZB+wdKV36trdbPNu7IsWSNF89RJGps3nl3hWSl4m+9qXqoHZ/exbNxlRHsASfcz38EYd5ALCgGYnM", - "YL7e1pyrN2LfjNbqO/b3E1i9g6Pd4B94hrc4o2qYyxofSDqQJtzpvuZlf+t9sqK1Bc33scxEovQCSlvw", - "4SdqLGXHp6PvFCMwK+87yvaCnBPqBe7DD6k/PmbfU4rnn7go4DqSeER6SAeopOXGKkyHXUT2SJyn90o9", - "7X/MW5P/ag2tAOB+0A89Q8LpYlvf1rMVST7FgjB142C8n6OQLDDNSk5QooZCJjAr9PiOJJ9CD+9UL9hn", - "PO4jkIQVwjfWRAi8JHs/U7t12hhBeYBYBRuxKwtO5J5cB8AHR/41B+l7ruucmLu6vnjZ3+Nh7cAHp00I", - "uC9OI6GkHYew26vv2Nyd71Hvmnfn0M9RH+l9530cakOeSHYCbgifqCiMF2gs+vBY3So/PHcXbHIvZVcg", - "b3RDO4LEDQgeQoG9pC3/MjS4k262bmcMJg8AbR+Z9MDajWA7kSl3DRWh8pNZBAXGejEHI7htybFeUueR", - "7EMyQ3AYQjTdVe1MNuHTH4Buhjb/APjtSjt3wO29iGfsuvaTz+CuBkPmPcmyv+dsk18UJJ+en7lZHEPI", - "pRoh3aorzfjAR5lOas+Ly2fC8ST4gf+vunzIjmFtVj277yxq4Jn/rQIDi6jm/wmcJjfblgeAQnZW62/Z", - "zSnRUCz1wnHO8u2alWJmKn707cEm9zLPjSIJyqzJEzcSj0EcBg5mQdPPD+SKlRLhOuJBv3CwqQ6pQAuc", - "eY9AnRxlrmdhh3M/1z4FZAwFV65/ovPsfR/V4x2/N+4jYoBWSR9vnf80CQ4+Br1VVNiXJfut1rdn73J9", - "Nc51Hl3LIgzm8kXGNo90A2wG08o3avI71HnuILMj1RltvzubDkf0zhfJ7stjH4Ad+BpAjRhlGwi63cmN", - "I7B0sYWd6400BtMP2Xs4TX+f5iMunRIwuFCtxGNBjAfo9uxaXwx40zU9v/ydOdgcy2TlJqAbNF8r4+kz", - "gX5QI7lfqnnt86zXOhNFKQgMspKyEAhkEW01fHP6jyodtcKLMSqw4hh5in4tCd86aVRrYcbNNTSOpGNN", - "GdHvag3CQbP4evtyPe8Loa8Pm56dxEoT3UQ20BffWCv5XXTUPIrsYDTwXV9sMUYEJysD1rUflFYFcurQ", - "N4xsJEWzcFTtl20jR4JNYTRnTxUbSuvyZI1N5HWVqmqlOpe9Xi/8Vsc8mYFQfdV1TF97PZAUGiWEw1ln", - "OF+WRikfJNO3S451h9Q9ya1PcuuT3PqvILf+e0p+IYYUZrcxBhOXHcOi2y4SpAI6zRdMR59A6DM8hl5j", - "mo1ORiuSZez/SF4KOc9YcpSSu5EtJzm6UT//kLEESYLX6kAhtd0IePvJ8bHfTTGbRpIZ2x1kRiO5hKo3", - "KiHAtQuZjKzvvz1Dt2eT08upmw9cQ+a7W8jjJFnC3LSwx9ZA41Zo0P3qrNwZTYgxX5mdnhY4WZHJN0cv", - "W5vcbDZHGD4fMb48Nn3F8evp2au3169UnyP5WRubXNsShSguhzvaOjXPb8+uX2hfnY6OGL08UhODA4rk", - "uKCjk9G3Ry9hLUpGgnt7bPbnYNBxXXerYPHwEuGCvA4aUVwc2wzGo0smZL1WUVXbMjEoP7B0azGIaKrg", - "pKg+/kVoNUSz8D4G3x2lcX9/79B42N03L1/uNHnD0nzfwsyLvwMxEuV6jfm2D1LtOzWujmPJWVmI4y/w", - "7/T8PnA+x1/0v9Pze7W4ZegFyRWRnJI70ipwGjuvn0jwuAonU+Q/I6VCflJLNfEkVP2ucKy+9GYnI5dW", - "S16ScRvANV1vhwrrHYenEPXX4XN8/OpIMeBQulDDIUDi2NRQqTkLLHNi43XC99dWFAtWgmgGvVVZ7drI", - "MqAs2yHuee+0j3DV95zfcNAhWLDfIeyCG4XOHjWBLFoTpQ4Clvw2cdIkhhHE5J2yQlgw06eb+9VJn+4l", - "QgzwAz1yJLHlIbBlUE7NA2PMsNyGQ7BmaB7WvfDEC5SJsH7zWqHKpuaQr6rom2RVjKlfCstUuzJ2Qb/Q", - "RgxVvAR8h0SQep6vhA3NrG47nb+XlnDwSZdi1eAUvbSgdeLm7YOb4xSeDIKo49Wv0eYBDz2dsI3GaUcy", - "mx3q0HsSqcVRoO+AolnodjkoIRnfjadDnLl4KEfvC8Y/xFF0z3ngu9gTnj/kSu4D+V1wwUQVk4lvQOjB", - "BxtNK6KhyKUTe+1jwYBg6kMgQu+0B8aF/gDkIegwHPA9SGBcpeL4S/WM4V5/Sx1WLbp0v5LnIliRa0UV", - "hdm2j75ubNv+rJuOHgj43QyTblRk5cswmWnnW1MqzYBlDwdAY2/6yVIrqGeIJqelnB4QB9yvnQq1rfYR", - "03PdZy07KLp9uPXFfyLj2xygI5CbAaaAegNHj7mDcc90ZuHdc9YPgHayEYSp7LRZMypiAmsUYjmUVBOq", - "R/S72L1gISgZKqQOQ0ePK1Z1lhlNkye8jAjtjg/PLXGo5Php0GLuWrcpBGXYXLp+QLaIVc6unjvb4oPu", - "vGo5EN/jSkZuTcL27bFvS9vE+1B3KFw/88CyR6yM4aDL1leAs+f2dV66ow3JsgmUEz82Jc5rvJnUcRKV", - "CFJwkuiaYRp7w0KJHQo8UO1Tv4DP/plbf9XogMcwICpqF7lA6czT88tAGNQfRywYx6apCdIjEy2Feopq", - "H1fCcVSGjUVuGQDbBDqGDkDCA51ZpUr50YxtcTNfNXCOpkkl9/f5PHrrBwHMIACpBlqzBNADDukmlP4s", - "Nq+bJOEBc56i6lkCSglvVDVR2k3lJLUFz6EGrskgHkwbPjYJTEzPFOGl4i5S13mPboilZFa/kXjgrkz2", - "EFjzBtdV2vUeTf1eO9mwJdUZJnY802AKepuDSDuMlPo4wUuT481LGeUmK6psc7YUfLZFREis886kdQ3s", - "4JQmhZ1XM96Joyk4g/vFuA7eXONPtnk0O3z4RtTZmHYHlg5Tscn79Y3vmVCnINoNQXJb1F/n3vMS71W5", - "9taY6hhEXdfeTYlibeFK909wls1x8klLVUHQm3r7Qsfc6DlNRiNzugbSDiKoIX1s0BPUsY/XP1+8e31e", - "SWXmadidiRdMOBNiIqisV7tgfEm0/hoEZPWee3/8thUulFB5R7bCZADTvzlJ+xxjgPrbhIxvsElxw+YK", - "8EfoTZlJWmTRSRyhVCP/VmEPSBoz309RnZh3PjSHeGWFcGs7VUPzC0EqXNRjJ8jpEJNnAtVvfXKSSBsY", - "+e7qtT5u8zfkV7QBsykVCbuDQFhzaYG0ScLXNCcOQJ8pEBV4TjMKUZgKXas8VEfo6tXZxZs3r96evzpX", - "kKii7GrAXXVfPRsmZSOZ9rqCYDtbgcuhxoQ3p/+A7arbV5epsFdN40gh6Zr+RqqL80wg8rkgHIo/PcLu", - "IMfFSkdj7RTQAHTWZPU0ib60LcwGuppjsynSyGdpc7U1FDjCj9CpGapKOVlTACqcvJMFFuoe0ByZIF+j", - "/YEm4RYSqRh8rUbWkDdB2rzpEa5yZdkS/KqLGUFngjDL9OhWezc39bzrUkgk8SdQUZmi9qy0aaVseglb", - "JGpZYiUEEr0AxumS5uqz2Qs1OWL5GCW2bjfOEZZSEebI+bqL3+uInQhbWHSdd1MHpWEv3ZraRjOhXIhb", - "aBg1qttq2H4YhVNz6oknNJ3oKHr988TSCTzPyES3+TCq6pkLJdxaMfLDyD3qBpNThAP9fHNzeY3mWOHA", - "u6vX4co1H5wcrx9G8FwhLo/Yhwo44wSnW51JTTX3chYDotap6Gy+VapzA3ITeNPop7BCt/z///f/CVQr", - "vChj9XvSTsF6pkE52iXQ6NuX33TorZ8nm81msmB8PSl5RnIlX6a+IhtOARYrmtyWN3QiSpITnWKxF8sC", - "vUEBMgl+oQ5StkV4AWgBqG1M9ko+opIurQ2IU/FJsdGM4E+RhIzhfDh2O4guDApBQw8hN1Bzmriqh5M1", - "sC2awt7IZ5yYhLG7lGxsPv+3WYn6DKY/sjJPG0YDMBL0BXPUGeYqLXpI2AYIBsKTpGiObPCYflmRN+AD", - "0dJhNfngMRlubMRXsb8F8l0MMfQ0LG7dB0XTYndbW/NFXqerb5hVDb7efvMV7Wj7W9C8HdnQEy97+b5G", - "tPTfxIi2F1Z1WnAf2V77VTHtyWJ7UGQrMI8zmapcQZ7ayNFwcUJtxsi2tkZBS9pTqueSSNEs+linsgYJ", - "2lHosWhXNLTlCx2d0I7XmrjbLBwsS7hbKMzO8ly0NOmfzqgSLxcSBkcyrOaIb0M++WNYu3uWGc2QvYcV", - "uzNX8J/XSlEZE/7IForOsgYDiMS/l2eh+yVvMDay23kXTrUahmuPE2KoZvvkZQinDV4FHwv/wQzC0Tw6", - "kTQS/3L2/L7Kzr5f20vK77PZkHGkrdf/5VHj/GMFpQN6wZmueaaO8LuX3wfy0Gkm+5ZJdKoLxEDTv3wb", - "rVmBXuWSyi26YQy9xnxJoMM3fw0QE8bQG5xvLdxFyNYQKcE+wExkTGKu+N56b6MaxIpvH0jMpekMDE0B", - "m9W5eflfZwUyNiqn8gfY6gpN9SqS1sh+oq7E7aUebBeSfC0rlhzWYyCDEeO2FkYwQ3MR255dUb1slkOV", - "tDXjoMrZp95uLkERycrYf6UCb1euS0U+1Cq/D33+Uedbbb5oNgKTKOdr2japWv2MudIxZ+VypVTrJobe", - "FS6GWs4TjwZSN8C2AuivcJ5mugSXzSpVRxYq+uq+RtSskSleVBLESvNYsYpCirxDUwrglV1aj8LvFI+o", - "n0Q6jz5ikSMP0/+tU6rLT7//g+hvXwapmwFIgEY5wOqgR9W16LRYu6Wb4Px0klrQDrBS+TkRK/PZOoAq", - "szZbhHwWrvdthYXRdJUyBo4LUcKUizKLIHcYQ+AuH45Mdqi81icytk6R2rMIDjOHYNpUF1E/j8KbMssU", - "3bGIEtRIh6gYAOy2L+VB886qPKkhfZ1vC8mWHBcrWwoL5ylbe5WRHJ3Pkm4S1y78qpmOWN+72jpZzGD9", - "o10mLqKNDCrO4aGF7QEkbsjyu/XJFsp98Dq03HGGxaU9xhFTMopym3rIgkibHBKdYL537fFyI3GY2OIc", - "sFxe1fIzCdDsEvtnb0jGDhZ8HM6mH0XyPQUyBmSpz6WlhN2AAPADTlHtfmuReS8VWDet7/RP2Hp1T484", - "WjxWA0Z45f1w3qrIXBP127PrKIENSTV6Am24P5Art6O49YF1v64qbn2638tDrgKKCnQtpefm2SENIlTH", - "F76BlmX676eaWQDqzOth7RDynz/phk+6YZ9uON/Wqp/7tMt/gKbtXl4Cf2DDYWXRyY4fx+gv8jMkysow", - "XTsqpI/GNvfS1OkJuVQO8L4ZVuK+b3ZTPZU2t94eScX6wLwkUk/uKDfG7G7Ubved31EY0H1Jv87B5l1n", - "+wjzRXUmu/uaqwPe/Z2yLq/RL0ucW5N9BUX3OfnBhIrbxmy21O5BxYr2e+RmkZ9DPUgOFqU6dBqHWAGj", - "QdkbmiWtBlChw79e/vMia/UulqaJQ7O/xtvf28uvga2NKXdC1q/Ob4dhujvLIxDk3wXFfw9y7ApzB6XH", - "rZpXX4UiB2si7UCTCx88IVxV3UDf1RhWJ9w9OT7OWIKzFRPy5H+9/M+XI3UgZogmTmiz/UTbBlNdIrrh", - "Pm2+Cxm1Mcuua+A41TYC5n3tsV8RnMkVsiXmTD/9q/7x/uP9fwcAAP//IJXMYS/QAAA=", + "H4sIAAAAAAAC/+x9/XLbtvbgq2C0O5NkRrZz+/G7e73/rGunrdok9rUdZ+40GQ1EQhIaimAB0LKa8cy+", + "xr7ePslvcACQAAmQlGylvbf+K7GIz4OD841zPo8StipYTnIpRsefRyJZkhWG/54kCRHimn0i+SURBcsF", + "UT+nRCScFpKyfHQ8esNSkqE540g3R9Ae2Q6Ho/Go4KwgXFICo2JoNpWqWXu46yVBugWCFogKUZIUzTZI", + "qk+lXDJOf8eqORKE3xKuppCbgoyOR0Jymi9G9+NRMs1ZngTWewVNUMJyiWmu/osRNEWSoRlBpSCp+m/C", + "CZYEYVRwxuaIzVHBhCBCqInZHH0iG7TCknCKM7Rekhxx8ltJhNRDJpykJJcUZ13Lm5K7gnIipjQAikku", + "yYJwlJKcwagKABmdE0lXBFG1/YTlqVCrUZ/MmM58VI+gJuya6Lp7XPc4woNzMudELLvO1DTRo4zRekmT", + "JUpw7oKczdSRoJysvTlFEIIiYUXgeM8vrifnb09ejxGdIwpHkOBMja62Ap3sQdVYlWSU5PJ/IyaXhK+p", + "IGN0+eqf7yaXr86Cc8Oypvrn0GbVFws9F4sDgwH0fispJ+no+Bf/cngTfRyPJJWZ6hu6l9XAbPYrSeRo", + "PLo7kHgh1KCMpsk3CR19vB+PTiu8PKOiyPBG7cC/oDOcfFpwVubpNGEZ4+q3FggyluCMRD4tmPrwPzmZ", + "j45H/+OoJjBHhrocvVZt7sejHK/Cg0hyJzumL3kW+P2+hlF7nxEIKQDB6fAGfCZClDhPyI9USMY3Z1ji", + "9ml3NkecFJwItXGNZlVjwAfVGi11c4Q5xxtEMrIiuWwTzbrrlKZdy0CTMMo6A6hvoj3GT1fnb80y2Nxd", + "K7RXg1JJViJ8WvoH6F1RG6DR0xTLwC15VTVAZ1iS4JItjCJDWIB3D0B4uCfhhjrMaYSDSI5zgRPYRAjm", + "1/X3MNAbV9s/wmp1gaP5GELjBn4hPuTmBxH7UpO/LlZ+Pjk7RQ5SGYrZxss54yscGOp7+L3iTPVIM6I4", + "WZQcwvhs3kc+fnp/fQHtDO4F0PnEYjJ8H7CSodjdONaOI7OA3p42DxG32mfUJXEVEq5ShEOfILj9IiAb", + "af4lyplQu8lltmnya+ys4hC9eXd1rZi6oXxaNPIoH8qZRJzIkucRHIgKb9FV7kGCO324BAfLpY8pxtWA", + "VLOxnJzPR8e/tHH2cwPl7hV+xS6rC1VvlXPvFhtJvBMujcthZvTWHbkqO8sxVxLLUnTyRAFN2hdDVF0j", + "d/5zz/7MAKZ5cGdXXpPhVFr3Oy8C53UO/xFABVRfuA3eqfjbHLaXvi2opQzcxau7ZInzBTlxtbVTlpIB", + "rIfovnAHS7lECUsJmnO20vjHEVM/t/bIiqk6jAH7rFo6e+1d8MM3HiPo9gtaPRQE8s7IKT3nDM2GbX47", + "EWOSU0mxJIozfXM6GXDYtkeLmVXC3WVM8vAsAdOUSEyzEBUohWQr+jsRaL3EEn2ieaoImlEwjSi4xiCj", + "M7Sgt8BGbk6vwlQ/w3SlBFIcOkkNZNjZBScHFqCKaKoj/D5j60M1tN7uFeG3NFH6tRQIC3R+AT3XOMuI", + "RLgoMprA7trUo1oJydOC0TwA5FP1Hdnvloab/QIyrZeEexIRDInU5tASC8NNa2UZzyXhSJQAuXmZZRuE", + "E7VlQNRehV0r2VNqjnxaifdGl/OX/+7ytct3ABdMV8W+3X1h9B5Adoiu8ScilOSRqD0lBLFbwo12P12T", + "LPuUs3XF5lGBOV4RSfghmszRjKmr1rFIhPO0PRjmBASagrNbmirJQ0sS5lbbkepdqJ2taZZZAQYlgKKR", + "ljSvuHBBcpoe2GYHttnx0VEXvKuVDjGFadw7WrIsJdxFQY2xekhUbz5h+ZwuSqPJvbt83ad7egvo4Nju", + "h+4RrfwVEmyUZtgUQAUSS1ZmqcLthOWCwk4F0uMoxcyKSSOldx4oKa1nCdaIEd0NNOhRysmqyADjQqqm", + "+RjQYvQlNaLZekkz4t/QhOVJVqZaoqMCpFGutdbDysgFxjI1cMHZXA1BRXW0WpYuFYMqM0mLzJ/erCx8", + "5Rcc5zJiJzOUKMG5vTr2IkAvo7bJJWflYqnX7tzXa/V33dChVyD9a0C4fDT3rcqK0Pq2ZGCyNEdqNxwJ", + "SQoBZKF9t1Myx2Um1Xw+E1JDBOHgCifBK3iLs5IYhaWySjbYoUJTxbsK/FtJrEFTUz4kFWujolJ9ZorL", + "gcGznB0YxQ0Wq+2hsGFLBddULiPzqR0CeSB3EgkiUVmgtIQVF5zcUlYKB1K1JRUpCkxviUDYbE3B2z/D", + "MaJSK4sUMJSov2luV20XfeIv2ogDdvsBEGlTm4V4PZ9eiNFP355fV7hCc+RJPppXzzO21qSj4OQAV5x8", + "qvFEWP02eN6W+kdQ/1QTXFFzCcBhc4iwDXJXECUWKGHBXD+N0wXhij6pIwCS7COxtdOgM42jcCmahvte", + "G3q1Pvguhi3M1Z3bF0udfy1e+OvTjG07A2MpCJ8WNJ/Wku2O4th3jGUE5wZPRUESOt8AL1wSuVSXwKq+", + "9ebN2ev9gQSi1oMuJm8Rzpjqa++UdYZprAVjiY9PBjxqKfUJzfSa1EY1R64EkrSSSNobtjuZZ3ihCH2q", + "Lg3IvXojSrbNkWPWRMAPzMCK6hg5KrAQx7IekfJ31RH6DV1DlISY6YvN54RPHT4bFDbNYiIimMNXDGWu", + "yWOBhbrGGblVrIjmWnZQsG0QaBYYHE4dXZVFwbgUWgD98fr6Av3w6hpoPfxxSVLKSSIPzbQCrfCmMq/9", + "81JjkCPEWcIOgrwCoEJOuGlCcVuQ/eWSUI5WbKau7vtK4wgbwu/CQokHFkt+Ha1FX3rGOck0SOgc5YSk", + "EaOfvdLtmS78G6PB9gPJCQfEPb++QIWWkyvY9pumgpgxbmvHMYTdBd9vLqwXycdSl56ckTlgCsu/p5kk", + "XPTZwi86O4NFPdRgkgYJbVHygomwTy7CLk7SFEbEmfGvAr9I1UxEWKEOiL9wtElAwx+1oqEUbsJjrpgY", + "7THQ7DqIWzNq6Chc0tNhWnCsGIGbMTnrN7gEhzOdP0b3FkU0tROFX46HM2iOqAmo4V5dJkJ9PAH5tNKV", + "NGekSmCao1L4NsdKDwhaKjybdVRJojn6dS2eayC+QIyjXwXLs/S5HumF0YNB09jS8L1XBXTv2t9pG8wI", + "PIgBPUObi3ooRgN9jDnZv2gBDBtK8cKjP9iKnSwVm8oXIWAvcYbzBcjlOE21DmT0WTaP2SQU8wjHb6SO", + "rq2HUPoNW1GpKJfYCElWCNw3YMgxbLDH9lH7KbrOJmR1vx+PUrbCIdZ4Br9vsW9NETWHfkPkkkVA8O5y", + "YiHQ7qK5vtbpQhCaUy4kIulX3377t3+gopxlNAF3G5ujs8kZem6kBRDMtcXhbHL2og+acfy0SDYQRSv3", + "cYv0/7oOmJGqWCB0RRc5SdFP76+Voln5FdXWat9i3K0d0Qfr8cETdxXwxOmpVPdDdFpyrh2xoOnm2QYJ", + "LUOS1GmokOLZr2v5rF8WchY3BhA4bKmC1VDP3GsT/tPgqZmcSnInd4vmgTGHBfCcKzXvwmr9IsYXQUNT", + "56b1vgJTLlwhurIbaLtSSbPU2EAZJ2GtGz2//P70v/7+zT9eaLVF4zh0MgYkrTJoDd7a+UFz9McDu1aI", + "R2vbdFhwM18FSTgJQ7lllYgLeDvGQvgzjJ0VN9dn53IQrXlwA+/yBScF5gTcHYpNnUSEt5hwZPoj7S+B", + "sC3fHLS9B8rQ90NF31csP9zgVRYk9t5EZ2aAhr1wW+PSDeCzjcIQWvf8MFJK4odRtxXokU495GAddEqP", + "c+L9BoUBRx4NpfHOPO5905f/mWhcf/+e2+7BU/Fn4jUid0kPzTsECp1YknQaHG77DVycXHYvO2Ys8ILl", + "IDrHGAYIKouErdp2QzeeqTVNyyw2z9h6q7unjR5W6Uq/z9gapNxO7a06h3EMEwIWhGH4uiXyd+hUAUQf", + "EMmHy5SSPNHLDMuEH1SjDyNjyDU2/rQyKBnjf/C80hBSnGlM0LHgxoXl6JS1T2fGyjwsXj9+/OEgBA73", + "/IMDEe+mlVYoAEHdAw3gY41Cu+LeJRFlJrfGwBhx3UtwW40JLQwL++pomkxjg2kJsN5LFd8W4NKSbwJo", + "dPnuFaJzN9TCBERuiET4FtMMzzJiHWHGcnN+YZ+6aMcn6EnWwF8HlEimO6BmwCeiuZAEp43A7sr99PyM", + "zAnnXnAfmFlfRHwB4bjm2nfug9FCowsHDSoNx8Rug2kjSpmSLBVbijjOUjvmGmx9vCjFMiTvDRFRS7Fs", + "SCimcxc5/wOE01ik3TiyHBchesAzFDFA2tleIoRug6XArphfE0qdl6sZOA+xbD4DqWJ/DSuw6uO7y4kb", + "DowFwqhggkp6S2wUsSIAfo86klggLGHAlAqlZBnnZOzZHJqVUlMSuSlogrNso+O9MqxmzDZILBmX6Dk5", + "XByO0YzINSE5+hY8U//18qVd6IvYmzAtYpacxl6E1ZsAYVBBWweqsMCiq6AtJiRJDSEEkCk4CZovMnJQ", + "CnhpRjgx4eAavqIgCUDRc421gw3CzvRe84m7Ve+lXQO/Y4g51LJySRZUSMJByj8FYesV54zHMRxaoq8O", + "X9aBGWoIE69FVOcOfgzfAz4BgDU6uTqdTMwY4ILU0AkyVWjVbXv/sVzh/IATnAID1KND4InTzuKznrWy", + "QqZkVi4W4ckbZ6X35BxML1AfcDpR2t59LlGibmwoYY9DA4Amnl+1rWROPZeWqQ1Jqm3GJE8PwBhlIny8", + "y9AVYRi84e8uX9slQIDEmsxQgRfEqJIg8TrecjxjpexTIsA8l8guGVs3FjXJ1VGNG6FVTuiPCsKKjFjE", + "pwpaVXySnn7s0ESywjRDOE05EWLbh3B1BFzXqmt08GPfsAsSIHRZxtZVRF4VO0BSbcYUx4GItDEqeX5M", + "iZwfg1FTHEP4+DFMdaCmOg6EOG23zV/Xn0Kx17DuZ0JzxPdkhn4mG3RFJEpZUq7UnmDZ1XNgG3dUb/qZ", + "cNwHbpBh7V5Uc/fioGUK1p6eBJf2/Kf3P7/wFrjL0vyXsL1LMyKCYVqKmalulXel4z4ULKPJZtgEYJ0Q", + "OqJv6VOKgtNbnGyQHq4+G+PGV6POiEBLttbSBSkytoEWjC9wXsd5ZRlJpBgr1BRjxAlAbAzyghJJMiaI", + "QAXhAmIFIBAsrDrpgBe1sa5bYy+Dba9DkCcVDWhAEFUBYaB/wZUSNvamfW2cq7jdXfDMocNuvRcH2L74", + "Cc4h0M78GjEiBojB9hc5EuIRSpogCpyQA6H0OAjDyagANVvHgOglRLfSem7X/96fzeUa87CL/ASVOf2t", + "JM6bXov9IL6id+8mZy8QFkI78Lx3/ygltyRTfBYxjuw8+nKLJeFVjJMvPBm4w50y03qjVgNpfptucrwy", + "LIUbUSFigqq2eku4CApLJ8h8CmzYR/t6GVVL2MsHF6ARx4DOPmA3Cibk6SriM76sAnvMvI1oY92vWpw2", + "S3Thbs5yMkae12iqZP/mbzMsaHKI3rKcVBHQahZDm3VjgZ7noNUgXBRibAPf1B8vnFwUOZNoiW8J0mOL", + "Kk71ODhpGGbiwQRZEr4CQ6EwL4Qqktw42waF1rHaHCeyBOuODrsTS1pU2psn6GETJO6O5jcAO5LQt9WS", + "HZ+FdrvpO2TiB4nVvY/pwL1bXzOFfriKh7Rx9k0pvMflGhJu+pIIVANMtfUx+EzlWqnvWBpEdCW++nKv", + "sWhbrd2HwX9K1aD2RgeBpz8bXV474u0VthSa5XNav0+zi3SIS6XmN0hK76o6Xw5Fj0T31XYTPYBiGi8h", + "24z5WVER/anzqJ7Upie16UltelKbntSmJ7XpSW16Upue1Ka/vNrkudXbsZOeFtGJZ74E9bFHIdvS0XEl", + "Gd8ptYuQjG+b1EQ1C5LgzhjJLxce5vi4YakOrLvhNNChHRtki3Qyu4C9I5dM3/a2i2d7V6RYkuarhygy", + "dTav3LtC8jLR175UHdTub06jaaDqCJbgc66HP+IwDwDmNCORGczXm5pz9Ubsm9Fafcf+fgKrd3C0G/wD", + "z/AGZ1QNc1HjA0kH0oRb3dekFGg9jFa0tqD5LpaZSJReQGkLvjhFjaVs+Wb1nWIEZuV9R9lekHNCvcB9", + "+CH1x8fsekrxxBfnBVxHEo9ID+kAlbTcWIXpsI3IHonz9J7Hp/2viGvyX62hFQDcD/qhZ0g4nW/q23q6", + "JMmnWBCmbhyM93MUkjmmWckJStRQyARmhR7fkeRT6OGd6gX7jMd9BLK/QvjGigiBF2TnZ2o3ThsjKA8Q", + "q2AjdmXBidyT6wD44Mi/5iB9z3WdE3NX1xcv+0c8rB344LQJAffFaSSUtOMQtnv1HZu78z3qbfPu7Ps5", + "6iO977yPQ23IE8lOwA3hExWF8QKNRR8eq1vlh+dug03upewK5I1uaEuQuAHBQyiwly3m34YGd9LN1u2M", + "weQBoO0jkx5YuxFsKzLlrqEiVH4WjaDAWC9mbwS3LTnWS+o8kl1IZggOQ4imu6qtySZ8+hPQzdDmHwC/", + "bWnnFri9E/GMXdd+8hnc1WDIvCdZ9nPO1vl5QfLJ2ambPjKEXKoR0q268psPfJTp5BQ9v3gmHE+CH/j/", + "qsuH7BjWptWz+85qCp753yowsIhq/h/AaXK9aXkAKKSFtf6W7ZwSDcVSLxznLN+sWCmmptRI3x5sVjHz", + "3CiSGc2aPHEj4xnEYeBg+jX9/EAuWSkRriMe9AsHm2ORCjTHmfcI1EmO5noWtjj3M+1TQMZQcOn6JzrP", + "3vdRPd7xe+M+IgZolfTx1vmLSXDwMeitosK+LNlttb49e5vrq3Gu8+haFmEwl88ztn6kG2BTp1a+UZPf", + "oU6wByklqU6l+83pZDiid75Idl8e+wDswNcAasQo20DQbU9uHIGliy1sXeikMZh+yN7Dafr7NB9x6VyE", + "wYVqJR4LYjxAN6dX+mLAm67J2cUfzMFmWCZLN/PdoPlaqVafCfSdGsn9Us1rn2e91pkoSkFgkKWUhUAg", + "i2ir4ZuTf1V5sBVejFGBFcfIU/RbSfjGyd9aCzNurqFxJA9syoh+V2sQDprF19uXZHpXCH152PTsJFYT", + "6Tqygb74xlrJ76Kj5lFkB6OB7/piizEiOFkasK78oLQqkFOHvmFkIymaFatqv2wbORJsKrI5e6rYUFrX", + "RWtsIq/LY1Ur1Un09XrhtzrmyQyE6quuY/ra64Fs1CghHM46w/miNEr5IJm+XeusO6TuSW59kluf5NZ/", + "B7n1P1PyCzGkMLuNMZi47BgW3baRIBXQaT5nOvoEQp/hMfQK02x0PFqSLGP/R/JSyFnGksOU3I5sHcvR", + "tfr5u4wlSBK8UgcKqe1GwNuPj478borZNJLM2O4gMxrJJVQ2UgkBrl3IZGR9//Upujk9OLmYuInINWS+", + "uYE8TpIlzE0Le2QNNG5pCN2vTgee0YQY85XZ6UmBkyU5+OrwZWuT6/X6EMPnQ8YXR6avOHo9OX319uqV", + "6nMo77SxybUtUYjicrijLZDz/Ob06oX21enoiNHLQzUxOKBIjgs6Oh59ffgS1qJkJLi3R2Z/DgYd1QW/", + "ChYPLxEuyOugEcXFsU2dPLpgQtZrFVWZLxOD8h1LNxaDiKYKTm7so1+FVkM0C+9j8N1RGvf39w6Nh919", + "9fLlVpM3LM33Lcw8/xmIkShXK8w3fZBq36lxdRwLzspCHH2Gfydn94HzOfqs/52c3avFLUIvSC6J5JTc", + "klZl1dh5/UCCx1U4mSJ/idQo+UEt1cSTUPW7wrH60pudjFxaLXlJxm0A13S9HSqsdxyeQtRfh8/x8Ysj", + "xYBD6UINhwCJI1O8peYssMwDG68Tvr+2lFmwBEUz6K3KatdGlgH14PZxz3unfYSrvuP8hoMOwYLdDmEb", + "3Ch09qgDyKJ1oNRBwJLfD5w0iWEEMXmnrBAWzPTp5n510qd7iRAD/ECPHElsuQ9sGZRTc88YMyy34RCs", + "GZqHdSc88QJlIqzfvFaosqk55KuqNidZFWPq1+AyZbaMXdCv8BFDFS8B3z4RpJ7nC2FDM6vbVufvpSUc", + "fNKlWDY4RS8taJ24efvg5jiFJ4Mg6niFc7R5wENPJ2yjcdqRzGb7OvSeRGpxFOg7oGgWum0OSkjGt+Pp", + "EGcuHsrR+4Lx93EU3XPu+S72hOcPuZK7QH4bXDBRxeTANyD04IONphXRUOTSib32sWBAMPU+EKF32j3j", + "Qn8A8hB0GA74HiQwrlJx9Ll6xnCvv6UOqxZdul/JcxEsBbakisJs2kdfN7Ztf9RNRw8E/HaGSTcqsvJl", + "mMy0s42p0WbAsoMDoLE3/WSpFdQzRJPTUk4PiAPu106F2lb7iOm57rOWLRTdPtz67D+R8W0O0BHIzQBT", + "QL2Bw8fcwbhnOrPw7jnrB0Bb2QjCVHbSrBkVMYE1CrHsS6oJ1SP6Q+xesBCUDBVSh6GjxxWrAs+MpskT", + "XkaEdseH59ZWVHL8JGgxd63bFIIybC5dPyBbxEp2V8+dbdVDd161HIjvcSUjtxhi+/bYt6Vt4r2vOxQu", + "3Lln2SNWP3HQZeur/Nlz+zov3eGaZNkB1DE/MrXVa7w5qOMkKhGk4CTRNcM09oaFEjsUeKDap34On/0z", + "t/6q0R6PYUBU1DZygdKZJ2cXgTCoP49YMI5NUxOkRyZaCvUU1T6qhOOoDBuL3DIAtgl0DB2AhAc6s0qV", + "8qMZ2+JmvmrgHE2TSu7v83n01g8CmEEAUg20ZgmgBxzSdSj9WWxeN0nCA+Y8QdWzBJQS3qhqorSbyklq", + "K61D8V2TQTyYNnxsEpiYninCC8VdpC4wH90QS8m0fiPxwF2Z7CGw5jWuy8PrPZrCwXayYUuqM0xseabB", + "FPQ2B5F2GCn18QAvTI43L2WUm6yoss3ZGvTZBhEhsc47k9bFt4NTmhR2XrF6J46m4AzuF+M6eHOFP9nm", + "0ezw4RtRZ2PaHlg6TMUm79c3vmdCnYJoOwTJESvwb6UJiPET71W59laY6hhEXVDfTYlibeFK909wls1w", + "8klLVUHQm0L/Qsfc6DlNRiNzugbSDiKoIX1s0BPUsY9XP56/e31WSWXmaditiRdMOBPiQFBZr3bO+IJo", + "/TUIyOo99+74bStcKKHylmyEyQCmf3OS9jnGAPW3CRlfY5Pihs0U4A/RmzKTtMiikzhCqUb+jcIekDSm", + "vp+iOjHvfGgO8coK4VZ2qobmF4JUuKjHVpDTISbPBKrf+uQkkTYw8t3la33c5m/Ir2gDZlMqEnYLgbDm", + "0gJpk4SvaE4cgD5TICrwjGYUojAVulZ5qA7R5avT8zdvXr09e3WmIFFF2dWAu+y+ejZMykYy7XQFwXa2", + "BJdDjQlvTv4F21W3ry5TYa+axpFC0hX9nVQX55lA5K4gHIo/PcLuIMfFUkdjbRXQAHTWZPU0ib60LcwG", + "uppjsynSyJ20udoaChzhh+jEDFWlnKwpABVO3skCC3UPaI5MkK/R/kCTcAuJVAy+ViNryJsgbd70CFe5", + "smztf9XFjKAzQZhlenSrvZvret5VKSSS+BOoqExRe1batFI2vYQtErUosRICiV4A43RBc/XZ7IWaHLF8", + "jBJbtxvnCEupCHPkfN3F73TEToQtLLrOu6mD0rCXbk1to5lQLsQtNIwa1W01bD+Mwqk59cQHND3QUfT6", + "5wNLJ/AsIwe6zYdRVc9cKOHWipEfRu5RN5icIhzox+vriys0wwoH3l2+Dleu+eDkeP0wgucKcXnEPlTA", + "GSc43ehMaqq5l7MYELVORWfzrVKdG5CbwJtGPw6F91XL//9//59AtcKLMla/J+0UrKcalKNtAo2+fvlV", + "h956d7Berw/mjK8OSp6RXMmXqa/IhlOAxYomt+UNnYiS5ESnWOzFskBvUIBMgl+og5RtEJ4DWgBqG5O9", + "ko+opAtrA+JUfFJsNCP4UyQhYzgfjt0OonODQtDQQ8g11JwmrurhZA1si6awN3KHE5MwdpuSjc3n/zYr", + "UZ/B9HtW5mnDaABGgr5gjjrDXKVFDwnbAMFAeJIUzZENHtMvK/IGfCBaOqwm7z0mw42N+CL2t0C+iyGG", + "nobFrfugaFpsb2trvsjrdPUNs6rB15uvvqAdbXcLmrcjG3riZS/f1YiW/ocY0XbCqk4L7iPba78opj1Z", + "bPeKbAXmcSZTlSvIUxs5Gi5OqM0Y2cbWKGhJe0r1XBApmkUf61TWIEE7Cj0W7YqGtnyhoxPa8VoTd5uF", + "g2UJtwuF2Vqei5Ym/csZVeLlQsLgSIbVHPFtyMd/Dmt3zzKjGbJ3sGJ35gr+61opKmPCn9lC0VnWYACR", + "+M/yLHS/5A3GRnY778KpVsNw7XFCDNVsn7wM4bTBy+Bj4T+ZQTiaRyeSRuLfzp7fV9nZ92t7Sfl9Nhsy", + "jrT1+r89apx/rKB0QC841TXP1BF+8/LbQB46zWTfMolOdIEYaPq3r6M1K9CrXFK5QdeModeYLwh0+Oof", + "AWLCGHqD842FuwjZGiIl2AeYiYxJzBXfW+9tVINY8e09ibk0nYKhKWCzOjMv/+usQMZG5VT+AFtdoale", + "RdIa2U/Ulbi50INtQ5KvZMWSw3oMZDBi3NbCCGZoLmLbsyuql81yqJK2YhxUOfvU280lKCJZGfuvVODt", + "ylWpyIda5behz9/rfKvNF81GYBLlbEXbJlWrnzFXOuasXCyVat3E0NvCxVDLeeLRQOoG2FYA/SXO00yX", + "4LJZperIQkVf3deImjUyxYtKglhpHitWUUiRd2hKAby0S+tR+J3iEfWTSOfRRyxy5GH6v3VKdfnpd38Q", + "/fXLIHUzAAnQKAdYHfSouhadFmu3dBOcn05SC9oBVio/J2JpPlsHUGXWZvOQz8L1vi2xMJquUsbAcSFK", + "mHJeZhHkDmMI3OX9kckOldf6RMbWKVJ7FsFh5hBMm+oi6udReFNmmaI7FlGCGukQFQOA3falPGjeaZUn", + "NaSv800h2YLjYmlLYeE8ZSuvMpKj81nSTeLahV810xHre1dbJ4sZrH+0y8RFtJFBxTk8tLA9gMQNWX63", + "PtlCuQ9eh5Y7zrC4tMc4YkpGUW5TD1kQaZNDohPM9649Xm4kDhNbnAOWy6tafiYBml1i/+wNydjBgo/D", + "2fSjSL4nQMaALPW5tJSwGxAAvsMpqt1vLTLvpQLrpvWd/glbr+7pEUeLx2rACK+8H85bFZlron5zehUl", + "sCGpRk+gDfd7cuV2FLfes+7XVcWtT/d7uc9VQFGBrqX03Dw7pEGE6vjCN9CyTP/9VDMLQJ15PawdQv7z", + "J93wSTfs0w1nm1r1c592+Q/QtN3LS+APbDisLDrZ8eMY/VneQaKsDNOVo0L6aGxzL02cnpBLZQ/vm2El", + "7vtmN9VTaXPr7ZBUrA/MCyL15I5yY8zuRu123/kdhgHdl/TrDGzedbaPMF9UZ7K9r7k64O3fKevyGv2y", + "xJk12VdQdJ+T702ouGnMZkvt7lWsaL9Hbhb52deD5GBRqn2ncYgVMBqUvaFZ0moAFdr/6+W/LrJW72Jp", + "mjg0+0u8/b25+BLY2phyK2T94vx2GKa7szwCQf5DUPyPIMeuMLdXetyqefVFKHKwJtIWNLnwwRPCVdUN", + "9F2NYXXC3eOjo4wlOFsyIY//18u/vxypAzFDNHFCm+0PtG0w1SWiG+7T5ruQURuz7LoGjlNtI2De1x77", + "JcGZXCJbYs7007/qH+8/3v93AAAA//8du2A2qNAAAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/cmd/vc-rest/startcmd/start.go b/cmd/vc-rest/startcmd/start.go index 84c67ad58..a4a55fbba 100644 --- a/cmd/vc-rest/startcmd/start.go +++ b/cmd/vc-rest/startcmd/start.go @@ -106,7 +106,7 @@ import ( oidc4vpclaimsstoremongo "github.com/trustbloc/vcs/pkg/storage/mongodb/oidc4vpclaimsstore" oidc4vpnoncestoremongo "github.com/trustbloc/vcs/pkg/storage/mongodb/oidc4vpnoncestore" oidc4vptxstoremongo "github.com/trustbloc/vcs/pkg/storage/mongodb/oidc4vptxstore" - "github.com/trustbloc/vcs/pkg/storage/mongodb/requestobjectstore" + requestobjectstoremongo "github.com/trustbloc/vcs/pkg/storage/mongodb/requestobjectstore" "github.com/trustbloc/vcs/pkg/storage/mongodb/vcissuancehistorystore" "github.com/trustbloc/vcs/pkg/storage/mongodb/vcstatusstore" "github.com/trustbloc/vcs/pkg/storage/redis" @@ -119,7 +119,7 @@ import ( oidc4vptxstoreredis "github.com/trustbloc/vcs/pkg/storage/redis/oidc4vptxstore" "github.com/trustbloc/vcs/pkg/storage/s3/credentialoffer" cslstores3 "github.com/trustbloc/vcs/pkg/storage/s3/cslvcstore" - requestobjectstore2 "github.com/trustbloc/vcs/pkg/storage/s3/requestobjectstore" + requestobjectstores3 "github.com/trustbloc/vcs/pkg/storage/s3/requestobjectstore" ) const ( @@ -1151,9 +1151,9 @@ func createRequestObjectStore( otelaws.AppendMiddlewares(&cfg.APIOptions, otelaws.WithTracerProvider(otel.GetTracerProvider())) } - return requestobjectstore2.NewStore(s3.NewFromConfig(cfg), s3Bucket, s3Region, s3HostName), nil + return requestobjectstores3.NewStore(s3.NewFromConfig(cfg), s3Bucket, s3Region, s3HostName), nil default: - return requestobjectstore.NewStore(mongoDbClient), nil + return requestobjectstoremongo.NewStore(mongoDbClient), nil } } diff --git a/component/wallet-cli/pkg/oidc4vp/models.go b/component/wallet-cli/pkg/oidc4vp/models.go index e9ab42fbb..e6ae4536d 100644 --- a/component/wallet-cli/pkg/oidc4vp/models.go +++ b/component/wallet-cli/pkg/oidc4vp/models.go @@ -45,16 +45,20 @@ type IDTokenVPToken struct { PresentationSubmission *presexch.PresentationSubmission `json:"presentation_submission"` } +type Claims = map[string]interface{} + type IDTokenClaims struct { - VPToken IDTokenVPToken `json:"_vp_token"` - Nonce string `json:"nonce"` - Exp int64 `json:"exp"` - Iss string `json:"iss"` - Aud string `json:"aud"` - Sub string `json:"sub"` - Nbf int64 `json:"nbf"` - Iat int64 `json:"iat"` - Jti string `json:"jti"` + // ScopeAdditionalClaims stores claims retrieved using additional scope. + ScopeAdditionalClaims map[string]Claims `json:"_scope,omitempty"` //additional scope -> claims + VPToken IDTokenVPToken `json:"_vp_token"` + Nonce string `json:"nonce"` + Exp int64 `json:"exp"` + Iss string `json:"iss"` + Aud string `json:"aud"` + Sub string `json:"sub"` + Nbf int64 `json:"nbf"` + Iat int64 `json:"iat"` + Jti string `json:"jti"` } type VPTokenClaims struct { diff --git a/component/wallet-cli/pkg/oidc4vp/oidc4vp_flow.go b/component/wallet-cli/pkg/oidc4vp/oidc4vp_flow.go index 22d19ff8a..226a54d2d 100644 --- a/component/wallet-cli/pkg/oidc4vp/oidc4vp_flow.go +++ b/component/wallet-cli/pkg/oidc4vp/oidc4vp_flow.go @@ -44,6 +44,9 @@ import ( const ( linkedDomainsService = "LinkedDomains" tokenLifetimeSeconds = 600 + + customScopeTimeDetails = "timedetails" + customScopeWalletDetails = "walletdetails" ) type Flow struct { @@ -409,7 +412,7 @@ func (f *Flow) sendAuthorizationResponse( return fmt.Errorf("no matching credentials found") } - idToken, err := f.createIDToken(presentationSubmission, requestObject.ClientID, requestObject.Nonce) + idToken, err := f.createIDToken(presentationSubmission, requestObject.ClientID, requestObject.Nonce, requestObject.Scope) if err != nil { return fmt.Errorf("create id token: %w", err) } @@ -569,9 +572,15 @@ func (f *Flow) signPresentationLDP( func (f *Flow) createIDToken( presentationSubmission *presexch.PresentationSubmission, - clientID, nonce string, + clientID, nonce, requestObjectScope string, ) (string, error) { + scopeAdditionalClaims, err := ExtractCustomScopeClaims(requestObjectScope) + if err != nil { + return "", fmt.Errorf("extractAdditionalClaims: %w", err) + } + idToken := &IDTokenClaims{ + ScopeAdditionalClaims: scopeAdditionalClaims, VPToken: IDTokenVPToken{ PresentationSubmission: presentationSubmission, }, @@ -672,3 +681,31 @@ func WithDomainMatchingDisabled() Opt { opts.disableDomainMatching = true } } + +// ExtractCustomScopeClaims returns Claims associated with custom scope. +func ExtractCustomScopeClaims(requestObjectScope string) (map[string]Claims, error) { + chunks := strings.Split(requestObjectScope, "+") + if len(chunks) == 1 { + return nil, nil + } + + switch chunks[1] { + case customScopeTimeDetails: + return map[string]Claims{ + chunks[1]: { + "timestamp": time.Now().Format(time.RFC3339), + "uuid": uuid.NewString(), + }, + }, nil + case customScopeWalletDetails: + return map[string]Claims{ + chunks[1]: { + "wallet_version": "1.0", + "uuid": uuid.NewString(), + }, + }, nil + + default: + return nil, fmt.Errorf("unexpected custom scope \"%s\" supplied", chunks[1]) + } +} diff --git a/component/wallet-cli/pkg/walletrunner/models.go b/component/wallet-cli/pkg/walletrunner/models.go index ccabae92b..03933a1c3 100644 --- a/component/wallet-cli/pkg/walletrunner/models.go +++ b/component/wallet-cli/pkg/walletrunner/models.go @@ -47,16 +47,20 @@ type IDTokenVPToken struct { PresentationSubmission *presexch.PresentationSubmission `json:"presentation_submission"` } +type Claims = map[string]interface{} + type IDTokenClaims struct { - VPToken IDTokenVPToken `json:"_vp_token"` - Nonce string `json:"nonce"` - Exp int64 `json:"exp"` - Iss string `json:"iss"` - Aud string `json:"aud"` - Sub string `json:"sub"` - Nbf int64 `json:"nbf"` - Iat int64 `json:"iat"` - Jti string `json:"jti"` + // ScopeAdditionalClaims stores claims retrieved using additional scope. + ScopeAdditionalClaims map[string]Claims `json:"_scope,omitempty"` //additional scope -> claims + VPToken IDTokenVPToken `json:"_vp_token"` + Nonce string `json:"nonce"` + Exp int64 `json:"exp"` + Iss string `json:"iss"` + Aud string `json:"aud"` + Sub string `json:"sub"` + Nbf int64 `json:"nbf"` + Iat int64 `json:"iat"` + Jti string `json:"jti"` } type VPTokenClaims struct { diff --git a/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4vp.go b/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4vp.go index 5a2b20802..7500161b8 100644 --- a/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4vp.go +++ b/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4vp.go @@ -12,6 +12,7 @@ import ( "crypto/tls" "encoding/json" "fmt" + "github.com/trustbloc/vcs/component/wallet-cli/pkg/oidc4vp" "io" "log" "net/http" @@ -349,8 +350,14 @@ func (e *VPFlowExecutor) QueryCredentialFromWalletMultiVP() error { return nil } -func (e *VPFlowExecutor) getIDTokenClaims(requestPresentationSubmission *presexch.PresentationSubmission) *IDTokenClaims { +func (e *VPFlowExecutor) getIDTokenClaims(requestPresentationSubmission *presexch.PresentationSubmission, requestObjectScope string) (*IDTokenClaims, error) { + scopeAdditionalClaims, err := oidc4vp.ExtractCustomScopeClaims(requestObjectScope) + if err != nil { + return nil, fmt.Errorf("ExtractCustomScopeClaims: %w", err) + } + return &IDTokenClaims{ + ScopeAdditionalClaims: scopeAdditionalClaims, VPToken: IDTokenVPToken{ PresentationSubmission: requestPresentationSubmission, }, @@ -362,7 +369,7 @@ func (e *VPFlowExecutor) getIDTokenClaims(requestPresentationSubmission *presexc Nbf: time.Now().Unix(), Iat: time.Now().Unix(), Jti: uuid.NewString(), - } + }, nil } func (e *VPFlowExecutor) signIDTokenJWT(idToken *IDTokenClaims, signatureType vcs.SignatureType) (string, error) { @@ -431,7 +438,12 @@ func (e *VPFlowExecutor) CreateAuthorizedResponse(o ...RPConfigOverride) (string var signedIDToken string - signedIDToken, err = e.signIDTokenJWT(e.getIDTokenClaims(e.requestPresentationSubmission), e.walletSignType) + idToken, err := e.getIDTokenClaims(e.requestPresentationSubmission, e.requestObject.Scope) + if err != nil { + return "", err + } + + signedIDToken, err = e.signIDTokenJWT(idToken, e.walletSignType) if err != nil { return "", err } diff --git a/docs/v1/openapi.yaml b/docs/v1/openapi.yaml index 90868f53f..fec3aa045 100644 --- a/docs/v1/openapi.yaml +++ b/docs/v1/openapi.yaml @@ -1221,6 +1221,9 @@ components: type: string purpose: type: string + scope: + type: string + description: Additional scope that defines custom claims requested from Holder to Verifier. presentationDefinitionFilters: $ref: '#/components/schemas/PresentationDefinitionFilters' PresentationDefinitionFilters: diff --git a/pkg/kms/aws/service_mocks.go b/pkg/kms/aws/service_mocks.go index 3675c654a..ac8806377 100644 --- a/pkg/kms/aws/service_mocks.go +++ b/pkg/kms/aws/service_mocks.go @@ -337,4 +337,4 @@ func (m *MockmetricsProvider) VerifyTime(value time.Duration) { func (mr *MockmetricsProviderMockRecorder) VerifyTime(value interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifyTime", reflect.TypeOf((*MockmetricsProvider)(nil).VerifyTime), value) -} \ No newline at end of file +} diff --git a/pkg/kms/mocks/kms_mocks.go b/pkg/kms/mocks/kms_mocks.go index 5fe7d704d..8ca0def08 100644 --- a/pkg/kms/mocks/kms_mocks.go +++ b/pkg/kms/mocks/kms_mocks.go @@ -96,4 +96,4 @@ func (m *MockVCSKeyManager) SupportedKeyTypes() []kms.KeyType { func (mr *MockVCSKeyManagerMockRecorder) SupportedKeyTypes() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SupportedKeyTypes", reflect.TypeOf((*MockVCSKeyManager)(nil).SupportedKeyTypes)) -} \ No newline at end of file +} diff --git a/pkg/observability/tracing/wrappers/oidc4vp/oidc4vp_wrapper.go b/pkg/observability/tracing/wrappers/oidc4vp/oidc4vp_wrapper.go index 31899758f..fc1ab1966 100644 --- a/pkg/observability/tracing/wrappers/oidc4vp/oidc4vp_wrapper.go +++ b/pkg/observability/tracing/wrappers/oidc4vp/oidc4vp_wrapper.go @@ -33,15 +33,21 @@ func Wrap(svc Service, tracer trace.Tracer) *Wrapper { return &Wrapper{svc: svc, tracer: tracer} } -func (w *Wrapper) InitiateOidcInteraction(ctx context.Context, presentationDefinition *presexch.PresentationDefinition, purpose string, profile *profileapi.Verifier) (*oidc4vp.InteractionInfo, error) { +func (w *Wrapper) InitiateOidcInteraction( + ctx context.Context, + presentationDefinition *presexch.PresentationDefinition, + purpose string, + customScope string, + profile *profileapi.Verifier) (*oidc4vp.InteractionInfo, error) { ctx, span := w.tracer.Start(ctx, "oidc4vp.InitiateOidcInteraction") defer span.End() span.SetAttributes(attribute.String("profile_id", profile.ID)) span.SetAttributes(attribute.String("purpose", purpose)) + span.SetAttributes(attribute.String("custom_cope", customScope)) span.SetAttributes(attributeutil.JSON("presentation_definition", presentationDefinition)) - resp, err := w.svc.InitiateOidcInteraction(ctx, presentationDefinition, purpose, profile) + resp, err := w.svc.InitiateOidcInteraction(ctx, presentationDefinition, purpose, customScope, profile) if err != nil { return nil, err } @@ -49,14 +55,14 @@ func (w *Wrapper) InitiateOidcInteraction(ctx context.Context, presentationDefin return resp, nil } -func (w *Wrapper) VerifyOIDCVerifiablePresentation(ctx context.Context, txID oidc4vp.TxID, token []*oidc4vp.ProcessedVPToken) error { +func (w *Wrapper) VerifyOIDCVerifiablePresentation(ctx context.Context, txID oidc4vp.TxID, authResponse *oidc4vp.AuthorizationResponseParsed) error { ctx, span := w.tracer.Start(ctx, "oidc4vp.VerifyOIDCVerifiablePresentation") defer span.End() span.SetAttributes(attribute.String("tx_id", string(txID))) - span.SetAttributes(attributeutil.JSON("token", token, attributeutil.WithRedacted("#.Presentation"))) + span.SetAttributes(attributeutil.JSON("token", authResponse.VPTokens, attributeutil.WithRedacted("#.Presentation"))) - return w.svc.VerifyOIDCVerifiablePresentation(ctx, txID, token) + return w.svc.VerifyOIDCVerifiablePresentation(ctx, txID, authResponse) } func (w *Wrapper) GetTx(ctx context.Context, id oidc4vp.TxID) (*oidc4vp.Transaction, error) { diff --git a/pkg/observability/tracing/wrappers/oidc4vp/oidc4vp_wrapper_test.go b/pkg/observability/tracing/wrappers/oidc4vp/oidc4vp_wrapper_test.go index 5eadcb55b..d3e96253a 100644 --- a/pkg/observability/tracing/wrappers/oidc4vp/oidc4vp_wrapper_test.go +++ b/pkg/observability/tracing/wrappers/oidc4vp/oidc4vp_wrapper_test.go @@ -24,11 +24,11 @@ func TestWrapper_InitiateOidcInteraction(t *testing.T) { ctrl := gomock.NewController(t) svc := NewMockService(ctrl) - svc.EXPECT().InitiateOidcInteraction(gomock.Any(), &presexch.PresentationDefinition{}, "purpose", &profileapi.Verifier{}).Times(1) + svc.EXPECT().InitiateOidcInteraction(gomock.Any(), &presexch.PresentationDefinition{}, "purpose", "additionalScope", &profileapi.Verifier{}).Times(1) w := Wrap(svc, trace.NewNoopTracerProvider().Tracer("")) - _, err := w.InitiateOidcInteraction(context.Background(), &presexch.PresentationDefinition{}, "purpose", &profileapi.Verifier{}) + _, err := w.InitiateOidcInteraction(context.Background(), &presexch.PresentationDefinition{}, "purpose", "additionalScope", &profileapi.Verifier{}) require.NoError(t, err) } @@ -36,11 +36,11 @@ func TestWrapper_VerifyOIDCVerifiablePresentation(t *testing.T) { ctrl := gomock.NewController(t) svc := NewMockService(ctrl) - svc.EXPECT().VerifyOIDCVerifiablePresentation(gomock.Any(), oidc4vp.TxID("txID"), []*oidc4vp.ProcessedVPToken{}).Times(1) + svc.EXPECT().VerifyOIDCVerifiablePresentation(gomock.Any(), oidc4vp.TxID("txID"), &oidc4vp.AuthorizationResponseParsed{VPTokens: []*oidc4vp.ProcessedVPToken{}}).Times(1) w := Wrap(svc, trace.NewNoopTracerProvider().Tracer("")) - err := w.VerifyOIDCVerifiablePresentation(context.Background(), "txID", []*oidc4vp.ProcessedVPToken{}) + err := w.VerifyOIDCVerifiablePresentation(context.Background(), "txID", &oidc4vp.AuthorizationResponseParsed{VPTokens: []*oidc4vp.ProcessedVPToken{}}) require.NoError(t, err) } diff --git a/pkg/restapi/v1/verifier/controller.go b/pkg/restapi/v1/verifier/controller.go index 27e9c3a85..cfcfb5cb8 100644 --- a/pkg/restapi/v1/verifier/controller.go +++ b/pkg/restapi/v1/verifier/controller.go @@ -60,7 +60,7 @@ var ( errMissedField = errors.New("missed field") ) -type authorizationResponse struct { +type rawAuthorizationResponse struct { IDToken string VPToken []string State string @@ -72,10 +72,12 @@ type IDTokenVPToken struct { } type IDTokenClaims struct { - VPToken IDTokenVPToken `json:"_vp_token"` - Nonce string `json:"nonce"` - Aud string `json:"aud"` - Exp int64 `json:"exp"` + // CustomScopeClaims stores claims retrieved using custom scope. + CustomScopeClaims map[string]oidc4vp.Claims `json:"_scope,omitempty"` + VPToken IDTokenVPToken `json:"_vp_token"` + Nonce string `json:"nonce"` + Aud string `json:"aud"` + Exp int64 `json:"exp"` } type VPTokenClaims struct { @@ -346,7 +348,7 @@ func (c *Controller) initiateOidcInteraction( errors.New("OIDC not configured")) } - pd, err := findPresentationDefinition(profile, strPtrToStr(data.PresentationDefinitionId)) + pd, err := findPresentationDefinition(profile, lo.FromPtr(data.PresentationDefinitionId)) if err != nil { return nil, resterr.NewValidationError(resterr.InvalidValue, "presentationDefinitionID", err) } @@ -362,7 +364,8 @@ func (c *Controller) initiateOidcInteraction( logger.Debugc(ctx, "InitiateOidcInteraction applied filters to pd", logfields.WithPresDefID(pd.ID)) } - result, err := c.oidc4VPService.InitiateOidcInteraction(ctx, pd, strPtrToStr(data.Purpose), profile) + result, err := c.oidc4VPService.InitiateOidcInteraction( + ctx, pd, lo.FromPtr(data.Purpose), lo.FromPtr(data.Scope), profile) if err != nil { return nil, resterr.NewSystemError(resterr.VerifierOIDC4vpSvcComponent, "InitiateOidcInteraction", err) } @@ -460,21 +463,22 @@ func (c *Controller) CheckAuthorizationResponse(e echo.Context) error { log.WithDuration(time.Since(startTime))) }() - authResp, err := validateAuthorizationResponse(e) + rawAuthResp, err := validateAuthorizationResponse(e) if err != nil { return err } - processedTokens, err := c.verifyAuthorizationResponseTokens(ctx, authResp) + authorisationResponseParsed, err := c.verifyAuthorizationResponseTokens(ctx, rawAuthResp) if err != nil { if tenantID, e := util.GetTenantIDFromRequest(e); e == nil { - c.sendFailedEvent(ctx, authResp.State, tenantID, "", "", err) + c.sendFailedEvent(ctx, rawAuthResp.State, tenantID, "", "", err) } return err } - err = c.oidc4VPService.VerifyOIDCVerifiablePresentation(ctx, oidc4vp.TxID(authResp.State), processedTokens) + err = c.oidc4VPService.VerifyOIDCVerifiablePresentation( + ctx, oidc4vp.TxID(rawAuthResp.State), authorisationResponseParsed) if err != nil { return err } @@ -562,8 +566,8 @@ func (c *Controller) accessOIDC4VPTx(ctx context.Context, txID string) (*oidc4vp func (c *Controller) verifyAuthorizationResponseTokens( ctx context.Context, - authResp *authorizationResponse, -) ([]*oidc4vp.ProcessedVPToken, error) { + authResp *rawAuthorizationResponse, +) (*oidc4vp.AuthorizationResponseParsed, error) { startTime := time.Now() defer func() { logger.Debugc(ctx, "validateResponseAuthTokens", log.WithDuration(time.Since(startTime))) @@ -618,7 +622,10 @@ func (c *Controller) verifyAuthorizationResponseTokens( }) } - return processedVPTokens, nil + return &oidc4vp.AuthorizationResponseParsed{ + CustomScopeClaims: idTokenClaims.CustomScopeClaims, + VPTokens: processedVPTokens, + }, nil } func validateIDToken(idToken string, verifier jwt.ProofChecker) (*IDTokenClaims, error) { @@ -644,7 +651,18 @@ func validateIDToken(idToken string, verifier jwt.ProofChecker) (*IDTokenClaims, } } + var customScopeClaims map[string]oidc4vp.Claims + if val := v.Get("_scope"); val != nil { + sb, err = val.Object() + if err == nil { + if err = json.Unmarshal(sb.MarshalTo([]byte{}), &customScopeClaims); err != nil { + return nil, fmt.Errorf("decode _scope: %w", err) + } + } + } + idTokenClaims := &IDTokenClaims{ + CustomScopeClaims: customScopeClaims, VPToken: IDTokenVPToken{ PresentationSubmission: presentationSubmission, }, @@ -752,7 +770,7 @@ func (c *Controller) validateVPTokenLDP(vpToken string) (*VPTokenClaims, error) }, nil } -func validateAuthorizationResponse(ctx echo.Context) (*authorizationResponse, error) { +func validateAuthorizationResponse(ctx echo.Context) (*rawAuthorizationResponse, error) { startTime := time.Now().UTC() defer func() { logger.Debugc(ctx.Request().Context(), @@ -770,7 +788,7 @@ func validateAuthorizationResponse(ctx echo.Context) (*authorizationResponse, er return nil, resterr.NewValidationError(resterr.InvalidValue, "body", err) } - res := &authorizationResponse{} + res := &rawAuthorizationResponse{} err = decodeFormValue(&res.IDToken, "id_token", req.PostForm) if err != nil { @@ -995,11 +1013,3 @@ func getVerifyPresentationOptions(options *VerifyPresentationOptions) *verifypre return result } - -func strPtrToStr(str *string) string { - if str == nil { - return "" - } - - return *str -} diff --git a/pkg/restapi/v1/verifier/controller_test.go b/pkg/restapi/v1/verifier/controller_test.go index 8399edc50..b86bd5bc0 100644 --- a/pkg/restapi/v1/verifier/controller_test.go +++ b/pkg/restapi/v1/verifier/controller_test.go @@ -21,6 +21,7 @@ import ( "github.com/golang/mock/gomock" "github.com/labstack/echo/v4" + "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" vdrmock "github.com/trustbloc/did-go/vdr/mock" @@ -521,7 +522,13 @@ func TestController_CheckAuthorizationResponse(t *testing.T) { }) t.Run("Success JWT", func(t *testing.T) { + customScopeClaims := map[string]oidc4vp.Claims{ + "customScope": { + "key1": "value2", + }, + } signedClaimsJWTResult := testutil.SignedClaimsJWT(t, &IDTokenClaims{ + CustomScopeClaims: customScopeClaims, VPToken: IDTokenVPToken{ PresentationSubmission: map[string]interface{}{}}, Nonce: validNonce, @@ -560,14 +567,15 @@ func TestController_CheckAuthorizationResponse(t *testing.T) { DocumentLoader: testutil.DocumentLoader(t), }) - processedVPToken, err := c.verifyAuthorizationResponseTokens(context.TODO(), &authorizationResponse{ + authorisationResponseParsed, err := c.verifyAuthorizationResponseTokens(context.TODO(), &rawAuthorizationResponse{ IDToken: signedClaimsJWTResult.JWT, VPToken: []string{vpToken}, State: "txid", }) require.NoError(t, err) - require.Contains(t, processedVPToken[0].Presentation.Type, "PresentationSubmission") + require.Equal(t, customScopeClaims, authorisationResponseParsed.CustomScopeClaims) + require.Contains(t, authorisationResponseParsed.VPTokens[0].Presentation.Type, "PresentationSubmission") }) t.Run("Success LDP", func(t *testing.T) { @@ -614,14 +622,16 @@ func TestController_CheckAuthorizationResponse(t *testing.T) { DocumentLoader: testutil.DocumentLoader(t), }) - processedVPToken, err := c.verifyAuthorizationResponseTokens(context.TODO(), &authorizationResponse{ + authorisationResponseParsed, err := c.verifyAuthorizationResponseTokens(context.TODO(), &rawAuthorizationResponse{ IDToken: signedClaimsJWTResult.JWT, VPToken: []string{string(vpToken)}, State: "txid", }) require.NoError(t, err) - require.Contains(t, processedVPToken[0].Presentation.Type, "PresentationSubmission") + + require.Nil(t, authorisationResponseParsed.CustomScopeClaims) + require.Contains(t, authorisationResponseParsed.VPTokens[0].Presentation.Type, "PresentationSubmission") }) t.Run("Presentation submission missed", func(t *testing.T) { @@ -1859,7 +1869,7 @@ func TestController_InitiateOidcInteraction(t *testing.T) { mockProfileSvc := NewMockProfileService(gomock.NewController(t)) oidc4VPSvc := NewMockOIDC4VPService(gomock.NewController(t)) - oidc4VPSvc.EXPECT().InitiateOidcInteraction(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + oidc4VPSvc.EXPECT().InitiateOidcInteraction(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). AnyTimes().Return(&oidc4vp.InteractionInfo{}, nil) t.Run("Success", func(t *testing.T) { @@ -1909,7 +1919,7 @@ func TestController_initiateOidcInteraction(t *testing.T) { mockProfileSvc := NewMockProfileService(gomock.NewController(t)) oidc4VPSvc := NewMockOIDC4VPService(gomock.NewController(t)) - oidc4VPSvc.EXPECT().InitiateOidcInteraction(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + oidc4VPSvc.EXPECT().InitiateOidcInteraction(gomock.Any(), gomock.Any(), gomock.Any(), "test_scope", gomock.Any()). AnyTimes().Return(&oidc4vp.InteractionInfo{}, nil) t.Run("Success", func(t *testing.T) { @@ -1919,7 +1929,9 @@ func TestController_initiateOidcInteraction(t *testing.T) { OIDCVPService: oidc4VPSvc, }) - result, err := controller.initiateOidcInteraction(context.TODO(), &InitiateOIDC4VPData{}, + result, err := controller.initiateOidcInteraction(context.TODO(), &InitiateOIDC4VPData{ + Scope: lo.ToPtr("test_scope"), + }, &profileapi.Verifier{ OrganizationID: tenantID, Active: true, @@ -1950,6 +1962,7 @@ func TestController_initiateOidcInteraction(t *testing.T) { result, err := controller.initiateOidcInteraction(context.TODO(), &InitiateOIDC4VPData{ + Scope: lo.ToPtr("test_scope"), PresentationDefinitionFilters: &PresentationDefinitionFilters{ Fields: &fields, }, @@ -1994,6 +2007,7 @@ func TestController_initiateOidcInteraction(t *testing.T) { result, err := controller.initiateOidcInteraction(context.TODO(), &InitiateOIDC4VPData{ + Scope: lo.ToPtr("test_scope"), PresentationDefinitionId: &pdID, PresentationDefinitionFilters: &PresentationDefinitionFilters{ Fields: &fields, @@ -2153,7 +2167,8 @@ func TestController_initiateOidcInteraction(t *testing.T) { t.Run("oidc4VPService.InitiateOidcInteraction failed", func(t *testing.T) { oidc4VPSvc := NewMockOIDC4VPService(gomock.NewController(t)) - oidc4VPSvc.EXPECT().InitiateOidcInteraction(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + oidc4VPSvc.EXPECT(). + InitiateOidcInteraction(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). AnyTimes().Return(nil, errors.New("fail")) controller := NewController(&Config{ diff --git a/pkg/restapi/v1/verifier/openapi.gen.go b/pkg/restapi/v1/verifier/openapi.gen.go index 640a718ff..69f00e062 100644 --- a/pkg/restapi/v1/verifier/openapi.gen.go +++ b/pkg/restapi/v1/verifier/openapi.gen.go @@ -16,6 +16,9 @@ type InitiateOIDC4VPData struct { PresentationDefinitionFilters *PresentationDefinitionFilters `json:"presentationDefinitionFilters,omitempty"` PresentationDefinitionId *string `json:"presentationDefinitionId,omitempty"` Purpose *string `json:"purpose,omitempty"` + + // Additional scope that defines custom claims requested from Holder to Verifier. + Scope *string `json:"scope,omitempty"` } // InitiateOIDC4VPResponse defines model for InitiateOIDC4VPResponse. diff --git a/pkg/service/oidc4vp/api.go b/pkg/service/oidc4vp/api.go index 945b760b0..3a03ac61b 100644 --- a/pkg/service/oidc4vp/api.go +++ b/pkg/service/oidc4vp/api.go @@ -22,6 +22,15 @@ type InteractionInfo struct { TxID TxID } +type Claims = map[string]interface{} + +type AuthorizationResponseParsed struct { + // CustomScopeClaims stores additional claims provided by Holder + // caused by custom scope as a part of Initiate Credential Presentation request. + CustomScopeClaims map[string]Claims + VPTokens []*ProcessedVPToken +} + type ProcessedVPToken struct { Nonce string ClientID string @@ -31,12 +40,13 @@ type ProcessedVPToken struct { } type CredentialMetadata struct { - Format vcsverifiable.Format `json:"format"` - Type []string `json:"type"` - SubjectData interface{} `json:"subjectData"` - Issuer interface{} `json:"issuer"` + Format vcsverifiable.Format `json:"format,omitempty"` + Type []string `json:"type,omitempty"` + SubjectData interface{} `json:"subjectData,omitempty"` + Issuer interface{} `json:"issuer,omitempty"` IssuanceDate *util.TimeWrapper `json:"issuanceDate,omitempty"` ExpirationDate *util.TimeWrapper `json:"expirationDate,omitempty"` + CustomClaims map[string]Claims `json:"customClaims,omitempty"` } type ServiceInterface interface { @@ -44,9 +54,10 @@ type ServiceInterface interface { ctx context.Context, presentationDefinition *presexch.PresentationDefinition, purpose string, + customScope string, profile *profileapi.Verifier, ) (*InteractionInfo, error) - VerifyOIDCVerifiablePresentation(ctx context.Context, txID TxID, token []*ProcessedVPToken) error + VerifyOIDCVerifiablePresentation(ctx context.Context, txID TxID, authResponse *AuthorizationResponseParsed) error GetTx(ctx context.Context, id TxID) (*Transaction, error) RetrieveClaims(ctx context.Context, tx *Transaction, profile *profileapi.Verifier) map[string]CredentialMetadata DeleteClaims(ctx context.Context, receivedClaimsID string) error diff --git a/pkg/service/oidc4vp/claims.go b/pkg/service/oidc4vp/claims.go index 54ac60aab..815c26a73 100644 --- a/pkg/service/oidc4vp/claims.go +++ b/pkg/service/oidc4vp/claims.go @@ -55,6 +55,19 @@ func (tm *TxManager) ClaimsToClaimsRaw(data *ReceivedClaims) (*ReceivedClaimsRaw raw.Credentials[key] = cl } + if len(data.CustomScopeClaims) > 0 { + raw.CustomScopeClaims = make(map[string][]byte, len(data.CustomScopeClaims)) + + for customScope, claims := range data.CustomScopeClaims { + cl, err := json.Marshal(claims) + if err != nil { + return nil, fmt.Errorf("serialize received claims %w", err) + } + + raw.CustomScopeClaims[customScope] = cl + } + } + return raw, nil } @@ -88,5 +101,18 @@ func (tm *TxManager) DecryptClaims(ctx context.Context, data *ClaimData) (*Recei final.Credentials[k] = parsed } + if len(raw.CustomScopeClaims) > 0 { + final.CustomScopeClaims = make(map[string]Claims, len(raw.CustomScopeClaims)) + + for customScope, claimsRaw := range raw.CustomScopeClaims { + var claims Claims + if err = json.Unmarshal(claimsRaw, &claims); err != nil { + return nil, fmt.Errorf("received custom scope claims deserialize: %w", err) + } + + final.CustomScopeClaims[customScope] = claims + } + } + return final, nil } diff --git a/pkg/service/oidc4vp/oidc4vp_service.go b/pkg/service/oidc4vp/oidc4vp_service.go index 5cdc818bd..fa0f6826b 100644 --- a/pkg/service/oidc4vp/oidc4vp_service.go +++ b/pkg/service/oidc4vp/oidc4vp_service.go @@ -46,7 +46,10 @@ import ( var logger = log.New("oidc4vp-service") -const vpSubmissionProperty = "presentation_submission" +const ( + vpSubmissionProperty = "presentation_submission" + customScopeProperty = "_scope" +) var ErrDataNotFound = errors.New("data not found") @@ -55,7 +58,9 @@ type eventService interface { } type transactionManager interface { - CreateTx(pd *presexch.PresentationDefinition, profileID, profileVersion string) (*Transaction, string, error) + CreateTx( + pd *presexch.PresentationDefinition, + profileID, profileVersion, customScope string) (*Transaction, string, error) StoreReceivedClaims(txID TxID, claims *ReceivedClaims) error DeleteReceivedClaims(claimsID string) error GetByOneTimeToken(nonce string) (*Transaction, bool, error) @@ -233,6 +238,7 @@ func (s *Service) InitiateOidcInteraction( ctx context.Context, presentationDefinition *presexch.PresentationDefinition, purpose string, + customScope string, profile *profileapi.Verifier, ) (*InteractionInfo, error) { logger.Debugc(ctx, "InitiateOidcInteraction begin") @@ -242,7 +248,7 @@ func (s *Service) InitiateOidcInteraction( errors.New("profile signing did can't be nil")) } - tx, nonce, err := s.transactionManager.CreateTx(presentationDefinition, profile.ID, profile.Version) + tx, nonce, err := s.transactionManager.CreateTx(presentationDefinition, profile.ID, profile.Version, customScope) if err != nil { return nil, resterr.NewSystemError(resterr.VerifierTxnMgrComponent, "create-txn", fmt.Errorf("fail to create oidc tx: %w", err)) @@ -250,7 +256,7 @@ func (s *Service) InitiateOidcInteraction( logger.Debugc(ctx, "InitiateOidcInteraction tx created", log.WithTxID(string(tx.ID))) - token, err := s.createRequestObjectJWT(presentationDefinition, tx, nonce, purpose, profile) + token, err := s.createRequestObjectJWT(presentationDefinition, tx, nonce, purpose, customScope, profile) if err != nil { s.sendFailedTransactionEvent(ctx, tx, profile, err) @@ -362,7 +368,11 @@ func (s *Service) verifyTokens( return verifiedPresentations, nil } -func (s *Service) VerifyOIDCVerifiablePresentation(ctx context.Context, txID TxID, tokens []*ProcessedVPToken) error { +func (s *Service) VerifyOIDCVerifiablePresentation( + ctx context.Context, + txID TxID, + authResponse *AuthorizationResponseParsed, +) error { logger.Debugc(ctx, "VerifyOIDCVerifiablePresentation begin") startTime := time.Now() @@ -370,14 +380,14 @@ func (s *Service) VerifyOIDCVerifiablePresentation(ctx context.Context, txID TxI logger.Debugc(ctx, "VerifyOIDCVerifiablePresentation", log.WithDuration(time.Since(startTime))) }() - if len(tokens) == 0 { + if len(authResponse.VPTokens) == 0 { // this should never happen return resterr.NewValidationError(resterr.InvalidValue, "tokens", fmt.Errorf("must have at least one token")) } // All tokens have same nonce - tx, validNonce, err := s.transactionManager.GetByOneTimeToken(tokens[0].Nonce) + tx, validNonce, err := s.transactionManager.GetByOneTimeToken(authResponse.VPTokens[0].Nonce) if err != nil { return resterr.NewSystemError(resterr.VerifierTxnMgrComponent, "get-by-one-time-token", fmt.Errorf("get tx by nonce failed: %w", err)) @@ -388,6 +398,16 @@ func (s *Service) VerifyOIDCVerifiablePresentation(ctx context.Context, txID TxI fmt.Errorf("invalid nonce")) } + // If custom scope was requested, but no relevant additional claims were supplied by Holder. + noAdditionalClaims := tx.CustomScope != "" && len(authResponse.CustomScopeClaims[tx.CustomScope]) == 0 + // If custom scope was not requested, but additional claims were supplied by Holder. + unexpectedAdditionalClaims := tx.CustomScope == "" && len(authResponse.CustomScopeClaims) != 0 + + if noAdditionalClaims || unexpectedAdditionalClaims { + return resterr.NewValidationError(resterr.InvalidValue, "_scope", + fmt.Errorf("invalid _scope")) + } + logger.Debugc(ctx, "VerifyOIDCVerifiablePresentation nonce verified") profile, err := s.profileService.GetProfile(tx.ProfileID, tx.ProfileVersion) @@ -402,14 +422,14 @@ func (s *Service) VerifyOIDCVerifiablePresentation(ctx context.Context, txID TxI logger.Debugc(ctx, "VerifyOIDCVerifiablePresentation profile fetched", logfields.WithProfileID(profile.ID)) - logger.Debugc(ctx, fmt.Sprintf("VerifyOIDCVerifiablePresentation count of tokens is %v", len(tokens))) + logger.Debugc(ctx, fmt.Sprintf("VerifyOIDCVerifiablePresentation count of tokens is %v", len(authResponse.VPTokens))) - verifiedPresentations, err := s.verifyTokens(ctx, tx, profile, tokens) + verifiedPresentations, err := s.verifyTokens(ctx, tx, profile, authResponse.VPTokens) if err != nil { return err } - err = s.extractClaimData(ctx, tx, tokens, profile, verifiedPresentations) + err = s.extractClaimData(ctx, tx, authResponse, profile, verifiedPresentations) if err != nil { s.sendFailedTransactionEvent(ctx, tx, profile, err) @@ -477,6 +497,13 @@ func (s *Service) RetrieveClaims( result[credContents.ID] = credMeta } + + if len(tx.ReceivedClaims.CustomScopeClaims) > 0 { + result[customScopeProperty] = CredentialMetadata{ + CustomClaims: tx.ReceivedClaims.CustomScopeClaims, + } + } + logger.Debugc(ctx, "RetrieveClaims succeed") if err := s.sendTxEvent(ctx, spi.VerifierOIDCInteractionClaimsRetrieved, tx, profile); err != nil { @@ -508,13 +535,13 @@ func (s *Service) getDataIntegrityVerifier() (*dataintegrity.Verifier, error) { func (s *Service) extractClaimData( ctx context.Context, tx *Transaction, - tokens []*ProcessedVPToken, + authResponse *AuthorizationResponseParsed, profile *profileapi.Verifier, verifiedPresentations map[string]*ProcessedVPToken, ) error { var presentations []*verifiable.Presentation - for _, token := range tokens { + for _, token := range authResponse.VPTokens { // TODO: think about better solution. If jwt is set, its wrap vp into sub object "vp" and this breaks Match token.Presentation.JWT = "" presentations = append(presentations, token.Presentation) @@ -566,7 +593,12 @@ func (s *Service) extractClaimData( storeCredentials[inputDescID] = mc.Credential } - err = s.transactionManager.StoreReceivedClaims(tx.ID, &ReceivedClaims{Credentials: storeCredentials}) + receivedClaims := &ReceivedClaims{ + CustomScopeClaims: authResponse.CustomScopeClaims, + Credentials: storeCredentials, + } + + err = s.transactionManager.StoreReceivedClaims(tx.ID, receivedClaims) if err != nil { return resterr.NewSystemError(resterr.VerifierTxnMgrComponent, "store-received-claims", fmt.Errorf("store received claims: %w", err)) @@ -609,6 +641,7 @@ func (s *Service) createRequestObjectJWT(presentationDefinition *presexch.Presen tx *Transaction, nonce string, purpose string, + additionalScope string, profile *profileapi.Verifier) (string, error) { kms, err := s.kmsRegistry.GetKeyManager(profile.KMSConfig) if err != nil { @@ -619,7 +652,7 @@ func (s *Service) createRequestObjectJWT(presentationDefinition *presexch.Presen vpFormats := GetSupportedVPFormats( kms.SupportedKeyTypes(), profile.Checks.Presentation.Format, profile.Checks.Credential.Format) - ro := s.createRequestObject(presentationDefinition, vpFormats, tx, nonce, purpose, profile) + ro := s.createRequestObject(presentationDefinition, vpFormats, tx, nonce, purpose, additionalScope, profile) signatureTypes := vcsverifiable.GetSignatureTypesByKeyTypeFormat(profile.OIDCConfig.KeyType, vcsverifiable.Jwt) if len(signatureTypes) < 1 { @@ -711,6 +744,7 @@ func (s *Service) createRequestObject( tx *Transaction, nonce string, purpose string, + additionalScope string, profile *profileapi.Verifier) *RequestObject { tokenLifetime := s.tokenLifetime now := time.Now() @@ -720,7 +754,7 @@ func (s *Service) createRequestObject( ISS: profile.SigningDID.DID, ResponseType: "id_token", ResponseMode: "post", - Scope: "openid", + Scope: getScope(additionalScope), Nonce: nonce, ClientID: profile.SigningDID.DID, RedirectURI: s.redirectURL, @@ -739,6 +773,15 @@ func (s *Service) createRequestObject( } } +func getScope(additionalScope string) string { + scope := "openid" + if additionalScope != "" { + scope += "+" + additionalScope + } + + return scope +} + type JWSSigner struct { keyID string signer vc.SignerAlgorithm diff --git a/pkg/service/oidc4vp/oidc4vp_service_test.go b/pkg/service/oidc4vp/oidc4vp_service_test.go index 6d248c6ae..a2d79df2b 100644 --- a/pkg/service/oidc4vp/oidc4vp_service_test.go +++ b/pkg/service/oidc4vp/oidc4vp_service_test.go @@ -58,6 +58,7 @@ var ( const ( profileID = "testProfileID" profileVersion = "v1.0" + customScope = "customScope" ) func TestService_InitiateOidcInteraction(t *testing.T) { @@ -74,11 +75,13 @@ func TestService_InitiateOidcInteraction(t *testing.T) { &vcskms.MockKMS{Signer: customSigner}, nil) txManager := NewMockTransactionManager(gomock.NewController(t)) - txManager.EXPECT().CreateTx(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(&oidc4vp.Transaction{ - ID: "TxID1", - ProfileID: "test4", - PresentationDefinition: &presexch.PresentationDefinition{}, - }, "nonce1", nil) + txManager.EXPECT().CreateTx(gomock.Any(), gomock.Any(), gomock.Any(), customScope).AnyTimes(). + Return(&oidc4vp.Transaction{ + ID: "TxID1", + ProfileID: "test4", + PresentationDefinition: &presexch.PresentationDefinition{}, + CustomScope: customScope, + }, "nonce1", nil) requestObjectPublicStore := NewMockRequestObjectPublicStore(gomock.NewController(t)) requestObjectPublicStore.EXPECT().Publish(gomock.Any(), gomock.Any()). AnyTimes().DoAndReturn(func(ctx context.Context, token string) (string, error) { @@ -130,7 +133,7 @@ func TestService_InitiateOidcInteraction(t *testing.T) { t.Run("Success", func(t *testing.T) { info, err := s.InitiateOidcInteraction(context.TODO(), &presexch.PresentationDefinition{ ID: "test", - }, "test", correctProfile) + }, "test", customScope, correctProfile) require.NoError(t, err) require.NotNil(t, info) @@ -141,7 +144,8 @@ func TestService_InitiateOidcInteraction(t *testing.T) { require.NoError(t, copier.Copy(incorrectProfile, correctProfile)) incorrectProfile.SigningDID = nil - info, err := s.InitiateOidcInteraction(context.TODO(), &presexch.PresentationDefinition{}, "test", incorrectProfile) + info, err := s.InitiateOidcInteraction( + context.TODO(), &presexch.PresentationDefinition{}, "test", customScope, incorrectProfile) require.Error(t, err) require.Nil(t, info) @@ -150,7 +154,7 @@ func TestService_InitiateOidcInteraction(t *testing.T) { t.Run("Tx create failed", func(t *testing.T) { txManagerErr := NewMockTransactionManager(gomock.NewController(t)) txManagerErr.EXPECT().CreateTx( - gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(nil, "", errors.New("fail")) + gomock.Any(), gomock.Any(), gomock.Any(), customScope).AnyTimes().Return(nil, "", errors.New("fail")) withError := oidc4vp.NewService(&oidc4vp.Config{ EventSvc: &mockEvent{}, @@ -165,6 +169,7 @@ func TestService_InitiateOidcInteraction(t *testing.T) { context.TODO(), &presexch.PresentationDefinition{}, "test", + customScope, correctProfile, ) @@ -190,6 +195,7 @@ func TestService_InitiateOidcInteraction(t *testing.T) { context.TODO(), &presexch.PresentationDefinition{}, "test", + customScope, correctProfile, ) @@ -214,6 +220,7 @@ func TestService_InitiateOidcInteraction(t *testing.T) { context.TODO(), &presexch.PresentationDefinition{}, "test", + customScope, correctProfile, ) @@ -226,7 +233,8 @@ func TestService_InitiateOidcInteraction(t *testing.T) { require.NoError(t, copier.Copy(incorrectProfile, correctProfile)) incorrectProfile.SigningDID.KMSKeyID = "invalid" - info, err := s.InitiateOidcInteraction(context.TODO(), &presexch.PresentationDefinition{}, "test", incorrectProfile) + info, err := s.InitiateOidcInteraction( + context.TODO(), &presexch.PresentationDefinition{}, "test", customScope, incorrectProfile) require.Error(t, err) require.Nil(t, info) @@ -237,7 +245,8 @@ func TestService_InitiateOidcInteraction(t *testing.T) { require.NoError(t, copier.Copy(incorrectProfile, correctProfile)) incorrectProfile.OIDCConfig.KeyType = "invalid" - info, err := s.InitiateOidcInteraction(context.TODO(), &presexch.PresentationDefinition{}, "test", incorrectProfile) + info, err := s.InitiateOidcInteraction( + context.TODO(), &presexch.PresentationDefinition{}, "test", customScope, incorrectProfile) require.Error(t, err) require.Nil(t, info) @@ -291,33 +300,51 @@ func TestService_VerifyOIDCVerifiablePresentation(t *testing.T) { presentationVerifier.EXPECT().VerifyPresentation(context.Background(), gomock.Any(), gomock.Any(), gomock.Any()). AnyTimes().Return(nil, nil, nil) - t.Run("Success", func(t *testing.T) { - err := s.VerifyOIDCVerifiablePresentation(context.Background(), "txID1", - []*oidc4vp.ProcessedVPToken{{ - Nonce: "nonce1", - Presentation: vp, - SignerDIDID: issuer, - VpTokenFormat: vcsverifiable.Jwt, - }}) + t.Run("Success without custom claims", func(t *testing.T) { + txManager2 := NewMockTransactionManager(gomock.NewController(t)) - require.NoError(t, err) - }) + txManager2.EXPECT().GetByOneTimeToken("nonce1").AnyTimes().Return(&oidc4vp.Transaction{ + ID: "txID1", + ProfileID: profileID, + ProfileVersion: profileVersion, + PresentationDefinition: pd, + }, true, nil) - t.Run("Unsupported vp token format", func(t *testing.T) { - err := s.VerifyOIDCVerifiablePresentation(context.Background(), "txID1", - []*oidc4vp.ProcessedVPToken{{ - Nonce: "nonce1", - Presentation: vp, - SignerDIDID: issuer, - VpTokenFormat: vcsverifiable.Ldp, - }}) + txManager2.EXPECT().StoreReceivedClaims(oidc4vp.TxID("txID1"), gomock.Any()).Times(1). + DoAndReturn(func(txID oidc4vp.TxID, claims *oidc4vp.ReceivedClaims) error { + require.Nil(t, claims.CustomScopeClaims) - require.ErrorContains(t, err, "profile does not support ldp vp_token format") + return nil + }) + + s2 := oidc4vp.NewService(&oidc4vp.Config{ + EventSvc: &mockEvent{}, + EventTopic: spi.VerifierEventTopic, + TransactionManager: txManager2, + PresentationVerifier: presentationVerifier, + ProfileService: profileService, + DocumentLoader: loader, + VDR: vdr, + }) + + err = s2.VerifyOIDCVerifiablePresentation(context.Background(), "txID1", + &oidc4vp.AuthorizationResponseParsed{ + CustomScopeClaims: nil, + VPTokens: []*oidc4vp.ProcessedVPToken{{ + Nonce: "nonce1", + Presentation: vp, + SignerDIDID: issuer, + VpTokenFormat: vcsverifiable.Jwt, + }}, + }, + ) + + require.NoError(t, err) }) - t.Run("Success - two VP tokens (merged)", func(t *testing.T) { + t.Run("Success - two VP tokens (merged) with custom claims", func(t *testing.T) { var descriptors []*presexch.InputDescriptor - err := json.Unmarshal([]byte(twoInputDescriptors), &descriptors) + err = json.Unmarshal([]byte(twoInputDescriptors), &descriptors) require.NoError(t, err) defs := &presexch.PresentationDefinition{ @@ -379,32 +406,66 @@ func TestService_VerifyOIDCVerifiablePresentation(t *testing.T) { ProfileID: profileID, ProfileVersion: profileVersion, PresentationDefinition: defs, + CustomScope: customScope, }, true, nil) - txManager2.EXPECT().StoreReceivedClaims(oidc4vp.TxID("txID1"), gomock.Any()).AnyTimes().Return(nil) + txManager2.EXPECT().StoreReceivedClaims(oidc4vp.TxID("txID1"), gomock.Any()).Times(1). + DoAndReturn(func(txID oidc4vp.TxID, claims *oidc4vp.ReceivedClaims) error { + require.Equal(t, map[string]oidc4vp.Claims{ + customScope: { + "key1": "value1", + }, + }, claims.CustomScopeClaims) + + return nil + }) err = s2.VerifyOIDCVerifiablePresentation(context.Background(), "txID1", - []*oidc4vp.ProcessedVPToken{ - { - Nonce: "nonce1", - Presentation: vp1, - SignerDIDID: issuer1, - VpTokenFormat: vcsverifiable.Jwt, + &oidc4vp.AuthorizationResponseParsed{ + CustomScopeClaims: map[string]oidc4vp.Claims{ + customScope: { + "key1": "value1", + }, }, - { - Nonce: "nonce1", - Presentation: vp2, - SignerDIDID: issuer2, - VpTokenFormat: vcsverifiable.Jwt, + VPTokens: []*oidc4vp.ProcessedVPToken{ + { + Nonce: "nonce1", + Presentation: vp1, + SignerDIDID: issuer1, + VpTokenFormat: vcsverifiable.Jwt, + }, + { + Nonce: "nonce1", + Presentation: vp2, + SignerDIDID: issuer2, + VpTokenFormat: vcsverifiable.Jwt, + }, }, - }) + }, + ) require.NoError(t, err) }) + t.Run("Unsupported vp token format", func(t *testing.T) { + err = s.VerifyOIDCVerifiablePresentation(context.Background(), "txID1", + &oidc4vp.AuthorizationResponseParsed{ + CustomScopeClaims: nil, + VPTokens: []*oidc4vp.ProcessedVPToken{{ + Nonce: "nonce1", + Presentation: vp, + SignerDIDID: issuer, + VpTokenFormat: vcsverifiable.Ldp, + }}, + }, + ) + + require.ErrorContains(t, err, "profile does not support ldp vp_token format") + }) + t.Run("Error - Two VP tokens without presentation ID", func(t *testing.T) { var descriptors []*presexch.InputDescriptor - err := json.Unmarshal([]byte(twoInputDescriptors), &descriptors) + err = json.Unmarshal([]byte(twoInputDescriptors), &descriptors) require.NoError(t, err) defs := &presexch.PresentationDefinition{ @@ -474,41 +535,51 @@ func TestService_VerifyOIDCVerifiablePresentation(t *testing.T) { vp2.ID = "" err = s2.VerifyOIDCVerifiablePresentation(context.Background(), "txID1", - []*oidc4vp.ProcessedVPToken{ - { - Nonce: "nonce1", - Presentation: vp1, - SignerDIDID: issuer1, - VpTokenFormat: vcsverifiable.Jwt, - }, - { - Nonce: "nonce1", - Presentation: vp2, - SignerDIDID: issuer2, - VpTokenFormat: vcsverifiable.Jwt, + &oidc4vp.AuthorizationResponseParsed{ + CustomScopeClaims: nil, + VPTokens: []*oidc4vp.ProcessedVPToken{ + { + Nonce: "nonce1", + Presentation: vp1, + SignerDIDID: issuer1, + VpTokenFormat: vcsverifiable.Jwt, + }, + { + Nonce: "nonce1", + Presentation: vp2, + SignerDIDID: issuer2, + VpTokenFormat: vcsverifiable.Jwt, + }, }, - }) + }, + ) require.Error(t, err) require.Contains(t, err.Error(), "duplicate presentation ID: ") }) t.Run("Must have at least one token", func(t *testing.T) { - err := s.VerifyOIDCVerifiablePresentation(context.Background(), "txID1", - []*oidc4vp.ProcessedVPToken{}) + err = s.VerifyOIDCVerifiablePresentation(context.Background(), "txID1", + &oidc4vp.AuthorizationResponseParsed{ + CustomScopeClaims: nil, + VPTokens: []*oidc4vp.ProcessedVPToken{}, + }, + ) require.Error(t, err) require.Contains(t, err.Error(), "must have at least one token") }) t.Run("VC subject is not much with vp signer", func(t *testing.T) { - err := s.VerifyOIDCVerifiablePresentation(context.Background(), "txID1", - []*oidc4vp.ProcessedVPToken{{ - Nonce: "nonce1", - Presentation: vp, - SignerDIDID: "did:example1:ebfeb1f712ebc6f1c276e12ec21", - VpTokenFormat: vcsverifiable.Jwt, - }}) + err = s.VerifyOIDCVerifiablePresentation(context.Background(), "txID1", + &oidc4vp.AuthorizationResponseParsed{ + CustomScopeClaims: nil, + VPTokens: []*oidc4vp.ProcessedVPToken{{ + Nonce: "nonce1", + Presentation: vp, + SignerDIDID: "did:example1:ebfeb1f712ebc6f1c276e12ec21", + VpTokenFormat: vcsverifiable.Jwt, + }}}) require.Contains(t, err.Error(), "does not match with vp signer") }) @@ -527,28 +598,95 @@ func TestService_VerifyOIDCVerifiablePresentation(t *testing.T) { DocumentLoader: loader, }) - err := withError.VerifyOIDCVerifiablePresentation(context.Background(), "txID1", - []*oidc4vp.ProcessedVPToken{{ - Nonce: "nonce1", - Presentation: vp, - SignerDIDID: "did:example123:ebfeb1f712ebc6f1c276e12ec21", - }}) + err = withError.VerifyOIDCVerifiablePresentation(context.Background(), "txID1", + &oidc4vp.AuthorizationResponseParsed{ + CustomScopeClaims: nil, + VPTokens: []*oidc4vp.ProcessedVPToken{{ + Nonce: "nonce1", + Presentation: vp, + SignerDIDID: "did:example123:ebfeb1f712ebc6f1c276e12ec21", + }}}) require.Contains(t, err.Error(), "invalid nonce1") }) t.Run("Invalid Nonce 2", func(t *testing.T) { - err := s.VerifyOIDCVerifiablePresentation(context.Background(), "txID2", - []*oidc4vp.ProcessedVPToken{{ - Nonce: "nonce1", - Presentation: vp, - SignerDIDID: "did:example123:ebfeb1f712ebc6f1c276e12ec21", - }}) + err = s.VerifyOIDCVerifiablePresentation(context.Background(), "txID2", + &oidc4vp.AuthorizationResponseParsed{ + CustomScopeClaims: nil, + VPTokens: []*oidc4vp.ProcessedVPToken{{ + Nonce: "nonce1", + Presentation: vp, + SignerDIDID: "did:example123:ebfeb1f712ebc6f1c276e12ec21", + }}}) require.Contains(t, err.Error(), "invalid nonce") }) - t.Run("Invalid Nonce", func(t *testing.T) { + t.Run("Invalid _scope", func(t *testing.T) { + errTxManager := NewMockTransactionManager(gomock.NewController(t)) + withError := oidc4vp.NewService(&oidc4vp.Config{ + TransactionManager: errTxManager, + }) + + errTxManager.EXPECT().GetByOneTimeToken("nonce1").AnyTimes().Return(&oidc4vp.Transaction{ + ID: "txID1", + ProfileID: profileID, + ProfileVersion: profileVersion, + PresentationDefinition: pd, + CustomScope: customScope, + }, true, nil) + + err = withError.VerifyOIDCVerifiablePresentation(context.Background(), "txID1", + &oidc4vp.AuthorizationResponseParsed{ + CustomScopeClaims: nil, + VPTokens: []*oidc4vp.ProcessedVPToken{{ + Nonce: "nonce1", + }}}) + + require.Contains(t, err.Error(), "invalid _scope") + }) + + t.Run("Invalid _scope 2", func(t *testing.T) { + errTxManager := NewMockTransactionManager(gomock.NewController(t)) + withError := oidc4vp.NewService(&oidc4vp.Config{ + TransactionManager: errTxManager, + }) + + errTxManager.EXPECT().GetByOneTimeToken("nonce1").AnyTimes().Return(&oidc4vp.Transaction{ + ID: "txID1", + ProfileID: profileID, + ProfileVersion: profileVersion, + PresentationDefinition: pd, + CustomScope: customScope, + }, true, nil) + + err = withError.VerifyOIDCVerifiablePresentation(context.Background(), "txID1", + &oidc4vp.AuthorizationResponseParsed{ + CustomScopeClaims: map[string]oidc4vp.Claims{ + "customScope2": {}, + }, + VPTokens: []*oidc4vp.ProcessedVPToken{{ + Nonce: "nonce1", + }}}) + + require.Contains(t, err.Error(), "invalid _scope") + }) + + t.Run("Invalid _scope 3", func(t *testing.T) { + err := s.VerifyOIDCVerifiablePresentation(context.Background(), "txID1", + &oidc4vp.AuthorizationResponseParsed{ + CustomScopeClaims: map[string]oidc4vp.Claims{ + customScope: {}, + }, + VPTokens: []*oidc4vp.ProcessedVPToken{{ + Nonce: "nonce1", + }}}) + + require.Contains(t, err.Error(), "invalid _scope") + }) + + t.Run("Get profile error", func(t *testing.T) { errProfileService := NewMockProfileService(gomock.NewController(t)) errProfileService.EXPECT().GetProfile(profileID, profileVersion).Times(1).Return(nil, errors.New("get profile error")) @@ -563,11 +701,13 @@ func TestService_VerifyOIDCVerifiablePresentation(t *testing.T) { }) err := withError.VerifyOIDCVerifiablePresentation(context.Background(), "txID1", - []*oidc4vp.ProcessedVPToken{{ - Nonce: "nonce1", - Presentation: vp, - SignerDIDID: "did:example123:ebfeb1f712ebc6f1c276e12ec21", - }}) + &oidc4vp.AuthorizationResponseParsed{ + CustomScopeClaims: nil, + VPTokens: []*oidc4vp.ProcessedVPToken{{ + Nonce: "nonce1", + Presentation: vp, + SignerDIDID: "did:example123:ebfeb1f712ebc6f1c276e12ec21", + }}}) require.Contains(t, err.Error(), "get profile error") }) @@ -588,23 +728,27 @@ func TestService_VerifyOIDCVerifiablePresentation(t *testing.T) { }) err := withError.VerifyOIDCVerifiablePresentation(context.Background(), "txID1", - []*oidc4vp.ProcessedVPToken{{ - Nonce: "nonce1", - Presentation: vp, - SignerDIDID: "did:example123:ebfeb1f712ebc6f1c276e12ec21", - VpTokenFormat: vcsverifiable.Jwt, - }}) + &oidc4vp.AuthorizationResponseParsed{ + CustomScopeClaims: nil, + VPTokens: []*oidc4vp.ProcessedVPToken{{ + Nonce: "nonce1", + Presentation: vp, + SignerDIDID: "did:example123:ebfeb1f712ebc6f1c276e12ec21", + VpTokenFormat: vcsverifiable.Jwt, + }}}) require.Contains(t, err.Error(), "verification failed") }) t.Run("Match failed", func(t *testing.T) { err := s.VerifyOIDCVerifiablePresentation(context.Background(), "txID1", - []*oidc4vp.ProcessedVPToken{{ - Nonce: "nonce1", - Presentation: &verifiable.Presentation{}, - VpTokenFormat: vcsverifiable.Jwt, - }}) + &oidc4vp.AuthorizationResponseParsed{ + CustomScopeClaims: nil, + VPTokens: []*oidc4vp.ProcessedVPToken{{ + Nonce: "nonce1", + Presentation: &verifiable.Presentation{}, + VpTokenFormat: vcsverifiable.Jwt, + }}}) require.Contains(t, err.Error(), "match:") }) @@ -631,12 +775,14 @@ func TestService_VerifyOIDCVerifiablePresentation(t *testing.T) { }) err := withError.VerifyOIDCVerifiablePresentation(context.Background(), "txID1", - []*oidc4vp.ProcessedVPToken{{ - Nonce: "nonce1", - Presentation: vp, - SignerDIDID: issuer, - VpTokenFormat: vcsverifiable.Jwt, - }}) + &oidc4vp.AuthorizationResponseParsed{ + CustomScopeClaims: nil, + VPTokens: []*oidc4vp.ProcessedVPToken{{ + Nonce: "nonce1", + Presentation: vp, + SignerDIDID: issuer, + VpTokenFormat: vcsverifiable.Jwt, + }}}) require.Contains(t, err.Error(), "store error") }) @@ -690,7 +836,7 @@ func TestService_DeleteClaims(t *testing.T) { func TestService_RetrieveClaims(t *testing.T) { loader := testutil.DocumentLoader(t) - t.Run("Success JWT", func(t *testing.T) { + t.Run("Success JWT with custom claims", func(t *testing.T) { mockEventSvc := NewMockeventService(gomock.NewController(t)) mockEventSvc.EXPECT().Publish(gomock.Any(), spi.VerifierEventTopic, gomock.Any()).DoAndReturn( expectedPublishEventFunc(t, spi.VerifierOIDCInteractionClaimsRetrieved), @@ -705,9 +851,17 @@ func TestService_RetrieveClaims(t *testing.T) { require.NoError(t, err) claims := svc.RetrieveClaims(context.Background(), &oidc4vp.Transaction{ - ReceivedClaims: &oidc4vp.ReceivedClaims{Credentials: map[string]*verifiable.Credential{ - "id": jwtvc, - }}}, &profileapi.Verifier{}) + ReceivedClaims: &oidc4vp.ReceivedClaims{ + Credentials: map[string]*verifiable.Credential{ + "id": jwtvc, + }, + CustomScopeClaims: map[string]oidc4vp.Claims{ + customScope: { + "key1": "value1", + }, + }, + }, + }, &profileapi.Verifier{}) require.NotNil(t, claims) subjects, ok := claims["http://example.gov/credentials/3732"].SubjectData.([]map[string]interface{}) @@ -718,9 +872,13 @@ func TestService_RetrieveClaims(t *testing.T) { require.NotEmpty(t, claims["http://example.gov/credentials/3732"].Issuer) require.NotEmpty(t, claims["http://example.gov/credentials/3732"].IssuanceDate) require.Empty(t, claims["http://example.gov/credentials/3732"].ExpirationDate) + require.Equal(t, + oidc4vp.CredentialMetadata{CustomClaims: map[string]oidc4vp.Claims{customScope: {"key1": "value1"}}}, + claims["_scope"], + ) }) - t.Run("Success JsonLD", func(t *testing.T) { + t.Run("Success JsonLD without custom claims", func(t *testing.T) { mockEventSvc := NewMockeventService(gomock.NewController(t)) mockEventSvc.EXPECT().Publish(gomock.Any(), spi.VerifierEventTopic, gomock.Any()).DoAndReturn( expectedPublishEventFunc(t, spi.VerifierOIDCInteractionClaimsRetrieved), @@ -747,6 +905,7 @@ func TestService_RetrieveClaims(t *testing.T) { require.NotEmpty(t, claims["http://example.gov/credentials/3732"].Issuer) require.NotEmpty(t, claims["http://example.gov/credentials/3732"].IssuanceDate) require.NotEmpty(t, claims["http://example.gov/credentials/3732"].ExpirationDate) + require.Empty(t, claims["_scope"]) }) t.Run("Empty claims", func(t *testing.T) { diff --git a/pkg/service/oidc4vp/txmanager.go b/pkg/service/oidc4vp/txmanager.go index 58be700ef..55c5d74be 100644 --- a/pkg/service/oidc4vp/txmanager.go +++ b/pkg/service/oidc4vp/txmanager.go @@ -36,15 +36,18 @@ type Transaction struct { PresentationDefinition *presexch.PresentationDefinition ReceivedClaims *ReceivedClaims ReceivedClaimsID string + CustomScope string } type ReceivedClaims struct { - Credentials map[string]*verifiable.Credential `json:"credentials"` + CustomScopeClaims map[string]Claims + Credentials map[string]*verifiable.Credential } // ReceivedClaimsRaw is temporary struct for parsing to ReceivedClaims, as we need to unmarshal credentials separately. type ReceivedClaimsRaw struct { - Credentials map[string][]byte `json:"credentials"` + Credentials map[string][]byte `json:"credentials"` + CustomScopeClaims map[string][]byte `json:"customScopeClaims,omitempty"` } type ClaimData struct { @@ -57,7 +60,7 @@ type TransactionUpdate struct { } type txStore interface { - Create(pd *presexch.PresentationDefinition, profileID, profileVersion string) (TxID, *Transaction, error) + Create(pd *presexch.PresentationDefinition, profileID, profileVersion, customScope string) (TxID, *Transaction, error) Update(update TransactionUpdate) error Get(txID TxID) (*Transaction, error) } @@ -106,8 +109,8 @@ func NewTxManager( // CreateTx creates transaction and generate one time access token. func (tm *TxManager) CreateTx( - pd *presexch.PresentationDefinition, profileID, profileVersion string) (*Transaction, string, error) { - txID, tx, err := tm.txStore.Create(pd, profileID, profileVersion) + pd *presexch.PresentationDefinition, profileID, profileVersion, customScope string) (*Transaction, string, error) { + txID, tx, err := tm.txStore.Create(pd, profileID, profileVersion, customScope) if err != nil { return nil, "", fmt.Errorf("oidc tx create failed: %w", err) } diff --git a/pkg/service/oidc4vp/txmanager_test.go b/pkg/service/oidc4vp/txmanager_test.go index b8aca9af5..09123bce7 100644 --- a/pkg/service/oidc4vp/txmanager_test.go +++ b/pkg/service/oidc4vp/txmanager_test.go @@ -27,8 +27,16 @@ import ( func TestTxManager_CreateTx(t *testing.T) { t.Run("Success", func(t *testing.T) { store := NewMockTxStore(gomock.NewController(t)) - store.EXPECT().Create(gomock.Any(), profileID, profileVersion).Return( - oidc4vp.TxID("txID"), &oidc4vp.Transaction{ID: "txID", ProfileID: profileID, ProfileVersion: profileVersion}, nil) + store.EXPECT().Create(gomock.Any(), profileID, profileVersion, customScope).Return( + oidc4vp.TxID("txID"), + &oidc4vp.Transaction{ + ID: "txID", + ProfileID: profileID, + ProfileVersion: profileVersion, + CustomScope: customScope, + }, + nil, + ) claimsStore := NewMockTxClaimsStore(gomock.NewController(t)) @@ -41,18 +49,20 @@ func TestTxManager_CreateTx(t *testing.T) { manager := oidc4vp.NewTxManager(nonceStore, store, claimsStore, crypto, testutil.DocumentLoader(t)) - tx, nonce, err := manager.CreateTx(&presexch.PresentationDefinition{}, profileID, profileVersion) + tx, nonce, err := manager.CreateTx(&presexch.PresentationDefinition{}, profileID, profileVersion, customScope) require.NoError(t, err) require.NotEmpty(t, nonce) require.NotNil(t, tx) require.Equal(t, profileID, tx.ProfileID) require.Equal(t, profileVersion, tx.ProfileVersion) + require.Equal(t, customScope, tx.CustomScope) }) t.Run("Fail", func(t *testing.T) { store := NewMockTxStore(gomock.NewController(t)) - store.EXPECT().Create(gomock.Any(), profileID, profileVersion).Return(oidc4vp.TxID(""), nil, errors.New("test error")) + store.EXPECT().Create(gomock.Any(), profileID, profileVersion, customScope). + Return(oidc4vp.TxID(""), nil, errors.New("test error")) claimsStore := NewMockTxClaimsStore(gomock.NewController(t)) @@ -62,14 +72,14 @@ func TestTxManager_CreateTx(t *testing.T) { manager := oidc4vp.NewTxManager(nonceStore, store, claimsStore, crypto, testutil.DocumentLoader(t)) - _, _, err := manager.CreateTx(&presexch.PresentationDefinition{}, profileID, profileVersion) + _, _, err := manager.CreateTx(&presexch.PresentationDefinition{}, profileID, profileVersion, customScope) require.Contains(t, err.Error(), "test error") }) t.Run("Fail", func(t *testing.T) { store := NewMockTxStore(gomock.NewController(t)) - store.EXPECT().Create(gomock.Any(), profileID, profileVersion).Return(oidc4vp.TxID("txID"), nil, nil) + store.EXPECT().Create(gomock.Any(), profileID, profileVersion, "").Return(oidc4vp.TxID("txID"), nil, nil) claimsStore := NewMockTxClaimsStore(gomock.NewController(t)) @@ -81,7 +91,7 @@ func TestTxManager_CreateTx(t *testing.T) { manager := oidc4vp.NewTxManager(nonceStore, store, claimsStore, crypto, testutil.DocumentLoader(t)) - _, _, err := manager.CreateTx(&presexch.PresentationDefinition{}, profileID, profileVersion) + _, _, err := manager.CreateTx(&presexch.PresentationDefinition{}, profileID, profileVersion, "") require.Contains(t, err.Error(), "test error") }) @@ -221,6 +231,7 @@ func TestTxManager_Get(t *testing.T) { "sd": vcSD, "ldp": ld, }, + CustomScopeClaims: nil, } raw, err := manager.ClaimsToClaimsRaw(rs) assert.NoError(t, err) @@ -238,6 +249,91 @@ func TestTxManager_Get(t *testing.T) { require.Equal(t, "org_id", tx.ProfileID) require.Equal(t, tx.ReceivedClaimsID, "claims_id") require.NotNil(t, tx.ReceivedClaims) + require.Nil(t, tx.ReceivedClaims.CustomScopeClaims) + }) + + t.Run("Success - with claims ID and custom claims", func(t *testing.T) { + store := NewMockTxStore(gomock.NewController(t)) + store.EXPECT().Get(oidc4vp.TxID("txID")).Return( + &oidc4vp.Transaction{ + ID: "txID", + ProfileID: "org_id", + ReceivedClaimsID: "claims_id"}, + nil) + + encryptedClaims := []byte{0x0, 0x1, 0x2} + nonce := []byte{0x3, 0x4} + + claimsStore := NewMockTxClaimsStore(gomock.NewController(t)) + crypto := NewMockDataProtector(gomock.NewController(t)) + + chunks := &dataprotect.EncryptedData{ + Encrypted: encryptedClaims, + EncryptedNonce: nonce, + } + claimsStore.EXPECT().Get(gomock.Any()).Return(&oidc4vp.ClaimData{ + EncryptedData: chunks, + }, nil) + + nonceStore := NewMockTxNonceStore(gomock.NewController(t)) + + manager := oidc4vp.NewTxManager(nonceStore, store, claimsStore, crypto, + testutil.DocumentLoader(t)) + + crypto.EXPECT().Decrypt(gomock.Any(), chunks). + DoAndReturn(func(ctx context.Context, chunks1 *dataprotect.EncryptedData) ([]byte, error) { + assert.Equal(t, chunks, chunks1) + + vc, err := verifiable.ParseCredential([]byte(sampleVCJWT), + verifiable.WithJSONLDDocumentLoader(testutil.DocumentLoader(t)), + verifiable.WithDisabledProofCheck()) + assert.NoError(t, err) + vcSD, err := verifiable.ParseCredential([]byte(sampleVCJWT), + verifiable.WithJSONLDDocumentLoader(testutil.DocumentLoader(t)), + verifiable.WithDisabledProofCheck()) + assert.NoError(t, err) + ld, err := verifiable.ParseCredential([]byte(sampleVCJsonLD), + verifiable.WithJSONLDDocumentLoader(testutil.DocumentLoader(t)), + verifiable.WithDisabledProofCheck()) + assert.NoError(t, err) + + rs := &oidc4vp.ReceivedClaims{ + Credentials: map[string]*verifiable.Credential{ + "jwt": vc, + "sd": vcSD, + "ldp": ld, + }, + CustomScopeClaims: map[string]oidc4vp.Claims{ + customScope: { + "key1": "value1", + "key2": float64(2), + "key3": map[string]interface{}{"key4": "value4"}, + }, + }, + } + raw, err := manager.ClaimsToClaimsRaw(rs) + assert.NoError(t, err) + + b, err := json.Marshal(raw) + assert.NoError(t, err) + + return b, nil + }) + + tx, err := manager.Get("txID") + + require.NoError(t, err) + require.NotNil(t, tx) + require.Equal(t, "org_id", tx.ProfileID) + require.Equal(t, tx.ReceivedClaimsID, "claims_id") + require.NotNil(t, tx.ReceivedClaims) + require.Equal(t, map[string]oidc4vp.Claims{ + customScope: { + "key1": "value1", + "key2": float64(2), + "key3": map[string]interface{}{"key4": "value4"}, + }, + }, tx.ReceivedClaims.CustomScopeClaims) }) t.Run("Success - claims not found", func(t *testing.T) { diff --git a/pkg/storage/mongodb/oidc4vptxstore/oidc4vp_tx_store.go b/pkg/storage/mongodb/oidc4vptxstore/oidc4vp_tx_store.go index 5d1f781ea..09f2d1480 100644 --- a/pkg/storage/mongodb/oidc4vptxstore/oidc4vp_tx_store.go +++ b/pkg/storage/mongodb/oidc4vptxstore/oidc4vp_tx_store.go @@ -33,6 +33,7 @@ type txDocument struct { ProfileVersion string `bson:"profileVersion"` PresentationDefinition map[string]interface{} `bson:"presentationDefinition"` ReceivedClaimsID string `bson:"receivedClaimsID"` + CustomScope string `bson:"customScope,omitempty"` ExpireAt time.Time `bson:"expire_at"` } @@ -81,8 +82,10 @@ func (p *TxStore) migrate(ctx context.Context) error { } // Create creates transaction document in a database. -func (p *TxStore) Create(pd *presexch.PresentationDefinition, profileID, profileVersion string) (oidc4vp.TxID, - *oidc4vp.Transaction, error) { +func (p *TxStore) Create( + pd *presexch.PresentationDefinition, + profileID, profileVersion, customScope string, +) (oidc4vp.TxID, *oidc4vp.Transaction, error) { ctxWithTimeout, cancel := p.mongoClient.ContextWithTimeout() defer cancel() @@ -98,6 +101,7 @@ func (p *TxStore) Create(pd *presexch.PresentationDefinition, profileID, profile ProfileID: profileID, ProfileVersion: profileVersion, PresentationDefinition: pdContent, + CustomScope: customScope, } result, err := collection.InsertOne(ctxWithTimeout, txDoc) @@ -202,5 +206,6 @@ func txFromDocument(txDoc *txDocument) (*oidc4vp.Transaction, error) { ProfileVersion: txDoc.ProfileVersion, PresentationDefinition: pd, ReceivedClaimsID: txDoc.ReceivedClaimsID, + CustomScope: txDoc.CustomScope, }, nil } diff --git a/pkg/storage/mongodb/oidc4vptxstore/oidc4vp_tx_store_test.go b/pkg/storage/mongodb/oidc4vptxstore/oidc4vp_tx_store_test.go index 643fa7745..5ac7f9207 100644 --- a/pkg/storage/mongodb/oidc4vptxstore/oidc4vp_tx_store_test.go +++ b/pkg/storage/mongodb/oidc4vptxstore/oidc4vp_tx_store_test.go @@ -40,6 +40,7 @@ const ( profileID = "testProfileID" profileVersion = "v1.0" + customScope = "customScope" ) func TestTxStore_Success(t *testing.T) { @@ -60,13 +61,13 @@ func TestTxStore_Success(t *testing.T) { }() t.Run("Create tx", func(t *testing.T) { - id, _, err := store.Create(&presexch.PresentationDefinition{}, profileID, profileVersion) + id, _, err := store.Create(&presexch.PresentationDefinition{}, profileID, profileVersion, customScope) require.NoError(t, err) require.NotNil(t, id) }) t.Run("Create tx then Get by id", func(t *testing.T) { - id, _, err := store.Create(&presexch.PresentationDefinition{}, profileID, profileVersion) + id, _, err := store.Create(&presexch.PresentationDefinition{}, profileID, profileVersion, customScope) require.NoError(t, err) require.NotNil(t, id) @@ -74,10 +75,11 @@ func TestTxStore_Success(t *testing.T) { tx, err := store.Get(id) require.NoError(t, err) require.NotNil(t, tx) + require.Equal(t, customScope, tx.CustomScope) }) t.Run("Create tx then update with received claims ID", func(t *testing.T) { - id, _, err := store.Create(&presexch.PresentationDefinition{}, profileID, profileVersion) + id, _, err := store.Create(&presexch.PresentationDefinition{}, profileID, profileVersion, "") require.NoError(t, err) require.NotNil(t, id) @@ -92,6 +94,7 @@ func TestTxStore_Success(t *testing.T) { require.NoError(t, err) require.NotNil(t, tx) require.Nil(t, tx.ReceivedClaims) + require.Empty(t, tx.CustomScope) }) } @@ -163,7 +166,7 @@ func TestTxStore_Fails(t *testing.T) { storeExpired, err := NewTxStore(context.Background(), client, testutil.DocumentLoader(t), 1) require.NoError(t, err) - id, _, err := storeExpired.Create(&presexch.PresentationDefinition{}, profileID, profileVersion) + id, _, err := storeExpired.Create(&presexch.PresentationDefinition{}, profileID, profileVersion, customScope) require.NoError(t, err) require.NotNil(t, id) diff --git a/pkg/storage/redis/oidc4vptxstore/doc.go b/pkg/storage/redis/oidc4vptxstore/doc.go index 37d736f0f..d8ea353a3 100644 --- a/pkg/storage/redis/oidc4vptxstore/doc.go +++ b/pkg/storage/redis/oidc4vptxstore/doc.go @@ -19,6 +19,7 @@ type txDocument struct { ReceivedClaimsID string `json:"receivedClaimsId,omitempty"` PresentationDefinition *presexch.PresentationDefinition `json:"presentationDefinition"` ExpireAt time.Time `json:"expireAt"` + CustomScope string `json:"customScope,omitempty"` } func (d *txDocument) MarshalBinary() ([]byte, error) { diff --git a/pkg/storage/redis/oidc4vptxstore/oidc4vp_tx_store.go b/pkg/storage/redis/oidc4vptxstore/oidc4vp_tx_store.go index ec0af84c8..14e56daed 100644 --- a/pkg/storage/redis/oidc4vptxstore/oidc4vp_tx_store.go +++ b/pkg/storage/redis/oidc4vptxstore/oidc4vp_tx_store.go @@ -46,8 +46,10 @@ func NewTxStore( } // Create creates transaction document in a database. -func (p *TxStore) Create(pd *presexch.PresentationDefinition, profileID, profileVersion string) (oidc4vp.TxID, - *oidc4vp.Transaction, error) { +func (p *TxStore) Create( + pd *presexch.PresentationDefinition, + profileID, profileVersion, customScope string, +) (oidc4vp.TxID, *oidc4vp.Transaction, error) { ctxWithTimeout, cancel := p.redisClient.ContextWithTimeout() defer cancel() @@ -56,6 +58,7 @@ func (p *TxStore) Create(pd *presexch.PresentationDefinition, profileID, profile ProfileID: profileID, ProfileVersion: profileVersion, PresentationDefinition: pd, + CustomScope: customScope, } txID := uuid.NewString() @@ -135,6 +138,7 @@ func txFromDocument(id oidc4vp.TxID, txDoc *txDocument) *oidc4vp.Transaction { ProfileVersion: txDoc.ProfileVersion, PresentationDefinition: txDoc.PresentationDefinition, ReceivedClaimsID: txDoc.ReceivedClaimsID, + CustomScope: txDoc.CustomScope, } } diff --git a/pkg/storage/redis/oidc4vptxstore/oidc4vp_tx_store_test.go b/pkg/storage/redis/oidc4vptxstore/oidc4vp_tx_store_test.go index 533594174..2edc96841 100644 --- a/pkg/storage/redis/oidc4vptxstore/oidc4vp_tx_store_test.go +++ b/pkg/storage/redis/oidc4vptxstore/oidc4vp_tx_store_test.go @@ -34,6 +34,7 @@ const ( profileID = "testProfileID" profileVersion = "v1.0" + customScope = "customScope" ) func TestTxStore_Success(t *testing.T) { @@ -53,13 +54,13 @@ func TestTxStore_Success(t *testing.T) { }() t.Run("Create tx", func(t *testing.T) { - id, _, err := store.Create(&presexch.PresentationDefinition{}, profileID, profileVersion) + id, _, err := store.Create(&presexch.PresentationDefinition{}, profileID, profileVersion, customScope) require.NoError(t, err) require.NotNil(t, id) }) t.Run("Create tx then Get by id", func(t *testing.T) { - id, _, err := store.Create(&presexch.PresentationDefinition{}, profileID, profileVersion) + id, _, err := store.Create(&presexch.PresentationDefinition{}, profileID, profileVersion, customScope) require.NoError(t, err) require.NotNil(t, id) @@ -67,15 +68,17 @@ func TestTxStore_Success(t *testing.T) { tx, err := store.Get(id) require.NoError(t, err) require.NotNil(t, tx) + require.Equal(t, customScope, tx.CustomScope) }) t.Run("Create tx then update with received claims ID", func(t *testing.T) { - id, txCreate, err := store.Create(&presexch.PresentationDefinition{ID: "test"}, profileID, profileVersion) + id, txCreate, err := store.Create(&presexch.PresentationDefinition{ID: "test"}, profileID, profileVersion, "") require.NoError(t, err) require.NotNil(t, id) require.NotNil(t, txCreate) require.Empty(t, txCreate.ReceivedClaimsID) + require.Empty(t, txCreate.CustomScope) err = store.Update(oidc4vp.TransactionUpdate{ ID: id, @@ -90,6 +93,7 @@ func TestTxStore_Success(t *testing.T) { require.NotNil(t, txUpdate) require.Nil(t, txUpdate.ReceivedClaims) require.Equal(t, txCreate, txUpdate) + require.Empty(t, txCreate.CustomScope) }) } @@ -122,7 +126,7 @@ func TestTxStore_Fails(t *testing.T) { t.Run("test expiration", func(t *testing.T) { storeExpired := NewTxStore(client, testutil.DocumentLoader(t), 1) - id, _, err := storeExpired.Create(&presexch.PresentationDefinition{}, profileID, profileVersion) + id, _, err := storeExpired.Create(&presexch.PresentationDefinition{}, profileID, profileVersion, customScope) require.NoError(t, err) require.NotNil(t, id) diff --git a/test/bdd/features/oidc4vc_api.feature b/test/bdd/features/oidc4vc_api.feature index cb83efc0c..0cfe9a081 100644 --- a/test/bdd/features/oidc4vc_api.feature +++ b/test/bdd/features/oidc4vc_api.feature @@ -77,6 +77,24 @@ Feature: OIDC4VC REST API | bank_issuer_sdjwt_v5/v1.0 | CrudeProductCredential | crudeProductCredentialTemplateID | v_myprofile_jwt_whitelist/v1.0 | 3c8b1d9a-limit-disclosure-optional-fields | unit_of_measure_barrel,api_gravity,category,supplier_address | | bank_issuer/v1.0 | UniversityDegreeCredential | universityDegreeTemplateID | v_myprofile_jwt_whitelist/v1.0 | 32f54163-no-limit-disclosure-single-field | degree_type_id | + @oidc4vc_rest_auth_flow_additional_scope + Scenario Outline: OIDC credential issuance and verification Auth flow including additional scope + Given Profile "" issuer has been authorized with username "profile-user-issuer-1" and password "profile-user-issuer-1-pwd" + And User holds credential "" with templateID "" + And Profile "" verifier has been authorized with username "profile-user-verifier-1" and password "profile-user-verifier-1-pwd" + + When User interacts with Wallet to initiate credential issuance using authorization code flow with client registration method "" + Then credential is issued + Then User interacts with Verifier and initiate OIDC4VP interaction under "" profile with presentation definition ID "" and fields "" and custom scope "" + And Verifier with profile "" retrieves interactions claims with additional claims associated with custom scope "" + Then we wait 2 seconds + And Verifier with profile "" requests deleted interactions claims + + Examples: + | issuerProfile | credentialType | clientRegistrationMethod | credentialTemplate | verifierProfile | presentationDefinitionID | fields | customScope | +# SDJWT issuer, JWT verifier, no limit disclosure in PD query. + | bank_issuer/v1.0 | UniversityDegreeCredential | dynamic | universityDegreeTemplateID | v_myprofile_jwt/v1.0 | 32f54163-no-limit-disclosure-single-field | degree_type_id | timedetails | + # Error cases @oidc4vc_rest_pre_auth_flow_trustlist_fail @@ -205,9 +223,9 @@ Feature: OIDC4VC REST API When User initiates credential issuance flow and receives "expected status code 200 but got status code 403" error Examples: - | issuerProfile | credentialType | credentialTemplate | verifierProfile | + | issuerProfile | credentialType | credentialTemplate | verifierProfile | # SDJWT issuer, JWT verifier, no limit disclosure in PD query. - | bank_issuer/v1.0 | UniversityDegreeCredential | universityDegreeTemplateID | v_myprofile_jwt/v1.0 | + | bank_issuer/v1.0 | UniversityDegreeCredential | universityDegreeTemplateID | v_myprofile_jwt/v1.0 | Scenario Outline: OIDC credential verification without required role Given Profile "" issuer has been authorized with username "profile-user-issuer-1" and password "profile-user-issuer-1-pwd" @@ -219,7 +237,6 @@ Feature: OIDC4VC REST API Then credential is issued Then User interacts with Verifier and initiate OIDC4VP interaction under "" profile with presentation definition ID "" and fields "" and receives "expected status code 200 but got status code 403" error Examples: - | issuerProfile | credentialType | clientRegistrationMethod | credentialTemplate | verifierProfile | presentationDefinitionID | fields | + | issuerProfile | credentialType | clientRegistrationMethod | credentialTemplate | verifierProfile | presentationDefinitionID | fields | # SDJWT issuer, JWT verifier, no limit disclosure in PD query. - | bank_issuer/v1.0 | UniversityDegreeCredential | dynamic | universityDegreeTemplateID | v_myprofile_jwt/v1.0 | 32f54163-no-limit-disclosure-single-field | degree_type_id | - + | bank_issuer/v1.0 | UniversityDegreeCredential | dynamic | universityDegreeTemplateID | v_myprofile_jwt/v1.0 | 32f54163-no-limit-disclosure-single-field | degree_type_id | diff --git a/test/bdd/pkg/v1/oidc4vc/models.go b/test/bdd/pkg/v1/oidc4vc/models.go index 788c88bfa..fdc5ed59c 100644 --- a/test/bdd/pkg/v1/oidc4vc/models.go +++ b/test/bdd/pkg/v1/oidc4vc/models.go @@ -6,7 +6,10 @@ SPDX-License-Identifier: Apache-2.0 package oidc4vc -import vcsverifiable "github.com/trustbloc/vcs/pkg/doc/verifiable" +import ( + util "github.com/trustbloc/did-go/doc/util/time" + vcsverifiable "github.com/trustbloc/vcs/pkg/doc/verifiable" +) type initiateOIDC4CIRequest struct { ClaimData *map[string]interface{} `json:"claim_data,omitempty"` @@ -94,3 +97,13 @@ type credentialIssuanceHistoryData struct { Issuer string `json:"issuer"` TransactionId string `json:"transaction_id,omitempty"` } + +type retrievedCredentialsClaims struct { + Format vcsverifiable.Format `json:"format,omitempty"` + Type []string `json:"type,omitempty"` + SubjectData []map[string]interface{} `json:"subjectData,omitempty"` + Issuer map[string]interface{} `json:"issuer,omitempty"` + IssuanceDate *util.TimeWrapper `json:"issuanceDate,omitempty"` + ExpirationDate *util.TimeWrapper `json:"expirationDate,omitempty"` + CustomClaims map[string]map[string]interface{} `json:"customClaims,omitempty"` +} diff --git a/test/bdd/pkg/v1/oidc4vc/oidc4vp.go b/test/bdd/pkg/v1/oidc4vc/oidc4vp.go index babd6b4bc..56a901f05 100644 --- a/test/bdd/pkg/v1/oidc4vc/oidc4vp.go +++ b/test/bdd/pkg/v1/oidc4vc/oidc4vp.go @@ -62,7 +62,13 @@ func (s *Steps) authorizeVerifierProfileUser(profileVersionedID, username, passw return nil } +type initiateOIDCVPFlowOpt func(d *initiateOIDC4VPData) + func (s *Steps) runOIDC4VPFlow(profileVersionedID, pdID, fields string) error { + return s.runOIDC4VPFlowWithOpts(profileVersionedID, pdID, fields) +} + +func (s *Steps) runOIDC4VPFlowWithOpts(profileVersionedID, pdID, fields string, opts ...initiateOIDCVPFlowOpt) error { s.verifierProfile = s.bddContext.VerifierProfiles[profileVersionedID] s.presentationDefinitionID = pdID @@ -75,12 +81,18 @@ func (s *Steps) runOIDC4VPFlow(profileVersionedID, pdID, fields string) error { fieldsArr := strings.Split(fields, ",") - reqBody, err := json.Marshal(&initiateOIDC4VPData{ + d := &initiateOIDC4VPData{ PresentationDefinitionId: pdID, PresentationDefinitionFilters: &presentationDefinitionFilters{ Fields: &fieldsArr, }, - }) + } + + for _, f := range opts { + f(d) + } + + reqBody, err := json.Marshal(d) if err != nil { return err } @@ -107,8 +119,14 @@ func (s *Steps) runOIDC4VPFlow(profileVersionedID, pdID, fields string) error { return nil } +func (s *Steps) runOIDC4VPFlowWithCustomScope(profileVersionedID, pdID, fields, customScope string) error { + return s.runOIDC4VPFlowWithOpts(profileVersionedID, pdID, fields, func(d *initiateOIDC4VPData) { + d.Scope = customScope + }) +} + func (s *Steps) runOIDC4VPFlowWithError(profileVersionedID, pdID, fields, errorContains string) error { - err := s.runOIDC4VPFlow(profileVersionedID, pdID, fields) + err := s.runOIDC4VPFlowWithOpts(profileVersionedID, pdID, fields) if err == nil { return errors.New("error expected") } @@ -154,15 +172,48 @@ func (s *Steps) retrieveInteractionsClaim(profile string) error { return err } - return s.validateRetrievedInteractionsClaim(claims) + var credentialClaims map[string]retrievedCredentialsClaims + if err = json.Unmarshal(claims, &credentialClaims); err != nil { + return err + } + + return s.validateRetrievedInteractionsClaim(credentialClaims) } -func (s *Steps) validateRetrievedInteractionsClaim(claimsBytes []byte) error { - var claims map[string]interface{} - if err := json.Unmarshal(claimsBytes, &claims); err != nil { +func (s *Steps) retrieveInteractionsClaimWithCustomScope(profile, customScope string) error { + if err := s.waitForOIDCInteractionSucceededEvent(profile); err != nil { + return err + } + + token := s.bddContext.Args[getOrgAuthTokenKey(s.verifierProfile.ID+"/"+s.verifierProfile.Version)] + endpointURL := fmt.Sprintf(RetrieveInteractionsClaimURLFormat, s.vpClaimsTransactionID) + + claims, err := s.walletRunner.NewVPFlowExecutor(true).RetrieveInteractionsClaim(endpointURL, token) + if err != nil { + return err + } + + var credentialClaims map[string]retrievedCredentialsClaims + if err = json.Unmarshal(claims, &credentialClaims); err != nil { return err } + customClaimsMetadata, ok := credentialClaims["_scope"] + if !ok { + return errors.New("_scope claim expected") + } + + customScopeClaims, ok := customClaimsMetadata.CustomClaims[customScope] + if !ok || len(customScopeClaims) == 0 { + return fmt.Errorf("no additional claims supplied for custom scope %s", customScope) + } + + delete(credentialClaims, "_scope") + + return s.validateRetrievedInteractionsClaim(credentialClaims) +} + +func (s *Steps) validateRetrievedInteractionsClaim(credentialClaims map[string]retrievedCredentialsClaims) error { // Check amount. var pd *presexch.PresentationDefinition for _, verifierPD := range s.verifierProfile.PresentationDefinitions { @@ -172,10 +223,10 @@ func (s *Steps) validateRetrievedInteractionsClaim(claimsBytes []byte) error { } } - if len(claims) != len(pd.InputDescriptors) { + if len(credentialClaims) != len(pd.InputDescriptors) { return fmt.Errorf("unexpected retrieved credentials amount. Expected %d, got %d", len(pd.InputDescriptors), - len(claims), + len(credentialClaims), ) } @@ -198,7 +249,7 @@ func (s *Steps) validateRetrievedInteractionsClaim(claimsBytes []byte) error { issuedVCID[vcParsed.Contents().ID] = struct{}{} } - for retrievedVCID := range claims { + for retrievedVCID := range credentialClaims { _, exist := issuedVCID[retrievedVCID] if !exist { return fmt.Errorf("unexpected credential ID %s", retrievedVCID) @@ -256,6 +307,8 @@ func (s *Steps) waitForEvent(eventType string) (string, error) { } type initiateOIDC4VPData struct { + // Additional scope that defines custom claims requested from Holder to Verifier. + Scope string `json:"scope,omitempty"` PresentationDefinitionId string `json:"presentationDefinitionId,omitempty"` PresentationDefinitionFilters *presentationDefinitionFilters `json:"presentationDefinitionFilters,omitempty"` } diff --git a/test/bdd/pkg/v1/oidc4vc/steps.go b/test/bdd/pkg/v1/oidc4vc/steps.go index 9b0608eee..2810e8785 100644 --- a/test/bdd/pkg/v1/oidc4vc/steps.go +++ b/test/bdd/pkg/v1/oidc4vc/steps.go @@ -128,7 +128,9 @@ func (s *Steps) RegisterSteps(sc *godog.ScenarioContext) { // VP. sc.Step(`^User interacts with Verifier and initiate OIDC4VP interaction under "([^"]*)" profile with presentation definition ID "([^"]*)" and fields "([^"]*)"$`, s.runOIDC4VPFlow) + sc.Step(`^User interacts with Verifier and initiate OIDC4VP interaction under "([^"]*)" profile with presentation definition ID "([^"]*)" and fields "([^"]*)" and custom scope "([^"]*)"$`, s.runOIDC4VPFlowWithCustomScope) sc.Step(`^Verifier with profile "([^"]*)" retrieves interactions claims$`, s.retrieveInteractionsClaim) + sc.Step(`^Verifier with profile "([^"]*)" retrieves interactions claims with additional claims associated with custom scope "([^"]*)"$`, s.retrieveInteractionsClaimWithCustomScope) sc.Step(`^wallet configured to use hardcoded vp_token format "([^"]*)" for OIDC4VP interaction$`, s.setHardcodedVPTokenFormat) // Errors.