diff --git a/go.mod b/go.mod index c8c2ab1..619f8e5 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ toolchain go1.24.5 require ( github.com/MetaMask/go-did-it v1.0.0-pre1 + github.com/avast/retry-go/v4 v4.6.1 github.com/ipfs/go-cid v0.5.0 github.com/ipld/go-ipld-prime v0.21.0 github.com/multiformats/go-multibase v0.2.0 diff --git a/go.sum b/go.sum index 5f9198d..71d4ac2 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/MetaMask/go-did-it v1.0.0-pre1 h1:NTGAC7z52TwFegEF7c+csUr/6Al1nAo6ValAAxOsjto= github.com/MetaMask/go-did-it v1.0.0-pre1/go.mod h1:7m9syDnXFTg5GmUEcydpO4Rs3eYT4McFH7vCw5fp3A4= +github.com/avast/retry-go/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIcMk= +github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/pkg/container/containertest/Base64StdPadding b/pkg/container/containertest/Base64StdPadding index 58936c0..e912350 100644 --- a/pkg/container/containertest/Base64StdPadding +++ b/pkg/container/containertest/Base64StdPadding @@ -1 +1 @@ -BoWZjdG4tdjGKWQGeglhAa8vQAimsd6982rgPP0e1ccInV/4cjWzjR71iyXGc2qVOGyEEZMT+e/Zfaw1WzVS5Ybgtpjw5/845jAPNbJv1DqJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rdnF0eDhkWFZ3NzZkTnc2U0FZUXJ3cFEzQlI4S3NwZ2FUN0Rwd3F3OW1zbXhjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rcDZmMng2TW1xY1BDVWJhRVBxM0VqYjc4cjI3TFVKTEJWYUxjZ2hZaFR6TXJjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rcDZmMng2TW1xY1BDVWJhRVBxM0VqYjc4cjI3TFVKTEJWYUxjZ2hZaFR6TXJkbWV0YaNoNTIxZDIzZDBqMzMwNTJkZjgzOGg3NDVlZWFlZGpkYzg2NDM2M2QxaDgzZjY5OGYzajNhMDA3ZWY4Mjllbm9uY2VMwB+HSucZ5j+7FG/wWQGeglhAFgW0vCDmL/dzrIIT5oVRXsA1b/Fk5AiKhFlAVXRSXFoKuCixvn17vePm8anPkPewtZS7QiekJYwt9a0Aoe/CBaJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rcGNrS01FM3c3UWlvdWV3WkpLUFI0cDF5ak4yUDRjOXF3aDc1NVFqeTNyMURjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1ramlzQmZ3RE5SSHlYZmljd1k1M2MxaVpHdnBqaWVlclBuc1JxSEN2emhvOXpjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1ramlzQmZ3RE5SSHlYZmljd1k1M2MxaVpHdnBqaWVlclBuc1JxSEN2emhvOXpkbWV0YaNoMTllZGI5NWFqN2ZkNThkYjc5M2gyY2FhY2NiYmo0NWJkNmQ1ODY2aDg4ZmY0N2E3amU2YjgzNjZlNGRlbm9uY2VMCrOVvOHN5HLH2khEWQGeglhAf4mTe11QRPxtukpFKL6IaKRxrZPSytRgfOHYXGHLrImx0j0qbdPkQLaQeajQAG2vYnrjqG74v+lRhHIFTLsRCKJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rcWREc2FqdDhUQXBkeGtNbmRHQXoxa1FNY0w4bWtxYXNWR1ZoZm5MRnRIeFRjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1raDlicHdRWjNycFZ0dlZXOFhtRUJEc01ucUJBZG00Q2MzczFWeTE3VUhDR1NjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1raDlicHdRWjNycFZ0dlZXOFhtRUJEc01ucUJBZG00Q2MzczFWeTE3VUhDR1NkbWV0YaNoMDhjYTQ0MmNqMTBiNmU5Y2JjY2hjNTZmMjUwM2o1ZTJlNmU0ZDNiaGU3ODNhNTcxajY2OWVmZTI2Y2Nlbm9uY2VMm+hnFEzvWMyIwPWpWQGeglhAg2jLUqHQRlsIoBt7jaif9n1AFiuODDXx0lxtIhZLK52E9AjdPl+MRYaE4HrYXLUZdsACOovH6YrS8onV0E2ZA6JhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rb1dIQXN2d2ZRWnZDamdCYWhlSjRMWVlXYWJ2MTVjcmpSVGJEWWpGM2ZHbWVjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rcXFhcVdMZEVRWTVBUkV1c2U0UUduWUtDVTY5OHFleU5Lc0ZTZzRKdkVzWUxjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rcXFhcVdMZEVRWTVBUkV1c2U0UUduWUtDVTY5OHFleU5Lc0ZTZzRKdkVzWUxkbWV0YaNoMmE5NmNjNmVqNGNlM2ZjMTdjZGg3ZmMzZTdkM2piMjkwZTMyNDE3aGUwMDFmYzdmajJjOTAwNWY0NmRlbm9uY2VMC3IjW2PprOmuLut7WQGeglhAviUPHHfU+n8H1vyKyatGMS2YvJUvSvBxjC9hQSw7wIVbYAUJIgRr54V6vAifE2O4CvldXb16Qkqh1w9LnhX+A6JhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1ra0o3TTNzUXdOR0hFdEZOZGd4UFV5V1lXdGRWdXhLVXFiWndGdTU0TkVkOThjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rdkZVcGhKcGpaVFZHYzRqdjVjWnJWampuV2JQWUM1UHhvVWduenhuVVZzMUJjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rdkZVcGhKcGpaVFZHYzRqdjVjWnJWampuV2JQWUM1UHhvVWduenhuVVZzMUJkbWV0YaNoMGYzOTk5M2VqNWMwOGI2NzEwNWgzNmFmNTMzNmpiNGU0MWRlZTFmaGMyOWVjZWM1ajdlMGZjMzE4OWZlbm9uY2VMwsoostxej7C8KxTfWQGeglhAABlu2b+acxlXwvhl3Cvr1t6wTRgDZyhwui2K//dXhixO+IBslbAM9a4W7wssVcpaA1M/U91DwRH6JjG8lfwnA6JhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rbW9iNGlDczdXRHdRNU1KNDNOZERzaEplbXF4ZjF4RHBrWnJ0NFFYdUNTaURjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rcG1BY0dLdWtLQ281czlzQzhpUGVaajkyMWc3YzRWeVM3WEFNcjR2ZGZSN1BjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rcG1BY0dLdWtLQ281czlzQzhpUGVaajkyMWc3YzRWeVM3WEFNcjR2ZGZSN1BkbWV0YaNoMjk3MTk1OTRqM2ZkYjI4MDA3Ymg4M2I4ZTQ2OGpiMDlhMDY3OWQxaGEwNjQzYmI4amE1ZWQ4NjcwYmRlbm9uY2VMBevInlpWjVqplZC8WQGeglhAVuW7Dme4SHo/P7+mB67OfD6DK6NBnZy1cyBAsbk76HL42MjYzPp8fi8+JkoZDs4g24iXi9vixQhfpPG8GfrUAKJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rcmJUbWRXaDZ2TUg2eDZFNlI3UnNpUEpIdFloM3hlSzdtTnFpSzJoekVxZ0VjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1ralZtNUdqblhLU2I4d0s3VDF2dmU4eHRuWlJvaUVYUjJGSGdQYVF5M2lEeWhjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1ralZtNUdqblhLU2I4d0s3VDF2dmU4eHRuWlJvaUVYUjJGSGdQYVF5M2lEeWhkbWV0YaNoODU4MzY2ODhqNGZlNzI1Mzg2ZWg4NjNiMTIxOWo2MmY2Nzg1YWFmaGE5Y2M0MTEyajExMjgwZTM5NTBlbm9uY2VMUPFgyqtzEkRPXukGWQGeglhAIHB/6ndHOw4ruRy33I9yJej9A/ec3mGJsIEmNr33/rErTXlcNx+YH269sRCO9xOoRXFugTFDkAr92gjoUAeEAKJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rb3U4ZkRxc0xjcXVNaWRlRzZGdlpteFJOdG9EOVU0S2dIWjgycVBxOHJLR25jY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rc0RrenVVMncyeFZocVFGTjJVdHhyWkE1QWlIdWpHeGdEWHBueXdoNGV0TGRjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rc0RrenVVMncyeFZocVFGTjJVdHhyWkE1QWlIdWpHeGdEWHBueXdoNGV0TGRkbWV0YaNoNzIwMzUzNGRqYTMwMDY2Nzk4Y2g5ZDM1ZmUwOWoxZTg0Mzc3MTU0aGE4ZWY4YjM3ajIzMDM5YTljMTdlbm9uY2VMVLWsemG3WIh8/QshWQGeglhAR0zlf5Hc4m8hEwf664w9Cim5PHJftx0Y/rno06f9lTth3JuvY8znsXvl7Ym+ShIjTk9H9uJcjpVaNABfkr2YCKJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rcUU3SkptM3pkdXoxV2hFenpRcFZQa01SODdRaG5jRlpDNmpnMlZOV2JvVHNjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rbTN1ZWF5cXo5akJXa2NRcWI2RDJpbkc1cVk4OTNOVWtnTWdXWmtudU1vcVJjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rbTN1ZWF5cXo5akJXa2NRcWI2RDJpbkc1cVk4OTNOVWtnTWdXWmtudU1vcVJkbWV0YaNoNmUyNzYwOThqZDQwOGRhYmJjZmhiMmE5ZGVhZGo2MWFjZmNlOWMwaGVhNWRjY2QwamVjYWUyMDVkMmNlbm9uY2VMcrvruHcj5dHWBhjMWQGeglhARe/SIBfmxJzT9pT650Vr3xmTvOhB2vOO3SsxEk5emAbTD0msHSWiwROgpbcsVOczBAMpR3MDlv+KDWzU5vKyBaJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rZkhTbW12TEppM1R5QmF3emczbmlFWUxOQzZnREd1NW9xcnhueFo5U3FkYk5jY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rblhhaXl1cUdXdTFrWTh2Rm1tZ2lMRjR1YnB4bW8zZlg3RlNpdlhBcmFac1djcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rblhhaXl1cUdXdTFrWTh2Rm1tZ2lMRjR1YnB4bW8zZlg3RlNpdlhBcmFac1dkbWV0YaNoODg2NGQ5MmNqNTZhNjZhYjY5NWg4OGI3NjAwOGpmMjQwM2FkYTExaGZiY2Q1NDQ5amE4ZGVhNzhiMmZlbm9uY2VM2bLoKvbEKP43Vixd \ No newline at end of file +BoWZjdG4tdjGKWQGiglhABYakVJ3qMsyohPE5jEbaJ/o7OgvQT9dI/drXscOq+F6V8twJV6xRIXVYzWsoga7BYsf2XwTf+b+PEEQGRkOSAaJhaEg0Ae0B7QETcXN1Y2FuL2RsZ0AxLjAuMC1yYy4xqGNhdWR4OGRpZDprZXk6ejZNa3JYd0hzYmtXSjg0ZkVpOUt3QjJSQzRwUFN0WG1QbkJXd2RQR3V0Tk56eXhBY2NtZGgvZm9vL2JhcmNleHAaaJIvt2Npc3N4OGRpZDprZXk6ejZNa3ZWTlNMWlI1ZlFRMlpkeDhGZGRtcXoxTEx0dzFtMzVKc1VhVjdiSmszblpwY3BvbIGDY2FsbGMuW12DYT5mLnZhbHVlAmNzdWJ4OGRpZDprZXk6ejZNa3ZWTlNMWlI1ZlFRMlpkeDhGZGRtcXoxTEx0dzFtMzVKc1VhVjdiSmszblpwZG1ldGGjaDEzYjcyOWRiamJiYjZmZGNkOTJoMWY5M2U1YTBqZTA4MTk0MDdlOGg3M2Y4MWIzMWowNzQ1ZTQ0MzI3ZW5vbmNlTBGodc59dCiOB1O9lVkBooJYQPMo/EMhJhThi8Ab9zxcWlsBdJJf/WQnp7biGcrSOr/F5YrvumbUj31pd7s8mggD6BLSmi/Ch6wqQztRGD3Atw2iYWhINAHtAe0BE3FzdWNhbi9kbGdAMS4wLjAtcmMuMahjYXVkeDhkaWQ6a2V5Ono2TWtmcmVKb2o4cGtqamE1OHJUTTVEc3k0OGd3MWhaU0hmY1pXSHREcTI4WVp0QmNjbWRoL2Zvby9iYXJjZXhwGmiSL7djaXNzeDhkaWQ6a2V5Ono2TWtwZ1k1b3pUUWVMRnIxUENtV0UyU1lDWjZmNUZhckZERUF4Y1JmSEZQcFpycmNwb2yBg2NhbGxjLltdg2E+Zi52YWx1ZQJjc3VieDhkaWQ6a2V5Ono2TWtwZ1k1b3pUUWVMRnIxUENtV0UyU1lDWjZmNUZhckZERUF4Y1JmSEZQcFpycmRtZXRho2g5NTkyYTVkNWo1OGM5ZTYyN2E3aGNlYjhhMDVmamRlZjI1ZGNiN2JoZGZjOWE4OGVqNmVhZjQ1YTllNGVub25jZUzWFKuHIJ8sGrSYiD5ZAaKCWEDh97Nhb9gfjBT5wMlUe/VKpKXtO5C/P9/DOngvpVrc2fjxLmU+izfdFIMfsZc4fqFWXRGa7mM/vqFKsK7tAxoPomFoSDQB7QHtARNxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rd1JmN2VDczRBWkc1RGhuNXJmcEZvNzhRTEhpbnNCc01TeFZ1eHpOeUM5aW5jY21kaC9mb28vYmFyY2V4cBpoki+3Y2lzc3g4ZGlkOmtleTp6Nk1raVo1c1lrMjF3VFVkZjc0NGNZRDFMSlgyS1Z1UlNxc29mN05TcEJISjVSVm1jcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1raVo1c1lrMjF3VFVkZjc0NGNZRDFMSlgyS1Z1UlNxc29mN05TcEJISjVSVm1kbWV0YaNoMDU2ZWE4NzlqMzY0MmIyZDE3MWgzNDI4ZmExY2pjNTE1ODlhNTFiaDc4ODQ2MDI2ajhiNGVlMzhmZTRlbm9uY2VMlJH6zxWeWLgrPFL8WQGiglhAodgbslQnxQ6LbUhJ2ZVE6YXZmJzsXpptz45Px8HX67seRXLboSiTpJGX7CWn2l/C3Bkvpc9wWr4os4UmAP8WCaJhaEg0Ae0B7QETcXN1Y2FuL2RsZ0AxLjAuMC1yYy4xqGNhdWR4OGRpZDprZXk6ejZNa3Jpbng5WHNwVEdaMjlhSHBQdWkxUXhwUTFNRko3b2lnSERVbWdINWN4MUtZY2NtZGgvZm9vL2JhcmNleHAaaJIvt2Npc3N4OGRpZDprZXk6ejZNa2pRa3hFbUNVSHdVTEV0R1VZOTFLMUdlY2NScHJVdkI0QndXVUQ4Z0hGcVpUY3BvbIGDY2FsbGMuW12DYT5mLnZhbHVlAmNzdWJ4OGRpZDprZXk6ejZNa2pRa3hFbUNVSHdVTEV0R1VZOTFLMUdlY2NScHJVdkI0QndXVUQ4Z0hGcVpUZG1ldGGjaDcxOTBjZmE5ajM4NmU0OGFiZmJoN2U1OTdjZTlqZDFiYmI2ZjYxNGg5ZTljYTc4OWpjZWM5Zjk5NTgxZW5vbmNlTL6XetkzghuY2J6Y51kBooJYQHoWRs/WJJro50YyRnRzlhpvn1JL6uD2z6bHo5F4Ke9ui0WIr6kSxQKcwbT1/6EkrFAL4n/FlxXG73ba31zgwwSiYWhINAHtAe0BE3FzdWNhbi9kbGdAMS4wLjAtcmMuMahjYXVkeDhkaWQ6a2V5Ono2TWt3ZW9hbVp1VFBrVVFRUlhhaXIyb0hKem1QYzVFYnh6UkExeXZ2RjU2SFJ3YmNjbWRoL2Zvby9iYXJjZXhwGmiSL7djaXNzeDhkaWQ6a2V5Ono2TWttYWRLaFJqR2FOV2MzQzczbzUxb0E0QzY5U01jaHBBUFJYU1A2V2NYelFwWGNwb2yBg2NhbGxjLltdg2E+Zi52YWx1ZQJjc3VieDhkaWQ6a2V5Ono2TWttYWRLaFJqR2FOV2MzQzczbzUxb0E0QzY5U01jaHBBUFJYU1A2V2NYelFwWGRtZXRho2gxYzgxYmJjMGo4OWEyNjJhYjNlaDU4ZTVjZjc5ajk0NmZkZDcwOTNoNjUyYTFiOWRqNDQwZDg0YzNiY2Vub25jZUxV6CeWkqLbqyInfX1ZAaKCWECPCG7QlXAncItxluVoom9ExdZB36bJgVcC8atGlNkZztgwgi2qRn/PAPJ+izizRxPvXo2HeVl8oFMx+eVU9I0IomFoSDQB7QHtARNxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1ramFDdzNqNTRFcGN0SGFVdjNqeGVTQkRhd2RNTW5EYXNLcERLMVozQmtNYmRjY21kaC9mb28vYmFyY2V4cBpoki+3Y2lzc3g4ZGlkOmtleTp6Nk1rbUVBZVZYdVNncWhybzNZMUNETEtjN0pDbUhMNmM1eHFlM0ZIVkdhZU52UjljcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rbUVBZVZYdVNncWhybzNZMUNETEtjN0pDbUhMNmM1eHFlM0ZIVkdhZU52UjlkbWV0YaNoN2U3NjA3NWNqZWYyNDEzMGMyM2g4ZGI3YTA0YWpmMDUwMDRiOGZiaGQwMTJkNTNiamMwYTk0ZTU2OGZlbm9uY2VM0A2enQKwnAQODN09WQGiglhAH7cY+u6guPL+h4Gl1MD99HMYxzauamvTl9BDhm+gf1qI5wyyJZz9s1jDn9DIV0kc+X4zOukFEc2mxgi3B3DmDqJhaEg0Ae0B7QETcXN1Y2FuL2RsZ0AxLjAuMC1yYy4xqGNhdWR4OGRpZDprZXk6ejZNa3FhNmNoU2ZMM2NHYWNFamVxalNnY3pLZmlYNmRWM0xRbWMzQ1JWVnhDVmg4Y2NtZGgvZm9vL2JhcmNleHAaaJIvt2Npc3N4OGRpZDprZXk6ejZNa3VKQWQ4b3BQUkxWNzJXZXp0UFFIcnhkZENwcTM3UDFQeDFRNXlhVEdhdjl2Y3BvbIGDY2FsbGMuW12DYT5mLnZhbHVlAmNzdWJ4OGRpZDprZXk6ejZNa3VKQWQ4b3BQUkxWNzJXZXp0UFFIcnhkZENwcTM3UDFQeDFRNXlhVEdhdjl2ZG1ldGGjaDA2ODc4MmY3amRhMGJjNGUzN2RoNGEyY2U0YzFqNTJkNGQzMmVmN2hhZGU5MGE1OGphOTk1ODdhZTliZW5vbmNlTB8pysCUZIh8y43VnFkBooJYQLaRhdQ03p5fKqnz8nRjb9h+uFvrVK4Ke7JlGuhnEL8LUQzjbDckjicDfkZX//duNEV3qKyTHiSGvU1FQo9nyAiiYWhINAHtAe0BE3FzdWNhbi9kbGdAMS4wLjAtcmMuMahjYXVkeDhkaWQ6a2V5Ono2TWtxcWlienI5amYxTjkxQVY1eUVLYzRjSzRaMWlVMkpXRzdndWJLdWhoZTR6a2NjbWRoL2Zvby9iYXJjZXhwGmiSL7djaXNzeDhkaWQ6a2V5Ono2TWtqeFIyVGFBUWNqRERXTjl5dloyY3BFYUVrbncydmkxNENLTmhBSzQ0TGZ1Y2Nwb2yBg2NhbGxjLltdg2E+Zi52YWx1ZQJjc3VieDhkaWQ6a2V5Ono2TWtqeFIyVGFBUWNqRERXTjl5dloyY3BFYUVrbncydmkxNENLTmhBSzQ0TGZ1Y2RtZXRho2gyMDkyZjYwYmpiMzcwZjMwNzI3aDQ3OGI0Y2M1ajk4YjdkYjAxMzJoNmQ0YmFhZjVqYmVkNGUxYjJmM2Vub25jZUx+tmRsecAtdwXDqY1ZAaKCWEB3o5gA741QVQnxqMkgt7DwS/4WgoReEKdUCz4RpaM5LDeJNg4vi8rOUTaU4VhpowgDxn+lBGraJUbp2Y5L/q8PomFoSDQB7QHtARNxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rdHhRUjg5b0F4VG1kdjNTOFFhbVk2aXdGYlp6SDZ1QnlFQWZLOGNpY0RkU3JjY21kaC9mb28vYmFyY2V4cBpoki+3Y2lzc3g4ZGlkOmtleTp6Nk1raTgyc2dFa1ZrYnNjZEJ5ZDY4WVoyQ2ViSmh4QXlzSnpDZHluOTFhRHROdjhjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1raTgyc2dFa1ZrYnNjZEJ5ZDY4WVoyQ2ViSmh4QXlzSnpDZHluOTFhRHROdjhkbWV0YaNoMmI1MTllNzNqM2E3MTc5NmQ3MWg5YWE3NjQwMGoxM2FiZGYwNWQ2aDlmZmU1ZTliajY5YmU3NjI5ZDFlbm9uY2VMyL0L8CRxcMsMq5hbWQGiglhA3YnA0f5qejI6yGKfabYt1rojIQkTKSdLfMLSwhl9LR+aHXivxgKWzy/W3wbHOjG5JdEb7sl/yH0G4SFcpTSOCaJhaEg0Ae0B7QETcXN1Y2FuL2RsZ0AxLjAuMC1yYy4xqGNhdWR4OGRpZDprZXk6ejZNa3VqVTVCTFhQZkt6OVlVUmNZY1IzNVN1eFV1UmNEajZOZmtpV1c0dEhReUJQY2NtZGgvZm9vL2JhcmNleHAaaJIvt2Npc3N4OGRpZDprZXk6ejZNa3MydVJxUEV5akdBM3Z4eEVEZmNHSzdpZ2tWN1JwSGltNTVqamlKazYzcTJzY3BvbIGDY2FsbGMuW12DYT5mLnZhbHVlAmNzdWJ4OGRpZDprZXk6ejZNa3MydVJxUEV5akdBM3Z4eEVEZmNHSzdpZ2tWN1JwSGltNTVqamlKazYzcTJzZG1ldGGjaGNlOTk3ZWVjamFkZDA2NzBhMTdoZDczMjA4YTFqZDhmNTllZjc3N2hkYTg3MDQwMWpiNzVhNTRlNTYzZW5vbmNlTBZguMsZADREIeYq5g== \ No newline at end of file diff --git a/pkg/container/containertest/Base64StdPaddingGzipped b/pkg/container/containertest/Base64StdPaddingGzipped index d619ebd..1fdd6b3 100644 --- a/pkg/container/containertest/Base64StdPaddingGzipped +++ b/pkg/container/containertest/Base64StdPaddingGzipped @@ -1 +1 @@ -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 \ No newline at end of file +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 \ No newline at end of file diff --git a/pkg/container/containertest/Base64URL b/pkg/container/containertest/Base64URL index 9fe7b78..2113e30 100644 --- a/pkg/container/containertest/Base64URL +++ b/pkg/container/containertest/Base64URL @@ -1 +1 @@ -CoWZjdG4tdjGKWQGeglhACpXpdxwBGhOBgP0i_jkbddYEXF1SIqsFv3_hqgTujlJB1hiKUArH2AIsVSKtpGH-g1tI691qlTZrWoWbReObC6JhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1raEFHdzlXbk1yMlpoRHlBalpVVFJ0UEZURmh6WXN4SjF2R211NXVEdDJFcFRjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rc1FtZVcxb3RUeWNQeWRnQ1hxakxRS2JaRUZSMlF5cXJwYWZUZjVhNWFiYVdjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rc1FtZVcxb3RUeWNQeWRnQ1hxakxRS2JaRUZSMlF5cXJwYWZUZjVhNWFiYVdkbWV0YaNoYzdjOWYzM2VqYTYxMzQ2ZTVhMGhmNzNhNDJjOWplZDU4YWQ0ZTg1aGZlZjI5NTlmajBjY2NmN2I5NjFlbm9uY2VM4c8LJwfjIdKZbUd-WQGeglhAGRizpuAMSDUiVK3a7JS_Wr-Z1lqn9eASP5He4iZi2lsXcqgKOozEHOBddL302HETEZyWvZJpjsr7pRDl8nvcA6JhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1raHo4bWtKOTZOQjN5Y1VnNHcyeWVQbzd3cnJwYlVYZkZVNEhyQ2tEaEg0aEJjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1raEZ3SmhiNENaQVBScHlXWmMxNXA5NU04NDE3RW1RbUdIU3liMlRwOW95QVpjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1raEZ3SmhiNENaQVBScHlXWmMxNXA5NU04NDE3RW1RbUdIU3liMlRwOW95QVpkbWV0YaNoMTYwMTM4YzZqMmJkYWYzNzcxYWg4M2Y4MTdkM2pjM2IwYjdmNjQ5aDkwZGMxODA5amMxMDZiOTJmYWZlbm9uY2VM0GJFExCefuuIz3DAWQGeglhAKgFiPWomi3iuL4RcE_XMscG8JjAUBCTsCu9_aQY6U6sA_E2GnlI37ASYcnqgM0Kk4BLjta2nHgKh6T26CtY-AaJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rdkc0aXJlN3J6YmtTYXR1MVl4c0J1R3FLczlrTmdHWnRUWFFCNkhXRm1iUlRjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rdnlwMWdWNlM3TFhMbUNOUEhRblR4VnBjeFJTNlhuZkdIaExycHVldk4xZ1VjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rdnlwMWdWNlM3TFhMbUNOUEhRblR4VnBjeFJTNlhuZkdIaExycHVldk4xZ1VkbWV0YaNoYWE2NmY4NDdqOGQyZTIzNTM3ZWhjMzExYjNkNWozOTZlYTJjZTE5aGQ2NmM3YmVkajg3MGQ1YjNhNmRlbm9uY2VMw0wBN_QOVu5WH3ZUWQGeglhA4uBojvXBeoEJ4qnDmtHrtZArqBOj1jRMQrXOxy75JD2xpBhhxW_c90ofhjexFy5m-TM1a9pciFfVdgUwc8a7AqJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1razVKRXNDUW1tWGZoMU5XazJVSjJBZ01zYmRxNDhmU1JoaXdiTmJmN0ZVbVFjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1ra0VXanFxRkVwZUJlakJBREpneTNRdmZBU01KSkZjc0FIZmRqNkpkSlJKSnZjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1ra0VXanFxRkVwZUJlakJBREpneTNRdmZBU01KSkZjc0FIZmRqNkpkSlJKSnZkbWV0YaNoZGVjOTBjOWFqYjZiNGU2NTRlY2hlMGYyN2VmNGo2YTJhZWZjM2U5aGY1NjNjNjEwajNhM2EyNzA4MDJlbm9uY2VMdQysOoQ6Wn3YZJ9KWQGeglhA58PG4OcNtwHt9nXjgcTBebRvhQhVTG6tYyJvTNeFaJj9V-JD3jiT4ZgLJKFenedmwNA0Et31Jfu6MNqkiYUcCaJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1ramQ5THQ0WXRKNFU4TjRRc29hUVRveEFqekxlMkZnbVZXUkhycTltQUFmTHVjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rbkJITFl3N0tpbzVwb2NhclVoTW9BWmEzb25nZXpjcmJpcHRLRkxGU1REM1RjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rbkJITFl3N0tpbzVwb2NhclVoTW9BWmEzb25nZXpjcmJpcHRLRkxGU1REM1RkbWV0YaNoNDNmNmZjNjlqOWUxMGNhM2I5NGg2YjNjMjYxY2pjMzBkZDlmYTI2aGNkMDAwYzU1ajA2YjZjMDNiM2Zlbm9uY2VMbwisskfMLx4-gstgWQGeglhAIgN-nA7mA4g2FKr6_CR3kCPzcegoqgIDWk1qCotQWLo0ntG2WoSfnJvPWj9o1HLtxTAKAWzexFwiBTamow2EA6JhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rdm5lcWd0ZG52cHVmdHQ0emJHSEZxMnRMSjRkc1dWN2N2NHhrbjlGajlXUjZjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rcmhpc3JqVkFWOVROQ3dkejY4NURpdHY2eWRaSjIzWkh0cUFjM0E4SHJvUUhjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rcmhpc3JqVkFWOVROQ3dkejY4NURpdHY2eWRaSjIzWkh0cUFjM0E4SHJvUUhkbWV0YaNoMGM3OGVmMmFqMjI1ZDgxYTg3ZGgxMzIyZWU5MmozZTJjZjNmOWZjaDQ1YjM1MWYzamExYTMzOTU1YzZlbm9uY2VMGkPaJ3crRATPQ7jrWQGeglhAFK5Ex__PUA3vkGgS6JKYwyGf03AvjNKLW7S8aDnC4lRCh0VjOdMQ1xHHsV6fsqBDcPC6dwqpFTHOO9YbMPBNCaJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rZkZSRndzVVNNc1hzWVRvWGtWZ01mQnBwbTdxRkpqc2g4Z2FSZjY2Q1FvMmhjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rdE5UdUhUakI2bWhhMkNKZFFRQ2FyUFE5cGN5a3RQcE5DVmg0TVhMVlNQNlFjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rdE5UdUhUakI2bWhhMkNKZFFRQ2FyUFE5cGN5a3RQcE5DVmg0TVhMVlNQNlFkbWV0YaNoMWU2Zjc4YzVqOWRhOThkYWVjNmg4YjRjYTg1OGo4MjNmY2Y3MjliaGYyN2I1YzA1ajU4NjkyMDEyMWVlbm9uY2VMrIMpUz2MjCvzE0tMWQGeglhAy9s8amUFAKS7soQbA4oFzhU4cVRJ73LhUFicmTM3q5dd3vVfy-xeq7yhc-8orqEvsdiXPRVbfZdFtjmEbjRSCqJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1raVoxMUxWUm9keU0yRmI2VjlUbmF5M3g4bVhVa3p0R0c3TlVxZHFrOHRYYkFjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rcFVKc3J5b2V1Mnp6WVNBdng3Z1VRcG0yc2JyRmVFSnFNVERIYVVnSjhrd3djcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rcFVKc3J5b2V1Mnp6WVNBdng3Z1VRcG0yc2JyRmVFSnFNVERIYVVnSjhrd3dkbWV0YaNoMjdiYjAzZGVqOTY2NzgxZTQwOGg0NDFjMGIzMGo0NDhmNzEwZmU0aDc0MmM0NDQ5ajdiZjVjMjhiMjJlbm9uY2VM7ctTlMIv0HNvXrsUWQGeglhAUgkAhE9RqmEC6Yx5EUrhcq61qNc3tJ1x1bfAuPO01gxVkD9v4PebljD1PVtYE8uFgQtMFA0u0oI4mHTBHcx8CKJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rdmZNNVdxZ1JlWThWN3FLRHdWaWs3NHRLVmhjNktOeEoxcHQ2MTNGMktySnpjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rdTV6WmhUajdENTF0azNGRTRNYUtCVm9paU1rRnpGNHdNOFl4SFhybmpUdlZjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rdTV6WmhUajdENTF0azNGRTRNYUtCVm9paU1rRnpGNHdNOFl4SFhybmpUdlZkbWV0YaNoNWViOWY3YmJqOGFjYTI4MDhjOGhhZTFmMTA4MWo0ODllM2U1OTk4aGFmOTUwZTU0ajM1ZTMzZDNkMTVlbm9uY2VMn16caMQ_aieJes1jWQGeglhA3zJcxENVVf7UDXPewhqsoEHO6Y2fAjOkpjAjdpz244-w2GUand8PTxp1E7ET8FqFx9gD74WjPbzamIoaWsCzAqJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1ra252UjFjb2hRNzdNaU1mZ3JWelpvdVlGaHk1RTh2YUNXckFvdk1zNXJvZkxjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rbmFhZmdwaUU0OHo3WjQ5N1F6NkVXQzI2ZlluRlI4NXJjYm5kV3Q4UFZoVk1jcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rbmFhZmdwaUU0OHo3WjQ5N1F6NkVXQzI2ZlluRlI4NXJjYm5kV3Q4UFZoVk1kbWV0YaNoMmViYjFjMmZqYmE5ZGE1OGFkM2g2Yzk3OWU1YWoxNzM3YjBmN2Q4aDkxNGM3NDE5ajZjMGE4ODhkNzBlbm9uY2VMgiJAdle-huUpDQ2L \ No newline at end of file +CoWZjdG4tdjGKWQGiglhAXK6LJ-9JgdpCjLi54ixA5OFBFp7vBU02xIDUImpOqQivENmMUTPlVBMItJicruJkQW9TFYGIgdU834Sc-cX0DqJhaEg0Ae0B7QETcXN1Y2FuL2RsZ0AxLjAuMC1yYy4xqGNhdWR4OGRpZDprZXk6ejZNa216WnZIQVhqWEN0QlJ2TFZhbWpBcmdOTUtXclpiQlozeXhlRmVSeTVic3p5Y2NtZGgvZm9vL2JhcmNleHAaaJIvt2Npc3N4OGRpZDprZXk6ejZNa3Q2NndmdXF5Zzd3RDdiS1VCQnhtbmlLOEFjY2dGYkpIZFo4U3hReHVGcG5uY3BvbIGDY2FsbGMuW12DYT5mLnZhbHVlAmNzdWJ4OGRpZDprZXk6ejZNa3Q2NndmdXF5Zzd3RDdiS1VCQnhtbmlLOEFjY2dGYkpIZFo4U3hReHVGcG5uZG1ldGGjaDBhY2E0NmVmajU4NDMyMWIxZWJoMWEzMjY0ZTFqMTIxMDg0OWM0MGgzYzAzZGJlZmphODI4ZTRmNjYwZW5vbmNlTKalaRQbcv4bIk1TTFkBooJYQEiwfwLY7-QP5CMQZZxZ43YFfownZz6nZ-jIiWSV40KCxlgrbFwI4SMqbZqGI-Liw6kzbpZDbYxM2gfz9euI7QqiYWhINAHtAe0BE3FzdWNhbi9kbGdAMS4wLjAtcmMuMahjYXVkeDhkaWQ6a2V5Ono2TWttZ0d1azhFQjF3NUU2Nm9WQWlYd0MxZE1UTTRNcnFRTGdjMlhkTFRpMzdFZmNjbWRoL2Zvby9iYXJjZXhwGmiSL7djaXNzeDhkaWQ6a2V5Ono2TWttSFJMaTh6Y0NXTTZTanVSOVE3eWlGc3d1QnRqZHlYTmd0TUZ0dFhodjkyZmNwb2yBg2NhbGxjLltdg2E-Zi52YWx1ZQJjc3VieDhkaWQ6a2V5Ono2TWttSFJMaTh6Y0NXTTZTanVSOVE3eWlGc3d1QnRqZHlYTmd0TUZ0dFhodjkyZmRtZXRho2gxNjk2MTc3OWpiMTVjMjNkODM3aDkxMTRkODhiajkxZWYwZjBhMGFoYzNhZGJhZmNqZWI1MGIxODk2MGVub25jZUw1LGc2wZfkb6IbLW9ZAaKCWEAWLLHLuchRVuT6He5PokIoGcpkDmCD8WSdTYFxIT-owTrprc-XR2bVF_KmVr_dnA7KDIUhyEv4c6WoO_euiAsMomFoSDQB7QHtARNxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1ra0ZyZkE5b2tQclFueHhrN2JMTUFWZUw0RkNwUHZwZDhvWWhVMW5OdDR2d2ZjY21kaC9mb28vYmFyY2V4cBpoki-3Y2lzc3g4ZGlkOmtleTp6Nk1rblk3eG1zaDQ3R3UxNkVKNDV5amF3YXdGQVZmYWVSN2hDVXFGYXpOUkJBU1ljcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rblk3eG1zaDQ3R3UxNkVKNDV5amF3YXdGQVZmYWVSN2hDVXFGYXpOUkJBU1lkbWV0YaNoOWRiNTk5M2NqYWYwMTg0NjkyOWhiNDY0YjI3OGo1NGY1YjI3ZTIxaGYzNDNmYmEyajA5YWVhZGJjYmRlbm9uY2VMFp4p-yZ4Ogcmi0vIWQGiglhA1zTngEaE-B2DWSRtYnTIVBw8B_hvjK-Xo1HJZJBmTor-ebgmRED4VSEO6ALSnJV94w0UVE5xtYIaFXMTWD0ABaJhaEg0Ae0B7QETcXN1Y2FuL2RsZ0AxLjAuMC1yYy4xqGNhdWR4OGRpZDprZXk6ejZNa3NTTnQ5WVZUYnBheEZFNFJXUWsyQUpUYjFZNDZKTmNaY2ZvRHF3b0dUQnMyY2NtZGgvZm9vL2JhcmNleHAaaJIvt2Npc3N4OGRpZDprZXk6ejZNa3dNd3VjQjQ1MXZ0MWVxMTJzRERWeUx5WnR2RHM3TmoxMVlNQ0pLUVF3UEx3Y3BvbIGDY2FsbGMuW12DYT5mLnZhbHVlAmNzdWJ4OGRpZDprZXk6ejZNa3dNd3VjQjQ1MXZ0MWVxMTJzRERWeUx5WnR2RHM3TmoxMVlNQ0pLUVF3UEx3ZG1ldGGjaDg4MmJmZTdlajg2NjVjZjRhZDNoYWE4Zjg0ZWJqNjEyZDA1YTBkY2hkZmMxM2ExOWpmY2NkNGFmNjM5ZW5vbmNlTAJEDRnTi6GKtY7R6lkBooJYQL4aUlU_8vy0StepejrIgZyVdJaBitXDohffD2K31q8h47gTEIFGTpiRXLPVFNClpcfq8vqz7tY4s4aFkfaf3gKiYWhINAHtAe0BE3FzdWNhbi9kbGdAMS4wLjAtcmMuMahjYXVkeDhkaWQ6a2V5Ono2TWtuSmNrRVE0NzJzaW1lZDI5M3VFVDRmTVNLa1hIekpNR0FGNndodzJiUlVxcWNjbWRoL2Zvby9iYXJjZXhwGmiSL7djaXNzeDhkaWQ6a2V5Ono2TWtyTkFmNWlKc3BLbzNYQW5NZUFyN3ZtR1VvaXU0Sk1YcVI4Qk5RdnAza0dpRGNwb2yBg2NhbGxjLltdg2E-Zi52YWx1ZQJjc3VieDhkaWQ6a2V5Ono2TWtyTkFmNWlKc3BLbzNYQW5NZUFyN3ZtR1VvaXU0Sk1YcVI4Qk5RdnAza0dpRGRtZXRho2gzMTkxYzk5MWpjMmFjMGZiNGQ3aDNjMTVjODE2ajFiNWJkNzUwZmRoM2ZhN2Y3ZmJqY2NkN2EwM2E0YmVub25jZUxWk3ydedhXvgUkFh9ZAaKCWECJZ55_QKiJk3hKybyAvAEkiE51lyY2mNdGOfG7RTtkiQZt0l9O_-D-41Fq7qe-zeLcZOOb96n615aLCmwFqHIDomFoSDQB7QHtARNxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rbXpZWUxIM2IzY1FUV3RSQ2JTdGRLSEhDNHBTRlM0bW15R3h0R3ZiVm5Qa3hjY21kaC9mb28vYmFyY2V4cBpoki-3Y2lzc3g4ZGlkOmtleTp6Nk1rdnJHREprakJ1RTF4UnVqYngzTTRwQ21oRm5RYUVxaVBzZndaNzZiOTVrNXVjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rdnJHREprakJ1RTF4UnVqYngzTTRwQ21oRm5RYUVxaVBzZndaNzZiOTVrNXVkbWV0YaNoOTM2YmFiOWZqMDcxNDUwMzRiZGhhMzg1ZGI3OGpjYWQ0NWFmYjVhaGI4ZWRlNzU5ajlmNzU3NGYzMzNlbm9uY2VMKkjI37EdjtTTE2K0WQGiglhA5BhIT_LE47HO7eR1I5F0o4SsBb8DulP9ADLQLAbINm3-w8u_tsuO1F8JIwBNgra3k2djQgsHN7wFf4eikBd0DqJhaEg0Ae0B7QETcXN1Y2FuL2RsZ0AxLjAuMC1yYy4xqGNhdWR4OGRpZDprZXk6ejZNa3ZGdkFRM2ZuZTVLeWE3MVM5R0RyYnRxM0pFVTQ0eEt3OENXZ29wUTVmRzl5Y2NtZGgvZm9vL2JhcmNleHAaaJIvt2Npc3N4OGRpZDprZXk6ejZNa25ZcE5mZllhUHg1OVhhb1JVSnFyb2N3WThuTFRUdllpdnRoWTZiWXdKVmtHY3BvbIGDY2FsbGMuW12DYT5mLnZhbHVlAmNzdWJ4OGRpZDprZXk6ejZNa25ZcE5mZllhUHg1OVhhb1JVSnFyb2N3WThuTFRUdllpdnRoWTZiWXdKVmtHZG1ldGGjaDZjODdjM2RkamQzNmUzMjFlMGVoYjQzZjdlZGJqOTkyZmY2YmI1YmhmMzI1ZmJlYWpiYThmZGMxZjEzZW5vbmNlTIkknZW6tK1-EdreVFkBooJYQKyH4lVF6wSWRtLC5rNeMG93E9szx0tQ752O0A33oY7AWiyCmooVQ2QcwvNO4eYAhZ6O07UnT9cjRMJEJhh8VQiiYWhINAHtAe0BE3FzdWNhbi9kbGdAMS4wLjAtcmMuMahjYXVkeDhkaWQ6a2V5Ono2TWtxS3oxeVo3WHk2UndUZDlQZ2VuN29DWGh2SHRTM29wTlNCUTV4aHRUd0h6ZmNjbWRoL2Zvby9iYXJjZXhwGmiSL7djaXNzeDhkaWQ6a2V5Ono2TWtzTFlUMk53V0hTZ3pYTFZ2d291cGM0OTZ3Ym1mc0dac0NtWWdtdThlZlJoa2Nwb2yBg2NhbGxjLltdg2E-Zi52YWx1ZQJjc3VieDhkaWQ6a2V5Ono2TWtzTFlUMk53V0hTZ3pYTFZ2d291cGM0OTZ3Ym1mc0dac0NtWWdtdThlZlJoa2RtZXRho2gxMmZjNjdkNmpiYmQ3ZDZiMzlmaDdmY2IyZGQxajIzOTUyMmRkY2NoYjU3NzA3YTVqY2M5ZjE2MDU0ZWVub25jZUwyFq7E45CwN9A_5e9ZAaKCWEBkB2rGLezpJf2uyxS71bzCTucR4-WM-Jl13g_jLXAoiBBn7J6y6Ueak050Nm_2St91WQpXkCq8IQIwUqvCtCoComFoSDQB7QHtARNxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rcHhBWkN6Z0xWa0Y2Zkx2RzdDSmdIRlUzSG50aHZnWE41d2Y5M3FiZnNTV1ZjY21kaC9mb28vYmFyY2V4cBpoki-3Y2lzc3g4ZGlkOmtleTp6Nk1rbXk2aUxjWEpOdlYycEpyWU03WE52VTZRbVFFVFVZbVZ1dUhvd1VkWTlkSFJjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rbXk2aUxjWEpOdlYycEpyWU03WE52VTZRbVFFVFVZbVZ1dUhvd1VkWTlkSFJkbWV0YaNoMmY2ZjcyZWRqYmE3YzMzMmZmZWhkNDUyYjFkMWo1ZGM5MDgxMDhkaGQ2NDIyNzQxajg1ZGQ1ODExMmJlbm9uY2VMk3PNrYZB3Hz0inGWWQGiglhAF2lX0RhdPOAhwR0O_peTUIZJLRoWYrzNSGTMO9aATJJ8gUvQvwY6K2WDBifwzFo6Wn_FfVNYdwDj9LmvpcTlAKJhaEg0Ae0B7QETcXN1Y2FuL2RsZ0AxLjAuMC1yYy4xqGNhdWR4OGRpZDprZXk6ejZNa3NIdWtwZXBBM2Z3cmVlbm9KaGU5MWdaaWtHWG95VjZ4NFZHbTEzYXQxazJFY2NtZGgvZm9vL2JhcmNleHAaaJIvt2Npc3N4OGRpZDprZXk6ejZNa2dveTFRYnBUcm1oOGhadXRBVVB0cU14cW9rWXZOb0RDUUh1MkNCdUU4MnNKY3BvbIGDY2FsbGMuW12DYT5mLnZhbHVlAmNzdWJ4OGRpZDprZXk6ejZNa2dveTFRYnBUcm1oOGhadXRBVVB0cU14cW9rWXZOb0RDUUh1MkNCdUU4MnNKZG1ldGGjaDAyM2YxYjEyajAyNDkzZjhjYWFoMmM1ZjQ1ZGJqNzllY2M1MGM1MGg5MDFmNWI5OWpiN2JlODk4YjI4ZW5vbmNlTDz3ETxDC8vlSUNQBA \ No newline at end of file diff --git a/pkg/container/containertest/Base64URLGzipped b/pkg/container/containertest/Base64URLGzipped index 4c43905..72a392c 100644 --- a/pkg/container/containertest/Base64URLGzipped +++ b/pkg/container/containertest/Base64URLGzipped @@ -1 +1 @@ -PH4sIAAAAAAAA_5zWa5cUxR3HcVcRiejmuCII3siKwgnubnd1dVW1eIS5rzM9w8zs3HZETFd19_T07PTcZ2cH1KMgoB41YgIo3nM0G1GCRhCRiILIEaIhIEo0Bi9c9ewaFfEazYMd8iCPdvIGvk8-p_71e1RnZaujyt_e2_LQLYk5Fbb1QefFR9-_5LzWO59-9PfWoWV_nN_f177z06eH31j13Je8-Wpk35bTccKjtV_22MRfnz9DufuSg8Ntn02KzZtw83sLxj-mGE441FIoVZhidal9qTl8J9fJdRRZJz_IlIpaI2pavTKjDVxZR_5MQXa4aFIFgr-gMCrrOTWUqWNajNZERzin0GLRz7IxsTcfAgnGsqrRpedyXVQpMq2Wn5K68dmLWLpU-p9msRbQ-usuHEsGorZK0BbwxWzxvCdrhDK2vL23UMCRTM0RlStVxPK5vpsXMaWvj3Vee90i5Wq9s6r0VbRTWalC__-smtXKyu8MERNOwJSZkCACeAFgzkC6xGkYAhOLqkJEiBXJkJAuCUBUTMIA4KEgAEWzchbT5DXHfxwc7LiqNnPmRwdHgBa8NO6jDQ9uvW6wa7DHYetfP3DxO4s7JhybLa1YcerUV-Yae7aDww_P2_zjvH9euWqOOW3yjBWtO2__bKzLvfnR_enEpmaBMtFuy4qEsgrlqRMpPm86mql6apjI4V63V_bFgl5WT-USpQhDowVK2amiFmM8tEUsiRJcCeowHM9AvVQreLGE83LMkcBBI99dAE0ANZNtAGFewJjwkilBJPG8TphoKEQhmihKpsgBXuIVhXEGRRQpRGAmhzkRqIpOaQNo4afuoTWTj60cfjw5fQQoxy98ZPGmF3YsWLJo8voJ587Y9-X38geyLBq_nHXZIy9fPun5m1pebqVzv7J-NuaM08XEzYl3v3kmtfuCr2Yc-VAa3vtxS7NAnpKSKsBEzVaMphWdFB0kgNSEgHpwiPe7a0qaxYtVG_SVq_7RAhkey-asYmfUZ1XKMbGSr-Vo1UZ60rZyNV6Uy8l8MY_iES1ic0abAGom2wCCEqcLVFRNpEKBaghAaCBJUYgKOVNVGMQCUyXRQBqFTJWwKQkIIoZFQW8AvZV64bRDez_6flYbbmu8oDa_euu2yJB9w1vX3uO-YrZvobj2yK07V7-1YebjrRcgbsvS4fiyJ9_gpx4KvT5n_A7PTz-cqF_asuuaoTFnn2N-bbU2CaSHi0bJlrEi3kyh6NFrgqOu59VKOJ2t9Xj4sllT4wXqJdFQfzI2aiBvIGSG_Zls2IsDSdKblRV3QlQz2X7Z2evudVBk16QIM1yiCZsBaiLbAOIEnROQppu8AjgKiKqIhgQVwDQCTCKJkqCIBIqGpoicCilvcoxDWJewcPLE3TD0fHzb0y_BrtRd2gjQN3fd--36ju_CYzuV8adcsOeA95UvVv-09N1t05_bN7DllouWTN-18r4nTm-9Y8uZ2L02fj1XLizHlzyfrZ-x65Pt503b0uyJs0oDhpWuSh4vwFpJjlcDciKRCbtQTzmvxkWrKIWCUQm51FomO1ogKyMLyXSPx0xHxKrixwFT7a9XgzUUTuedXgDi8XKmgGJmJOwMNQHUTLYBhERGkE6YCTXEYYAYxxuE5xGnCbopAkWjAtCgbhCMKAUQmQqkvA4wJaQBpNyhPfDyK9Ijs-yOv40ArTv_hoMtSbB12NjtzD1Ef35ow47k_D3frn41f-2LX6YmdTxzzba3t180_7XFV5m-fz304tj2Td-t7Nx_oblszv7ctI2Hmz1xhayl1myOkhEOxUt2S_S4shFvMI7dIbseyPa4AlalJ8q6g2KhGBktUJGVfQEaSsXtsK7EnXnC6zIP84ZHrYUyGR9vcxXj9lwuaIvG7E0ANZM9-QcBHkNCNFMFEKhYFTRiSBoUOV0RTE3DKmWMCJKhijzgeJWakgoIwyLUuAZQSlq-Y177mBOSHt49AjRF-PBAe9d1H3ffcvyZczrHLAhP9X3jC5y797YFa-mNxYl_evDqo4fu_8NZh3d88un1R1f_5cfBf8_76p3tv_psyooll_k-nj2mWaC6b8BCmVrCytYjIFZKF1wytmUFKdijVuoCLIdrBV8dk3AxbIwWqJRAkpyRMnkY9ReLcipX8PR3u6RcrheykFINGCCc7-lJ25LMpjUB1Ez25IljDHFYpCYUiUYR4yVkUMKYIEiSiTDBSFcRgAajPAOU6ibjmShBToOwAXRf-6V31zceuXHdjHV9I0D7Jn7wgXzr4TVX_-PYw_mpLatO69_qW7R1Q-m8o9OcsGP2rM_f3lM_3n0_-aL18zWHV-eHzkik9r4_LriMt16a98Tm889sdiTASDqRwFLW3evj6_mU6i96cqInGEY4Ea0F1brlsPUXJSlCrcpogfRCzGll1YKRsqXrWEuVajq21wK5buKv5FxOV7wQy4Wz5Wwg3NRIaCbbABKopoi6BkymSSrTRCpSQ8UqEEQBmkTTBQp0VcSGBgAvYaiZCgQMUiz89wXdVtm7cNtjqvLmb5ZtHAHq2vfX9Uem2q-ZXTzw3oub0ruemj52f29hwuWeZ2_6_qylidcO5d9p3310-_wptTV3Dv1WXZzbuLsNDU2-dzDy7Sf84MxxTQKZAxWi5wO0Agci0WTOnStHklUpbPaHSi7JcsV9UbvH6_IHzHTVNeqRwLtlj6rV_T3ZehxGex2KLA3EXX4XhNmaGZdj-VjCRgy37HA4mhkJTWQbQBImhOkcbyIeQA1oiOcNBVBEdBWYlOc0IgBAREPnBYoABSavUAI0RhFuAM22T_nzs8NTTyz94YFVI0Bze_9-4utz3dvfW5B7fXL7uJWvr-1rO-uG5evusY_jlk-ynvzheOfE5IFIt4ieunAfO3bK-CWuX2w-0HbQP_fDi-8Y2NPsitNYLCfr2YLGEhrwg7LNoL3ZWKLSAzwDxNvNV_OA1L0oFVdToz5xuURswBupyoo7Ggua8Vg8kIjV5TLvcyCUDJsV7PXKuglTehn5mwBqJnvyBSEd6qpKTV5HCEOeU4mBCJSwpImmIkBd4BRGoUEUHUlMQ6amQVVVJB2CBpBwxRsH-9-cvnYnOfvd_wQAAP__hMJ751MQAAA \ No newline at end of file +PH4sIAAAAAAAA_5zW65MU1f3HcS7-hB8igpfIgqUxCqLC0tfTfUADMzs7M87ObubaOzMEsM-lp6eb6Zmee49GvEKwVBQRgygIUVHLCwoqoJAAikCQaJRICCqFGPCCcXVFoaBMJbukKs929h94P3nV-ZzvSg2XrEkV9u7kwFW3J6bv7bj7tflfrjm4e8_grc7Dke4jPx6c_Mm87olH_O9Of35_59-X7z2xe056dKx7156F074aOXzCcemCRe-Y4T3n3v7SNV-fdd4qVfcLA48NPDbwArtYxqo1mcxJT2ebmWZmUgE3s09jtUxqMsmQKSZ1ptRBu5lF0WK75ZbawpVO25NW2rLtfgG5XFZKtlwBr1GPVfyKU9YjZS_BOEv0yVouNxmpBUxr-TH6osmv4Uyx-L9NM5PUWqLtmqTohbQnkWp1i5JlxiOWLxAFAdaJeU13Z0gwWsN2AOdzc267A6tz5uDmGTPvUH-pNVfUOWU6CBfLqP9ZkqUl9fc6w3KqRmTVUDWGB1BiiKoDCBhJptRABAuCxrJY0zHPQBZjbLCY8hKCVBOplbMwDXbdsO9O4ZT5m20fTPywBymh3HfTBjDl5MzlT65xpm15kC4-f_2hlTtKyQUbrj78wBPfjn1e3T9i7OmuEfmt25d4z_q2K_0H36lRiw_9NbK0Szy0d3A_kPKuFk6qOYqVTSCj2uGNknxdT0BN0Etlqklxr8QaSqzF468oQl-R0oo7rztKQaK5crqzmMq7vOV4uh4jFZ2zPIrfFL0hS_ZYOFikDSA1ku1FAqJEZIRUA2gMy0OeY4EOKGVlHmCDIgZKKhYhp2NKGQR5YvBYZAWVp4j0Io1b4N905xU_e_SJj1-4pAfpbefa207fnMit_9XP39xxw7D3Fk0bH7vs09W_3uwcG3Vk5Nxv14yYP2Zj7lLPQ01Nnw0bsmXTssuZprL6-dX3zB3U9NH9q8_tB1KhgwUG8gA54HH5OcMyW1vqjmLnO11GNB0Ph6ut8WI1zAbdSMv2FclKhWK2riCS9trFvDvmd3tC0bpo8SAeC5QExZfiO3LeIOuKePkGkBrJnkEiWJYlnjEg1nhERSKouowIUgEvGUTGRBN5Ims6QiyVZVUwNIp4GUNOxr1I199x4OH_y6103Q-3DOhBuhYw1x1Y9eHYjejREfahp2Z9SNaqo83Xb0yff-mFr3_srFg7at3FV2aEy28atv2p8zbc-313YOQ3y65gm1Z9MHzdipHm0H4gFYmaLrQGWjpFLp4ICmagamWKHI560tlAsoa1uNcotoQKWj7fGuvz3Ck1v5hJgvZQJGYIKTlcFrypWocVLHPuEC_oWa2e0M3OVsen-BuZuwayZ-YOckQARDJ4TLAsChqUdVHVOBkA1pA5GVKGkaiqA04CDISiIcoUMhRjCHuRmjJraxO7f_ji-PiTE3pf0g83F4Zc-hF4-7uTV4XPG7xw986TC__2_qlnjrf9OLDrrKfeXioPGvtN0-NHRv8OHh7_5fzP1V3Lt40Zar-z-c3Vtx5O54b0AykTCrGsVS1iKmkdapQ1q6lqKWkV2gD1J3MIY09SlHw5O6yHgn1FqnoTLU66YobsbASoKq46rmoka2c8LomNkHBFYH2VVldNa-GTQgNIjWR7kTiVAIYRsYEFTcMCgpDqogw4TFXGwKqmYaACRtMJ4ClmEDAQFaHAYAbSXqRXxt9z8fIVxXOXjpjI9SDtXIkK6MJ3T6xbv-WuN7azi9K8UGl5a1t056x9G6fgxJJ5g03lSBk_O2juXV0Lnh2kVHfIwcqrKw-9dboId5X-eU5__iRPLhyxMsF03C9ZnWYKtRfZajIbUDjb68k7XFG2UL5adKesKOwrku42qkgrRaK2lI1jYJaMelnpaLf9RhW0sfmUrjk-FymaAcHBDSA1ku1FYjVeBBpGBqUCpjIUKdI5TuAwwqqhiQAAyvAQ6BoDBCgiwSCizMsSQhzfi_SP-2K_-PGE9saxTzMze5Dy-z0P7Di679olRys3fzdIXbZgbpvy0Krm5oXnnP_1w9yzj0yF8_64bCjzW2H22feEv48NLk6_8UD32fWPNvlf_CS18OUB_UDSE1WcaNUKSsqx_SjA-zNeLYI4T0cybfM4pbMg4AknnbbOetjqK1K-blcKUt4DWcNrVBzBIiJ2fGE7W8gCKZbI4WhBy4KkP9MWhA0gNZL9LxJLVMQhA_KEkTAjEkbnBZFILMQGQRLPi5gSpAOqEShqqoFFWZA4jCHoRRLeO9XcMqtwy7p35cd6kIY99snirRBtnvqcHN9benz4hCuG3LJt9bafZki7thTP_mHpi5PGXfbY6N1Nm68Z992tLD51S-q5DR98dsnhAbNSNzwZvOr_-4FUTdpy0XIjlg0nZSHkqaneUBVUCylgZEtmwQ3LhZrhR3WtkrX7PHegSpISVKtKuCYFyu11s5pK6FHbR112BNtsIZUOt1fSHVIuLzYydw1kz8ydoGqUF4nBizIHZYFyog5ZLHCU0QwEOZGROVEiusr824blDCgBSSIMgGdO8ClPP7KYPJnsQIeKG3uQrnz5teFvvD98hrrOc3nrmK3rn9s_-09PjBzaddOwxDl3Hz1xfEx810uPhvYcbL3sL9dNWjnuxamjXnCFEheAP29i5nXf3tGf6y7r6qwrJZfHlLyGLCvuWCf05Qp6JGDYBaNWqnvS5bKN_VRBVaWvSDnJBG4lHBEAl05yIccVIHZabmmLBttdobSjaKY3FRY9alvY42oAqZFsL5KKCAaEQkMlokgpyzKijhlRpaIoGZClCLCU1ahOIGQhFWSDQwLLqkhlzrwkvOH-ScEtSw68Omr87B6kpYWRa185WbvuC8u8aMC0i1pXbHeO72P9982fsHhh2xbf8WGe0y5Zq-wAk3YenZ05evG93MzVAx78aXJlxr1r2hce7A9Srqh6jIIVjNU7INENUg7Tqmz7tFBb2pfy-oq6zxuOOO0kWo27-3yC58tCwi900DxOtpk-k2txFCfuCGKllnOscqVk-W0N1dmoFcw3coI3kO1FElVWYhiiGhzgCS-xPES6zFKVAZxgyIAXJZWjAOsygppAEG_8Z-wIQZzai7Ri5eyp3V8981nL9fET_woAAP__svLMp3sQAAA \ No newline at end of file diff --git a/pkg/container/containertest/Bytes b/pkg/container/containertest/Bytes index 32c6d7e..c256776 100644 Binary files a/pkg/container/containertest/Bytes and b/pkg/container/containertest/Bytes differ diff --git a/pkg/container/containertest/BytesGzipped b/pkg/container/containertest/BytesGzipped index a698454..ff22ef1 100644 Binary files a/pkg/container/containertest/BytesGzipped and b/pkg/container/containertest/BytesGzipped differ diff --git a/pkg/container/reader.go b/pkg/container/reader.go index c96bf58..82c368d 100644 --- a/pkg/container/reader.go +++ b/pkg/container/reader.go @@ -142,12 +142,29 @@ func (ctn Reader) GetDelegation(cid cid.Cid) (*delegation.Token, error) { return nil, delegation.ErrDelegationNotFound } +// GetDelegationBundle is the same as GetToken but only return a delegation.Bundle, with the right type. +// If not found, delegation.ErrDelegationNotFound is returned. +func (ctn Reader) GetDelegationBundle(cid cid.Cid) (*delegation.Bundle, error) { + bndl, ok := ctn[cid] + if !ok { + return nil, delegation.ErrDelegationNotFound + } + if tkn, ok := bndl.token.(*delegation.Token); ok { + return &delegation.Bundle{ + Cid: cid, + Decoded: tkn, + Sealed: bndl.sealed, + }, nil + } + return nil, delegation.ErrDelegationNotFound +} + // GetAllDelegations returns all the delegation.Token in the container. -func (ctn Reader) GetAllDelegations() iter.Seq[delegation.Bundle] { - return func(yield func(delegation.Bundle) bool) { +func (ctn Reader) GetAllDelegations() iter.Seq[*delegation.Bundle] { + return func(yield func(*delegation.Bundle) bool) { for c, bndl := range ctn { if t, ok := bndl.token.(*delegation.Token); ok { - if !yield(delegation.Bundle{ + if !yield(&delegation.Bundle{ Cid: c, Decoded: t, Sealed: bndl.sealed, diff --git a/pkg/container/writer.go b/pkg/container/writer.go index a8ca0ff..1ae73ec 100644 --- a/pkg/container/writer.go +++ b/pkg/container/writer.go @@ -73,12 +73,12 @@ func (ctn Writer) ToBase64URLWriter(w io.Writer) error { return ctn.toWriter(headerBase64URL, w) } -// ToBase64URL encode the container into pre-gzipped base64 string, with URL-safe encoding and no padding. +// ToBase64URLGzipped encode the container into pre-gzipped base64 string, with URL-safe encoding and no padding. func (ctn Writer) ToBase64URLGzipped() (string, error) { return ctn.toString(headerBase64URLGzip) } -// ToBase64URLWriter is the same as ToBase64URL, but with an io.Writer. +// ToBase64URLGzipWriter is the same as ToBase64URL, but with an io.Writer. func (ctn Writer) ToBase64URLGzipWriter(w io.Writer) error { return ctn.toWriter(headerBase64URLGzip, w) } diff --git a/token/delegation/delegation_test.go b/token/delegation/delegation_test.go index 09dd312..5fde690 100644 --- a/token/delegation/delegation_test.go +++ b/token/delegation/delegation_test.go @@ -6,12 +6,12 @@ import ( "testing" "time" + "github.com/MetaMask/go-did-it/didtest" "github.com/stretchr/testify/require" "github.com/ucan-wg/go-ucan/pkg/command" "github.com/ucan-wg/go-ucan/pkg/policy" "github.com/ucan-wg/go-ucan/token/delegation" - "github.com/ucan-wg/go-ucan/token/internal/didtest" ) //go:embed testdata/new.dagjson diff --git a/token/delegation/delegationtest/generator/generator.go b/token/delegation/delegationtest/generator/generator.go index 4f26639..b9bcb83 100644 --- a/token/delegation/delegationtest/generator/generator.go +++ b/token/delegation/delegationtest/generator/generator.go @@ -12,6 +12,7 @@ import ( "github.com/MetaMask/go-did-it" didkeyctl "github.com/MetaMask/go-did-it/controller/did-key" "github.com/MetaMask/go-did-it/crypto" + "github.com/MetaMask/go-did-it/didtest" "github.com/ipfs/go-cid" "github.com/ucan-wg/go-ucan/pkg/command" @@ -19,7 +20,6 @@ import ( "github.com/ucan-wg/go-ucan/pkg/policy/policytest" "github.com/ucan-wg/go-ucan/token/delegation" "github.com/ucan-wg/go-ucan/token/delegation/delegationtest" - "github.com/ucan-wg/go-ucan/token/internal/didtest" ) const ( diff --git a/token/delegation/delegationtest/generator/main.go b/token/delegation/delegationtest/generator/main.go index e5d5fca..09618f4 100644 --- a/token/delegation/delegationtest/generator/main.go +++ b/token/delegation/delegationtest/generator/main.go @@ -1,7 +1,7 @@ package main import ( - "github.com/ucan-wg/go-ucan/token/internal/didtest" + "github.com/MetaMask/go-did-it/didtest" ) func main() { diff --git a/token/delegation/examples_test.go b/token/delegation/examples_test.go index 7537498..fdf2b2a 100644 --- a/token/delegation/examples_test.go +++ b/token/delegation/examples_test.go @@ -8,6 +8,7 @@ import ( "fmt" "time" + "github.com/MetaMask/go-did-it/didtest" "github.com/ipfs/go-cid" "github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime/codec/dagcbor" @@ -17,7 +18,6 @@ import ( "github.com/ucan-wg/go-ucan/pkg/policy" "github.com/ucan-wg/go-ucan/pkg/policy/literal" "github.com/ucan-wg/go-ucan/token/delegation" - "github.com/ucan-wg/go-ucan/token/internal/didtest" "github.com/ucan-wg/go-ucan/token/internal/envelope" ) diff --git a/token/delegation/schema_test.go b/token/delegation/schema_test.go index 33f6a01..7127755 100644 --- a/token/delegation/schema_test.go +++ b/token/delegation/schema_test.go @@ -5,12 +5,12 @@ import ( _ "embed" "testing" + "github.com/MetaMask/go-did-it/didtest" "github.com/ipld/go-ipld-prime" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/ucan-wg/go-ucan/token/delegation" - "github.com/ucan-wg/go-ucan/token/internal/didtest" "github.com/ucan-wg/go-ucan/token/internal/envelope" ) diff --git a/token/internal/didtest/crypto.go b/token/internal/didtest/crypto.go deleted file mode 100644 index 9836d4d..0000000 --- a/token/internal/didtest/crypto.go +++ /dev/null @@ -1,139 +0,0 @@ -// Package didtest provides Personas that can be used for testing. Each -// Persona has a name, crypto.PrivKey and associated crypto.PubKey and -// did.DID. -package didtest - -import ( - "encoding/base64" - "fmt" - - "github.com/MetaMask/go-did-it" - didkeyctl "github.com/MetaMask/go-did-it/controller/did-key" - "github.com/MetaMask/go-did-it/crypto" - "github.com/MetaMask/go-did-it/crypto/ed25519" -) - -const ( - // all are ed25519 as base64 - alicePrivKeyB64 = "zth/9cTSUVwlLzfEWwLCcOkaEmjrRGPOI6mOJksWAYZ3Toe7ymxAzDeiseyxbmEpJ81qYM3dZ8XrXqgonnTTEw==" - bobPrivKeyB64 = "+p1REV3MkUnLhUMbFe9RcSsmo33TT/FO85yaV+c6fiYJCBsdiwfMwodlkzSAG3sHQIuZj8qnJ678oJucYy7WEg==" - carolPrivKeyB64 = "aSu3vTwE7z3pXaTaAhVLeizuqnZUJZQHTCSLMLxyZh5LDoZQn80uoQgMEdsbOhR+zIqrjBn5WviGurDkKYVfug==" - danPrivKeyB64 = "s1zM1av6og3o0UMNbEs/RyezS7Nk/jbSYL2Z+xPEw9Cho/KuEAa75Sf4yJHclLwpKXNucbrZ2scE8Iy8K05KWQ==" - erinPrivKeyB64 = "+qHpaAR3iivWMEl+pkXmq+uJeHtqFiY++XOXtZ9Tu/WPABCO+eRFrTCLJykJEzAPGFmkJF8HQ7DMwOH7Ry3Aqw==" - frankPrivKeyB64 = "4k/1N0+Fq73DxmNbGis9PY2KgKxWmtDWhmi1E6sBLuGd7DS0TWjCn1Xa3lXkY49mFszMjhWC+V6DCBf7R68u4Q==" -) - -// Persona is a generic participant used for cryptographic testing. -type Persona int - -// The provided Personas were selected from the first few generic -// participants listed in this [table]. -// -// [table]: https://en.wikipedia.org/wiki/Alice_and_Bob#Cryptographic_systems -const ( - PersonaAlice Persona = iota + 1 - PersonaBob - PersonaCarol - PersonaDan - PersonaErin - PersonaFrank -) - -var privKeys map[Persona]crypto.PrivateKeySigningBytes - -func init() { - privKeys = make(map[Persona]crypto.PrivateKeySigningBytes, 6) - for persona, pB64 := range privKeyB64() { - privBytes, err := base64.StdEncoding.DecodeString(pB64) - if err != nil { - return - } - - privKey, err := ed25519.PrivateKeyFromBytes(privBytes) - if err != nil { - return - } - - privKeys[persona] = privKey - } -} - -// DID returns a did.DID based on the Persona's Ed25519 public key. -func (p Persona) DID() did.DID { - return didkeyctl.FromPrivateKey(p.PrivKey()) -} - -// Name returns the username of the Persona. -func (p Persona) Name() string { - name, ok := map[Persona]string{ - PersonaAlice: "Alice", - PersonaBob: "Bob", - PersonaCarol: "Carol", - PersonaDan: "Dan", - PersonaErin: "Erin", - PersonaFrank: "Frank", - }[p] - if !ok { - panic(fmt.Sprintf("Unknown persona: %v", p)) - } - - return name -} - -// PrivKey returns the Ed25519 private key for the Persona. -func (p Persona) PrivKey() crypto.PrivateKeySigningBytes { - res, ok := privKeys[p] - if !ok { - panic(fmt.Sprintf("Unknown persona: %v", p)) - } - return res -} - -func (p Persona) PrivKeyConfig() string { - res, ok := privKeyB64()[p] - if !ok { - panic(fmt.Sprintf("Unknown persona: %v", p)) - } - return res -} - -// PubKey returns the Ed25519 public key for the Persona. -func (p Persona) PubKey() crypto.PublicKey { - return p.PrivKey().Public() -} - -func privKeyB64() map[Persona]string { - return map[Persona]string{ - PersonaAlice: alicePrivKeyB64, - PersonaBob: bobPrivKeyB64, - PersonaCarol: carolPrivKeyB64, - PersonaDan: danPrivKeyB64, - PersonaErin: erinPrivKeyB64, - PersonaFrank: frankPrivKeyB64, - } -} - -// Personas returns an (alphabetically) ordered list of the defined -// Persona values. -func Personas() []Persona { - return []Persona{ - PersonaAlice, - PersonaBob, - PersonaCarol, - PersonaDan, - PersonaErin, - PersonaFrank, - } -} - -// DidToName retrieve the persona's name from its DID. -func DidToName(d did.DID) string { - return map[did.DID]string{ - PersonaAlice.DID(): "Alice", - PersonaBob.DID(): "Bob", - PersonaCarol.DID(): "Carol", - PersonaDan.DID(): "Dan", - PersonaErin.DID(): "Erin", - PersonaFrank.DID(): "Frank", - }[d] -} diff --git a/token/invocation/invocation_test.go b/token/invocation/invocation_test.go index 6d34a4c..72f798c 100644 --- a/token/invocation/invocation_test.go +++ b/token/invocation/invocation_test.go @@ -4,6 +4,7 @@ import ( _ "embed" "testing" + "github.com/MetaMask/go-did-it/didtest" "github.com/ipfs/go-cid" "github.com/stretchr/testify/require" @@ -11,7 +12,6 @@ import ( "github.com/ucan-wg/go-ucan/pkg/command" "github.com/ucan-wg/go-ucan/pkg/policy/policytest" "github.com/ucan-wg/go-ucan/token/delegation/delegationtest" - "github.com/ucan-wg/go-ucan/token/internal/didtest" "github.com/ucan-wg/go-ucan/token/invocation" ) diff --git a/toolkit/_example/Readme.md b/toolkit/_example/Readme.md new file mode 100644 index 0000000..282dcae --- /dev/null +++ b/toolkit/_example/Readme.md @@ -0,0 +1,36 @@ +## UCAN examples + +This directory contains an example of UCAN usage across multiple agents, and their respective implementations. + +Please note that UCAN in itself doesn't enforce any protocol, topology or transport, and as such what you have here is one possibility among many others. In particular: +- this example is really geared towards using UCAN for an HTTP API +- it uses a particular flavor of issuer protocol and token exchange. In particular, that issuer gives delegation tokens to anyone asking. + +Your situation may be different from this, and would call for a different setup. + +Remember that everything in `/toolkit` is essentially helpers, pre-made building blocks. You can use them, change them or make your own. + +## Scenario 1 + +Starting simple, if we run `service`, `service-issuer` and `alice-client-server`, we have the following scenario: + +![scenario 1](scenario1.png) + +- `service` controls the access to the resource (it's the `Executor` in that diagram). You can think about it as a proxy with authentication. +- `service-issuer` gives a delegation tokens to clients. `service` and `service-issuer` share the same DID and keypair. +- `alice-client-server` ask for a token, and periodically makes request + +## Scenario 2 + +Building on the previous scenario, we are adding sub-delegation. + +![scenario 2](scenario2.png) + +- `alice-client-server` still do the same thing, but also expose a similar token issuer with the same protocol (for simplicity in that example) +- `bob-client` request a delegation from Alice, and make **direct** request to the service + +Note a few things: +- Alice can finely tune what Bob can do +- Bob receives **two** delegations: the original one Alice got and a second one delegating some of that original power to him +- Bob can make direct calls to the service without having to be proxied somewhere +- The service doesn't have to know beforehand about Bob or what power is given to him diff --git a/toolkit/_example/_protocol-issuer/request-resolver.go b/toolkit/_example/_protocol-issuer/request-resolver.go new file mode 100644 index 0000000..47db4fd --- /dev/null +++ b/toolkit/_example/_protocol-issuer/request-resolver.go @@ -0,0 +1,41 @@ +package protocol + +import ( + "encoding/json" + "net/http" + + "github.com/MetaMask/go-did-it" + + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/toolkit/issuer" +) + +func RequestResolver(r *http.Request) (*issuer.ResolvedRequest, error) { + // Let's make up a simple json protocol + req := struct { + Audience string `json:"aud"` + Cmd string `json:"cmd"` + Subject string `json:"sub"` + }{} + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + return nil, err + } + aud, err := did.Parse(req.Audience) + if err != nil { + return nil, err + } + cmd, err := command.Parse(req.Cmd) + if err != nil { + return nil, err + } + sub, err := did.Parse(req.Subject) + if err != nil { + return nil, err + } + return &issuer.ResolvedRequest{ + Audience: aud, + Cmd: cmd, + Subject: sub, + }, nil +} diff --git a/toolkit/_example/_protocol-issuer/requester.go b/toolkit/_example/_protocol-issuer/requester.go new file mode 100644 index 0000000..a212578 --- /dev/null +++ b/toolkit/_example/_protocol-issuer/requester.go @@ -0,0 +1,60 @@ +package protocol + +import ( + "bytes" + "context" + "encoding/json" + "iter" + "log" + "net/http" + + "github.com/MetaMask/go-did-it" + + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/token/delegation" + + "github.com/ucan-wg/go-ucan/toolkit/client" + "github.com/ucan-wg/go-ucan/toolkit/issuer" +) + +var _ client.DelegationRequester = &Requester{} + +type Requester struct { + issuerURL string +} + +func NewRequester(issuerURL string) *Requester { + return &Requester{issuerURL: issuerURL} +} + +func (r Requester) RequestDelegation(ctx context.Context, audience did.DID, cmd command.Command, subject did.DID) (iter.Seq2[*delegation.Bundle, error], error) { + log.Printf("requesting delegation for %s on %s", cmd, subject) + + // we match the simple json protocol of the issuer + data := struct { + Audience string `json:"aud"` + Cmd string `json:"cmd"` + Subject string `json:"sub"` + }{ + Audience: audience.String(), + Cmd: cmd.String(), + Subject: subject.String(), + } + buf := &bytes.Buffer{} + err := json.NewEncoder(buf).Encode(data) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodPost, r.issuerURL, buf) + if err != nil { + return nil, err + } + + res, err := http.DefaultClient.Do(req.WithContext(ctx)) + if err != nil { + return nil, err + } + + return issuer.DecodeResponse(res, audience, cmd, subject) +} diff --git a/toolkit/_example/alice-client-issuer/alice.go b/toolkit/_example/alice-client-issuer/alice.go new file mode 100644 index 0000000..9531c02 --- /dev/null +++ b/toolkit/_example/alice-client-issuer/alice.go @@ -0,0 +1,154 @@ +package main + +import ( + "context" + "errors" + "fmt" + "io" + "log" + "net/http" + "net/url" + "os" + "os/signal" + "syscall" + "time" + + "github.com/MetaMask/go-did-it" + "github.com/MetaMask/go-did-it/crypto" + + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/pkg/container" + "github.com/ucan-wg/go-ucan/pkg/policy" + "github.com/ucan-wg/go-ucan/pkg/policy/literal" + "github.com/ucan-wg/go-ucan/token/delegation" + + example "github.com/ucan-wg/go-ucan/toolkit/_example" + protocol "github.com/ucan-wg/go-ucan/toolkit/_example/_protocol-issuer" + "github.com/ucan-wg/go-ucan/toolkit/client" + "github.com/ucan-wg/go-ucan/toolkit/issuer" + "github.com/ucan-wg/go-ucan/toolkit/server/bearer" +) + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + + // register as handler of the interrupt signal to trigger the teardown + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) + + go func() { + <-quit + cancel() + }() + + err := run(ctx, + example.AliceIssuerUrl, example.AlicePrivKey, example.AliceDid, + example.ServiceIssuerUrl, example.ServiceUrl, example.ServiceDid) + if err != nil { + log.Println(err) + os.Exit(1) + } +} + +func run(ctx context.Context, ownIssuerUrl string, priv crypto.PrivateKeySigningBytes, d did.DID, + serviceIssuerUrl string, serviceUrl string, serviceDid did.DID) error { + log.Printf("Alice DID is %s", d.String()) + + issuingLogic := func(iss did.DID, aud did.DID, cmd command.Command, subject did.DID) (*delegation.Token, error) { + log.Printf("issuing delegation to %v for %v to operate on %v", aud, cmd, subject) + + // As another example, we'll force Bob to use a specific HTTP sub-path + policies, err := policy.Construct( + policy.Equal(".http.path", literal.String(fmt.Sprintf("/%s/%s", iss.String(), aud.String()))), + ) + if err != nil { + return nil, err + } + + return delegation.New(iss, aud, cmd, policies, subject) + } + + cli, err := client.NewWithIssuer(priv, d, protocol.NewRequester("http://"+serviceIssuerUrl), issuingLogic) + if err != nil { + return err + } + + go startIssuerHttp(ctx, ownIssuerUrl, cli) + + for { + proofs, err := cli.PrepareInvoke(ctx, command.MustParse("/foo/bar"), serviceDid) + if err != nil { + return err + } + + err = makeRequest(ctx, d, serviceUrl, proofs) + if err != nil { + log.Println(err) + } + + select { + case <-ctx.Done(): + return nil + case <-time.After(20 * time.Second): + } + } +} + +func startIssuerHttp(ctx context.Context, issuerUrl string, cli *client.WithIssuer) { + handler := issuer.HttpWrapper(cli, protocol.RequestResolver) + + srv := &http.Server{ + Addr: issuerUrl, + Handler: handler, + } + + go func() { + if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { + log.Fatalf("listen: %s\n", err) + } + }() + + log.Printf("issuer listening on %s\n", srv.Addr) + + <-ctx.Done() + + if err := srv.Shutdown(ctx); err != nil && !errors.Is(err, context.Canceled) { + log.Printf("issuer error: %v\n", err) + } +} + +func makeRequest(ctx context.Context, clientDid did.DID, serviceUrl string, proofs container.Writer) error { + // we construct a URL that include the client DID as path, as requested by the UCAN policy we get issued + u, err := url.JoinPath("http://"+serviceUrl, clientDid.String()) + if err != nil { + return err + } + + req, err := http.NewRequest(http.MethodGet, u, nil) + if err != nil { + return err + } + + err = bearer.AddBearerContainerCompressed(req.Header, proofs) + if err != nil { + return err + } + + res, err := http.DefaultClient.Do(req.WithContext(ctx)) + if err != nil { + return err + } + + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + body, err := io.ReadAll(res.Body) + if err != nil { + return fmt.Errorf("unexpected status code: %d, error reading body: %w", res.StatusCode, err) + } + return fmt.Errorf("unexpected status code: %d, body: %v", res.StatusCode, string(body)) + } + + log.Printf("response status code: %d", res.StatusCode) + + return nil +} diff --git a/toolkit/_example/bob-client/bob.go b/toolkit/_example/bob-client/bob.go new file mode 100644 index 0000000..feb7413 --- /dev/null +++ b/toolkit/_example/bob-client/bob.go @@ -0,0 +1,115 @@ +package main + +import ( + "context" + "fmt" + "io" + "log" + "net/http" + "net/url" + "os" + "os/signal" + "syscall" + "time" + + "github.com/MetaMask/go-did-it" + didkeyctl "github.com/MetaMask/go-did-it/controller/did-key" + "github.com/MetaMask/go-did-it/crypto/ed25519" + + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/pkg/container" + + example "github.com/ucan-wg/go-ucan/toolkit/_example" + protocol "github.com/ucan-wg/go-ucan/toolkit/_example/_protocol-issuer" + "github.com/ucan-wg/go-ucan/toolkit/client" + "github.com/ucan-wg/go-ucan/toolkit/server/bearer" +) + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + + // register as handler of the interrupt signal to trigger the teardown + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) + + go func() { + <-quit + cancel() + }() + + err := run(ctx, example.AliceIssuerUrl, example.AliceDid, example.ServiceUrl, example.ServiceDid) + if err != nil { + log.Println(err) + os.Exit(1) + } +} + +func run(ctx context.Context, aliceUrl string, aliceDid did.DID, serverUrl string, serviceDid did.DID) error { + // Let's generate a keypair for our client: + pub, priv, err := ed25519.GenerateKeyPair() + if err != nil { + return err + } + d := didkeyctl.FromPublicKey(pub) + + log.Printf("Bob DID is %s", d.String()) + + cli, err := client.NewClient(priv, d, protocol.NewRequester("http://"+aliceUrl)) + if err != nil { + return err + } + + for { + proofs, err := cli.PrepareInvoke(ctx, command.MustParse("/foo/bar"), serviceDid) + if err != nil { + return err + } + + err = makeRequest(ctx, d, serverUrl, aliceDid, proofs) + if err != nil { + log.Println(err) + } + + select { + case <-ctx.Done(): + return nil + case <-time.After(1 * time.Second): + } + } +} + +func makeRequest(ctx context.Context, clientDid did.DID, serviceUrl string, aliceDid did.DID, proofs container.Writer) error { + // we construct a URL that include our DID and Alice DID as path, as requested by the UCAN policy we get issued + u, err := url.JoinPath("http://"+serviceUrl, aliceDid.String(), clientDid.String()) + if err != nil { + return err + } + + req, err := http.NewRequest(http.MethodGet, u, nil) + if err != nil { + return err + } + + err = bearer.AddBearerContainerCompressed(req.Header, proofs) + if err != nil { + return err + } + + res, err := http.DefaultClient.Do(req.WithContext(ctx)) + if err != nil { + return err + } + + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + body, err := io.ReadAll(res.Body) + if err != nil { + return fmt.Errorf("unexpected status code: %d, error reading body: %w", res.StatusCode, err) + } + return fmt.Errorf("unexpected status code: %d, body: %v", res.StatusCode, string(body)) + } + + log.Printf("response status code: %d", res.StatusCode) + + return nil +} diff --git a/toolkit/_example/diagram.puml b/toolkit/_example/diagram.puml new file mode 100644 index 0000000..9891a39 --- /dev/null +++ b/toolkit/_example/diagram.puml @@ -0,0 +1,57 @@ +@startuml +left to right direction + +rectangle Service as owner { + rectangle Issuer as issuer + rectangle Executor as exec +} + +node resource as res + +owner --> res : Controls +exec --> res : Allow access to + +rectangle "Alice" as alice { + rectangle Client as aliceclient + +} + +aliceclient --> issuer : [1] request delegation +aliceclient <-- issuer : [2] issue delegation A + +aliceclient --> exec : [3] make request with A +@enduml + + +@startuml +left to right direction + +rectangle Service as owner { + rectangle Issuer as issuer + rectangle Executor as exec +} + +node resource as res + +owner --> res : Controls +exec --> res : Allow access to + +rectangle "Alice" as alice { + rectangle Client as aliceclient + rectangle Issuer as aliceissuer +} + +aliceclient --> issuer : [1] request delegation +aliceclient <-- issuer : [2] issue delegation A + +aliceclient --> exec : [3] make request with A + +rectangle "Bob" as bob { + rectangle Client as bobclient +} + +bobclient --> aliceissuer : [4] request delegation +bobclient <-- aliceissuer : [5] issue delegation B\nalso returns A + +bobclient -down-> exec : [6] make request with A+B +@enduml \ No newline at end of file diff --git a/toolkit/_example/scenario1.png b/toolkit/_example/scenario1.png new file mode 100644 index 0000000..91851be Binary files /dev/null and b/toolkit/_example/scenario1.png differ diff --git a/toolkit/_example/scenario2.png b/toolkit/_example/scenario2.png new file mode 100644 index 0000000..92a49e9 Binary files /dev/null and b/toolkit/_example/scenario2.png differ diff --git a/toolkit/_example/service-issuer/issuer.go b/toolkit/_example/service-issuer/issuer.go new file mode 100644 index 0000000..f3e9dcd --- /dev/null +++ b/toolkit/_example/service-issuer/issuer.go @@ -0,0 +1,97 @@ +package main + +import ( + "context" + "errors" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/MetaMask/go-did-it" + "github.com/MetaMask/go-did-it/crypto" + + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/pkg/policy" + "github.com/ucan-wg/go-ucan/pkg/policy/literal" + "github.com/ucan-wg/go-ucan/token/delegation" + + example "github.com/ucan-wg/go-ucan/toolkit/_example" + protocol "github.com/ucan-wg/go-ucan/toolkit/_example/_protocol-issuer" + "github.com/ucan-wg/go-ucan/toolkit/issuer" +) + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + + // register as handler of the interrupt signal to trigger the teardown + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) + + go func() { + <-quit + cancel() + }() + + err := run(ctx, example.ServiceIssuerUrl, example.ServicePrivKey) + if err != nil { + log.Println(err) + os.Exit(1) + } +} + +func run(ctx context.Context, issuerUrl string, servicePrivKey crypto.PrivateKeySigningBytes) error { + issuingLogic := func(iss did.DID, aud did.DID, cmd command.Command) (*delegation.Token, error) { + log.Printf("issuing delegation to %v for %v", aud, cmd) + + // We construct an arbitrary policy. + // Here, we enforce that the caller uses its own DID endpoint (an arbitrary construct for this example). + // You will notice that the server doesn't need to know about this logic to enforce it. + policies, err := policy.Construct( + policy.Or( + // allow exact path + policy.Equal(".http.path", literal.String(fmt.Sprintf("/%s", aud.String()))), + // allow sub-path + policy.Like(".http.path", fmt.Sprintf("/%s/*", aud.String())), + ), + ) + if err != nil { + return nil, err + } + + return delegation.Root(iss, aud, cmd, policies, + // let's add an expiration, this will force the client to renew its token. + delegation.WithExpirationIn(10*time.Second), + ) + } + + rootIssuer, err := issuer.NewRootIssuer(servicePrivKey, issuingLogic) + if err != nil { + return err + } + + handler := issuer.HttpWrapper(rootIssuer, protocol.RequestResolver) + + srv := &http.Server{ + Addr: issuerUrl, + Handler: handler, + } + + go func() { + if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { + log.Fatalf("listen: %s\n", err) + } + }() + + log.Printf("listening on %s\n", srv.Addr) + + <-ctx.Done() + + if err := srv.Shutdown(ctx); err != nil && !errors.Is(err, context.Canceled) { + return err + } + return nil +} diff --git a/toolkit/_example/service/service.go b/toolkit/_example/service/service.go new file mode 100644 index 0000000..beb1f6c --- /dev/null +++ b/toolkit/_example/service/service.go @@ -0,0 +1,88 @@ +package main + +import ( + "context" + "errors" + "log" + "net/http" + "os" + "os/signal" + "syscall" + + "github.com/MetaMask/go-did-it" + + example "github.com/ucan-wg/go-ucan/toolkit/_example" + "github.com/ucan-wg/go-ucan/toolkit/server/exectx" +) + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + + // register as handler of the interrupt signal to trigger the teardown + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) + + go func() { + <-quit + cancel() + }() + + err := run(ctx, example.ServiceUrl, example.ServiceDid) + if err != nil { + log.Println(err) + os.Exit(1) + } +} + +func run(ctx context.Context, serviceUrl string, serviceDID did.DID) error { + log.Printf("service DID is %s\n", serviceDID.String()) + + // we'll make a simple handling pipeline: + // - exectx.ExtractMW to extract and decode the UCAN context, verify the service DID + // - exectx.HttpExtArgsVerify to verify the HTTP policies + // - exectx.EnforceMW to perform the final UCAN checks + // - our handler to execute the commands + + var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ucanCtx, ok := exectx.FromContext(r.Context()) + if !ok { + http.Error(w, "no ucan-ctx found", http.StatusInternalServerError) + return + } + + switch ucanCtx.Command().String() { + case "/foo/bar": + log.Printf("handled command %v at %v for %v", ucanCtx.Command(), r.URL.Path, ucanCtx.Invocation().Issuer()) + log.Printf("proof is %v", ucanCtx.Invocation().Proof()) + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("OK")) + default: + http.Error(w, "unknown UCAN commmand", http.StatusBadRequest) + return + } + }) + + handler = exectx.EnforceMW(handler) + handler = exectx.HttpExtArgsVerify(handler) + handler = exectx.ExtractMW(handler, serviceDID) + + srv := &http.Server{ + Addr: serviceUrl, + Handler: handler, + } + + go func() { + if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { + log.Fatalf("listen: %s\n", err) + } + }() + + log.Printf("listening on %s\n", srv.Addr) + + <-ctx.Done() + + if err := srv.Shutdown(ctx); err != nil && !errors.Is(err, context.Canceled) { + return err + } + return nil +} diff --git a/toolkit/_example/shared_values.go b/toolkit/_example/shared_values.go new file mode 100644 index 0000000..468b00d --- /dev/null +++ b/toolkit/_example/shared_values.go @@ -0,0 +1,37 @@ +package example + +import ( + "encoding/base64" + + "github.com/MetaMask/go-did-it" + didkeyctl "github.com/MetaMask/go-did-it/controller/did-key" + "github.com/MetaMask/go-did-it/crypto" + "github.com/MetaMask/go-did-it/crypto/ed25519" +) + +// Endpoints + +var ServiceUrl = ":8080" +var ServiceIssuerUrl = ":8081" + +var AliceIssuerUrl = ":8082" + +// Service + +var ServicePrivKey crypto.PrivateKeySigningBytes +var ServiceDid did.DID + +// Alice + +var AlicePrivKey crypto.PrivateKeySigningBytes +var AliceDid did.DID + +func init() { + servPrivRaw, _ := base64.StdEncoding.DecodeString("HVcbgoj30c+7zoQzUgpl7Jc7bkXoyvo9bMX5OHaAohpv036EMxuWXGqmEWhFKHPEuRAaIGSURK8pyUYOAseiiQ==") + ServicePrivKey, _ = ed25519.PrivateKeyFromBytes(servPrivRaw) + ServiceDid = didkeyctl.FromPrivateKey(ServicePrivKey) + + alicePrivRaw, _ := base64.StdEncoding.DecodeString("jIIk/4ZBgIzx7fU41AWYRUDjgQmgFTIXxN4WeZAPCjwE04oLfiHgNjwIIZi97a6WwSIL5tFGdkrqDkSmDx95tw==") + AlicePrivKey, _ = ed25519.PrivateKeyFromBytes(alicePrivRaw) + AliceDid = didkeyctl.FromPrivateKey(AlicePrivKey) +} diff --git a/toolkit/client/client.go b/toolkit/client/client.go new file mode 100644 index 0000000..6236ebe --- /dev/null +++ b/toolkit/client/client.go @@ -0,0 +1,126 @@ +package client + +import ( + "context" + "fmt" + "iter" + + "github.com/MetaMask/go-did-it" + "github.com/MetaMask/go-did-it/crypto" + "github.com/ipfs/go-cid" + + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/pkg/container" + "github.com/ucan-wg/go-ucan/pkg/policy" + "github.com/ucan-wg/go-ucan/token/delegation" + "github.com/ucan-wg/go-ucan/token/invocation" +) + +type Client struct { + did did.DID + privKey crypto.PrivateKeySigningBytes + + pool *Pool + requester DelegationRequester +} + +func NewClient(privKey crypto.PrivateKeySigningBytes, d did.DID, requester DelegationRequester) (*Client, error) { + return &Client{ + did: d, + privKey: privKey, + pool: NewPool(), + requester: requester, + }, nil +} + +// AddBundle adds a delegation.Bundle to the client's pool. +func (c *Client) AddBundle(bundle *delegation.Bundle) { + c.pool.AddBundle(bundle) +} + +// AddBundles adds a sequence of delegation.Bundles to the client's pool. +func (c *Client) AddBundles(bundles iter.Seq[*delegation.Bundle]) { + c.pool.AddBundles(bundles) +} + +// PrepareInvoke returns an invocation, bundled in a container.Writer with the necessary proofs. +func (c *Client) PrepareInvoke(ctx context.Context, cmd command.Command, subject did.DID, opts ...invocation.Option) (container.Writer, error) { + proof, err := c.findProof(ctx, cmd, subject) + if err != nil { + return nil, err + } + + inv, err := invocation.New(c.did, cmd, subject, proof, opts...) + if err != nil { + return nil, err + } + + invSealed, _, err := inv.ToSealed(c.privKey) + if err != nil { + return nil, err + } + + cont := container.NewWriter() + cont.AddSealed(invSealed) + for bundle, err := range c.pool.GetBundles(proof) { + if err != nil { + return nil, err + } + cont.AddSealed(bundle.Sealed) + } + + return cont, nil +} + +// PrepareDelegation returns a new delegation for a third party DID, bundled in a container.Writer with the necessary proofs. +func (c *Client) PrepareDelegation(ctx context.Context, aud did.DID, cmd command.Command, subject did.DID, policies policy.Policy, opts ...delegation.Option) (container.Writer, error) { + proof, err := c.findProof(ctx, cmd, subject) + if err != nil { + return nil, err + } + + dlg, err := delegation.New(c.did, aud, cmd, policies, subject, opts...) + if err != nil { + return nil, err + } + + dlgSealed, _, err := dlg.ToSealed(c.privKey) + if err != nil { + return nil, err + } + + cont := container.NewWriter() + cont.AddSealed(dlgSealed) + for bundle, err := range c.pool.GetBundles(proof) { + if err != nil { + return nil, err + } + cont.AddSealed(bundle.Sealed) + } + + return cont, nil +} + +func (c *Client) findProof(ctx context.Context, cmd command.Command, subject did.DID) ([]cid.Cid, error) { + var proof []cid.Cid + + // do we already have a valid proof? + if proof = c.pool.FindProof(c.did, cmd, subject); len(proof) == 0 { + // we need to request a new proof + proofBundles, err := c.requester.RequestDelegation(ctx, c.did, cmd, subject) + if err != nil { + return nil, fmt.Errorf("requesting delegation: %w", err) + } + + // cache the new proofs + for bundle, err := range proofBundles { + if err != nil { + return nil, err + } + proof = append(proof, bundle.Cid) + c.pool.AddBundle(bundle) + } + } + + return proof, nil +} diff --git a/toolkit/client/client_test.go b/toolkit/client/client_test.go new file mode 100644 index 0000000..541e1a0 --- /dev/null +++ b/toolkit/client/client_test.go @@ -0,0 +1,77 @@ +package client + +import ( + "context" + "fmt" + "iter" + "time" + + "github.com/MetaMask/go-did-it" + "github.com/MetaMask/go-did-it/didtest" + + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/pkg/policy" + "github.com/ucan-wg/go-ucan/token/delegation" + "github.com/ucan-wg/go-ucan/token/invocation" +) + +func ExampleNewClient() { + servicePersona := didtest.PersonaAlice + clientPersona := didtest.PersonaBob + + // requester is an adaptor for a real world issuer, we use a mock in that example + requester := &requesterMock{persona: servicePersona} + + client, err := NewClient(clientPersona.PrivKey(), clientPersona.DID(), requester) + handleError(err) + + cont, err := client.PrepareInvoke( + context.Background(), + command.New("crud", "add"), + servicePersona.DID(), + // extra invocation parameters: + invocation.WithExpirationIn(10*time.Minute), + invocation.WithArgument("foo", "bar"), + invocation.WithMeta("baz", 1234), + ) + handleError(err) + + // this container holds the invocation and all the delegation proofs + b64, err := cont.ToBase64StdPadding() + handleError(err) + + fmt.Println(b64) +} + +func handleError(err error) { + if err != nil { + panic(err) + } +} + +type requesterMock struct { + persona didtest.Persona +} + +func (r requesterMock) RequestDelegation(_ context.Context, audience did.DID, cmd command.Command, _ did.DID) (iter.Seq2[*delegation.Bundle, error], error) { + // the mock issue whatever the client asks: + dlg, err := delegation.Root(r.persona.DID(), audience, cmd, policy.Policy{}) + if err != nil { + return nil, err + } + + dlgBytes, dlgCid, err := dlg.ToSealed(r.persona.PrivKey()) + if err != nil { + return nil, err + } + + bundle := &delegation.Bundle{ + Cid: dlgCid, + Decoded: dlg, + Sealed: dlgBytes, + } + + return func(yield func(*delegation.Bundle, error) bool) { + yield(bundle, nil) + }, nil +} diff --git a/toolkit/client/clientissuer.go b/toolkit/client/clientissuer.go new file mode 100644 index 0000000..4653fdc --- /dev/null +++ b/toolkit/client/clientissuer.go @@ -0,0 +1,104 @@ +package client + +import ( + "context" + "fmt" + "iter" + + "github.com/MetaMask/go-did-it" + "github.com/MetaMask/go-did-it/crypto" + "github.com/ipfs/go-cid" + + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/token/delegation" +) + +// DlgIssuingLogic is a function that decides what powers are given to a client. +// - issuer: the DID of our issuer +// - audience: the DID of the client, also the issuer of the invocation token +// - cmd: the command to execute +// - subject: the DID of the resource to operate on, also the subject (or audience if defined) of the invocation token +// Note: you can read it as "(audience) wants to do (cmd) on (subject)". +// Note: you can decide to match the input parameters exactly or issue a broader power, as long as it allows the +// expected action. If you don't want to give that power, return an error instead. +type DlgIssuingLogic func(iss did.DID, aud did.DID, cmd command.Command, subject did.DID) (*delegation.Token, error) + +var _ DelegationRequester = &WithIssuer{} + +type WithIssuer struct { + *Client + logic DlgIssuingLogic +} + +func NewWithIssuer(privKey crypto.PrivateKeySigningBytes, d did.DID, requester DelegationRequester, logic DlgIssuingLogic) (*WithIssuer, error) { + client, err := NewClient(privKey, d, requester) + if err != nil { + return nil, err + } + return &WithIssuer{Client: client, logic: logic}, nil +} + +// RequestDelegation retrieve chain of delegation for the given parameters. +// - audience: the DID of the client, also the issuer of the invocation token +// - cmd: the command to execute +// - subject: the DID of the resource to operate on, also the subject (or audience if defined) of the invocation token +// Note: you can read it as "(audience) does (cmd) on (subject)". +// Note: the returned delegation(s) don't have to match exactly the parameters, as long as they allow them. +func (c *WithIssuer) RequestDelegation(ctx context.Context, audience did.DID, cmd command.Command, subject did.DID) (iter.Seq2[*delegation.Bundle, error], error) { + var proof []cid.Cid + + // is there already a valid proof chain? + if proof = c.pool.FindProof(audience, cmd, subject); len(proof) > 0 { + return c.pool.GetBundles(proof), nil + } + + // do we have the power to delegate this? + if proof = c.pool.FindProof(c.did, cmd, subject); len(proof) == 0 { + // we need to request a new proof + proofBundles, err := c.requester.RequestDelegation(ctx, c.did, cmd, subject) + if err != nil { + return nil, err + } + + // cache the new proofs + for bundle, err := range proofBundles { + if err != nil { + return nil, err + } + proof = append(proof, bundle.Cid) + c.pool.AddBundle(bundle) + } + } + + // run the custom logic to get what we actually issue + dlg, err := c.logic(c.did, audience, cmd, subject) + if err != nil { + return nil, err + } + if dlg.IsRoot() { + return nil, fmt.Errorf("issuing logic should return a non-root delegation") + } + + // sign and cache the new token + dlgBytes, dlgCid, err := dlg.ToSealed(c.privKey) + if err != nil { + return nil, err + } + bundle := &delegation.Bundle{ + Cid: dlgCid, + Decoded: dlg, + Sealed: dlgBytes, + } + + // output the relevant delegations + return func(yield func(*delegation.Bundle, error) bool) { + if !yield(bundle, nil) { + return + } + for b, err := range c.pool.GetBundles(proof) { + if !yield(b, err) { + return + } + } + }, nil +} diff --git a/toolkit/client/pool.go b/toolkit/client/pool.go new file mode 100644 index 0000000..71d9d69 --- /dev/null +++ b/toolkit/client/pool.go @@ -0,0 +1,91 @@ +package client + +import ( + "fmt" + "iter" + "sync" + "time" + + "github.com/MetaMask/go-did-it" + "github.com/ipfs/go-cid" + + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/token/delegation" +) + +type Pool struct { + mu sync.RWMutex + dlgs map[cid.Cid]*delegation.Bundle +} + +func NewPool() *Pool { + return &Pool{dlgs: make(map[cid.Cid]*delegation.Bundle)} +} + +func (p *Pool) AddBundle(bundle *delegation.Bundle) { + p.mu.Lock() + defer p.mu.Unlock() + p.dlgs[bundle.Cid] = bundle +} + +func (p *Pool) AddBundles(bundles iter.Seq[*delegation.Bundle]) { + for bundle := range bundles { + p.AddBundle(bundle) + } +} + +// FindProof find in the pool the best (shortest, smallest in bytes) chain of delegation(s) matching the given invocation parameters. +// - issuer: the DID of the client, also the issuer of the invocation token +// - cmd: the command to execute +// - subject: the DID of the resource to operate on, also the subject (or audience if defined) of the invocation token +// Note: you can read it as "(issuer) wants to do (cmd) on (subject)". +// Note: the returned delegation(s) don't have to match exactly the parameters, as long as they allow them. +// Note: the implemented algorithm won't perform well with a large number of delegations. +func (p *Pool) FindProof(issuer did.DID, cmd command.Command, subject did.DID) []cid.Cid { + // TODO: move to some kind of background trim job? + p.trim() + + p.mu.RLock() + defer p.mu.RUnlock() + + return FindProof(func() iter.Seq[*delegation.Bundle] { + return func(yield func(*delegation.Bundle) bool) { + for _, bundle := range p.dlgs { + if !yield(bundle) { + return + } + } + } + }, issuer, cmd, subject) +} + +func (p *Pool) GetBundles(cids []cid.Cid) iter.Seq2[*delegation.Bundle, error] { + p.mu.RLock() + defer p.mu.RUnlock() + + return func(yield func(*delegation.Bundle, error) bool) { + for _, c := range cids { + if b, ok := p.dlgs[c]; ok { + if !yield(b, nil) { + return + } + } else { + yield(nil, fmt.Errorf("bundle not found")) + return + } + } + } +} + +// trim removes expired tokens +func (p *Pool) trim() { + p.mu.Lock() + defer p.mu.Unlock() + + now := time.Now() + for c, bundle := range p.dlgs { + if bundle.Decoded.Expiration() != nil && bundle.Decoded.Expiration().Before(now) { + delete(p.dlgs, c) + } + } +} diff --git a/toolkit/client/proof.go b/toolkit/client/proof.go new file mode 100644 index 0000000..14d557d --- /dev/null +++ b/toolkit/client/proof.go @@ -0,0 +1,98 @@ +package client + +import ( + "iter" + "math" + + "github.com/MetaMask/go-did-it" + "github.com/ipfs/go-cid" + + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/token/delegation" +) + +// FindProof find in the pool the best (shortest, smallest in bytes) chain of delegation(s) matching the given invocation parameters. +// - issuer: the DID of the client, also the issuer of the invocation token +// - cmd: the command to execute +// - subject: the DID of the resource to operate on, also the subject (or audience if defined) of the invocation token +// The returned delegation chain is ordered starting from the leaf (the one matching the invocation) to the root +// (the one given by the service). +// Note: you can read it as "(issuer) wants to do (cmd) on (subject)". +// Note: the returned delegation(s) don't have to match exactly the parameters, as long as they allow them. +// Note: the implemented algorithm won't perform well with a large number of delegations. +func FindProof(dlgs func() iter.Seq[*delegation.Bundle], issuer did.DID, cmd command.Command, subject did.DID) []cid.Cid { + // TODO: maybe that should be part of delegation.Token directly? + dlgMatch := func(dlg *delegation.Token, issuer did.DID, cmd command.Command, subject did.DID) bool { + // The Subject of each delegation must equal the invocation's Subject (or Audience if defined). - 4f + if !dlg.Subject().Equal(subject) { + return false + } + // The first proof must be issued to the Invoker (audience DID). - 4c + // The Issuer of each delegation must be the Audience in the next one. - 4d + if !dlg.Audience().Equal(issuer) { + return false + } + // The command of each delegation must "allow" the one before it. - 4g + if !dlg.Command().Covers(cmd) { + return false + } + // Time bound - 3b, 3c + if !dlg.IsValidNow() { + return false + } + return true + } + + // STEP 1: Find the possible leaf delegations, directly matching the invocation parameters + var candidateLeaf []*delegation.Bundle + + for bundle := range dlgs() { + if !dlgMatch(bundle.Decoded, issuer, cmd, subject) { + continue + } + candidateLeaf = append(candidateLeaf, bundle) + } + + // STEP 2: Perform a depth-first search on the DAG of connected delegations, for each of our candidates + type state struct { + bundle *delegation.Bundle + path []cid.Cid + size int + } + + var bestSize = math.MaxInt + var bestProof []cid.Cid + + for _, leaf := range candidateLeaf { + var stack = []state{{bundle: leaf, path: []cid.Cid{leaf.Cid}, size: len(leaf.Sealed)}} + + for len(stack) > 0 { + // dequeue a delegation + cur := stack[len(stack)-1] + stack = stack[:len(stack)-1] + at := cur.bundle + + // if it's a root delegation, we found a valid proof + if at.Decoded.Issuer().Equal(at.Decoded.Subject()) { + if len(bestProof) == 0 || len(cur.path) < len(bestProof) || len(cur.path) == len(bestProof) && cur.size < bestSize { + bestProof = append([]cid.Cid{}, cur.path...) // make a copy + bestSize = cur.size + continue + } + } + + // find parent delegation for our current delegation + for candidate := range dlgs() { + if !dlgMatch(candidate.Decoded, at.Decoded.Issuer(), at.Decoded.Command(), subject) { + continue + } + + newPath := append([]cid.Cid{}, cur.path...) // make a copy + newPath = append(newPath, candidate.Cid) + stack = append(stack, state{bundle: candidate, path: newPath, size: cur.size + len(candidate.Sealed)}) + } + } + } + + return bestProof +} diff --git a/toolkit/client/proof_test.go b/toolkit/client/proof_test.go new file mode 100644 index 0000000..c729418 --- /dev/null +++ b/toolkit/client/proof_test.go @@ -0,0 +1,39 @@ +package client + +import ( + "iter" + "testing" + + "github.com/MetaMask/go-did-it/didtest" + "github.com/stretchr/testify/require" + + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/token/delegation" + "github.com/ucan-wg/go-ucan/token/delegation/delegationtest" +) + +func TestFindProof(t *testing.T) { + dlgs := func() iter.Seq[*delegation.Bundle] { + return func(yield func(*delegation.Bundle) bool) { + for _, bundle := range delegationtest.AllBundles { + if !yield(&bundle) { + return + } + } + } + } + + require.Equal(t, delegationtest.ProofAliceBob, + FindProof(dlgs, didtest.PersonaBob.DID(), delegationtest.NominalCommand, didtest.PersonaAlice.DID())) + require.Equal(t, delegationtest.ProofAliceBobCarol, + FindProof(dlgs, didtest.PersonaCarol.DID(), delegationtest.NominalCommand, didtest.PersonaAlice.DID())) + require.Equal(t, delegationtest.ProofAliceBobCarolDan, + FindProof(dlgs, didtest.PersonaDan.DID(), delegationtest.NominalCommand, didtest.PersonaAlice.DID())) + require.Equal(t, delegationtest.ProofAliceBobCarolDanErin, + FindProof(dlgs, didtest.PersonaErin.DID(), delegationtest.NominalCommand, didtest.PersonaAlice.DID())) + require.Equal(t, delegationtest.ProofAliceBobCarolDanErinFrank, + FindProof(dlgs, didtest.PersonaFrank.DID(), delegationtest.NominalCommand, didtest.PersonaAlice.DID())) + + // wrong command + require.Empty(t, FindProof(dlgs, didtest.PersonaBob.DID(), command.New("foo"), didtest.PersonaAlice.DID())) +} diff --git a/toolkit/client/requester.go b/toolkit/client/requester.go new file mode 100644 index 0000000..3e683e5 --- /dev/null +++ b/toolkit/client/requester.go @@ -0,0 +1,53 @@ +package client + +import ( + "context" + "iter" + "time" + + "github.com/MetaMask/go-did-it" + "github.com/avast/retry-go/v4" + + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/token/delegation" +) + +type DelegationRequester interface { + // RequestDelegation retrieve a delegation or chain of delegation for the given parameters. + // - cmd: the command to execute + // - audience: the DID of the client, also the issuer of the invocation token + // - subject: the DID of the resource to operate on, also the subject (or audience if defined) of the invocation token + // The returned delegations MUST be ordered starting from the leaf (the one matching the invocation) to the root + // (the one given by the service). + // Note: you can read it as "(audience) wants to do (cmd) on (subject)". + // Note: the returned delegation(s) don't have to match exactly the parameters, as long as they allow them. + RequestDelegation(ctx context.Context, audience did.DID, cmd command.Command, subject did.DID) (iter.Seq2[*delegation.Bundle, error], error) +} + +var _ DelegationRequester = &withRetry{} + +type withRetry struct { + requester DelegationRequester + initialDelay time.Duration + maxAttempts uint +} + +// RequesterWithRetry wraps a DelegationRequester to perform exponential backoff, +// with an initial delay and a maximum attempt count. +func RequesterWithRetry(requester DelegationRequester, initialDelay time.Duration, maxAttempt uint) DelegationRequester { + return &withRetry{ + requester: requester, + initialDelay: initialDelay, + maxAttempts: maxAttempt, + } +} + +func (w withRetry) RequestDelegation(ctx context.Context, audience did.DID, cmd command.Command, subject did.DID) (iter.Seq2[*delegation.Bundle, error], error) { + return retry.DoWithData(func() (iter.Seq2[*delegation.Bundle, error], error) { + return w.requester.RequestDelegation(ctx, audience, cmd, subject) + }, + retry.Context(ctx), + retry.Delay(w.initialDelay), + retry.Attempts(w.maxAttempts), + ) +} diff --git a/toolkit/issuer/http_wrapper.go b/toolkit/issuer/http_wrapper.go new file mode 100644 index 0000000..e9fa220 --- /dev/null +++ b/toolkit/issuer/http_wrapper.go @@ -0,0 +1,94 @@ +package issuer + +import ( + "fmt" + "io" + "iter" + "net/http" + + "github.com/MetaMask/go-did-it" + + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/pkg/container" + "github.com/ucan-wg/go-ucan/token/delegation" + "github.com/ucan-wg/go-ucan/toolkit/client" +) + +type RequestResolver func(r *http.Request) (*ResolvedRequest, error) + +type ResolvedRequest struct { + Audience did.DID + Cmd command.Command + Subject did.DID +} + +// HttpWrapper implements an HTTP transport for a UCAN issuer. +// It provides a common response shape, while allowing customisation of the request. +func HttpWrapper(requester client.DelegationRequester, resolver RequestResolver) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + resolved, err := resolver(r) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + dlgs, err := requester.RequestDelegation(r.Context(), resolved.Audience, resolved.Cmd, resolved.Subject) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + cont := container.NewWriter() + for bundle, err := range dlgs { + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + cont.AddSealed(bundle.Sealed) + } + + err = cont.ToBytesGzippedWriter(w) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) +} + +func DecodeResponse(res *http.Response, audience did.DID, cmd command.Command, subject did.DID) (iter.Seq2[*delegation.Bundle, error], error) { + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + msg, err := io.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("request failed with status %d, then failed to read response body: %w", res.StatusCode, err) + } + return nil, fmt.Errorf("request failed with status %d: %s", res.StatusCode, msg) + } + cont, err := container.FromReader(res.Body) + if err != nil { + return nil, err + } + + // the container doesn't guarantee the ordering, so we must order the delegation in a chain + proof := client.FindProof(func() iter.Seq[*delegation.Bundle] { + return cont.GetAllDelegations() + }, audience, cmd, subject) + + return func(yield func(*delegation.Bundle, error) bool) { + for _, c := range proof { + bndl, err := cont.GetDelegationBundle(c) + if err != nil { + yield(nil, err) + return + } + if !yield(bndl, nil) { + return + } + } + }, nil +} diff --git a/toolkit/issuer/root_issuer.go b/toolkit/issuer/root_issuer.go new file mode 100644 index 0000000..fe77691 --- /dev/null +++ b/toolkit/issuer/root_issuer.go @@ -0,0 +1,76 @@ +package issuer + +import ( + "context" + "fmt" + "iter" + + "github.com/MetaMask/go-did-it" + didkeyctl "github.com/MetaMask/go-did-it/controller/did-key" + "github.com/MetaMask/go-did-it/crypto" + + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/token/delegation" + "github.com/ucan-wg/go-ucan/toolkit/client" +) + +// RootIssuingLogic is a function that decides what powers are given to a client. +// - issuer: the DID of our issuer +// - audience: the DID of the client, also the issuer of the invocation token +// - cmd: the command to execute +// Note: you can read it as "(audience) wants to do (cmd) on (subject)". +// Note: you can decide to match the input parameters exactly or issue a broader power, as long as it allows the +// expected action. If you don't want to give that power, return an error instead. +type RootIssuingLogic func(iss did.DID, aud did.DID, cmd command.Command) (*delegation.Token, error) + +var _ client.DelegationRequester = &RootIssuer{} + +// RootIssuer is an implementation of a root delegation issuer. +// Note: Your actual needs for an issuer can easily be different (caching...) than the choices made here. +// Feel free to replace this component with your own flavor. +type RootIssuer struct { + did did.DID + privKey crypto.PrivateKeySigningBytes + + logic RootIssuingLogic +} + +func NewRootIssuer(privKey crypto.PrivateKeySigningBytes, logic RootIssuingLogic) (*RootIssuer, error) { + d := didkeyctl.FromPrivateKey(privKey) + return &RootIssuer{ + did: d, + privKey: privKey, + logic: logic, + }, nil +} + +func (r *RootIssuer) RequestDelegation(ctx context.Context, audience did.DID, cmd command.Command, subject did.DID) (iter.Seq2[*delegation.Bundle, error], error) { + if !subject.Equal(r.did) { + return nil, fmt.Errorf("subject DID doesn't match the issuer DID") + } + + // run the custom logic to get what we actually issue + dlg, err := r.logic(r.did, audience, cmd) + if err != nil { + return nil, err + } + if !dlg.IsRoot() { + return nil, fmt.Errorf("issuing logic should return a root delegation") + } + + // sign and cache the new token + dlgBytes, dlgCid, err := dlg.ToSealed(r.privKey) + if err != nil { + return nil, err + } + bundle := &delegation.Bundle{ + Cid: dlgCid, + Decoded: dlg, + Sealed: dlgBytes, + } + + // output the root delegation + return func(yield func(*delegation.Bundle, error) bool) { + yield(bundle, nil) + }, nil +} diff --git a/toolkit/server/bearer/bearer.go b/toolkit/server/bearer/bearer.go new file mode 100644 index 0000000..8328330 --- /dev/null +++ b/toolkit/server/bearer/bearer.go @@ -0,0 +1,64 @@ +package bearer + +import ( + "fmt" + "net/http" + "strings" + + "github.com/ucan-wg/go-ucan/pkg/container" +) + +var ErrNoUcan = fmt.Errorf("no ucan") +var ErrContainerMalformed = fmt.Errorf("malformed container") + +// ExtractBearerContainer extract a full UCAN container from an HTTP "Authorization: Bearer" header. +// It can return: +// - ErrNoUcan if no such HTTP header is found +// - ErrContainerMalformed if the container or token can't be decoded or if a token is invalid (bad signature ...) +func ExtractBearerContainer(h http.Header) (container.Reader, error) { + header := h.Get("Authorization") + if header == "" { + return nil, ErrNoUcan + } + + if !strings.HasPrefix(header, "Bearer ") { + return nil, ErrNoUcan + } + + // skip prefix + reader := strings.NewReader(header[len("Bearer "):]) + + // read container from any supported format + ctn, err := container.FromReader(reader) + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrContainerMalformed, err) + } + + return ctn, nil +} + +// AddBearerContainer adds the given UCAN container into an `Authorization: Bearer` HTTP header. +// +// You should use this with HTTP/2 or HTTP/3, as both of those already compress their header. +func AddBearerContainer(h http.Header, container container.Writer) error { + str, err := container.ToBase64StdPadding() + if err != nil { + return err + } + + h.Set("Authorization", fmt.Sprintf("Bearer %s", str)) + return nil +} + +// AddBearerContainerCompressed adds the given UCAN container into an `Authorization: Bearer` HTTP header. +// +// You should use this with HTTP/1, as it doesn't compress its headers. +func AddBearerContainerCompressed(h http.Header, container container.Writer) error { + str, err := container.ToBase64StdPaddingGzipped() + if err != nil { + return err + } + + h.Set("Authorization", fmt.Sprintf("Bearer %s", str)) + return nil +} diff --git a/toolkit/server/bearer/bearer_test.go b/toolkit/server/bearer/bearer_test.go new file mode 100644 index 0000000..23c5b03 --- /dev/null +++ b/toolkit/server/bearer/bearer_test.go @@ -0,0 +1,34 @@ +package bearer + +import ( + "net/http" + "testing" + + _ "github.com/MetaMask/go-did-it/verifiers/did-key" + "github.com/stretchr/testify/require" + + "github.com/ucan-wg/go-ucan/pkg/container" + "github.com/ucan-wg/go-ucan/pkg/container/containertest" +) + +func TestHTTPBearer(t *testing.T) { + for _, fn := range []func(h http.Header, container container.Writer) error{ + AddBearerContainer, + AddBearerContainerCompressed, + } { + r, err := http.NewRequest(http.MethodPost, "/foo/bar", nil) + require.NoError(t, err) + + cont, err := container.FromBytes(containertest.Bytes) + require.NoError(t, err) + + err = fn(r.Header, cont.ToWriter()) + require.NoError(t, err) + + contRead, err := ExtractBearerContainer(r.Header) + require.NoError(t, err) + + require.NotEmpty(t, contRead) + require.Equal(t, cont, contRead) + } +} diff --git a/toolkit/server/exectx/ctxvalue.go b/toolkit/server/exectx/ctxvalue.go new file mode 100644 index 0000000..115289f --- /dev/null +++ b/toolkit/server/exectx/ctxvalue.go @@ -0,0 +1,16 @@ +package exectx + +import "context" + +type ctxKey struct{} + +// AddUcanCtxToContext insert a UcanCtx into a go context. +func AddUcanCtxToContext(ctx context.Context, ucanCtx *UcanCtx) context.Context { + return context.WithValue(ctx, ctxKey{}, ucanCtx) +} + +// FromContext retrieve a UcanCtx from a go context. +func FromContext(ctx context.Context) (*UcanCtx, bool) { + ucanCtx, ok := ctx.Value(ctxKey{}).(*UcanCtx) + return ucanCtx, ok +} diff --git a/toolkit/server/exectx/middlewares.go b/toolkit/server/exectx/middlewares.go new file mode 100644 index 0000000..b46969f --- /dev/null +++ b/toolkit/server/exectx/middlewares.go @@ -0,0 +1,87 @@ +package exectx + +import ( + "errors" + "net/http" + + "github.com/MetaMask/go-did-it" + + "github.com/ucan-wg/go-ucan/toolkit/server/bearer" +) + +// ExtractMW returns an HTTP middleware tasked with: +// - extracting UCAN credentials from the `Authorization: Bearer ` HTTP header +// - performing basic checks, and returning HTTP errors if necessary +// - verify that the invocation targets our service +// - exposing those credentials in the go context as a UcanCtx for further usage +func ExtractMW(next http.Handler, serviceDID did.DID) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctn, err := bearer.ExtractBearerContainer(r.Header) + if errors.Is(err, bearer.ErrNoUcan) { + http.Error(w, "no UCAN auth", http.StatusUnauthorized) + return + } + if errors.Is(err, bearer.ErrContainerMalformed) { + http.Error(w, "malformed token", http.StatusBadRequest) + return + } + if err != nil { + // should not happen, defensive programming + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // prepare a UcanCtx from the container, for further evaluation in the server pipeline + ucanCtx, err := FromContainer(ctn) + if err != nil { + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + + if !ucanCtx.Invocation().Subject().Equal(serviceDID) { + http.Error(w, "UCAN delegation doesn't match the service DID", http.StatusUnauthorized) + return + } + + // insert into the go context + r = r.WithContext(AddUcanCtxToContext(r.Context(), ucanCtx)) + + next.ServeHTTP(w, r) + }) +} + +// HttpExtArgsVerify returns an HTTP middleware tasked with verifying the UCAN policies applying on the HTTP request. +func HttpExtArgsVerify(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ucanCtx, ok := FromContext(r.Context()) + if !ok { + http.Error(w, "no ucan-ctx found", http.StatusInternalServerError) + return + } + + if err := ucanCtx.VerifyHttp(r); err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + + next.ServeHTTP(w, r) + }) +} + +// EnforceMW returns an HTTP middleware tasked with the final verification of the UCAN policies. +func EnforceMW(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ucanCtx, ok := FromContext(r.Context()) + if !ok { + http.Error(w, "no ucan-ctx found", http.StatusInternalServerError) + return + } + + if err := ucanCtx.ExecutionAllowed(); err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + + next.ServeHTTP(w, r) + }) +} diff --git a/toolkit/server/exectx/middlewares_test.go b/toolkit/server/exectx/middlewares_test.go new file mode 100644 index 0000000..08c6865 --- /dev/null +++ b/toolkit/server/exectx/middlewares_test.go @@ -0,0 +1,129 @@ +package exectx + +import ( + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/MetaMask/go-did-it/didtest" + "github.com/ipfs/go-cid" + "github.com/stretchr/testify/require" + + "github.com/ucan-wg/go-ucan/pkg/container" + "github.com/ucan-wg/go-ucan/token/delegation" + "github.com/ucan-wg/go-ucan/token/invocation" +) + +func TestExtractMW(t *testing.T) { + const service = didtest.PersonaAlice + const client = didtest.PersonaBob + const cmd = "/foo/bar" + + for _, tc := range []struct { + name string + addHeaderFn func(func(key string, value string)) + expectedStatusCode int + successful bool + }{ + { + name: "no auth", + addHeaderFn: func(f func(key string, value string)) {}, + expectedStatusCode: http.StatusUnauthorized, + successful: false, + }, + { + name: "wrong kind of auth", + addHeaderFn: func(f func(key string, value string)) { + f("Authorization", "Basic foobar") + }, + expectedStatusCode: http.StatusUnauthorized, + successful: false, + }, + { + name: "invalid container", + addHeaderFn: func(f func(key string, value string)) { + f("Authorization", "Bearer foobar") + }, + expectedStatusCode: http.StatusBadRequest, + successful: false, + }, + { + name: "valid containter, for incorrect service", + addHeaderFn: func(f func(key string, value string)) { + cont := container.NewWriter() + + const service2 = didtest.PersonaCarol + + dlg, _ := delegation.Root(service2.DID(), client.DID(), cmd, nil) + dlgByte, dlgCid, _ := dlg.ToSealed(service2.PrivKey()) + cont.AddSealed(dlgByte) + + inv, _ := invocation.New(client.DID(), cmd, service2.DID(), []cid.Cid{dlgCid}) + invBytes, _, _ := inv.ToSealed(client.PrivKey()) + cont.AddSealed(invBytes) + + contB64, _ := cont.ToBase64StdPadding() + + f("Authorization", "Bearer "+contB64) + }, + expectedStatusCode: http.StatusUnauthorized, + successful: false, + }, + { + name: "valid containter, missing invocation", + addHeaderFn: func(f func(key string, value string)) { + cont := container.NewWriter() + + dlg, _ := delegation.Root(service.DID(), client.DID(), cmd, nil) + dlgByte, _, _ := dlg.ToSealed(service.PrivKey()) + cont.AddSealed(dlgByte) + + contB64, _ := cont.ToBase64StdPadding() + + f("Authorization", "Bearer "+contB64) + }, + expectedStatusCode: http.StatusUnauthorized, + successful: false, + }, + { + name: "valid containter, valid tokens", + addHeaderFn: func(f func(key string, value string)) { + cont := container.NewWriter() + + dlg, _ := delegation.Root(service.DID(), client.DID(), cmd, nil) + dlgByte, dlgCid, _ := dlg.ToSealed(service.PrivKey()) + cont.AddSealed(dlgByte) + + inv, _ := invocation.New(client.DID(), cmd, service.DID(), []cid.Cid{dlgCid}) + invBytes, _, _ := inv.ToSealed(client.PrivKey()) + cont.AddSealed(invBytes) + + contB64, _ := cont.ToBase64StdPadding() + + f("Authorization", "Bearer "+contB64) + }, + expectedStatusCode: http.StatusOK, + successful: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, has := FromContext(r.Context()) + require.Equal(t, tc.successful, has) + + _, _ = io.WriteString(w, "OK") + }) + handler = ExtractMW(handler, service.DID()) + + req := httptest.NewRequest("GET", "https://example.com/foo", nil) + tc.addHeaderFn(req.Header.Set) + + w := httptest.NewRecorder() + handler.ServeHTTP(w, req) + + require.Equal(t, tc.expectedStatusCode, w.Code) + + }) + } +} diff --git a/toolkit/server/exectx/ucanctx.go b/toolkit/server/exectx/ucanctx.go new file mode 100644 index 0000000..11d99d0 --- /dev/null +++ b/toolkit/server/exectx/ucanctx.go @@ -0,0 +1,178 @@ +package exectx + +import ( + "errors" + "fmt" + "net/http" + "slices" + + "github.com/ipfs/go-cid" + "github.com/ipld/go-ipld-prime/datamodel" + + "github.com/ucan-wg/go-ucan/pkg/args" + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/pkg/container" + "github.com/ucan-wg/go-ucan/pkg/meta" + "github.com/ucan-wg/go-ucan/pkg/policy" + "github.com/ucan-wg/go-ucan/token/delegation" + "github.com/ucan-wg/go-ucan/token/invocation" + "github.com/ucan-wg/go-ucan/toolkit/server/extargs" +) + +var _ delegation.Loader = &UcanCtx{} + +// UcanCtx is a UCAN execution context meant to be inserted in the go context while handling a request. +// It allows to handle the control of the execution in multiple steps across different middlewares, +// as well as doing "bearer" types of controls, when arguments are derived from the request itself (HTTP, JsonRpc). +type UcanCtx struct { + inv *invocation.Token + dlgs map[cid.Cid]*delegation.Token + + policies policy.Policy // all policies combined + meta *meta.Meta // all meta combined, with no overwriting + + // argument sources + http *extargs.HttpExtArgs + custom map[string]*extargs.CustomExtArgs +} + +// FromContainer prepare a UcanCtx from a UCAN container, for further evaluation in a server pipeline. +// It is expected that the container holds a single invocation and the matching delegations. If not, +// an error is returned. +func FromContainer(cont container.Reader) (*UcanCtx, error) { + inv, err := cont.GetInvocation() + if err != nil { + return nil, fmt.Errorf("no invocation: %w", err) + } + + ctx := &UcanCtx{ + inv: inv, + dlgs: make(map[cid.Cid]*delegation.Token, len(cont)-1), + meta: meta.NewMeta(), + } + + // iterate in reverse, from the root delegation to the leaf + for _, c := range slices.Backward(inv.Proof()) { + // make sure we have the delegation + dlg, err := cont.GetDelegation(c) + if errors.Is(err, delegation.ErrDelegationNotFound) { + return nil, fmt.Errorf("delegation proof %s is missing", c) + } + if err != nil { + return nil, err + } + ctx.dlgs[c] = dlg + // accumulate the policies + ctx.policies = append(ctx.policies, dlg.Policy()...) + // accumulate the meta values, with no overwriting + ctx.meta.Include(dlg.Meta()) + } + + // DX: As the invocation is created without the delegation, no check is done that the proof chain (CIDs only) + // is ordered properly and not broken. We don't check that in the container either as it doesn't make any assumption + // on what is being carried around. That UcanCtx is the first place where we enforce having a single invocation and + // only the matching delegation. + // For sanity, we verify that the proofs are ordered properly. This will be checked later anyway, but it's cheap to + // verify here and catch an easy mistake. + chainTo := inv.Issuer() + for _, c := range inv.Proof() { + dlg := ctx.dlgs[c] + if !dlg.Audience().Equal(chainTo) { + return nil, fmt.Errorf("proof chain is broken or not ordered correctly") + } + chainTo = dlg.Issuer() + } + + return ctx, nil +} + +// Command returns the command triggered by the invocation. +func (ctn *UcanCtx) Command() command.Command { + return ctn.inv.Command() +} + +// Invocation returns the invocation.Token. +func (ctn *UcanCtx) Invocation() *invocation.Token { + return ctn.inv +} + +// GetDelegation returns the delegation.Token matching the given CID. +// If not found, delegation.ErrDelegationNotFound is returned. +func (ctn *UcanCtx) GetDelegation(cid cid.Cid) (*delegation.Token, error) { + if dlg, ok := ctn.dlgs[cid]; ok { + return dlg, nil + } + return nil, delegation.ErrDelegationNotFound +} + +// GetRootDelegation returns the delegation.Token at the root of the proof chain. +func (ctn *UcanCtx) GetRootDelegation() *delegation.Token { + proofs := ctn.inv.Proof() + c := proofs[len(proofs)-1] + return ctn.dlgs[c] +} + +// Policies return the full set of policy statements to satisfy. +func (ctn *UcanCtx) Policies() policy.Policy { + return ctn.policies +} + +// Meta returns all the meta values from the delegations. +// They are accumulated from the root delegation to the leaf delegation, with no overwriting. +func (ctn *UcanCtx) Meta() meta.ReadOnly { + return ctn.meta.ReadOnly() +} + +// VerifyHttp verify the delegation's policies against arguments constructed from the HTTP request. +// These arguments will be set in the `.http` argument key, at the root. +// This function can only be called once per context. +// After being used, those constructed arguments will be used in ExecutionAllowed as well. +func (ctn *UcanCtx) VerifyHttp(req *http.Request) error { + if ctn.http != nil { + panic("only use once per request context") + } + ctn.http = extargs.NewHttpExtArgs(ctn.policies, ctn.inv.Arguments(), req) + return ctn.http.Verify() +} + +// VerifyCustom verify the delegation's policies against arbitrary arguments provider through an IPLD MapAssembler. +// These arguments will be set under the given argument key, at the root. +// This function can only be called once per context and key. +// After being used, those constructed arguments will be used in ExecutionAllowed as well. +func (ctn *UcanCtx) VerifyCustom(key string, assembler func(ma datamodel.MapAssembler)) error { + if ctn.custom == nil { + ctn.custom = make(map[string]*extargs.CustomExtArgs) + } + if _, ok := ctn.custom[key]; ok { + panic("only use once per request context and key") + } + ctn.custom[key] = extargs.NewCustomExtArgs(key, ctn.policies, assembler) + return ctn.custom[key].Verify() +} + +// ExecutionAllowed does the final verification of the invocation. +// If VerifyHttp or VerifyJsonRpc has been used, those arguments are part of the verification. +func (ctn *UcanCtx) ExecutionAllowed() error { + return ctn.inv.ExecutionAllowedWithArgsHook(ctn, func(args args.ReadOnly) (*args.Args, error) { + newArgs := args.WriteableClone() + + if ctn.http != nil { + httpArgs, err := ctn.http.Args() + if err != nil { + return nil, err + } + newArgs.Include(httpArgs) + } + if ctn.custom != nil { + for _, cea := range ctn.custom { + customArgs, err := cea.Args() + if err != nil { + return nil, err + } + newArgs.Include(customArgs) + } + } + + return newArgs, nil + }) +} diff --git a/toolkit/server/exectx/ucanctx_test.go b/toolkit/server/exectx/ucanctx_test.go new file mode 100644 index 0000000..7ae8a58 --- /dev/null +++ b/toolkit/server/exectx/ucanctx_test.go @@ -0,0 +1,234 @@ +package exectx_test + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/MetaMask/go-did-it/didtest" + "github.com/ipfs/go-cid" + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/fluent/qp" + "github.com/stretchr/testify/require" + + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/pkg/container" + "github.com/ucan-wg/go-ucan/pkg/policy" + "github.com/ucan-wg/go-ucan/pkg/policy/literal" + "github.com/ucan-wg/go-ucan/token/delegation" + "github.com/ucan-wg/go-ucan/token/invocation" + "github.com/ucan-wg/go-ucan/toolkit/server/exectx" +) + +const ( + network = "eth-mainnet" +) + +func TestUcanCtxFullFlow(t *testing.T) { + // let's use some pre-made DID+privkey. + // use go-ucan/did to generate or parse them. + service := didtest.PersonaAlice + user := didtest.PersonaBob + + // DELEGATION: the service gives some power to the user, in the form of a root UCAN token. + // The command defines the shape of the arguments on which the policies operate, it is specific to that service. + // Policies define what the user can do. + + cmd := command.New("foo") + pol := policy.MustConstruct( + // some basic HTTP constraints + policy.Equal(".http.method", literal.String("GET")), + policy.Like(".http.path", "/foo/*"), + // some custom constraints + // Network + policy.Equal(".custom.ntwk", literal.String(network)), + // Quota + policy.LessThanOrEqual(".custom.quota.ur", literal.Int(1234)), + ) + + dlg, err := delegation.Root(service.DID(), user.DID(), cmd, pol, + delegation.WithExpirationIn(24*time.Hour), + ) + require.NoError(t, err) + dlgBytes, dlgCid, err := dlg.ToSealed(service.PrivKey()) + require.NoError(t, err) + + // INVOCATION: the user leverages the delegation (power) to make a request. + + inv, err := invocation.New(user.DID(), cmd, service.DID(), []cid.Cid{dlgCid}, + invocation.WithExpirationIn(10*time.Minute), + invocation.WithArgument("myarg", "hello"), // we can specify invocation parameters + ) + require.NoError(t, err) + invBytes, _, err := inv.ToSealed(user.PrivKey()) + require.NoError(t, err) + + // PACKAGING: no obligation for the transport, but the user needs to give the service the invocation + // and all the proof delegations. We can use a container for that. + cont := container.NewWriter() + cont.AddSealed(dlgBytes) + cont.AddSealed(invBytes) + contBytes, err := cont.ToBase64StdPadding() + require.NoError(t, err) + + // MAKING A REQUEST: we pass the container in the Bearer HTTP header + + req, err := http.NewRequest(http.MethodGet, "/foo/bar", nil) + require.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+string(contBytes)) + + // SERVER: Auth middleware + // - decode the container + // - create the context + + authMw := func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Note: we obviously want something more robust, this is an example + // Note: if an error occur, we'll want to return an HTTP 401 Unauthorized + data := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") + cont, err := container.FromString(data) + require.NoError(t, err) + ucanCtx, err := exectx.FromContainer(cont) + require.NoError(t, err) + + // insert into the go context + r = r.WithContext(exectx.AddUcanCtxToContext(r.Context(), ucanCtx)) + + next.ServeHTTP(w, r) + }) + } + + // SERVER: http checks + + httpMw := func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ucanCtx, ok := exectx.FromContext(r.Context()) + require.True(t, ok) + + err := ucanCtx.VerifyHttp(r) + if err != nil { + // This will print something like: + // `the following UCAN policy is not satisfied: ["==", ".http.path", "/foo"]` + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + next.ServeHTTP(w, r) + }) + } + + // SERVER: custom args checks + + customArgsMw := func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ucanCtx, ok := exectx.FromContext(r.Context()) + require.True(t, ok) + err := ucanCtx.VerifyCustom("custom", func(ma datamodel.MapAssembler) { + qp.MapEntry(ma, "ntwk", qp.String(network)) + qp.MapEntry(ma, "quota", qp.Map(1, func(ma datamodel.MapAssembler) { + qp.MapEntry(ma, "ur", qp.Int(1234)) + })) + }) + require.NoError(t, err) + next.ServeHTTP(w, r) + }) + } + + // SERVER: final handler + + handler := func(w http.ResponseWriter, r *http.Request) { + ucanCtx, ok := exectx.FromContext(r.Context()) + require.True(t, ok) + + if err := ucanCtx.ExecutionAllowed(); err != nil { + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + + w.WriteHeader(http.StatusOK) + } + + sut := authMw(httpMw(customArgsMw(http.HandlerFunc(handler)))) + + rec := httptest.NewRecorder() + sut.ServeHTTP(rec, req) + require.Equal(t, http.StatusOK, rec.Code) +} + +func TestGoCtx(t *testing.T) { + ctx := context.Background() + + ucanCtx, ok := exectx.FromContext(ctx) + require.False(t, ok) + require.Nil(t, ucanCtx) + + expected := &exectx.UcanCtx{} + + ctx = exectx.AddUcanCtxToContext(ctx, expected) + + got, ok := exectx.FromContext(ctx) + require.True(t, ok) + require.Equal(t, expected, got) +} + +func TestUcanCtx(t *testing.T) { + const service = didtest.PersonaAlice + const client1 = didtest.PersonaBob + const client2 = didtest.PersonaCarol + const cmd = "/foo/bar" + + cont := container.NewWriter() + + pol1 := policy.MustConstruct( + policy.Equal(".http.scheme", literal.String("https")), + ) + dlg1, err := delegation.Root(service.DID(), client1.DID(), cmd, pol1, + delegation.WithMeta("foo", "bar"), + ) + require.NoError(t, err) + dlg1Byte, dlg1Cid, err := dlg1.ToSealed(service.PrivKey()) + require.NoError(t, err) + cont.AddSealed(dlg1Byte) + + pol2 := policy.MustConstruct( + policy.Equal(".http.method", literal.String("GET")), + ) + dlg2, err := delegation.New(client1.DID(), client2.DID(), cmd, pol2, service.DID(), + delegation.WithMeta("foo", "foo"), // attempt to replace + ) + require.NoError(t, err) + dlg2Byte, dlg2Cid, err := dlg2.ToSealed(client1.PrivKey()) + require.NoError(t, err) + cont.AddSealed(dlg2Byte) + + inv, err := invocation.New(client2.DID(), cmd, service.DID(), []cid.Cid{dlg2Cid, dlg1Cid}) + require.NoError(t, err) + invBytes, _, err := inv.ToSealed(client2.PrivKey()) + require.NoError(t, err) + cont.AddSealed(invBytes) + + ctx, err := exectx.FromContainer(cont.ToReader()) + require.NoError(t, err) + + require.NotNil(t, ctx.Invocation()) + require.Equal(t, cmd, ctx.Command().String()) + require.Equal(t, 1, ctx.Meta().Len()) + require.Equal(t, "bar", must(ctx.Meta().GetString("foo"))) + require.Equal(t, service.DID(), ctx.GetRootDelegation().Issuer()) + require.Equal(t, append(pol1, pol2...), ctx.Policies()) + + require.ErrorContains(t, ctx.ExecutionAllowed(), `the following UCAN policy is not satisfied: ["==", ".http.method", "GET"]`) + + r := httptest.NewRequest(http.MethodGet, "https://foo/bar", nil) + require.NoError(t, ctx.VerifyHttp(r)) + require.NoError(t, ctx.ExecutionAllowed()) +} + +func must[T any](e T, err error) T { + if err != nil { + panic(err) + } + return e +} diff --git a/toolkit/server/extargs/Readme.md b/toolkit/server/extargs/Readme.md new file mode 100644 index 0000000..f44472f --- /dev/null +++ b/toolkit/server/extargs/Readme.md @@ -0,0 +1,63 @@ +## Motivations + +UCAN is normally a pure RPC construct, when the entirety of the request's parameters is part of the invocation, in the form of `args`. Those `args` are evaluated against the delegation's [policy](https://github.com/ucan-wg/delegation/tree/v1_ipld?tab=readme-ov-file#policy) to determine if the request is allowed or not, then the request handling happens purely based on those args and the `command`. In that setup, the service would have a single entry point. + +Unfortunately, we live in a world of REST APIs, or JSON-RPC. Some adaptations or concessions need to be made. + +In this package, we cross the chasm of the pure UCAN world into our practical needs. This can, however, be done in a reasonable way. + +## Example + +Below is an example of `args` in Dag-Json format, where the values are recomposed server-side from the HTTP request: + +```json +{ + "http": { + "scheme": "https", + "method": "POST", + "host": "example.com", + "path": "" + }, + "custom": { + "foo": "bar" + } +} +``` +Those `args` can be evaluated against a delegation's policy, for example: +```json +{ + "cmd": "/foo/bar", + "pol": [ + ["==", ".http.host", "example.com"], + ["like", ".custom.foo", "ba*"] + ] +} +``` + +## Security implications + +UCAN essentially aims for perfect security. By having external args, we break that security perimeter, and we now need to arbitrate between security and practicality. + +First, what are we breaking exactly? Normally, the invocation has all the parameters and is signed by the invoker. This means that an attacker cannot intercept (MITM) a request and change the parameters when relaying it to the server. As they are signed, that would make the request invalid. + +If we have external args, now an attacker can intercept the request, change it, and pretend to be that person doing other things than intended. **That may of may not be an actual problem, depending on the situation.** + +There is a way to get around that, and have the best of both worlds, but **it comes with a client side complexity**: we can hash the external values and put them into the invocation's `args`. For example: + +```json +{ + "http": "zQmSnuWmxptJZdLJpKRarxBMS2Ju2oANVrgbr2xWbie9b2D", + "custom": "zQmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9" +} +``` + +When the server receives the request, it can now reconstruct the values from the HTTP request, verify the hash and replace it with the real values for evaluation against the policies. **We are now back to the better security model**, but at the price of client-side complexity. + +## API design + +Therefore, the server-side logic is made to have this hashing optional: +- if present, the server honors the hash and enforces the security +- the client can opt out of passing that hash, and won't benefit from the enforced security + +The particular hash selected is SHA2-256 of the DAG-CBOR encoded argument, expressed in the form of a Multihash in raw bytes. +The arguments being hashed are the complete map of values, including the root key being replaced (for example `http` or `custom` here). \ No newline at end of file diff --git a/toolkit/server/extargs/custom.go b/toolkit/server/extargs/custom.go new file mode 100644 index 0000000..51accd1 --- /dev/null +++ b/toolkit/server/extargs/custom.go @@ -0,0 +1,85 @@ +package extargs + +import ( + "fmt" + "sync" + + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/fluent/qp" + "github.com/ipld/go-ipld-prime/node/basicnode" + + "github.com/ucan-wg/go-ucan/pkg/args" + "github.com/ucan-wg/go-ucan/pkg/policy" +) + +type CustomExtArgs struct { + key string + pol policy.Policy + originalArgs args.ReadOnly + assembler func(ma datamodel.MapAssembler) + + once sync.Once + args *args.Args + argsIpld ipld.Node +} + +func NewCustomExtArgs(key string, pol policy.Policy, assembler func(ma datamodel.MapAssembler)) *CustomExtArgs { + return &CustomExtArgs{key: key, pol: pol, assembler: assembler} +} + +func (cea *CustomExtArgs) Verify() error { + if err := cea.makeArgs(); err != nil { + return err + } + + // Note: CustomExtArgs doesn't support verifying a hash computed client-side like the other + // external args, as the arguments are by nature dynamic. The client can't generate a meaningful hash. + + ok, leaf := cea.pol.PartialMatch(cea.argsIpld) + if !ok { + return fmt.Errorf("the following UCAN policy is not satisfied: %v", leaf.String()) + } + return nil +} + +func (cea *CustomExtArgs) Args() (*args.Args, error) { + if err := cea.makeArgs(); err != nil { + return nil, err + } + return cea.args, nil +} + +func (cea *CustomExtArgs) makeArgs() error { + var outerErr error + cea.once.Do(func() { + var err error + + cea.args, err = makeCustomArgs(cea.key, cea.assembler) + if err != nil { + outerErr = err + return + } + + cea.argsIpld, err = cea.args.ToIPLD() + if err != nil { + outerErr = err + return + } + }) + return outerErr +} + +func makeCustomArgs(key string, assembler func(ma datamodel.MapAssembler)) (*args.Args, error) { + n, err := qp.BuildMap(basicnode.Prototype.Any, -1, assembler) + if err != nil { + return nil, err + } + + res := args.New() + err = res.Add(key, n) + if err != nil { + return nil, err + } + return res, nil +} diff --git a/toolkit/server/extargs/custom_test.go b/toolkit/server/extargs/custom_test.go new file mode 100644 index 0000000..86d53cd --- /dev/null +++ b/toolkit/server/extargs/custom_test.go @@ -0,0 +1,115 @@ +package extargs + +import ( + "fmt" + "testing" + + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/fluent/qp" + "github.com/stretchr/testify/require" + + "github.com/ucan-wg/go-ucan/pkg/policy" + "github.com/ucan-wg/go-ucan/pkg/policy/literal" +) + +func ExampleCustomExtArgs() { + // Note: this is an example for how to build arguments, but you likely want to use CustomExtArgs + // through UcanCtx. + + pol := policy.Policy{} // policies from the delegations + + // We will construct the following args: + // "key": { + // "ntwk":"eth-mainnet", + // "quota":{ + // "ur":1234, + // "udc":1234, + // "arch":1234, + // "down":1234, + // "store":1234, + // "up":1234 + // } + // } + customArgs := NewCustomExtArgs("key", pol, func(ma datamodel.MapAssembler) { + qp.MapEntry(ma, "ntwk", qp.String("eth-mainnet")) + qp.MapEntry(ma, "quota", qp.Map(6, func(ma datamodel.MapAssembler) { + qp.MapEntry(ma, "ur", qp.Int(1234)) + qp.MapEntry(ma, "udc", qp.Int(1234)) + qp.MapEntry(ma, "arch", qp.Int(1234)) + qp.MapEntry(ma, "down", qp.Int(1234)) + qp.MapEntry(ma, "store", qp.Int(1234)) + qp.MapEntry(ma, "up", qp.Int(1234)) + })) + }) + + err := customArgs.Verify() + fmt.Println(err) + + // Output: + // +} + +func TestCustom(t *testing.T) { + assembler := func(ma datamodel.MapAssembler) { + qp.MapEntry(ma, "ntwk", qp.String("eth-mainnet")) + qp.MapEntry(ma, "quota", qp.Map(6, func(ma datamodel.MapAssembler) { + qp.MapEntry(ma, "ur", qp.Int(1234)) + qp.MapEntry(ma, "udc", qp.Int(1234)) + qp.MapEntry(ma, "arch", qp.Int(1234)) + qp.MapEntry(ma, "down", qp.Int(1234)) + qp.MapEntry(ma, "store", qp.Int(1234)) + qp.MapEntry(ma, "up", qp.Int(1234)) + })) + } + + tests := []struct { + name string + pol policy.Policy + expected bool + }{ + { + name: "no policies", + pol: policy.Policy{}, + expected: true, + }, + { + name: "matching args", + pol: policy.MustConstruct( + policy.Equal(".key.ntwk", literal.String("eth-mainnet")), + policy.LessThanOrEqual(".key.quota.ur", literal.Int(1234)), + ), + expected: true, + }, + { + name: "wrong network", + pol: policy.MustConstruct( + policy.Equal(".key.ntwk", literal.String("avalanche-fuji")), + policy.LessThanOrEqual(".key.quota.ur", literal.Int(1234)), + ), + expected: false, + }, + { + name: "unrespected quota", + pol: policy.MustConstruct( + policy.Equal(".key.ntwk", literal.String("eth-mainnet")), + policy.LessThanOrEqual(".key.quota.ur", literal.Int(100)), + ), + expected: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + extArgs := NewCustomExtArgs("key", tc.pol, assembler) + + _, err := extArgs.Args() + require.NoError(t, err) + + if tc.expected { + require.NoError(t, extArgs.Verify()) + } else { + require.Error(t, extArgs.Verify()) + } + }) + } +} diff --git a/toolkit/server/extargs/http.go b/toolkit/server/extargs/http.go new file mode 100644 index 0000000..7c354cd --- /dev/null +++ b/toolkit/server/extargs/http.go @@ -0,0 +1,165 @@ +// Package extargs adds external arguments to the invocation's arguments before the policy is evaluated. +package extargs + +import ( + "bytes" + "fmt" + "net/http" + "sync" + + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/codec/dagcbor" + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/fluent/qp" + "github.com/ipld/go-ipld-prime/node/basicnode" + "github.com/multiformats/go-multihash" + + "github.com/ucan-wg/go-ucan/pkg/args" + "github.com/ucan-wg/go-ucan/pkg/policy" + "github.com/ucan-wg/go-ucan/token/invocation" +) + +// HttpArgsKey is the key in the args, used for: +// - if it exists in the invocation, holds a hash of the args derived from the HTTP request +// - in the final args to be evaluated against the policies, holds the args derived from the HTTP request +const HttpArgsKey = "http" + +type HttpExtArgs struct { + pol policy.Policy + originalArgs args.ReadOnly + req *http.Request + + once sync.Once + args *args.Args + argsIpld ipld.Node +} + +func NewHttpExtArgs(pol policy.Policy, originalArgs args.ReadOnly, req *http.Request) *HttpExtArgs { + return &HttpExtArgs{pol: pol, originalArgs: originalArgs, req: req} +} + +func (hea *HttpExtArgs) Verify() error { + if err := hea.makeArgs(); err != nil { + return err + } + + if err := hea.verifyHash(); err != nil { + return err + } + + ok, leaf := hea.pol.PartialMatch(hea.argsIpld) + if !ok { + return fmt.Errorf("the following UCAN policy is not satisfied: %v", leaf.String()) + } + return nil +} + +func (hea *HttpExtArgs) Args() (*args.Args, error) { + if err := hea.makeArgs(); err != nil { + return nil, err + } + return hea.args, nil +} + +func (hea *HttpExtArgs) makeArgs() error { + var outerErr error + hea.once.Do(func() { + var err error + hea.args, err = makeHttpArgs(hea.req) + if err != nil { + outerErr = err + return + } + + hea.argsIpld, err = hea.args.ToIPLD() + if err != nil { + outerErr = err + return + } + }) + return outerErr +} + +func (hea *HttpExtArgs) verifyHash() error { + n, err := hea.originalArgs.GetNode(HttpArgsKey) + if err != nil { + // no hash found, nothing to verify + return nil + } + + mhBytes, err := n.AsBytes() + if err != nil { + return fmt.Errorf("http args hash should be bytes") + } + + data, err := ipld.Encode(hea.argsIpld, dagcbor.Encode) + if err != nil { + return fmt.Errorf("can't encode derived args in dag-cbor: %w", err) + } + + sum, err := multihash.Sum(data, multihash.SHA2_256, -1) + if err != nil { + return err + } + + if !bytes.Equal(mhBytes, sum) { + return fmt.Errorf("derived args from http request don't match the expected hash") + } + + return nil +} + +// MakeHttpHash compute the hash of the derived arguments from the HTTP request. +// If that hash is inserted at the HttpArgsKey key in the invocation arguments, +// this increases the security as the UCAN token cannot be used with a different +// HTTP request. +// For convenience, the hash is returned as a ready-to-use invocation argument. +func MakeHttpHash(req *http.Request) (invocation.Option, error) { + // Note: the hash is computed on the full IPLD args, including HttpArgsKey + computedArgs, err := makeHttpArgs(req) + if err != nil { + return nil, err + } + + n, err := computedArgs.ToIPLD() + if err != nil { + return nil, err + } + + data, err := ipld.Encode(n, dagcbor.Encode) + if err != nil { + return nil, err + } + + sum, err := multihash.Sum(data, multihash.SHA2_256, -1) + if err != nil { + return nil, err + } + + return invocation.WithArgument(HttpArgsKey, []byte(sum)), nil +} + +func makeHttpArgs(req *http.Request) (*args.Args, error) { + n, err := qp.BuildMap(basicnode.Prototype.Any, 4, func(ma datamodel.MapAssembler) { + qp.MapEntry(ma, "scheme", qp.String(req.URL.Scheme)) // https + qp.MapEntry(ma, "method", qp.String(req.Method)) // GET + qp.MapEntry(ma, "host", qp.String(req.Host)) // example.com + qp.MapEntry(ma, "path", qp.String(req.URL.Path)) // /foo + + qp.MapEntry(ma, "headers", qp.Map(2, func(ma datamodel.MapAssembler) { + qp.MapEntry(ma, "Origin", qp.String(req.Header.Get("Origin"))) + qp.MapEntry(ma, "User-Agent", qp.String(req.Header.Get("User-Agent"))) + })) + }) + if err != nil { + return nil, err + } + + res := args.New() + err = res.Add(HttpArgsKey, n) + if err != nil { + return nil, err + } + + return res, nil +} diff --git a/toolkit/server/extargs/http_test.go b/toolkit/server/extargs/http_test.go new file mode 100644 index 0000000..3c1dbd6 --- /dev/null +++ b/toolkit/server/extargs/http_test.go @@ -0,0 +1,210 @@ +package extargs + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/MetaMask/go-did-it/didtest" + "github.com/multiformats/go-multihash" + "github.com/stretchr/testify/require" + + "github.com/ucan-wg/go-ucan/pkg/args" + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/pkg/policy" + "github.com/ucan-wg/go-ucan/pkg/policy/literal" + "github.com/ucan-wg/go-ucan/token/invocation" +) + +func TestHttp(t *testing.T) { + pol := policy.MustConstruct( + policy.Equal(".http.scheme", literal.String("http")), + policy.Equal(".http.method", literal.String("GET")), + policy.Equal(".http.host", literal.String("example.com")), + policy.Equal(".http.path", literal.String("/foo")), + policy.Like(".http.headers.User-Agent", "*Mozilla*"), + policy.Equal(".http.headers.Origin", literal.String("dapps.com")), + ) + + tests := []struct { + name string + method string + target string + headers map[string]string + expected bool + }{ + { + name: "happy path", + method: http.MethodGet, + target: "http://example.com/foo", + headers: map[string]string{ + "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0", + "Origin": "dapps.com", + }, + expected: true, + }, + { + name: "wrong scheme", + method: http.MethodGet, + target: "https://example.com/foo", + headers: map[string]string{ + "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0", + "Origin": "dapps.com", + }, + expected: false, + }, + { + name: "wrong method", + method: http.MethodPost, + target: "http://example.com/foo", + headers: map[string]string{ + "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0", + "Origin": "dapps.com", + }, + expected: false, + }, + { + name: "wrong host", + method: http.MethodGet, + target: "http://foo.com/foo", + headers: map[string]string{ + "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0", + "Origin": "dapps.com", + }, + expected: false, + }, + { + name: "wrong path", + method: http.MethodGet, + target: "http://example.com/bar", + headers: map[string]string{ + "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0", + "Origin": "dapps.com", + }, + expected: false, + }, + { + name: "wrong origin", + method: http.MethodGet, + target: "http://example.com/foo", + headers: map[string]string{ + "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0", + "Origin": "foo.com", + }, + expected: false, + }, + { + name: "wrong user-agent", + method: http.MethodGet, + target: "http://example.com/foo", + headers: map[string]string{ + "User-Agent": "Chrome/51.0.2704.103 Safari/537.36", + "Origin": "dapps.com", + }, + expected: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + handler := func(w http.ResponseWriter, r *http.Request) { + // we don't test the args hash here + emptyArgs := args.New().ReadOnly() + + ctx := NewHttpExtArgs(pol, emptyArgs, r) + + _, err := ctx.Args() + require.NoError(t, err) + + if tc.expected { + require.NoError(t, ctx.Verify()) + } else { + require.Error(t, ctx.Verify()) + } + } + + req := httptest.NewRequest(tc.method, tc.target, nil) + for k, v := range tc.headers { + req.Header.Set(k, v) + } + + w := httptest.NewRecorder() + handler(w, req) + }) + } +} + +func TestHttpHash(t *testing.T) { + servicePersona := didtest.PersonaAlice + clientPersona := didtest.PersonaBob + + req, err := http.NewRequest(http.MethodGet, "http://example.com/foo", nil) + require.NoError(t, err) + req.Header.Add("User-Agent", "Chrome/51.0.2704.103 Safari/537.36") + req.Header.Add("Origin", "dapps.com") + + pol := policy.MustConstruct( + policy.Equal(".http.scheme", literal.String("http")), + ) + + makeArg := func(data []byte, code uint64) invocation.Option { + mh, err := multihash.Sum(data, code, -1) + require.NoError(t, err) + return invocation.WithArgument(HttpArgsKey, []byte(mh)) + } + + tests := []struct { + name string + argOptions []invocation.Option + expected bool + }{ + { + name: "correct hash", + argOptions: []invocation.Option{must(MakeHttpHash(req))}, + expected: true, + }, + { + name: "non-matching hash", + argOptions: []invocation.Option{makeArg([]byte{1, 2, 3, 4}, multihash.SHA2_256)}, + expected: false, + }, + { + name: "wrong type of hash", + argOptions: []invocation.Option{makeArg([]byte{1, 2, 3, 4}, multihash.BLAKE3)}, + expected: false, + }, + { + name: "no hash", + argOptions: nil, + expected: true, // having a hash is not enforced + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + inv, err := invocation.New( + clientPersona.DID(), + command.MustParse("/foo"), + servicePersona.DID(), + nil, + tc.argOptions..., // inject hash argument, if any + ) + require.NoError(t, err) + + ctx := NewHttpExtArgs(pol, inv.Arguments(), req) + + if tc.expected { + require.NoError(t, ctx.Verify()) + } else { + require.Error(t, ctx.Verify()) + } + }) + } +} + +func must[T any](t T, err error) T { + if err != nil { + panic(err) + } + return t +}