refactor(ucan): migrate away from MPC-based signing in favor of @sonr.io/crypto/mpc
This commit is contained in:
20
go.mod
20
go.mod
@@ -3,31 +3,27 @@ module enclave
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/Oudwins/zog v0.22.0
|
||||
github.com/cosmos/cosmos-sdk v0.53.5
|
||||
github.com/extism/go-pdk v1.1.3
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/ipfs/go-cid v0.6.0
|
||||
github.com/libp2p/go-libp2p/core v0.43.0-rc2
|
||||
github.com/multiformats/go-multihash v0.2.3
|
||||
github.com/ipld/go-ipld-prime v0.21.0
|
||||
github.com/ncruces/go-sqlite3 v0.30.4
|
||||
github.com/sonr-io/crypto v1.0.1
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/ucan-wg/go-ucan v1.1.0
|
||||
golang.org/x/crypto v0.46.0
|
||||
lukechampine.com/blake3 v1.4.1
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/MetaMask/go-did-it v1.0.0-pre1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.24.3 // indirect
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect
|
||||
github.com/bwesterb/go-ristretto v1.2.3 // indirect
|
||||
github.com/consensys/gnark-crypto v0.19.0 // indirect
|
||||
github.com/cosmos/btcutil v1.0.5 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564 // indirect
|
||||
github.com/gtank/merlin v0.1.1 // indirect
|
||||
github.com/ipfs/go-cid v0.5.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 // indirect
|
||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||
@@ -35,14 +31,18 @@ require (
|
||||
github.com/multiformats/go-base32 v0.1.0 // indirect
|
||||
github.com/multiformats/go-base36 v0.2.0 // indirect
|
||||
github.com/multiformats/go-multibase v0.2.0 // indirect
|
||||
github.com/multiformats/go-multicodec v0.9.0 // indirect
|
||||
github.com/multiformats/go-multihash v0.2.3 // indirect
|
||||
github.com/multiformats/go-varint v0.1.0 // indirect
|
||||
github.com/ncruces/julianday v1.0.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/polydawn/refmt v0.89.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // indirect
|
||||
github.com/ucan-wg/go-varsig v1.0.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
lukechampine.com/blake3 v1.4.1 // indirect
|
||||
)
|
||||
|
||||
69
go.sum
69
go.sum
@@ -1,7 +1,8 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/Oudwins/zog v0.22.0 h1:HUJddjSQPyAp70m5toDDgaAVOMlJMQcjCTrjiO79bmA=
|
||||
github.com/Oudwins/zog v0.22.0/go.mod h1:c4ADJ2zNkJp37ZViNy1o3ZZoeMvO7UQVO7BaPtRoocg=
|
||||
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/bits-and-blooms/bitset v1.24.3 h1:Bte86SlO3lwPQqww+7BE9ZuUCKIjfqnG5jtEyqA9y9Y=
|
||||
github.com/bits-and-blooms/bitset v1.24.3/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ=
|
||||
@@ -10,28 +11,30 @@ github.com/bwesterb/go-ristretto v1.2.3 h1:1w53tCkGhCQ5djbat3+MH0BAQ5Kfgbt56UZQ/
|
||||
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/consensys/gnark-crypto v0.19.0 h1:zXCqeY2txSaMl6G5wFpZzMWJU9HPNh8qxPnYJ1BL9vA=
|
||||
github.com/consensys/gnark-crypto v0.19.0/go.mod h1:rT23F0XSZqE0mUA0+pRtnL56IbPxs6gp4CeRsBk4XS0=
|
||||
github.com/cosmos/btcutil v1.0.5 h1:t+ZFcX77LpKtDBhjucvnOH8C2l2ioGsBNEQ3jef8xFk=
|
||||
github.com/cosmos/btcutil v1.0.5/go.mod h1:IyB7iuqZMJlthe2tkIFL33xPyzbFYP0XVdS8P5lUPis=
|
||||
github.com/cosmos/cosmos-sdk v0.53.5 h1:JPue+SFn2gyDzTV9TYb8mGpuIH3kGt7WbGadulkpTcU=
|
||||
github.com/cosmos/cosmos-sdk v0.53.5/go.mod h1:AQJx0jpon70WAD4oOs/y+SlST4u7VIwEPR6F8S7JMdo=
|
||||
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.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||
github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564 h1:I6KUy4CI6hHjqnyJLNCEi7YHVMkwwtfSr2k9splgdSM=
|
||||
github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564/go.mod h1:yekO+3ZShy19S+bsmnERmznGy9Rfg6dWWWpiGJjNAz8=
|
||||
github.com/extism/go-pdk v1.1.3 h1:hfViMPWrqjN6u67cIYRALZTZLk/enSPpNKa+rZ9X2SQ=
|
||||
github.com/extism/go-pdk v1.1.3/go.mod h1:Gz+LIU/YCKnKXhgge8yo5Yu1F/lbv7KtKFkiCSzW/P4=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gtank/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is=
|
||||
github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s=
|
||||
github.com/ipfs/go-cid v0.6.0 h1:DlOReBV1xhHBhhfy/gBNNTSyfOM6rLiIx9J7A4DGf30=
|
||||
github.com/ipfs/go-cid v0.6.0/go.mod h1:NC4kS1LZjzfhK40UGmpXv5/qD2kcMzACYJNntCUiDhQ=
|
||||
github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg=
|
||||
github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk=
|
||||
github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH9C2E=
|
||||
github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
@@ -40,10 +43,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4=
|
||||
github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
|
||||
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
|
||||
github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg=
|
||||
github.com/libp2p/go-libp2p/core v0.43.0-rc2 h1:1X1aDJNWhMfodJ/ynbaGLkgnC8f+hfBIqQDrzxFZOqI=
|
||||
github.com/libp2p/go-libp2p/core v0.43.0-rc2/go.mod h1:NYeJ9lvyBv9nbDk2IuGb8gFKEOkIv/W5YRIy1pAJB2Q=
|
||||
github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 h1:hLDRPB66XQT/8+wG9WsDpiCvZf1yKO7sz7scAjSlBa0=
|
||||
github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM=
|
||||
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
||||
@@ -54,12 +53,10 @@ github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aG
|
||||
github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI=
|
||||
github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
|
||||
github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
|
||||
github.com/multiformats/go-multiaddr v0.16.0 h1:oGWEVKioVQcdIOBlYM8BH1rZDWOGJSqr9/BKl6zQ4qc=
|
||||
github.com/multiformats/go-multiaddr v0.16.0/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0=
|
||||
github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
|
||||
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
|
||||
github.com/multiformats/go-multicodec v0.9.1 h1:x/Fuxr7ZuR4jJV4Os5g444F7xC4XmyUaT/FWtE+9Zjo=
|
||||
github.com/multiformats/go-multicodec v0.9.1/go.mod h1:LLWNMtyV5ithSBUo3vFIMaeDy+h3EbkMTek1m+Fybbo=
|
||||
github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg=
|
||||
github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k=
|
||||
github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
|
||||
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
|
||||
github.com/multiformats/go-varint v0.1.0 h1:i2wqFp4sdl3IcIxfAonHQV9qU5OsZ4Ts9IOoETFs5dI=
|
||||
@@ -70,10 +67,19 @@ github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt
|
||||
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/polydawn/refmt v0.89.0 h1:ADJTApkvkeBZsN0tBTx8QjpD9JkmxbKp0cxfr9qszm4=
|
||||
github.com/polydawn/refmt v0.89.0/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
|
||||
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
||||
github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
|
||||
github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
|
||||
github.com/sonr-io/crypto v1.0.1 h1:pTsWbdvs8I8zTMalfCK7/ecCvFkBw9VIb/bKKGwMWGw=
|
||||
github.com/sonr-io/crypto v1.0.1/go.mod h1:f6YZo/FfbUQEEN8TMPAeFI8BOljbDNrui3IXuIzCa/E=
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
@@ -82,19 +88,30 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
|
||||
github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
|
||||
github.com/ucan-wg/go-ucan v1.1.0 h1:Z4RGSjJrpLN7S9u93Md036XbyYprloe1LUyDZe9rnWg=
|
||||
github.com/ucan-wg/go-ucan v1.1.0/go.mod h1:9Gnfx2XO5OCjL0PGipfDDgK423OAzyNEY+kJJQ5D4Qo=
|
||||
github.com/ucan-wg/go-varsig v1.0.0 h1:Hrc437Zg+B5Eoajg+qZQZI3Q3ocPyjlnp3/Bz9ZnlWw=
|
||||
github.com/ucan-wg/go-varsig v1.0.0/go.mod h1:Sakln6IPooDPH+ClQ0VvR09TuwUhHcfLqcPiPkMZGh0=
|
||||
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
|
||||
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 h1:bsqhLWFR6G6xiQcb+JoGqdKdRU6WzPWmK8E0jxTjzo4=
|
||||
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
package spec
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/sonr-io/crypto/mpc"
|
||||
)
|
||||
|
||||
// MPCSigningMethod implements the SigningMethod interface for MPC-based signing
|
||||
type MPCSigningMethod struct {
|
||||
Name string
|
||||
enclave mpc.Enclave
|
||||
}
|
||||
|
||||
// NewJWTSigningMethod creates a new MPC signing method with the given enclave
|
||||
func NewJWTSigningMethod(name string, enclave mpc.Enclave) *MPCSigningMethod {
|
||||
return &MPCSigningMethod{
|
||||
Name: name,
|
||||
enclave: enclave,
|
||||
}
|
||||
}
|
||||
|
||||
// WithEnclave sets the enclave for an existing signing method
|
||||
func (m *MPCSigningMethod) WithEnclave(enclave mpc.Enclave) *MPCSigningMethod {
|
||||
return &MPCSigningMethod{
|
||||
Name: m.Name,
|
||||
enclave: enclave,
|
||||
}
|
||||
}
|
||||
|
||||
// NewMPCSigningMethod is an alias for NewJWTSigningMethod for compatibility
|
||||
func NewMPCSigningMethod(name string, enclave mpc.Enclave) *MPCSigningMethod {
|
||||
return NewJWTSigningMethod(name, enclave)
|
||||
}
|
||||
|
||||
// Alg returns the signing method's name
|
||||
func (m *MPCSigningMethod) Alg() string {
|
||||
return m.Name
|
||||
}
|
||||
|
||||
// Verify verifies the signature using the MPC public key
|
||||
func (m *MPCSigningMethod) Verify(signingString string, signature []byte, key any) error {
|
||||
// Check if enclave is available
|
||||
if m.enclave == nil {
|
||||
return fmt.Errorf("MPC enclave not available for signature verification")
|
||||
}
|
||||
|
||||
// Decode the signature
|
||||
sig, err := base64.RawURLEncoding.DecodeString(string(signature))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode signature: %w", err)
|
||||
}
|
||||
|
||||
// Hash the signing string using SHA-256
|
||||
hasher := sha256.New()
|
||||
hasher.Write([]byte(signingString))
|
||||
digest := hasher.Sum(nil)
|
||||
|
||||
// Use MPC enclave to verify signature
|
||||
valid, err := m.enclave.Verify(digest, sig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to verify signature: %w", err)
|
||||
}
|
||||
|
||||
if !valid {
|
||||
return fmt.Errorf("signature verification failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sign signs the data using MPC
|
||||
func (m *MPCSigningMethod) Sign(signingString string, key any) ([]byte, error) {
|
||||
// Check if enclave is available
|
||||
if m.enclave == nil {
|
||||
return nil, fmt.Errorf("MPC enclave not available for signing")
|
||||
}
|
||||
|
||||
// Hash the signing string using SHA-256
|
||||
hasher := sha256.New()
|
||||
hasher.Write([]byte(signingString))
|
||||
digest := hasher.Sum(nil)
|
||||
|
||||
// Use MPC enclave to sign the digest
|
||||
sig, err := m.enclave.Sign(digest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign with MPC: %w", err)
|
||||
}
|
||||
|
||||
// Encode the signature as base64url
|
||||
encoded := base64.RawURLEncoding.EncodeToString(sig)
|
||||
return []byte(encoded), nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Register the MPC signing method factory
|
||||
jwt.RegisterSigningMethod("MPC256", func() jwt.SigningMethod {
|
||||
// This factory creates a new instance without enclave
|
||||
// The enclave will be provided when creating tokens
|
||||
return &MPCSigningMethod{
|
||||
Name: "MPC256",
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterMPCMethod registers an MPC signing method for the given algorithm name
|
||||
func RegisterMPCMethod(alg string) {
|
||||
jwt.RegisterSigningMethod(alg, func() jwt.SigningMethod {
|
||||
return &MPCSigningMethod{
|
||||
Name: alg,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,305 +0,0 @@
|
||||
package spec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/libp2p/go-libp2p/core/crypto"
|
||||
"github.com/sonr-io/crypto/keys"
|
||||
"github.com/sonr-io/crypto/mpc"
|
||||
"lukechampine.com/blake3"
|
||||
)
|
||||
|
||||
// KeyshareSource provides MPC-based UCAN token creation and validation
|
||||
type KeyshareSource interface {
|
||||
Address() string
|
||||
Issuer() string
|
||||
ChainCode() ([]byte, error)
|
||||
OriginToken() (*Token, error)
|
||||
SignData(data []byte) ([]byte, error)
|
||||
VerifyData(data []byte, sig []byte) (bool, error)
|
||||
Enclave() mpc.Enclave
|
||||
|
||||
// UCAN token creation methods
|
||||
NewOriginToken(
|
||||
audienceDID string,
|
||||
att []Attenuation,
|
||||
fct []Fact,
|
||||
notBefore, expires time.Time,
|
||||
) (*Token, error)
|
||||
NewAttenuatedToken(
|
||||
parent *Token,
|
||||
audienceDID string,
|
||||
att []Attenuation,
|
||||
fct []Fact,
|
||||
nbf, exp time.Time,
|
||||
) (*Token, error)
|
||||
}
|
||||
|
||||
// NewSource creates a new MPC-based keyshare source from an enclave
|
||||
func NewSource(enclave mpc.Enclave) (KeyshareSource, error) {
|
||||
if !enclave.IsValid() {
|
||||
return nil, fmt.Errorf("invalid MPC enclave provided")
|
||||
}
|
||||
|
||||
pubKeyBytes := enclave.PubKeyBytes()
|
||||
issuerDID, addr, err := getIssuerDIDFromBytes(pubKeyBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to derive issuer DID: %w", err)
|
||||
}
|
||||
|
||||
return &mpcKeyshareSource{
|
||||
enclave: enclave,
|
||||
issuerDID: issuerDID,
|
||||
addr: addr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// mpcKeyshareSource implements KeyshareSource using MPC enclave
|
||||
type mpcKeyshareSource struct {
|
||||
enclave mpc.Enclave
|
||||
issuerDID string
|
||||
addr string
|
||||
}
|
||||
|
||||
// Address returns the address derived from the enclave public key
|
||||
func (k *mpcKeyshareSource) Address() string {
|
||||
return k.addr
|
||||
}
|
||||
|
||||
// Issuer returns the DID of the issuer derived from the enclave public key
|
||||
func (k *mpcKeyshareSource) Issuer() string {
|
||||
return k.issuerDID
|
||||
}
|
||||
|
||||
// Enclave returns the underlying MPC enclave
|
||||
func (k *mpcKeyshareSource) Enclave() mpc.Enclave {
|
||||
return k.enclave
|
||||
}
|
||||
|
||||
// ChainCode derives a deterministic chain code from the enclave
|
||||
func (k *mpcKeyshareSource) ChainCode() ([]byte, error) {
|
||||
// Sign the address to create a deterministic chain code
|
||||
sig, err := k.SignData([]byte(k.addr))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign address for chain code: %w", err)
|
||||
}
|
||||
|
||||
// Hash the signature to create a 32-byte chain code
|
||||
hash := blake3.Sum256(sig)
|
||||
return hash[:32], nil
|
||||
}
|
||||
|
||||
// OriginToken creates a default origin token with basic capabilities
|
||||
func (k *mpcKeyshareSource) OriginToken() (*Token, error) {
|
||||
// Create basic capability for the MPC keyshare
|
||||
resource := &SimpleResource{
|
||||
Scheme: "mpc",
|
||||
Value: k.addr,
|
||||
URI: fmt.Sprintf("mpc://%s", k.addr),
|
||||
}
|
||||
|
||||
capability := &SimpleCapability{Action: "sign"}
|
||||
|
||||
attenuation := Attenuation{
|
||||
Capability: capability,
|
||||
Resource: resource,
|
||||
}
|
||||
|
||||
// Create token with no expiration for origin token
|
||||
zero := time.Time{}
|
||||
return k.NewOriginToken(k.issuerDID, []Attenuation{attenuation}, nil, zero, zero)
|
||||
}
|
||||
|
||||
// SignData signs data using the MPC enclave
|
||||
func (k *mpcKeyshareSource) SignData(data []byte) ([]byte, error) {
|
||||
if !k.enclave.IsValid() {
|
||||
return nil, fmt.Errorf("enclave is not valid")
|
||||
}
|
||||
|
||||
return k.enclave.Sign(data)
|
||||
}
|
||||
|
||||
// VerifyData verifies a signature using the MPC enclave
|
||||
func (k *mpcKeyshareSource) VerifyData(data []byte, sig []byte) (bool, error) {
|
||||
if !k.enclave.IsValid() {
|
||||
return false, fmt.Errorf("enclave is not valid")
|
||||
}
|
||||
|
||||
return k.enclave.Verify(data, sig)
|
||||
}
|
||||
|
||||
// NewOriginToken creates a new UCAN origin token using MPC signing
|
||||
func (k *mpcKeyshareSource) NewOriginToken(
|
||||
audienceDID string,
|
||||
att []Attenuation,
|
||||
fct []Fact,
|
||||
notBefore, expires time.Time,
|
||||
) (*Token, error) {
|
||||
return k.newToken(audienceDID, nil, att, fct, notBefore, expires)
|
||||
}
|
||||
|
||||
// NewAttenuatedToken creates a new attenuated UCAN token using MPC signing
|
||||
func (k *mpcKeyshareSource) NewAttenuatedToken(
|
||||
parent *Token,
|
||||
audienceDID string,
|
||||
att []Attenuation,
|
||||
fct []Fact,
|
||||
nbf, exp time.Time,
|
||||
) (*Token, error) {
|
||||
// Validate that new attenuations are more restrictive than parent
|
||||
if !isAttenuationSubset(att, parent.Attenuations) {
|
||||
return nil, fmt.Errorf("scope of ucan attenuations must be less than its parent")
|
||||
}
|
||||
|
||||
// Add parent as proof
|
||||
proofs := []Proof{}
|
||||
if parent.Raw != "" {
|
||||
proofs = append(proofs, Proof(parent.Raw))
|
||||
}
|
||||
proofs = append(proofs, parent.Proofs...)
|
||||
|
||||
return k.newToken(audienceDID, proofs, att, fct, nbf, exp)
|
||||
}
|
||||
|
||||
// newToken creates a new UCAN token with MPC signing
|
||||
func (k *mpcKeyshareSource) newToken(
|
||||
audienceDID string,
|
||||
proofs []Proof,
|
||||
att []Attenuation,
|
||||
fct []Fact,
|
||||
nbf, exp time.Time,
|
||||
) (*Token, error) {
|
||||
// Validate audience DID
|
||||
if !isValidDID(audienceDID) {
|
||||
return nil, fmt.Errorf("invalid audience DID: %s", audienceDID)
|
||||
}
|
||||
|
||||
// Create JWT with MPC signing method
|
||||
t := jwt.New(NewJWTSigningMethod("MPC256", k.enclave))
|
||||
|
||||
// Set UCAN version header
|
||||
t.Header[UCANVersionKey] = UCANVersion
|
||||
|
||||
var (
|
||||
nbfUnix int64
|
||||
expUnix int64
|
||||
)
|
||||
|
||||
if !nbf.IsZero() {
|
||||
nbfUnix = nbf.Unix()
|
||||
}
|
||||
if !exp.IsZero() {
|
||||
expUnix = exp.Unix()
|
||||
}
|
||||
|
||||
// Convert attenuations to claim format
|
||||
attClaims := make([]map[string]any, len(att))
|
||||
for i, a := range att {
|
||||
attClaims[i] = map[string]any{
|
||||
"can": a.Capability.GetActions(),
|
||||
"with": a.Resource.GetURI(),
|
||||
}
|
||||
}
|
||||
|
||||
// Convert proofs to strings
|
||||
proofStrings := make([]string, len(proofs))
|
||||
for i, proof := range proofs {
|
||||
proofStrings[i] = string(proof)
|
||||
}
|
||||
|
||||
// Convert facts to any slice
|
||||
factData := make([]any, len(fct))
|
||||
for i, fact := range fct {
|
||||
factData[i] = string(fact.Data)
|
||||
}
|
||||
|
||||
// Set claims
|
||||
claims := jwt.MapClaims{
|
||||
"iss": k.issuerDID,
|
||||
"aud": audienceDID,
|
||||
"att": attClaims,
|
||||
}
|
||||
|
||||
if nbfUnix > 0 {
|
||||
claims["nbf"] = nbfUnix
|
||||
}
|
||||
if expUnix > 0 {
|
||||
claims["exp"] = expUnix
|
||||
}
|
||||
if len(proofStrings) > 0 {
|
||||
claims["prf"] = proofStrings
|
||||
}
|
||||
if len(factData) > 0 {
|
||||
claims["fct"] = factData
|
||||
}
|
||||
|
||||
t.Claims = claims
|
||||
|
||||
// Sign the token using MPC enclave
|
||||
tokenString, err := t.SignedString(nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign token: %w", err)
|
||||
}
|
||||
|
||||
return &Token{
|
||||
Raw: tokenString,
|
||||
Issuer: k.issuerDID,
|
||||
Audience: audienceDID,
|
||||
ExpiresAt: expUnix,
|
||||
NotBefore: nbfUnix,
|
||||
Attenuations: att,
|
||||
Proofs: proofs,
|
||||
Facts: fct,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// isAttenuationSubset checks if child attenuations are a subset of parent attenuations
|
||||
func isAttenuationSubset(child, parent []Attenuation) bool {
|
||||
for _, childAtt := range child {
|
||||
if !containsAttenuation(parent, childAtt) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// containsAttenuation checks if the parent list contains an equivalent attenuation
|
||||
func containsAttenuation(parent []Attenuation, att Attenuation) bool {
|
||||
for _, parentAtt := range parent {
|
||||
if parentAtt.Resource.Matches(att.Resource) &&
|
||||
parentAtt.Capability.Contains(att.Capability) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isValidDID validates DID format
|
||||
func isValidDID(did string) bool {
|
||||
return did != "" && len(did) > 5 && strings.HasPrefix(did, "did:")
|
||||
}
|
||||
|
||||
// getIssuerDIDFromBytes creates an issuer DID and address from public key bytes
|
||||
func getIssuerDIDFromBytes(pubKeyBytes []byte) (string, string, error) {
|
||||
// Convert MPC public key bytes to libp2p crypto.PubKey
|
||||
pubKey, err := crypto.UnmarshalSecp256k1PublicKey(pubKeyBytes)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to unmarshal secp256k1 key: %w", err)
|
||||
}
|
||||
|
||||
// Create DID using the crypto/keys package
|
||||
did, err := keys.NewDID(pubKey)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to create DID: %w", err)
|
||||
}
|
||||
|
||||
didStr := did.String()
|
||||
|
||||
// Generate address from DID (simplified implementation)
|
||||
address := fmt.Sprintf("addr_%x", pubKeyBytes[:8])
|
||||
|
||||
return didStr, address, nil
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
package spec
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/types/bech32"
|
||||
)
|
||||
|
||||
// Token represents a UCAN JWT token with parsed claims
|
||||
type Token struct {
|
||||
Raw string `json:"raw"`
|
||||
Issuer string `json:"iss"`
|
||||
Audience string `json:"aud"`
|
||||
ExpiresAt int64 `json:"exp,omitempty"`
|
||||
NotBefore int64 `json:"nbf,omitempty"`
|
||||
Attenuations []Attenuation `json:"att"`
|
||||
Proofs []Proof `json:"prf,omitempty"`
|
||||
Facts []Fact `json:"fct,omitempty"`
|
||||
}
|
||||
|
||||
// Attenuation represents a UCAN capability attenuation
|
||||
type Attenuation struct {
|
||||
Capability Capability `json:"can"`
|
||||
Resource Resource `json:"with"`
|
||||
}
|
||||
|
||||
// Proof represents a UCAN delegation proof (either JWT or CID)
|
||||
type Proof string
|
||||
|
||||
// Fact represents arbitrary facts in UCAN tokens
|
||||
type Fact struct {
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
// Capability defines what actions can be performed
|
||||
type Capability interface {
|
||||
GetActions() []string
|
||||
Grants(abilities []string) bool
|
||||
Contains(other Capability) bool
|
||||
String() string
|
||||
}
|
||||
|
||||
// Resource defines what resource the capability applies to
|
||||
type Resource interface {
|
||||
GetScheme() string
|
||||
GetValue() string
|
||||
GetURI() string
|
||||
Matches(other Resource) bool
|
||||
}
|
||||
|
||||
// SimpleCapability implements Capability for single actions
|
||||
type SimpleCapability struct {
|
||||
Action string `json:"action"`
|
||||
}
|
||||
|
||||
func (c *SimpleCapability) GetActions() []string { return []string{c.Action} }
|
||||
func (c *SimpleCapability) Grants(abilities []string) bool {
|
||||
return len(abilities) == 1 && c.Action == abilities[0]
|
||||
}
|
||||
|
||||
func (c *SimpleCapability) Contains(
|
||||
other Capability,
|
||||
) bool {
|
||||
return c.Action == other.GetActions()[0]
|
||||
}
|
||||
func (c *SimpleCapability) String() string { return c.Action }
|
||||
|
||||
// SimpleResource implements Resource for basic URI resources
|
||||
type SimpleResource struct {
|
||||
Scheme string `json:"scheme"`
|
||||
Value string `json:"value"`
|
||||
URI string `json:"uri"`
|
||||
}
|
||||
|
||||
func (r *SimpleResource) GetScheme() string { return r.Scheme }
|
||||
func (r *SimpleResource) GetValue() string { return r.Value }
|
||||
func (r *SimpleResource) GetURI() string { return r.URI }
|
||||
func (r *SimpleResource) Matches(other Resource) bool { return r.URI == other.GetURI() }
|
||||
|
||||
// UCAN constants
|
||||
const (
|
||||
UCANVersion = "0.9.0"
|
||||
UCANVersionKey = "ucv"
|
||||
PrfKey = "prf"
|
||||
FctKey = "fct"
|
||||
AttKey = "att"
|
||||
CapKey = "cap"
|
||||
)
|
||||
|
||||
// CreateSimpleAttenuation creates a basic attenuation
|
||||
func CreateSimpleAttenuation(action, resourceURI string) Attenuation {
|
||||
return Attenuation{
|
||||
Capability: &SimpleCapability{Action: action},
|
||||
Resource: parseResourceURI(resourceURI),
|
||||
}
|
||||
}
|
||||
|
||||
// parseResourceURI creates a Resource from URI string
|
||||
func parseResourceURI(uri string) Resource {
|
||||
parts := strings.SplitN(uri, "://", 2)
|
||||
if len(parts) != 2 {
|
||||
return &SimpleResource{
|
||||
Scheme: "unknown",
|
||||
Value: uri,
|
||||
URI: uri,
|
||||
}
|
||||
}
|
||||
|
||||
return &SimpleResource{
|
||||
Scheme: parts[0],
|
||||
Value: parts[1],
|
||||
URI: uri,
|
||||
}
|
||||
}
|
||||
|
||||
// getIssuerDIDFromBytes creates an issuer DID and address from public key bytes (alternative implementation)
|
||||
func getIssuerDIDFromBytesAlt(pubKeyBytes []byte) (string, string, error) {
|
||||
addr, err := bech32.ConvertAndEncode("idx", pubKeyBytes)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to encode address: %w", err)
|
||||
}
|
||||
return fmt.Sprintf("did:sonr:%s", addr), addr, nil
|
||||
}
|
||||
@@ -1,860 +0,0 @@
|
||||
// Package ucan provides User-Controlled Authorization Networks (UCAN) implementation
|
||||
// for decentralized authorization and capability delegation in the Sonr network.
|
||||
// This package handles JWT-based tokens, cryptographic verification, and resource capabilities.
|
||||
package ucan
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Token represents a UCAN JWT token with parsed claims
|
||||
type Token struct {
|
||||
Raw string `json:"raw"`
|
||||
Issuer string `json:"iss"`
|
||||
Audience string `json:"aud"`
|
||||
ExpiresAt int64 `json:"exp,omitempty"`
|
||||
NotBefore int64 `json:"nbf,omitempty"`
|
||||
Attenuations []Attenuation `json:"att"`
|
||||
Proofs []Proof `json:"prf,omitempty"`
|
||||
Facts []Fact `json:"fct,omitempty"`
|
||||
}
|
||||
|
||||
// Attenuation represents a UCAN capability attenuation
|
||||
type Attenuation struct {
|
||||
Capability Capability `json:"can"`
|
||||
Resource Resource `json:"with"`
|
||||
}
|
||||
|
||||
// Proof represents a UCAN delegation proof (either JWT or CID)
|
||||
type Proof string
|
||||
|
||||
// Fact represents arbitrary facts in UCAN tokens
|
||||
type Fact struct {
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
// Capability defines what actions can be performed
|
||||
type Capability interface {
|
||||
// GetActions returns the list of actions this capability grants
|
||||
GetActions() []string
|
||||
// Grants checks if this capability grants the required abilities
|
||||
Grants(abilities []string) bool
|
||||
// Contains checks if this capability contains another capability
|
||||
Contains(other Capability) bool
|
||||
// String returns a string representation
|
||||
String() string
|
||||
}
|
||||
|
||||
// Resource defines what resource the capability applies to
|
||||
type Resource interface {
|
||||
// GetScheme returns the resource scheme (e.g., "https", "ipfs")
|
||||
GetScheme() string
|
||||
// GetValue returns the resource value/path
|
||||
GetValue() string
|
||||
// GetURI returns the full URI string
|
||||
GetURI() string
|
||||
// Matches checks if this resource matches another resource
|
||||
Matches(other Resource) bool
|
||||
}
|
||||
|
||||
// SimpleCapability implements Capability for single actions
|
||||
type SimpleCapability struct {
|
||||
Action string `json:"action"`
|
||||
}
|
||||
|
||||
// GetActions returns the single action
|
||||
func (c *SimpleCapability) GetActions() []string {
|
||||
return []string{c.Action}
|
||||
}
|
||||
|
||||
// Grants checks if the capability grants all required abilities
|
||||
func (c *SimpleCapability) Grants(abilities []string) bool {
|
||||
if len(abilities) != 1 {
|
||||
return false
|
||||
}
|
||||
return c.Action == abilities[0] || c.Action == "*"
|
||||
}
|
||||
|
||||
// Contains checks if this capability contains another capability
|
||||
func (c *SimpleCapability) Contains(other Capability) bool {
|
||||
if c.Action == "*" {
|
||||
return true
|
||||
}
|
||||
|
||||
otherActions := other.GetActions()
|
||||
if len(otherActions) != 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
return c.Action == otherActions[0]
|
||||
}
|
||||
|
||||
// String returns string representation
|
||||
func (c *SimpleCapability) String() string {
|
||||
return c.Action
|
||||
}
|
||||
|
||||
// MultiCapability implements Capability for multiple actions
|
||||
type MultiCapability struct {
|
||||
Actions []string `json:"actions"`
|
||||
}
|
||||
|
||||
// GetActions returns all actions
|
||||
func (c *MultiCapability) GetActions() []string {
|
||||
return c.Actions
|
||||
}
|
||||
|
||||
// Grants checks if the capability grants all required abilities
|
||||
func (c *MultiCapability) Grants(abilities []string) bool {
|
||||
actionSet := make(map[string]bool)
|
||||
for _, action := range c.Actions {
|
||||
actionSet[action] = true
|
||||
}
|
||||
|
||||
// Check if we have wildcard permission
|
||||
if actionSet["*"] {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check each required ability
|
||||
for _, ability := range abilities {
|
||||
if !actionSet[ability] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Contains checks if this capability contains another capability
|
||||
func (c *MultiCapability) Contains(other Capability) bool {
|
||||
actionSet := make(map[string]bool)
|
||||
for _, action := range c.Actions {
|
||||
actionSet[action] = true
|
||||
}
|
||||
|
||||
// Wildcard contains everything
|
||||
if actionSet["*"] {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if all other actions are contained
|
||||
for _, otherAction := range other.GetActions() {
|
||||
if !actionSet[otherAction] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// String returns string representation
|
||||
func (c *MultiCapability) String() string {
|
||||
return strings.Join(c.Actions, ",")
|
||||
}
|
||||
|
||||
// SimpleResource implements Resource for basic URI resources
|
||||
type SimpleResource struct {
|
||||
Scheme string `json:"scheme"`
|
||||
Value string `json:"value"`
|
||||
URI string `json:"uri"`
|
||||
}
|
||||
|
||||
// GetScheme returns the resource scheme
|
||||
func (r *SimpleResource) GetScheme() string {
|
||||
return r.Scheme
|
||||
}
|
||||
|
||||
// GetValue returns the resource value
|
||||
func (r *SimpleResource) GetValue() string {
|
||||
return r.Value
|
||||
}
|
||||
|
||||
// GetURI returns the full URI
|
||||
func (r *SimpleResource) GetURI() string {
|
||||
return r.URI
|
||||
}
|
||||
|
||||
// Matches checks if resources are equivalent
|
||||
func (r *SimpleResource) Matches(other Resource) bool {
|
||||
return r.URI == other.GetURI()
|
||||
}
|
||||
|
||||
// VaultResource represents vault-specific resources with metadata
|
||||
type VaultResource struct {
|
||||
SimpleResource
|
||||
VaultAddress string `json:"vault_address,omitempty"`
|
||||
EnclaveDataCID string `json:"enclave_data_cid,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// ServiceResource represents service-specific resources
|
||||
type ServiceResource struct {
|
||||
SimpleResource
|
||||
ServiceID string `json:"service_id"`
|
||||
Domain string `json:"domain"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// CreateSimpleAttenuation creates a basic attenuation
|
||||
func CreateSimpleAttenuation(action, resourceURI string) Attenuation {
|
||||
return Attenuation{
|
||||
Capability: &SimpleCapability{Action: action},
|
||||
Resource: parseResourceURI(resourceURI),
|
||||
}
|
||||
}
|
||||
|
||||
// CreateMultiAttenuation creates an attenuation with multiple actions
|
||||
func CreateMultiAttenuation(actions []string, resourceURI string) Attenuation {
|
||||
return Attenuation{
|
||||
Capability: &MultiCapability{Actions: actions},
|
||||
Resource: parseResourceURI(resourceURI),
|
||||
}
|
||||
}
|
||||
|
||||
// CreateVaultAttenuation creates a vault-specific attenuation
|
||||
func CreateVaultAttenuation(actions []string, enclaveDataCID, vaultAddress string) Attenuation {
|
||||
resource := &VaultResource{
|
||||
SimpleResource: SimpleResource{
|
||||
Scheme: "ipfs",
|
||||
Value: enclaveDataCID,
|
||||
URI: fmt.Sprintf("ipfs://%s", enclaveDataCID),
|
||||
},
|
||||
VaultAddress: vaultAddress,
|
||||
EnclaveDataCID: enclaveDataCID,
|
||||
}
|
||||
|
||||
return Attenuation{
|
||||
Capability: &MultiCapability{Actions: actions},
|
||||
Resource: resource,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateServiceAttenuation creates a service-specific attenuation
|
||||
func CreateServiceAttenuation(actions []string, serviceID, domain string) Attenuation {
|
||||
resourceURI := fmt.Sprintf("service://%s", serviceID)
|
||||
resource := &ServiceResource{
|
||||
SimpleResource: SimpleResource{
|
||||
Scheme: "service",
|
||||
Value: serviceID,
|
||||
URI: resourceURI,
|
||||
},
|
||||
ServiceID: serviceID,
|
||||
Domain: domain,
|
||||
}
|
||||
|
||||
return Attenuation{
|
||||
Capability: &MultiCapability{Actions: actions},
|
||||
Resource: resource,
|
||||
}
|
||||
}
|
||||
|
||||
// parseResourceURI creates a Resource from URI string
|
||||
func parseResourceURI(uri string) Resource {
|
||||
parts := strings.SplitN(uri, "://", 2)
|
||||
if len(parts) != 2 {
|
||||
return &SimpleResource{
|
||||
Scheme: "unknown",
|
||||
Value: uri,
|
||||
URI: uri,
|
||||
}
|
||||
}
|
||||
|
||||
return &SimpleResource{
|
||||
Scheme: parts[0],
|
||||
Value: parts[1],
|
||||
URI: uri,
|
||||
}
|
||||
}
|
||||
|
||||
// CapabilityTemplate provides validation and construction utilities
|
||||
type CapabilityTemplate struct {
|
||||
AllowedActions map[string][]string `json:"allowed_actions"` // resource_type -> []actions
|
||||
DefaultExpiration time.Duration `json:"default_expiration"` // default token lifetime
|
||||
MaxExpiration time.Duration `json:"max_expiration"` // maximum allowed lifetime
|
||||
}
|
||||
|
||||
// NewCapabilityTemplate creates a new capability template
|
||||
func NewCapabilityTemplate() *CapabilityTemplate {
|
||||
return &CapabilityTemplate{
|
||||
AllowedActions: make(map[string][]string),
|
||||
DefaultExpiration: 24 * time.Hour,
|
||||
MaxExpiration: 30 * 24 * time.Hour, // 30 days
|
||||
}
|
||||
}
|
||||
|
||||
// AddAllowedActions adds allowed actions for a resource type
|
||||
func (ct *CapabilityTemplate) AddAllowedActions(resourceType string, actions []string) {
|
||||
ct.AllowedActions[resourceType] = actions
|
||||
}
|
||||
|
||||
// ValidateAttenuation validates an attenuation against the template
|
||||
func (ct *CapabilityTemplate) ValidateAttenuation(att Attenuation) error {
|
||||
resourceType := att.Resource.GetScheme()
|
||||
allowedActions, exists := ct.AllowedActions[resourceType]
|
||||
|
||||
if !exists {
|
||||
// Allow unknown resource types for backward compatibility
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create action set for efficient lookup
|
||||
actionSet := make(map[string]bool)
|
||||
for _, action := range allowedActions {
|
||||
actionSet[action] = true
|
||||
}
|
||||
|
||||
// Check if all capability actions are allowed
|
||||
for _, action := range att.Capability.GetActions() {
|
||||
if action == "*" {
|
||||
// Wildcard requires explicit permission
|
||||
if !actionSet["*"] {
|
||||
return fmt.Errorf("wildcard action not allowed for resource type %s", resourceType)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if !actionSet[action] {
|
||||
return fmt.Errorf("action %s not allowed for resource type %s", action, resourceType)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateExpiration validates token expiration time
|
||||
func (ct *CapabilityTemplate) ValidateExpiration(expiresAt int64) error {
|
||||
if expiresAt == 0 {
|
||||
return nil // No expiration is allowed
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
expiry := time.Unix(expiresAt, 0)
|
||||
|
||||
if expiry.Before(now) {
|
||||
return fmt.Errorf("token expiration is in the past")
|
||||
}
|
||||
|
||||
if expiry.Sub(now) > ct.MaxExpiration {
|
||||
return fmt.Errorf("token expiration exceeds maximum allowed duration")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDefaultExpirationTime returns the default expiration timestamp
|
||||
func (ct *CapabilityTemplate) GetDefaultExpirationTime() int64 {
|
||||
return time.Now().Add(ct.DefaultExpiration).Unix()
|
||||
}
|
||||
|
||||
// StandardVaultTemplate returns a standard template for vault operations
|
||||
func StandardVaultTemplate() *CapabilityTemplate {
|
||||
template := NewCapabilityTemplate()
|
||||
template.AddAllowedActions(
|
||||
"ipfs",
|
||||
[]string{"read", "write", "sign", "export", "import", "delete", VaultAdminAction},
|
||||
)
|
||||
template.AddAllowedActions(
|
||||
"vault",
|
||||
[]string{"read", "write", "sign", "export", "import", "delete", "admin", "*"},
|
||||
)
|
||||
return template
|
||||
}
|
||||
|
||||
// StandardServiceTemplate returns a standard template for service operations
|
||||
func StandardServiceTemplate() *CapabilityTemplate {
|
||||
template := NewCapabilityTemplate()
|
||||
template.AddAllowedActions(
|
||||
"service",
|
||||
[]string{"read", "write", "admin", "register", "update", "delete"},
|
||||
)
|
||||
template.AddAllowedActions("https", []string{"read", "write"})
|
||||
template.AddAllowedActions("http", []string{"read", "write"})
|
||||
return template
|
||||
}
|
||||
|
||||
// AttenuationList provides utilities for working with multiple attenuations
|
||||
type AttenuationList []Attenuation
|
||||
|
||||
// Contains checks if the list contains attenuations for a specific resource
|
||||
func (al AttenuationList) Contains(resourceURI string) bool {
|
||||
for _, att := range al {
|
||||
if att.Resource.GetURI() == resourceURI {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetCapabilitiesForResource returns all capabilities for a specific resource
|
||||
func (al AttenuationList) GetCapabilitiesForResource(resourceURI string) []Capability {
|
||||
var capabilities []Capability
|
||||
for _, att := range al {
|
||||
if att.Resource.GetURI() == resourceURI {
|
||||
capabilities = append(capabilities, att.Capability)
|
||||
}
|
||||
}
|
||||
return capabilities
|
||||
}
|
||||
|
||||
// CanPerform checks if the attenuations allow specific actions on a resource
|
||||
func (al AttenuationList) CanPerform(resourceURI string, actions []string) bool {
|
||||
capabilities := al.GetCapabilitiesForResource(resourceURI)
|
||||
for _, cap := range capabilities {
|
||||
if cap.Grants(actions) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsSubsetOf checks if this list is a subset of another list
|
||||
func (al AttenuationList) IsSubsetOf(parent AttenuationList) bool {
|
||||
for _, childAtt := range al {
|
||||
if !parent.containsAttenuation(childAtt) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// containsAttenuation checks if the list contains an equivalent attenuation
|
||||
func (al AttenuationList) containsAttenuation(att Attenuation) bool {
|
||||
for _, parentAtt := range al {
|
||||
if parentAtt.Resource.Matches(att.Resource) {
|
||||
if parentAtt.Capability.Contains(att.Capability) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Module-Specific Capability Types
|
||||
|
||||
// DIDCapability implements Capability for DID module operations
|
||||
type DIDCapability struct {
|
||||
Action string `json:"action"`
|
||||
Actions []string `json:"actions,omitempty"`
|
||||
Caveats []string `json:"caveats,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// GetActions returns the actions this DID capability grants
|
||||
func (c *DIDCapability) GetActions() []string {
|
||||
if len(c.Actions) > 0 {
|
||||
return c.Actions
|
||||
}
|
||||
return []string{c.Action}
|
||||
}
|
||||
|
||||
// Grants checks if this capability grants the required abilities
|
||||
func (c *DIDCapability) Grants(abilities []string) bool {
|
||||
if c.Action == "*" {
|
||||
return true
|
||||
}
|
||||
|
||||
grantedActions := make(map[string]bool)
|
||||
for _, action := range c.GetActions() {
|
||||
grantedActions[action] = true
|
||||
}
|
||||
|
||||
for _, ability := range abilities {
|
||||
if !grantedActions[ability] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Contains checks if this capability contains another capability
|
||||
func (c *DIDCapability) Contains(other Capability) bool {
|
||||
if c.Action == "*" {
|
||||
return true
|
||||
}
|
||||
|
||||
ourActions := make(map[string]bool)
|
||||
for _, action := range c.GetActions() {
|
||||
ourActions[action] = true
|
||||
}
|
||||
|
||||
for _, otherAction := range other.GetActions() {
|
||||
if !ourActions[otherAction] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// String returns string representation
|
||||
func (c *DIDCapability) String() string {
|
||||
if len(c.Actions) > 1 {
|
||||
return strings.Join(c.Actions, ",")
|
||||
}
|
||||
return c.Action
|
||||
}
|
||||
|
||||
// DWNCapability implements Capability for DWN module operations
|
||||
type DWNCapability struct {
|
||||
Action string `json:"action"`
|
||||
Actions []string `json:"actions,omitempty"`
|
||||
Caveats []string `json:"caveats,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// GetActions returns the actions this DWN capability grants
|
||||
func (c *DWNCapability) GetActions() []string {
|
||||
if len(c.Actions) > 0 {
|
||||
return c.Actions
|
||||
}
|
||||
return []string{c.Action}
|
||||
}
|
||||
|
||||
// Grants checks if this capability grants the required abilities
|
||||
func (c *DWNCapability) Grants(abilities []string) bool {
|
||||
if c.Action == "*" {
|
||||
return true
|
||||
}
|
||||
|
||||
grantedActions := make(map[string]bool)
|
||||
for _, action := range c.GetActions() {
|
||||
grantedActions[action] = true
|
||||
}
|
||||
|
||||
for _, ability := range abilities {
|
||||
if !grantedActions[ability] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Contains checks if this capability contains another capability
|
||||
func (c *DWNCapability) Contains(other Capability) bool {
|
||||
if c.Action == "*" {
|
||||
return true
|
||||
}
|
||||
|
||||
ourActions := make(map[string]bool)
|
||||
for _, action := range c.GetActions() {
|
||||
ourActions[action] = true
|
||||
}
|
||||
|
||||
for _, otherAction := range other.GetActions() {
|
||||
if !ourActions[otherAction] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// String returns string representation
|
||||
func (c *DWNCapability) String() string {
|
||||
if len(c.Actions) > 1 {
|
||||
return strings.Join(c.Actions, ",")
|
||||
}
|
||||
return c.Action
|
||||
}
|
||||
|
||||
// DEXCapability implements Capability for DEX module operations
|
||||
type DEXCapability struct {
|
||||
Action string `json:"action"`
|
||||
Actions []string `json:"actions,omitempty"`
|
||||
Caveats []string `json:"caveats,omitempty"`
|
||||
MaxAmount string `json:"max_amount,omitempty"` // For swap limits
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// GetActions returns the actions this DEX capability grants
|
||||
func (c *DEXCapability) GetActions() []string {
|
||||
if len(c.Actions) > 0 {
|
||||
return c.Actions
|
||||
}
|
||||
return []string{c.Action}
|
||||
}
|
||||
|
||||
// Grants checks if this capability grants the required abilities
|
||||
func (c *DEXCapability) Grants(abilities []string) bool {
|
||||
if c.Action == "*" {
|
||||
return true
|
||||
}
|
||||
|
||||
grantedActions := make(map[string]bool)
|
||||
for _, action := range c.GetActions() {
|
||||
grantedActions[action] = true
|
||||
}
|
||||
|
||||
for _, ability := range abilities {
|
||||
if !grantedActions[ability] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Contains checks if this capability contains another capability
|
||||
func (c *DEXCapability) Contains(other Capability) bool {
|
||||
if c.Action == "*" {
|
||||
return true
|
||||
}
|
||||
|
||||
ourActions := make(map[string]bool)
|
||||
for _, action := range c.GetActions() {
|
||||
ourActions[action] = true
|
||||
}
|
||||
|
||||
for _, otherAction := range other.GetActions() {
|
||||
if !ourActions[otherAction] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// String returns string representation
|
||||
func (c *DEXCapability) String() string {
|
||||
if len(c.Actions) > 1 {
|
||||
return strings.Join(c.Actions, ",")
|
||||
}
|
||||
return c.Action
|
||||
}
|
||||
|
||||
// Module-Specific Resource Types
|
||||
|
||||
// DIDResource represents DID-specific resources
|
||||
type DIDResource struct {
|
||||
SimpleResource
|
||||
DIDMethod string `json:"did_method,omitempty"`
|
||||
DIDSubject string `json:"did_subject,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// DWNResource represents DWN-specific resources
|
||||
type DWNResource struct {
|
||||
SimpleResource
|
||||
RecordType string `json:"record_type,omitempty"`
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
Owner string `json:"owner,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// DEXResource represents DEX-specific resources
|
||||
type DEXResource struct {
|
||||
SimpleResource
|
||||
PoolID string `json:"pool_id,omitempty"`
|
||||
AssetPair string `json:"asset_pair,omitempty"`
|
||||
OrderID string `json:"order_id,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// SupportsDelegate Enhanced ServiceResource adds delegation capabilities
|
||||
func (r *ServiceResource) SupportsDelegate() bool {
|
||||
return r.Metadata != nil && r.Metadata["supports_delegation"] == "true"
|
||||
}
|
||||
|
||||
// Module-Specific Capability Templates
|
||||
|
||||
// StandardDIDTemplate returns a standard template for DID operations
|
||||
func StandardDIDTemplate() *CapabilityTemplate {
|
||||
template := NewCapabilityTemplate()
|
||||
template.AddAllowedActions("did", []string{
|
||||
"create", "register", "update", "deactivate", "revoke",
|
||||
"add-verification-method", "remove-verification-method",
|
||||
"add-service", "remove-service", "issue-credential",
|
||||
"revoke-credential", "link-wallet", "register-webauthn", "*",
|
||||
})
|
||||
return template
|
||||
}
|
||||
|
||||
// StandardDWNTemplate returns a standard template for DWN operations
|
||||
func StandardDWNTemplate() *CapabilityTemplate {
|
||||
template := NewCapabilityTemplate()
|
||||
template.AddAllowedActions("dwn", []string{
|
||||
"records-write", "records-delete", "protocols-configure",
|
||||
"permissions-grant", "permissions-revoke", "create", "read",
|
||||
"update", "delete", "*",
|
||||
})
|
||||
return template
|
||||
}
|
||||
|
||||
// EnhancedServiceTemplate returns enhanced service template with delegation support
|
||||
func EnhancedServiceTemplate() *CapabilityTemplate {
|
||||
template := NewCapabilityTemplate()
|
||||
template.AddAllowedActions("service", []string{
|
||||
"register", "update", "delete", "verify-domain",
|
||||
"initiate-domain-verification", "delegate", "*",
|
||||
})
|
||||
template.AddAllowedActions("svc", []string{
|
||||
"register", "verify-domain", "delegate", "*",
|
||||
})
|
||||
template.AddAllowedActions("https", []string{"read", "write"})
|
||||
template.AddAllowedActions("http", []string{"read", "write"})
|
||||
return template
|
||||
}
|
||||
|
||||
// StandardDEXTemplate returns a standard template for DEX operations
|
||||
func StandardDEXTemplate() *CapabilityTemplate {
|
||||
template := NewCapabilityTemplate()
|
||||
template.AddAllowedActions("dex", []string{
|
||||
"register-account", "swap", "provide-liquidity", "remove-liquidity",
|
||||
"create-limit-order", "cancel-order", "*",
|
||||
})
|
||||
template.AddAllowedActions("pool", []string{
|
||||
"swap", "provide-liquidity", "remove-liquidity", "*",
|
||||
})
|
||||
return template
|
||||
}
|
||||
|
||||
// Module-Specific Attenuation Constructors
|
||||
|
||||
// CreateDIDAttenuation creates a DID-specific attenuation
|
||||
func CreateDIDAttenuation(actions []string, didPattern string, caveats []string) Attenuation {
|
||||
resourceURI := fmt.Sprintf("did:%s", didPattern)
|
||||
resource := &DIDResource{
|
||||
SimpleResource: SimpleResource{
|
||||
Scheme: "did",
|
||||
Value: didPattern,
|
||||
URI: resourceURI,
|
||||
},
|
||||
}
|
||||
|
||||
return Attenuation{
|
||||
Capability: &DIDCapability{
|
||||
Actions: actions,
|
||||
Caveats: caveats,
|
||||
},
|
||||
Resource: resource,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateDWNAttenuation creates a DWN-specific attenuation
|
||||
func CreateDWNAttenuation(actions []string, recordPattern string, caveats []string) Attenuation {
|
||||
resourceURI := fmt.Sprintf("dwn:records/%s", recordPattern)
|
||||
resource := &DWNResource{
|
||||
SimpleResource: SimpleResource{
|
||||
Scheme: "dwn",
|
||||
Value: fmt.Sprintf("records/%s", recordPattern),
|
||||
URI: resourceURI,
|
||||
},
|
||||
RecordType: recordPattern,
|
||||
}
|
||||
|
||||
return Attenuation{
|
||||
Capability: &DWNCapability{
|
||||
Actions: actions,
|
||||
Caveats: caveats,
|
||||
},
|
||||
Resource: resource,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateDEXAttenuation creates a DEX-specific attenuation
|
||||
func CreateDEXAttenuation(actions []string, poolPattern string, caveats []string, maxAmount string) Attenuation {
|
||||
resourceURI := fmt.Sprintf("dex:pool/%s", poolPattern)
|
||||
resource := &DEXResource{
|
||||
SimpleResource: SimpleResource{
|
||||
Scheme: "dex",
|
||||
Value: fmt.Sprintf("pool/%s", poolPattern),
|
||||
URI: resourceURI,
|
||||
},
|
||||
PoolID: poolPattern,
|
||||
}
|
||||
|
||||
return Attenuation{
|
||||
Capability: &DEXCapability{
|
||||
Actions: actions,
|
||||
Caveats: caveats,
|
||||
MaxAmount: maxAmount,
|
||||
},
|
||||
Resource: resource,
|
||||
}
|
||||
}
|
||||
|
||||
// Cross-Module Capability Composition
|
||||
|
||||
// CrossModuleCapability allows composing capabilities across modules
|
||||
type CrossModuleCapability struct {
|
||||
Modules map[string]Capability `json:"modules"`
|
||||
}
|
||||
|
||||
// GetActions returns all actions across all modules
|
||||
func (c *CrossModuleCapability) GetActions() []string {
|
||||
var actions []string
|
||||
for _, cap := range c.Modules {
|
||||
actions = append(actions, cap.GetActions()...)
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
// Grants checks if required abilities are granted across modules
|
||||
func (c *CrossModuleCapability) Grants(abilities []string) bool {
|
||||
allActions := make(map[string]bool)
|
||||
for _, cap := range c.Modules {
|
||||
for _, action := range cap.GetActions() {
|
||||
allActions[action] = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, ability := range abilities {
|
||||
if !allActions[ability] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Contains checks if this cross-module capability contains another
|
||||
func (c *CrossModuleCapability) Contains(other Capability) bool {
|
||||
// For cross-module capabilities, check each module
|
||||
if otherCross, ok := other.(*CrossModuleCapability); ok {
|
||||
for module, otherCap := range otherCross.Modules {
|
||||
if ourCap, exists := c.Modules[module]; exists {
|
||||
if !ourCap.Contains(otherCap) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// For single capabilities, check if any module contains it
|
||||
for _, cap := range c.Modules {
|
||||
if cap.Contains(other) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// String returns string representation
|
||||
func (c *CrossModuleCapability) String() string {
|
||||
var moduleStrs []string
|
||||
for module, cap := range c.Modules {
|
||||
moduleStrs = append(moduleStrs, fmt.Sprintf("%s:%s", module, cap.String()))
|
||||
}
|
||||
return strings.Join(moduleStrs, ";")
|
||||
}
|
||||
|
||||
// Gasless Transaction Support
|
||||
|
||||
// GaslessCapability wraps other capabilities with gasless transaction support
|
||||
type GaslessCapability struct {
|
||||
Capability
|
||||
AllowGasless bool `json:"allow_gasless"`
|
||||
GasLimit uint64 `json:"gas_limit,omitempty"`
|
||||
}
|
||||
|
||||
// SupportsGasless returns whether this capability supports gasless transactions
|
||||
func (c *GaslessCapability) SupportsGasless() bool {
|
||||
return c.AllowGasless
|
||||
}
|
||||
|
||||
// GetGasLimit returns the gas limit for gasless transactions
|
||||
func (c *GaslessCapability) GetGasLimit() uint64 {
|
||||
return c.GasLimit
|
||||
}
|
||||
@@ -1,352 +0,0 @@
|
||||
// Package ucan provides User-Controlled Authorization Networks (UCAN) implementation
|
||||
// for decentralized authorization and capability delegation in the Sonr network.
|
||||
// This package handles JWT-based tokens, cryptographic verification, and resource capabilities.
|
||||
package ucan
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ed25519"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"hash"
|
||||
"strings"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
// SupportedSigningMethods returns the list of supported JWT signing methods for UCAN
|
||||
func SupportedSigningMethods() []jwt.SigningMethod {
|
||||
return []jwt.SigningMethod{
|
||||
jwt.SigningMethodRS256,
|
||||
jwt.SigningMethodRS384,
|
||||
jwt.SigningMethodRS512,
|
||||
jwt.SigningMethodEdDSA,
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateSignature validates the cryptographic signature of a UCAN token
|
||||
func ValidateSignature(tokenString string, verifyKey any) error {
|
||||
// Parse token without verification first to get signing method
|
||||
token, err := jwt.ParseWithClaims(
|
||||
tokenString,
|
||||
jwt.MapClaims{},
|
||||
func(token *jwt.Token) (any, error) {
|
||||
return verifyKey, nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("signature validation failed: %w", err)
|
||||
}
|
||||
|
||||
if !token.Valid {
|
||||
return fmt.Errorf("token signature is invalid")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExtractUnsignedToken extracts the unsigned portion of a JWT token (header + payload)
|
||||
func ExtractUnsignedToken(tokenString string) (string, error) {
|
||||
parts := strings.Split(tokenString, ".")
|
||||
if len(parts) != 3 {
|
||||
return "", fmt.Errorf("invalid JWT format: expected 3 parts, got %d", len(parts))
|
||||
}
|
||||
|
||||
return strings.Join(parts[:2], "."), nil
|
||||
}
|
||||
|
||||
// ExtractSignature extracts the signature portion of a JWT token
|
||||
func ExtractSignature(tokenString string) ([]byte, error) {
|
||||
parts := strings.Split(tokenString, ".")
|
||||
if len(parts) != 3 {
|
||||
return nil, fmt.Errorf("invalid JWT format: expected 3 parts, got %d", len(parts))
|
||||
}
|
||||
|
||||
signatureBytes, err := base64.RawURLEncoding.DecodeString(parts[2])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode signature: %w", err)
|
||||
}
|
||||
|
||||
return signatureBytes, nil
|
||||
}
|
||||
|
||||
// VerifyRSASignature verifies an RSA signature using the specified hash algorithm
|
||||
func VerifyRSASignature(
|
||||
signingString string,
|
||||
signature []byte,
|
||||
publicKey *rsa.PublicKey,
|
||||
hashAlg crypto.Hash,
|
||||
) error {
|
||||
// Create hash of signing string
|
||||
hasher := hashAlg.New()
|
||||
hasher.Write([]byte(signingString))
|
||||
hashed := hasher.Sum(nil)
|
||||
|
||||
// Verify signature
|
||||
err := rsa.VerifyPKCS1v15(publicKey, hashAlg, hashed, signature)
|
||||
if err != nil {
|
||||
return fmt.Errorf("RSA signature verification failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyEd25519Signature verifies an Ed25519 signature
|
||||
func VerifyEd25519Signature(
|
||||
signingString string,
|
||||
signature []byte,
|
||||
publicKey ed25519.PublicKey,
|
||||
) error {
|
||||
valid := ed25519.Verify(publicKey, []byte(signingString), signature)
|
||||
if !valid {
|
||||
return fmt.Errorf("Ed25519 signature verification failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetHashAlgorithmForMethod returns the appropriate hash algorithm for a JWT signing method
|
||||
func GetHashAlgorithmForMethod(method jwt.SigningMethod) (crypto.Hash, error) {
|
||||
switch method {
|
||||
case jwt.SigningMethodRS256:
|
||||
return crypto.SHA256, nil
|
||||
case jwt.SigningMethodRS384:
|
||||
return crypto.SHA384, nil
|
||||
case jwt.SigningMethodRS512:
|
||||
return crypto.SHA512, nil
|
||||
case jwt.SigningMethodEdDSA:
|
||||
// Ed25519 doesn't use a separate hash algorithm
|
||||
return crypto.Hash(0), nil
|
||||
default:
|
||||
return crypto.Hash(0), fmt.Errorf("unsupported signing method: %v", method)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateHasher creates a hasher for the given crypto.Hash algorithm
|
||||
func CreateHasher(hashAlg crypto.Hash) (hash.Hash, error) {
|
||||
switch hashAlg {
|
||||
case crypto.SHA256:
|
||||
return sha256.New(), nil
|
||||
case crypto.SHA384:
|
||||
return sha512.New384(), nil
|
||||
case crypto.SHA512:
|
||||
return sha512.New(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported hash algorithm: %v", hashAlg)
|
||||
}
|
||||
}
|
||||
|
||||
// SigningValidator provides cryptographic validation for UCAN tokens
|
||||
type SigningValidator struct {
|
||||
allowedMethods map[string]jwt.SigningMethod
|
||||
}
|
||||
|
||||
// NewSigningValidator creates a new signing validator with default allowed methods
|
||||
func NewSigningValidator() *SigningValidator {
|
||||
allowed := make(map[string]jwt.SigningMethod)
|
||||
for _, method := range SupportedSigningMethods() {
|
||||
allowed[method.Alg()] = method
|
||||
}
|
||||
|
||||
return &SigningValidator{
|
||||
allowedMethods: allowed,
|
||||
}
|
||||
}
|
||||
|
||||
// NewSigningValidatorWithMethods creates a validator with specific allowed methods
|
||||
func NewSigningValidatorWithMethods(methods []jwt.SigningMethod) *SigningValidator {
|
||||
allowed := make(map[string]jwt.SigningMethod)
|
||||
for _, method := range methods {
|
||||
allowed[method.Alg()] = method
|
||||
}
|
||||
|
||||
return &SigningValidator{
|
||||
allowedMethods: allowed,
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateSigningMethod checks if a signing method is allowed
|
||||
func (sv *SigningValidator) ValidateSigningMethod(method jwt.SigningMethod) error {
|
||||
if _, ok := sv.allowedMethods[method.Alg()]; !ok {
|
||||
return fmt.Errorf("signing method %s is not allowed", method.Alg())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateTokenSignature validates the cryptographic signature of a token
|
||||
func (sv *SigningValidator) ValidateTokenSignature(
|
||||
tokenString string,
|
||||
keyFunc jwt.Keyfunc,
|
||||
) (*jwt.Token, error) {
|
||||
// Parse with validation
|
||||
token, err := jwt.Parse(tokenString, keyFunc, jwt.WithValidMethods(sv.getAllowedMethodNames()))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("token signature validation failed: %w", err)
|
||||
}
|
||||
|
||||
// Additional signing method validation
|
||||
if err := sv.ValidateSigningMethod(token.Method); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// getAllowedMethodNames returns the names of allowed signing methods
|
||||
func (sv *SigningValidator) getAllowedMethodNames() []string {
|
||||
methods := make([]string, 0, len(sv.allowedMethods))
|
||||
for name := range sv.allowedMethods {
|
||||
methods = append(methods, name)
|
||||
}
|
||||
return methods
|
||||
}
|
||||
|
||||
// KeyValidator provides validation for cryptographic keys
|
||||
type KeyValidator struct{}
|
||||
|
||||
// NewKeyValidator creates a new key validator
|
||||
func NewKeyValidator() *KeyValidator {
|
||||
return &KeyValidator{}
|
||||
}
|
||||
|
||||
// ValidateRSAPublicKey validates an RSA public key for UCAN usage
|
||||
func (kv *KeyValidator) ValidateRSAPublicKey(key *rsa.PublicKey) error {
|
||||
if key == nil {
|
||||
return fmt.Errorf("RSA public key is nil")
|
||||
}
|
||||
|
||||
// Check minimum key size (2048 bits recommended for security)
|
||||
keySize := key.N.BitLen()
|
||||
if keySize < 2048 {
|
||||
return fmt.Errorf("RSA key size too small: %d bits (minimum 2048 bits required)", keySize)
|
||||
}
|
||||
|
||||
// Check maximum reasonable key size to prevent DoS
|
||||
if keySize > 8192 {
|
||||
return fmt.Errorf("RSA key size too large: %d bits (maximum 8192 bits allowed)", keySize)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateEd25519PublicKey validates an Ed25519 public key for UCAN usage
|
||||
func (kv *KeyValidator) ValidateEd25519PublicKey(key ed25519.PublicKey) error {
|
||||
if key == nil {
|
||||
return fmt.Errorf("Ed25519 public key is nil")
|
||||
}
|
||||
|
||||
if len(key) != ed25519.PublicKeySize {
|
||||
return fmt.Errorf(
|
||||
"invalid Ed25519 public key size: %d bytes (expected %d)",
|
||||
len(key),
|
||||
ed25519.PublicKeySize,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SignatureInfo contains information about a token's signature
|
||||
type SignatureInfo struct {
|
||||
Algorithm string
|
||||
KeyType string
|
||||
SigningString string
|
||||
Signature []byte
|
||||
Valid bool
|
||||
}
|
||||
|
||||
// ExtractSignatureInfo extracts signature information from a JWT token
|
||||
func ExtractSignatureInfo(tokenString string, verifyKey any) (*SignatureInfo, error) {
|
||||
// Parse token to get method and claims
|
||||
token, err := jwt.Parse(tokenString, func(t *jwt.Token) (any, error) {
|
||||
return verifyKey, nil
|
||||
})
|
||||
|
||||
var sigInfo SignatureInfo
|
||||
sigInfo.Valid = (err == nil && token.Valid)
|
||||
|
||||
if token != nil {
|
||||
sigInfo.Algorithm = token.Method.Alg()
|
||||
|
||||
// Get signing string
|
||||
parts := strings.Split(tokenString, ".")
|
||||
if len(parts) >= 2 {
|
||||
sigInfo.SigningString = strings.Join(parts[:2], ".")
|
||||
}
|
||||
|
||||
// Get signature
|
||||
if len(parts) == 3 {
|
||||
sig, decodeErr := base64.RawURLEncoding.DecodeString(parts[2])
|
||||
if decodeErr == nil {
|
||||
sigInfo.Signature = sig
|
||||
}
|
||||
}
|
||||
|
||||
// Determine key type
|
||||
switch verifyKey.(type) {
|
||||
case *rsa.PublicKey:
|
||||
sigInfo.KeyType = "RSA"
|
||||
case ed25519.PublicKey:
|
||||
sigInfo.KeyType = "Ed25519"
|
||||
default:
|
||||
sigInfo.KeyType = "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
return &sigInfo, err
|
||||
}
|
||||
|
||||
// SecurityConfig contains security configuration for UCAN validation
|
||||
type SecurityConfig struct {
|
||||
AllowedSigningMethods []jwt.SigningMethod
|
||||
MinRSAKeySize int
|
||||
MaxRSAKeySize int
|
||||
RequireSecureAlgs bool
|
||||
}
|
||||
|
||||
// DefaultSecurityConfig returns a secure default configuration
|
||||
func DefaultSecurityConfig() *SecurityConfig {
|
||||
return &SecurityConfig{
|
||||
AllowedSigningMethods: SupportedSigningMethods(),
|
||||
MinRSAKeySize: 2048,
|
||||
MaxRSAKeySize: 8192,
|
||||
RequireSecureAlgs: true,
|
||||
}
|
||||
}
|
||||
|
||||
// RestrictiveSecurityConfig returns a more restrictive configuration
|
||||
func RestrictiveSecurityConfig() *SecurityConfig {
|
||||
return &SecurityConfig{
|
||||
AllowedSigningMethods: []jwt.SigningMethod{
|
||||
jwt.SigningMethodRS256, // Only RS256 and EdDSA
|
||||
jwt.SigningMethodEdDSA,
|
||||
},
|
||||
MinRSAKeySize: 3072, // Higher minimum
|
||||
MaxRSAKeySize: 4096, // Lower maximum
|
||||
RequireSecureAlgs: true,
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateSecurityConfig validates that a security configuration is reasonable
|
||||
func ValidateSecurityConfig(config *SecurityConfig) error {
|
||||
if len(config.AllowedSigningMethods) == 0 {
|
||||
return fmt.Errorf("no signing methods allowed")
|
||||
}
|
||||
|
||||
if config.MinRSAKeySize < 1024 {
|
||||
return fmt.Errorf("minimum RSA key size too small: %d", config.MinRSAKeySize)
|
||||
}
|
||||
|
||||
if config.MaxRSAKeySize < config.MinRSAKeySize {
|
||||
return fmt.Errorf("maximum RSA key size smaller than minimum")
|
||||
}
|
||||
|
||||
if config.MaxRSAKeySize > 16384 {
|
||||
return fmt.Errorf("maximum RSA key size too large: %d", config.MaxRSAKeySize)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,595 +0,0 @@
|
||||
package ucan
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
var (
|
||||
// StandardTemplate provides default authorization template
|
||||
StandardTemplate = NewCapabilityTemplate()
|
||||
|
||||
// Revoked tokens tracking
|
||||
revokedTokens = make(map[string]bool)
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Setup standard templates with module-specific capabilities
|
||||
StandardTemplate.AddAllowedActions(
|
||||
"vault",
|
||||
[]string{"read", "write", "sign", "export", "import", "delete", "*"},
|
||||
)
|
||||
StandardTemplate.AddAllowedActions(
|
||||
"service",
|
||||
[]string{"read", "write", "register", "update", "delete"},
|
||||
)
|
||||
StandardTemplate.AddAllowedActions(
|
||||
"did",
|
||||
[]string{
|
||||
"create", "register", "update", "deactivate", "revoke",
|
||||
"add-verification-method", "remove-verification-method",
|
||||
"add-service", "remove-service", "issue-credential",
|
||||
"revoke-credential", "link-wallet", "register-webauthn", "*",
|
||||
},
|
||||
)
|
||||
StandardTemplate.AddAllowedActions(
|
||||
"dwn",
|
||||
[]string{
|
||||
"records-write", "records-delete", "protocols-configure",
|
||||
"permissions-grant", "permissions-revoke", "create", "read",
|
||||
"update", "delete", "*",
|
||||
},
|
||||
)
|
||||
StandardTemplate.AddAllowedActions(
|
||||
"dex",
|
||||
[]string{
|
||||
"register-account", "swap", "provide-liquidity", "remove-liquidity",
|
||||
"create-limit-order", "cancel-order", "*",
|
||||
},
|
||||
)
|
||||
StandardTemplate.AddAllowedActions(
|
||||
"pool",
|
||||
[]string{"swap", "provide-liquidity", "remove-liquidity", "*"},
|
||||
)
|
||||
StandardTemplate.AddAllowedActions(
|
||||
"svc",
|
||||
[]string{"register", "verify-domain", "delegate", "*"},
|
||||
)
|
||||
}
|
||||
|
||||
// GenerateJWTToken creates a UCAN JWT token with given capability and expiration
|
||||
func GenerateJWTToken(attenuation Attenuation, duration time.Duration) (string, error) {
|
||||
// Default expiration handling
|
||||
if duration == 0 {
|
||||
duration = 24 * time.Hour
|
||||
}
|
||||
|
||||
// Create JWT claims
|
||||
claims := jwt.MapClaims{
|
||||
"iss": "did:sonr:local", // Default issuer
|
||||
"exp": time.Now().Add(duration).Unix(),
|
||||
"iat": time.Now().Unix(),
|
||||
}
|
||||
|
||||
// Add capability to claims - separate resource and capability
|
||||
capabilityBytes, err := json.Marshal(map[string]any{
|
||||
"can": attenuation.Capability,
|
||||
"with": attenuation.Resource,
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to serialize capability: %v", err)
|
||||
}
|
||||
claims["can"] = base64.URLEncoding.EncodeToString(capabilityBytes)
|
||||
|
||||
// Create token
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
|
||||
// Dummy secret for signing - in real-world, use proper key management
|
||||
tokenString, err := token.SignedString([]byte("sonr-ucan-secret"))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to sign token: %v", err)
|
||||
}
|
||||
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
// VerifyJWTToken validates and parses a UCAN JWT token
|
||||
func VerifyJWTToken(tokenString string) (*Token, error) {
|
||||
// Check if token is revoked
|
||||
if revokedTokens[tokenString] {
|
||||
return nil, fmt.Errorf("token has been revoked")
|
||||
}
|
||||
|
||||
// Parse token with custom claims
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) {
|
||||
// Dummy secret verification - replace with proper key validation
|
||||
return []byte("sonr-ucan-secret"), nil
|
||||
}, jwt.WithLeeway(5*time.Minute))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("token parsing failed: %v", err)
|
||||
}
|
||||
|
||||
// Extract claims
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid token claims")
|
||||
}
|
||||
|
||||
// Manual expiration check
|
||||
exp, ok := claims["exp"].(float64)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no expiration time found")
|
||||
}
|
||||
if time.Now().Unix() > int64(exp) {
|
||||
return nil, fmt.Errorf("token has expired")
|
||||
}
|
||||
|
||||
// Decode capability
|
||||
capabilityStr, ok := claims["can"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no capability found in token")
|
||||
}
|
||||
|
||||
capabilityBytes, err := base64.URLEncoding.DecodeString(capabilityStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode capability: %v", err)
|
||||
}
|
||||
|
||||
// Parse capability and resource separately
|
||||
var capabilityMap map[string]any
|
||||
err = json.Unmarshal(capabilityBytes, &capabilityMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse capability: %v", err)
|
||||
}
|
||||
|
||||
// Determine capability type
|
||||
var capability Capability
|
||||
var capData map[string]any
|
||||
switch v := capabilityMap["can"].(type) {
|
||||
case map[string]any:
|
||||
capData = v
|
||||
case string:
|
||||
// If it's a string, assume it's a simple action
|
||||
capability = &SimpleCapability{Action: v}
|
||||
capData = nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid capability structure")
|
||||
}
|
||||
|
||||
// Parse capability if needed
|
||||
if capData != nil {
|
||||
// Attempt to infer capability type
|
||||
if actions, ok := capData["actions"].([]any); ok {
|
||||
// MultiCapability
|
||||
stringActions := make([]string, len(actions))
|
||||
for i, action := range actions {
|
||||
if str, ok := action.(string); ok {
|
||||
stringActions[i] = str
|
||||
}
|
||||
}
|
||||
capability = &MultiCapability{Actions: stringActions}
|
||||
} else if action, ok := capData["action"].(string); ok {
|
||||
// SingleCapability
|
||||
capability = &SimpleCapability{Action: action}
|
||||
} else {
|
||||
return nil, fmt.Errorf("unable to parse capability type")
|
||||
}
|
||||
}
|
||||
|
||||
// Parse resource
|
||||
var resourceData map[string]any
|
||||
switch resource := capabilityMap["with"].(type) {
|
||||
case map[string]any:
|
||||
resourceData = resource
|
||||
case string:
|
||||
// If it's a string, assume it's a simple URI
|
||||
resourceData = map[string]any{
|
||||
"Scheme": "generic",
|
||||
"Value": resource,
|
||||
"URI": resource,
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid resource structure")
|
||||
}
|
||||
|
||||
// Create resource based on scheme
|
||||
scheme, _ := resourceData["Scheme"].(string)
|
||||
value, _ := resourceData["Value"].(string)
|
||||
uri, _ := resourceData["URI"].(string)
|
||||
|
||||
resource := &SimpleResource{
|
||||
Scheme: scheme,
|
||||
Value: value,
|
||||
URI: uri,
|
||||
}
|
||||
|
||||
// Validate attenuation
|
||||
attenuation := Attenuation{
|
||||
Capability: capability,
|
||||
Resource: resource,
|
||||
}
|
||||
|
||||
// Use standard template to validate
|
||||
err = StandardTemplate.ValidateAttenuation(attenuation)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("capability validation failed: %v", err)
|
||||
}
|
||||
|
||||
// Construct Token object
|
||||
parsedToken := &Token{
|
||||
Raw: tokenString,
|
||||
Issuer: claims["iss"].(string),
|
||||
ExpiresAt: int64(exp),
|
||||
Attenuations: []Attenuation{attenuation},
|
||||
}
|
||||
|
||||
return parsedToken, nil
|
||||
}
|
||||
|
||||
// RevokeCapability adds a capability to the revocation list
|
||||
func RevokeCapability(attenuation Attenuation) error {
|
||||
// Generate token to get its string representation
|
||||
token, err := GenerateJWTToken(attenuation, time.Hour)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add to revoked tokens
|
||||
revokedTokens[token] = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewCapability is a helper function to create a basic capability
|
||||
func NewCapability(issuer, resource string, abilities []string) (Attenuation, error) {
|
||||
capability := &MultiCapability{Actions: abilities}
|
||||
resourceObj := &SimpleResource{
|
||||
Scheme: "generic",
|
||||
Value: resource,
|
||||
URI: resource,
|
||||
}
|
||||
|
||||
return Attenuation{
|
||||
Capability: capability,
|
||||
Resource: resourceObj,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Enhanced JWT generation functions for module-specific capabilities
|
||||
|
||||
// GenerateModuleJWTToken creates a UCAN JWT token with module-specific capabilities
|
||||
func GenerateModuleJWTToken(attenuations []Attenuation, issuer, audience string, duration time.Duration) (string, error) {
|
||||
if duration == 0 {
|
||||
duration = 24 * time.Hour
|
||||
}
|
||||
|
||||
// Create JWT claims with enhanced structure
|
||||
claims := jwt.MapClaims{
|
||||
"iss": issuer,
|
||||
"aud": audience,
|
||||
"exp": time.Now().Add(duration).Unix(),
|
||||
"iat": time.Now().Unix(),
|
||||
"nbf": time.Now().Unix(),
|
||||
}
|
||||
|
||||
// Add attenuations to claims with module-specific serialization
|
||||
attClaims := make([]map[string]any, len(attenuations))
|
||||
for i, att := range attenuations {
|
||||
attMap, err := serializeModuleAttenuation(att)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to serialize attenuation %d: %w", i, err)
|
||||
}
|
||||
attClaims[i] = attMap
|
||||
}
|
||||
claims["att"] = attClaims
|
||||
|
||||
// Create and sign token
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
tokenString, err := token.SignedString([]byte("sonr-ucan-secret"))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to sign token: %w", err)
|
||||
}
|
||||
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
// serializeModuleAttenuation serializes an attenuation based on its module type
|
||||
func serializeModuleAttenuation(att Attenuation) (map[string]any, error) {
|
||||
attMap := map[string]any{
|
||||
"with": att.Resource.GetURI(),
|
||||
}
|
||||
|
||||
scheme := att.Resource.GetScheme()
|
||||
switch scheme {
|
||||
case "did":
|
||||
return serializeDIDAttenuation(att, attMap)
|
||||
case "dwn":
|
||||
return serializeDWNAttenuation(att, attMap)
|
||||
case "dex", "pool":
|
||||
return serializeDEXAttenuation(att, attMap)
|
||||
case "service", "svc":
|
||||
return serializeServiceAttenuation(att, attMap)
|
||||
case "vault", "ipfs":
|
||||
return serializeVaultAttenuation(att, attMap)
|
||||
default:
|
||||
return serializeGenericAttenuation(att, attMap)
|
||||
}
|
||||
}
|
||||
|
||||
// serializeDIDAttenuation serializes DID-specific attenuations
|
||||
func serializeDIDAttenuation(att Attenuation, attMap map[string]any) (map[string]any, error) {
|
||||
didCap, ok := att.Capability.(*DIDCapability)
|
||||
if !ok {
|
||||
return serializeGenericAttenuation(att, attMap)
|
||||
}
|
||||
|
||||
if didCap.Action != "" {
|
||||
attMap["can"] = didCap.Action
|
||||
} else {
|
||||
attMap["can"] = didCap.Actions
|
||||
}
|
||||
|
||||
if len(didCap.Caveats) > 0 {
|
||||
attMap["caveats"] = didCap.Caveats
|
||||
}
|
||||
if len(didCap.Metadata) > 0 {
|
||||
attMap["metadata"] = didCap.Metadata
|
||||
}
|
||||
|
||||
return attMap, nil
|
||||
}
|
||||
|
||||
// serializeDWNAttenuation serializes DWN-specific attenuations
|
||||
func serializeDWNAttenuation(att Attenuation, attMap map[string]any) (map[string]any, error) {
|
||||
dwnCap, ok := att.Capability.(*DWNCapability)
|
||||
if !ok {
|
||||
return serializeGenericAttenuation(att, attMap)
|
||||
}
|
||||
|
||||
if dwnCap.Action != "" {
|
||||
attMap["can"] = dwnCap.Action
|
||||
} else {
|
||||
attMap["can"] = dwnCap.Actions
|
||||
}
|
||||
|
||||
if len(dwnCap.Caveats) > 0 {
|
||||
attMap["caveats"] = dwnCap.Caveats
|
||||
}
|
||||
if len(dwnCap.Metadata) > 0 {
|
||||
attMap["metadata"] = dwnCap.Metadata
|
||||
}
|
||||
|
||||
// Add DWN-specific fields
|
||||
if dwnRes, ok := att.Resource.(*DWNResource); ok {
|
||||
if dwnRes.RecordType != "" {
|
||||
attMap["record_type"] = dwnRes.RecordType
|
||||
}
|
||||
if dwnRes.Protocol != "" {
|
||||
attMap["protocol"] = dwnRes.Protocol
|
||||
}
|
||||
if dwnRes.Owner != "" {
|
||||
attMap["owner"] = dwnRes.Owner
|
||||
}
|
||||
}
|
||||
|
||||
return attMap, nil
|
||||
}
|
||||
|
||||
// serializeDEXAttenuation serializes DEX-specific attenuations
|
||||
func serializeDEXAttenuation(att Attenuation, attMap map[string]any) (map[string]any, error) {
|
||||
dexCap, ok := att.Capability.(*DEXCapability)
|
||||
if !ok {
|
||||
return serializeGenericAttenuation(att, attMap)
|
||||
}
|
||||
|
||||
if dexCap.Action != "" {
|
||||
attMap["can"] = dexCap.Action
|
||||
} else {
|
||||
attMap["can"] = dexCap.Actions
|
||||
}
|
||||
|
||||
if len(dexCap.Caveats) > 0 {
|
||||
attMap["caveats"] = dexCap.Caveats
|
||||
}
|
||||
if dexCap.MaxAmount != "" {
|
||||
attMap["max_amount"] = dexCap.MaxAmount
|
||||
}
|
||||
if len(dexCap.Metadata) > 0 {
|
||||
attMap["metadata"] = dexCap.Metadata
|
||||
}
|
||||
|
||||
// Add DEX-specific fields
|
||||
if dexRes, ok := att.Resource.(*DEXResource); ok {
|
||||
if dexRes.PoolID != "" {
|
||||
attMap["pool_id"] = dexRes.PoolID
|
||||
}
|
||||
if dexRes.AssetPair != "" {
|
||||
attMap["asset_pair"] = dexRes.AssetPair
|
||||
}
|
||||
if dexRes.OrderID != "" {
|
||||
attMap["order_id"] = dexRes.OrderID
|
||||
}
|
||||
}
|
||||
|
||||
return attMap, nil
|
||||
}
|
||||
|
||||
// serializeServiceAttenuation serializes Service-specific attenuations
|
||||
func serializeServiceAttenuation(att Attenuation, attMap map[string]any) (map[string]any, error) {
|
||||
// Service capabilities still use MultiCapability
|
||||
multiCap, ok := att.Capability.(*MultiCapability)
|
||||
if !ok {
|
||||
return serializeGenericAttenuation(att, attMap)
|
||||
}
|
||||
|
||||
attMap["can"] = multiCap.Actions
|
||||
|
||||
// Add service-specific fields
|
||||
if svcRes, ok := att.Resource.(*ServiceResource); ok {
|
||||
if svcRes.ServiceID != "" {
|
||||
attMap["service_id"] = svcRes.ServiceID
|
||||
}
|
||||
if svcRes.Domain != "" {
|
||||
attMap["domain"] = svcRes.Domain
|
||||
}
|
||||
if len(svcRes.Metadata) > 0 {
|
||||
attMap["metadata"] = svcRes.Metadata
|
||||
}
|
||||
}
|
||||
|
||||
return attMap, nil
|
||||
}
|
||||
|
||||
// serializeVaultAttenuation serializes Vault-specific attenuations
|
||||
func serializeVaultAttenuation(att Attenuation, attMap map[string]any) (map[string]any, error) {
|
||||
vaultCap, ok := att.Capability.(*VaultCapability)
|
||||
if !ok {
|
||||
return serializeGenericAttenuation(att, attMap)
|
||||
}
|
||||
|
||||
if vaultCap.Action != "" {
|
||||
attMap["can"] = vaultCap.Action
|
||||
} else {
|
||||
attMap["can"] = vaultCap.Actions
|
||||
}
|
||||
|
||||
if vaultCap.VaultAddress != "" {
|
||||
attMap["vault"] = vaultCap.VaultAddress
|
||||
}
|
||||
if len(vaultCap.Caveats) > 0 {
|
||||
attMap["caveats"] = vaultCap.Caveats
|
||||
}
|
||||
if vaultCap.EnclaveDataCID != "" {
|
||||
attMap["enclave_data_cid"] = vaultCap.EnclaveDataCID
|
||||
}
|
||||
if len(vaultCap.Metadata) > 0 {
|
||||
attMap["metadata"] = vaultCap.Metadata
|
||||
}
|
||||
|
||||
return attMap, nil
|
||||
}
|
||||
|
||||
// serializeGenericAttenuation serializes generic attenuations
|
||||
func serializeGenericAttenuation(att Attenuation, attMap map[string]any) (map[string]any, error) {
|
||||
actions := att.Capability.GetActions()
|
||||
if len(actions) == 1 {
|
||||
attMap["can"] = actions[0]
|
||||
} else {
|
||||
attMap["can"] = actions
|
||||
}
|
||||
return attMap, nil
|
||||
}
|
||||
|
||||
// Enhanced verification with module-specific support
|
||||
|
||||
// VerifyModuleJWTToken validates and parses a UCAN JWT token with module-specific capabilities
|
||||
func VerifyModuleJWTToken(tokenString string, expectedIssuer, expectedAudience string) (*Token, error) {
|
||||
// Check if token is revoked
|
||||
if revokedTokens[tokenString] {
|
||||
return nil, fmt.Errorf("token has been revoked")
|
||||
}
|
||||
|
||||
// Parse token with custom claims
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) {
|
||||
// Dummy secret verification - replace with proper key validation
|
||||
return []byte("sonr-ucan-secret"), nil
|
||||
}, jwt.WithLeeway(5*time.Minute))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("token parsing failed: %w", err)
|
||||
}
|
||||
|
||||
// Extract claims
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid token claims")
|
||||
}
|
||||
|
||||
// Validate issuer and audience if provided
|
||||
if expectedIssuer != "" {
|
||||
if iss, ok := claims["iss"].(string); !ok || iss != expectedIssuer {
|
||||
return nil, fmt.Errorf("invalid issuer: expected %s", expectedIssuer)
|
||||
}
|
||||
}
|
||||
if expectedAudience != "" {
|
||||
if aud, ok := claims["aud"].(string); !ok || aud != expectedAudience {
|
||||
return nil, fmt.Errorf("invalid audience: expected %s", expectedAudience)
|
||||
}
|
||||
}
|
||||
|
||||
// Manual expiration check
|
||||
exp, ok := claims["exp"].(float64)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no expiration time found")
|
||||
}
|
||||
if time.Now().Unix() > int64(exp) {
|
||||
return nil, fmt.Errorf("token has expired")
|
||||
}
|
||||
|
||||
// Parse attenuations with module-specific support
|
||||
attenuations, err := parseEnhancedAttenuations(claims)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse attenuations: %w", err)
|
||||
}
|
||||
|
||||
// Validate attenuations against templates
|
||||
for _, att := range attenuations {
|
||||
if err := StandardTemplate.ValidateAttenuation(att); err != nil {
|
||||
return nil, fmt.Errorf("capability validation failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Construct Token object
|
||||
issuer, _ := claims["iss"].(string)
|
||||
audience, _ := claims["aud"].(string)
|
||||
nbf, _ := claims["nbf"].(float64)
|
||||
|
||||
parsedToken := &Token{
|
||||
Raw: tokenString,
|
||||
Issuer: issuer,
|
||||
Audience: audience,
|
||||
ExpiresAt: int64(exp),
|
||||
NotBefore: int64(nbf),
|
||||
Attenuations: attenuations,
|
||||
}
|
||||
|
||||
return parsedToken, nil
|
||||
}
|
||||
|
||||
// parseEnhancedAttenuations parses attenuations with module-specific capabilities
|
||||
func parseEnhancedAttenuations(claims jwt.MapClaims) ([]Attenuation, error) {
|
||||
attClaims, ok := claims["att"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no attenuations found in token")
|
||||
}
|
||||
|
||||
attSlice, ok := attClaims.([]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid attenuations format")
|
||||
}
|
||||
|
||||
attenuations := make([]Attenuation, 0, len(attSlice))
|
||||
for i, attItem := range attSlice {
|
||||
attMap, ok := attItem.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid attenuation %d format", i)
|
||||
}
|
||||
|
||||
att, err := parseEnhancedAttenuation(attMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse attenuation %d: %w", i, err)
|
||||
}
|
||||
attenuations = append(attenuations, att)
|
||||
}
|
||||
|
||||
return attenuations, nil
|
||||
}
|
||||
|
||||
// parseEnhancedAttenuation parses a single attenuation with module-specific support
|
||||
func parseEnhancedAttenuation(attMap map[string]any) (Attenuation, error) {
|
||||
// Use the existing enhanced verifier logic
|
||||
verifier := &Verifier{} // Create temporary verifier for parsing
|
||||
return verifier.parseAttenuation(attMap)
|
||||
}
|
||||
@@ -1,625 +0,0 @@
|
||||
// Package ucan provides User-Controlled Authorization Networks (UCAN) implementation
|
||||
// for decentralized authorization and capability delegation in the Sonr network.
|
||||
// This package handles JWT-based tokens, cryptographic verification, and resource capabilities.
|
||||
package ucan
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/multiformats/go-multihash"
|
||||
"github.com/sonr-io/crypto/keys"
|
||||
"github.com/sonr-io/crypto/mpc"
|
||||
)
|
||||
|
||||
// MPCSigningMethod implements JWT signing using MPC enclaves
|
||||
type MPCSigningMethod struct {
|
||||
Name string
|
||||
enclave mpc.Enclave
|
||||
}
|
||||
|
||||
// NewMPCSigningMethod creates a new MPC-based JWT signing method
|
||||
func NewMPCSigningMethod(name string, enclave mpc.Enclave) *MPCSigningMethod {
|
||||
return &MPCSigningMethod{
|
||||
Name: name,
|
||||
enclave: enclave,
|
||||
}
|
||||
}
|
||||
|
||||
// Alg returns the signing method algorithm name
|
||||
func (m *MPCSigningMethod) Alg() string {
|
||||
return m.Name
|
||||
}
|
||||
|
||||
// Verify verifies a JWT signature using the MPC enclave
|
||||
func (m *MPCSigningMethod) Verify(signingString string, signature []byte, key any) error {
|
||||
// signature is already decoded bytes
|
||||
sig := signature
|
||||
|
||||
// Hash the signing string
|
||||
hasher := sha256.New()
|
||||
hasher.Write([]byte(signingString))
|
||||
digest := hasher.Sum(nil)
|
||||
|
||||
// Use MPC enclave to verify signature
|
||||
valid, err := m.enclave.Verify(digest, sig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to verify signature: %w", err)
|
||||
}
|
||||
|
||||
if !valid {
|
||||
return fmt.Errorf("signature verification failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sign signs a JWT string using the MPC enclave
|
||||
func (m *MPCSigningMethod) Sign(signingString string, key any) ([]byte, error) {
|
||||
// Hash the signing string
|
||||
hasher := sha256.New()
|
||||
hasher.Write([]byte(signingString))
|
||||
digest := hasher.Sum(nil)
|
||||
|
||||
// Use MPC enclave to sign the digest
|
||||
sig, err := m.enclave.Sign(digest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign with MPC: %w", err)
|
||||
}
|
||||
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
// MPCTokenBuilder creates UCAN tokens using MPC signing
|
||||
type MPCTokenBuilder struct {
|
||||
enclave mpc.Enclave
|
||||
issuerDID string
|
||||
address string
|
||||
signingMethod *MPCSigningMethod
|
||||
}
|
||||
|
||||
// NewMPCTokenBuilder creates a new MPC-based UCAN token builder
|
||||
func NewMPCTokenBuilder(enclave mpc.Enclave) (*MPCTokenBuilder, error) {
|
||||
if !enclave.IsValid() {
|
||||
return nil, fmt.Errorf("invalid MPC enclave provided")
|
||||
}
|
||||
|
||||
// Derive issuer DID and address from enclave public key
|
||||
pubKeyBytes := enclave.PubKeyBytes()
|
||||
issuerDID, address := deriveIssuerDIDFromBytes(pubKeyBytes)
|
||||
|
||||
signingMethod := NewMPCSigningMethod("MPC256", enclave)
|
||||
|
||||
return &MPCTokenBuilder{
|
||||
enclave: enclave,
|
||||
issuerDID: issuerDID,
|
||||
address: address,
|
||||
signingMethod: signingMethod,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetIssuerDID returns the issuer DID derived from the enclave
|
||||
func (b *MPCTokenBuilder) GetIssuerDID() string {
|
||||
return b.issuerDID
|
||||
}
|
||||
|
||||
// GetAddress returns the address derived from the enclave
|
||||
func (b *MPCTokenBuilder) GetAddress() string {
|
||||
return b.address
|
||||
}
|
||||
|
||||
// CreateOriginToken creates a new origin UCAN token using MPC signing
|
||||
func (b *MPCTokenBuilder) CreateOriginToken(
|
||||
audienceDID string,
|
||||
attenuations []Attenuation,
|
||||
facts []Fact,
|
||||
notBefore, expiresAt time.Time,
|
||||
) (*Token, error) {
|
||||
return b.createToken(audienceDID, nil, attenuations, facts, notBefore, expiresAt)
|
||||
}
|
||||
|
||||
// CreateDelegatedToken creates a delegated UCAN token using MPC signing
|
||||
func (b *MPCTokenBuilder) CreateDelegatedToken(
|
||||
parent *Token,
|
||||
audienceDID string,
|
||||
attenuations []Attenuation,
|
||||
facts []Fact,
|
||||
notBefore, expiresAt time.Time,
|
||||
) (*Token, error) {
|
||||
proofs, err := prepareDelegationProofs(parent, attenuations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b.createToken(audienceDID, proofs, attenuations, facts, notBefore, expiresAt)
|
||||
}
|
||||
|
||||
// createToken creates a UCAN token with MPC signing
|
||||
func (b *MPCTokenBuilder) createToken(
|
||||
audienceDID string,
|
||||
proofs []Proof,
|
||||
attenuations []Attenuation,
|
||||
facts []Fact,
|
||||
notBefore, expiresAt time.Time,
|
||||
) (*Token, error) {
|
||||
// Validate inputs
|
||||
if !isValidDID(audienceDID) {
|
||||
return nil, fmt.Errorf("invalid audience DID format: %s", audienceDID)
|
||||
}
|
||||
if len(attenuations) == 0 {
|
||||
return nil, fmt.Errorf("at least one attenuation is required")
|
||||
}
|
||||
|
||||
// Create JWT token with MPC signing method
|
||||
token := jwt.New(b.signingMethod)
|
||||
|
||||
// Set UCAN version in header
|
||||
token.Header["ucv"] = "0.9.0"
|
||||
|
||||
// Prepare time claims
|
||||
var nbfUnix, expUnix int64
|
||||
if !notBefore.IsZero() {
|
||||
nbfUnix = notBefore.Unix()
|
||||
}
|
||||
if !expiresAt.IsZero() {
|
||||
expUnix = expiresAt.Unix()
|
||||
}
|
||||
|
||||
// Convert attenuations to claim format
|
||||
attClaims := make([]map[string]any, len(attenuations))
|
||||
for i, att := range attenuations {
|
||||
attClaims[i] = map[string]any{
|
||||
"can": att.Capability.GetActions(),
|
||||
"with": att.Resource.GetURI(),
|
||||
}
|
||||
}
|
||||
|
||||
// Convert proofs to strings
|
||||
proofStrings := make([]string, len(proofs))
|
||||
for i, proof := range proofs {
|
||||
proofStrings[i] = string(proof)
|
||||
}
|
||||
|
||||
// Convert facts to any slice
|
||||
factData := make([]any, len(facts))
|
||||
for i, fact := range facts {
|
||||
// Facts are stored as raw JSON, convert to any
|
||||
factData[i] = string(fact.Data)
|
||||
}
|
||||
|
||||
// Set claims
|
||||
claims := jwt.MapClaims{
|
||||
"iss": b.issuerDID,
|
||||
"aud": audienceDID,
|
||||
"att": attClaims,
|
||||
}
|
||||
|
||||
if nbfUnix > 0 {
|
||||
claims["nbf"] = nbfUnix
|
||||
}
|
||||
if expUnix > 0 {
|
||||
claims["exp"] = expUnix
|
||||
}
|
||||
if len(proofStrings) > 0 {
|
||||
claims["prf"] = proofStrings
|
||||
}
|
||||
if len(factData) > 0 {
|
||||
claims["fct"] = factData
|
||||
}
|
||||
|
||||
token.Claims = claims
|
||||
|
||||
// Sign the token using MPC enclave (key parameter is ignored for MPC signing)
|
||||
tokenString, err := token.SignedString(nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign token with MPC: %w", err)
|
||||
}
|
||||
|
||||
return &Token{
|
||||
Raw: tokenString,
|
||||
Issuer: b.issuerDID,
|
||||
Audience: audienceDID,
|
||||
ExpiresAt: expUnix,
|
||||
NotBefore: nbfUnix,
|
||||
Attenuations: attenuations,
|
||||
Proofs: proofs,
|
||||
Facts: facts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateVaultCapabilityToken creates a vault-specific UCAN token
|
||||
func (b *MPCTokenBuilder) CreateVaultCapabilityToken(
|
||||
audienceDID string,
|
||||
vaultAddress string,
|
||||
enclaveDataCID string,
|
||||
actions []string,
|
||||
expiresAt time.Time,
|
||||
) (*Token, error) {
|
||||
// Create vault-specific attenuation
|
||||
attenuation := CreateVaultAttenuation(actions, enclaveDataCID, vaultAddress)
|
||||
|
||||
return b.CreateOriginToken(
|
||||
audienceDID,
|
||||
[]Attenuation{attenuation},
|
||||
nil,
|
||||
time.Time{}, // No not-before restriction
|
||||
expiresAt,
|
||||
)
|
||||
}
|
||||
|
||||
// MPCDIDResolver resolves DIDs with special handling for MPC-derived DIDs
|
||||
type MPCDIDResolver struct {
|
||||
enclave mpc.Enclave
|
||||
issuerDID string
|
||||
fallback DIDResolver
|
||||
}
|
||||
|
||||
// NewMPCDIDResolver creates a new MPC DID resolver
|
||||
func NewMPCDIDResolver(enclave mpc.Enclave, fallback DIDResolver) *MPCDIDResolver {
|
||||
pubKeyBytes := enclave.PubKeyBytes()
|
||||
issuerDID, _ := deriveIssuerDIDFromBytes(pubKeyBytes)
|
||||
|
||||
return &MPCDIDResolver{
|
||||
enclave: enclave,
|
||||
issuerDID: issuerDID,
|
||||
fallback: fallback,
|
||||
}
|
||||
}
|
||||
|
||||
// ResolveDIDKey resolves DID keys with MPC enclave support
|
||||
func (r *MPCDIDResolver) ResolveDIDKey(ctx context.Context, didStr string) (keys.DID, error) {
|
||||
// Check if this is the MPC-derived DID
|
||||
if didStr == r.issuerDID {
|
||||
return r.createDIDFromEnclave()
|
||||
}
|
||||
|
||||
// Fall back to standard DID resolution
|
||||
if r.fallback != nil {
|
||||
return r.fallback.ResolveDIDKey(ctx, didStr)
|
||||
}
|
||||
|
||||
// Default fallback to string parsing
|
||||
return keys.Parse(didStr)
|
||||
}
|
||||
|
||||
// createDIDFromEnclave creates a DID from the MPC enclave's public key
|
||||
func (r *MPCDIDResolver) createDIDFromEnclave() (keys.DID, error) {
|
||||
// This would need to be implemented based on how MPC public keys
|
||||
// are converted to the keys.DID format
|
||||
// For now, parse from the derived DID string
|
||||
return keys.Parse(r.issuerDID)
|
||||
}
|
||||
|
||||
// MPCVerifier provides UCAN verification with MPC support
|
||||
type MPCVerifier struct {
|
||||
*Verifier
|
||||
enclave mpc.Enclave
|
||||
}
|
||||
|
||||
// NewMPCVerifier creates a UCAN verifier with MPC support
|
||||
func NewMPCVerifier(enclave mpc.Enclave) *MPCVerifier {
|
||||
resolver := NewMPCDIDResolver(enclave, StringDIDResolver{})
|
||||
verifier := NewVerifier(resolver)
|
||||
|
||||
return &MPCVerifier{
|
||||
Verifier: verifier,
|
||||
enclave: enclave,
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyMPCToken verifies a UCAN token that may be signed with MPC
|
||||
func (v *MPCVerifier) VerifyMPCToken(ctx context.Context, tokenString string) (*Token, error) {
|
||||
// Try standard verification first
|
||||
token, err := v.VerifyToken(ctx, tokenString)
|
||||
if err == nil {
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// If standard verification fails, try MPC-specific verification
|
||||
return v.verifyWithMPC(ctx, tokenString)
|
||||
}
|
||||
|
||||
// verifyWithMPC attempts to verify using MPC signing method
|
||||
func (v *MPCVerifier) verifyWithMPC(_ context.Context, tokenString string) (*Token, error) {
|
||||
// Create MPC signing method for verification
|
||||
mpcMethod := NewMPCSigningMethod("MPC256", v.enclave)
|
||||
|
||||
// Parse with MPC method
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) {
|
||||
// Ensure the token uses MPC signing method
|
||||
if token.Method.Alg() != mpcMethod.Alg() {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Method)
|
||||
}
|
||||
// For MPC verification, the key is not used
|
||||
return nil, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("MPC token verification failed: %w", err)
|
||||
}
|
||||
|
||||
// Extract and parse claims
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid token claims type")
|
||||
}
|
||||
|
||||
ucanToken, err := v.parseUCANClaims(claims, tokenString)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse UCAN claims: %w", err)
|
||||
}
|
||||
|
||||
return ucanToken, nil
|
||||
}
|
||||
|
||||
// MPCTokenValidator provides comprehensive UCAN token validation with MPC support
|
||||
type MPCTokenValidator struct {
|
||||
*MPCVerifier
|
||||
enclaveValidation bool
|
||||
}
|
||||
|
||||
// NewMPCTokenValidator creates a comprehensive UCAN token validator with MPC support
|
||||
func NewMPCTokenValidator(enclave mpc.Enclave, enableEnclaveValidation bool) *MPCTokenValidator {
|
||||
verifier := NewMPCVerifier(enclave)
|
||||
return &MPCTokenValidator{
|
||||
MPCVerifier: verifier,
|
||||
enclaveValidation: enableEnclaveValidation,
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateTokenForVaultOperation performs comprehensive validation for vault operations
|
||||
func (v *MPCTokenValidator) ValidateTokenForVaultOperation(
|
||||
ctx context.Context,
|
||||
tokenString string,
|
||||
enclaveDataCID string,
|
||||
requiredAction string,
|
||||
vaultAddress string,
|
||||
) (*Token, error) {
|
||||
// Step 1: Verify token signature and structure
|
||||
token, err := v.VerifyMPCToken(ctx, tokenString)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("token verification failed: %w", err)
|
||||
}
|
||||
|
||||
// Step 2: Validate vault-specific capability
|
||||
if err := ValidateVaultTokenCapability(token, enclaveDataCID, requiredAction); err != nil {
|
||||
return nil, fmt.Errorf("vault capability validation failed: %w", err)
|
||||
}
|
||||
|
||||
// Step 3: Validate enclave data CID if enabled
|
||||
if v.enclaveValidation {
|
||||
if err := v.validateEnclaveDataCID(token, enclaveDataCID); err != nil {
|
||||
return nil, fmt.Errorf("enclave data validation failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Validate vault address if provided
|
||||
if vaultAddress != "" {
|
||||
if err := v.validateVaultAddress(token, vaultAddress); err != nil {
|
||||
return nil, fmt.Errorf("vault address validation failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5: Verify delegation chain if proofs exist
|
||||
if len(token.Proofs) > 0 {
|
||||
if err := v.VerifyDelegationChain(ctx, tokenString); err != nil {
|
||||
return nil, fmt.Errorf("delegation chain validation failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// ValidateTokenForResource validates token capabilities for a specific resource
|
||||
func (v *MPCTokenValidator) ValidateTokenForResource(
|
||||
ctx context.Context,
|
||||
tokenString string,
|
||||
resourceURI string,
|
||||
requiredAbilities []string,
|
||||
) (*Token, error) {
|
||||
token, err := v.VerifyCapability(ctx, tokenString, resourceURI, requiredAbilities)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("capability verification failed: %w", err)
|
||||
}
|
||||
|
||||
// Additional MPC-specific validation
|
||||
if v.enclaveValidation {
|
||||
if err := v.validateMPCIssuer(token); err != nil {
|
||||
return nil, fmt.Errorf("MPC issuer validation failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// validateEnclaveDataCID validates that the token contains the expected enclave data CID
|
||||
func (v *MPCTokenValidator) validateEnclaveDataCID(token *Token, expectedCID string) error {
|
||||
tokenCID, err := GetEnclaveDataCID(token)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to extract enclave data CID from token: %w", err)
|
||||
}
|
||||
|
||||
if tokenCID != expectedCID {
|
||||
return fmt.Errorf("enclave data CID mismatch: token=%s, expected=%s", tokenCID, expectedCID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateVaultAddress validates the vault address in token capabilities
|
||||
func (v *MPCTokenValidator) validateVaultAddress(token *Token, expectedAddress string) error {
|
||||
for _, att := range token.Attenuations {
|
||||
if vaultCap, ok := att.Capability.(*VaultCapability); ok {
|
||||
if vaultCap.VaultAddress != "" && vaultCap.VaultAddress != expectedAddress {
|
||||
return fmt.Errorf("vault address mismatch: token=%s, expected=%s",
|
||||
vaultCap.VaultAddress, expectedAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateMPCIssuer validates that the token issuer matches the MPC enclave
|
||||
func (v *MPCTokenValidator) validateMPCIssuer(token *Token) error {
|
||||
expectedIssuer, _ := deriveIssuerDIDFromBytes(v.enclave.PubKeyBytes())
|
||||
|
||||
if token.Issuer != expectedIssuer {
|
||||
return fmt.Errorf("token issuer does not match MPC enclave: token=%s, expected=%s",
|
||||
token.Issuer, expectedIssuer)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createMPCVaultAttenuation creates MPC-specific vault attenuations
|
||||
func createMPCVaultAttenuation(actions []string, enclaveDataCID, vaultAddress string) Attenuation {
|
||||
// Use the existing CreateVaultAttenuation function but add MPC-specific validation
|
||||
return CreateVaultAttenuation(actions, enclaveDataCID, vaultAddress)
|
||||
}
|
||||
|
||||
// containsAdminAction checks if actions contain admin-level permissions
|
||||
func containsAdminAction(actions []string) bool {
|
||||
adminActions := map[string]bool{
|
||||
"admin": true, "export": true, "import": true, "delete": true,
|
||||
}
|
||||
|
||||
for _, action := range actions {
|
||||
if adminActions[action] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ValidateEnclaveDataIntegrity validates enclave data against IPFS CID
|
||||
func ValidateEnclaveDataIntegrity(enclaveData *mpc.EnclaveData, expectedCID string) error {
|
||||
if enclaveData == nil {
|
||||
return fmt.Errorf("enclave data cannot be nil")
|
||||
}
|
||||
|
||||
// Basic validation of enclave structure
|
||||
if len(enclaveData.PubBytes) == 0 {
|
||||
return fmt.Errorf("enclave public key bytes cannot be empty")
|
||||
}
|
||||
|
||||
if enclaveData.PubHex == "" {
|
||||
return fmt.Errorf("enclave public key hex cannot be empty")
|
||||
}
|
||||
|
||||
// Implement IPFS CID validation against enclave data hash
|
||||
// Serialize the enclave data for consistent hashing
|
||||
enclaveDataBytes, err := enclaveData.Marshal()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal enclave data: %w", err)
|
||||
}
|
||||
|
||||
// 1. Hash the enclave data using SHA-256
|
||||
hasher := sha256.New()
|
||||
hasher.Write(enclaveDataBytes)
|
||||
digest := hasher.Sum(nil)
|
||||
|
||||
// 2. Create multihash with SHA-256 prefix
|
||||
mhash, err := multihash.EncodeName(digest, "sha2-256")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create multihash: %w", err)
|
||||
}
|
||||
|
||||
// 3. Create CID and compare with expected
|
||||
parsedExpectedCID, err := cid.Parse(expectedCID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse expected CID: %w", err)
|
||||
}
|
||||
|
||||
// Create CID v1 with dag-pb codec (IPFS default)
|
||||
calculatedCID := cid.NewCidV1(cid.DagProtobuf, mhash)
|
||||
|
||||
// Compare CIDs
|
||||
if !parsedExpectedCID.Equals(calculatedCID) {
|
||||
return fmt.Errorf(
|
||||
"CID verification failed: expected %s, calculated %s",
|
||||
parsedExpectedCID.String(),
|
||||
calculatedCID.String(),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MPCCapabilityBuilder helps build MPC-specific capabilities
|
||||
type MPCCapabilityBuilder struct {
|
||||
enclave mpc.Enclave
|
||||
builder *MPCTokenBuilder
|
||||
}
|
||||
|
||||
// NewMPCCapabilityBuilder creates a new MPC capability builder
|
||||
func NewMPCCapabilityBuilder(enclave mpc.Enclave) (*MPCCapabilityBuilder, error) {
|
||||
builder, err := NewMPCTokenBuilder(enclave)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create MPC token builder: %w", err)
|
||||
}
|
||||
|
||||
return &MPCCapabilityBuilder{
|
||||
enclave: enclave,
|
||||
builder: builder,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateVaultAdminCapability creates admin-level vault capabilities
|
||||
func (b *MPCCapabilityBuilder) CreateVaultAdminCapability(
|
||||
vaultAddress, enclaveDataCID string,
|
||||
) Attenuation {
|
||||
allActions := []string{"read", "write", "sign", "export", "import", "delete", "admin"}
|
||||
return CreateVaultAttenuation(allActions, enclaveDataCID, vaultAddress)
|
||||
}
|
||||
|
||||
// CreateVaultReadOnlyCapability creates read-only vault capabilities
|
||||
func (b *MPCCapabilityBuilder) CreateVaultReadOnlyCapability(
|
||||
vaultAddress, enclaveDataCID string,
|
||||
) Attenuation {
|
||||
readActions := []string{"read"}
|
||||
return CreateVaultAttenuation(readActions, enclaveDataCID, vaultAddress)
|
||||
}
|
||||
|
||||
// CreateVaultSigningCapability creates signing-specific vault capabilities
|
||||
func (b *MPCCapabilityBuilder) CreateVaultSigningCapability(
|
||||
vaultAddress, enclaveDataCID string,
|
||||
) Attenuation {
|
||||
signActions := []string{"read", "sign"}
|
||||
return CreateVaultAttenuation(signActions, enclaveDataCID, vaultAddress)
|
||||
}
|
||||
|
||||
// CreateCustomCapability creates a custom capability with specified actions
|
||||
func (b *MPCCapabilityBuilder) CreateCustomCapability(
|
||||
actions []string,
|
||||
vaultAddress, enclaveDataCID string,
|
||||
) Attenuation {
|
||||
return CreateVaultAttenuation(actions, enclaveDataCID, vaultAddress)
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
|
||||
// deriveIssuerDIDFromBytes creates issuer DID and address from public key bytes
|
||||
// Enhanced version using the crypto/keys package
|
||||
func deriveIssuerDIDFromBytes(pubKeyBytes []byte) (string, string) {
|
||||
// Use the enhanced NewFromMPCPubKey method from crypto/keys
|
||||
did, err := keys.NewFromMPCPubKey(pubKeyBytes)
|
||||
if err != nil {
|
||||
// Fallback to simplified implementation
|
||||
address := fmt.Sprintf("addr_%x", pubKeyBytes[:8])
|
||||
issuerDID := fmt.Sprintf("did:sonr:%s", address)
|
||||
return issuerDID, address
|
||||
}
|
||||
|
||||
// Use the proper DID generation and address derivation
|
||||
didStr := did.String()
|
||||
address, err := did.Address()
|
||||
if err != nil {
|
||||
// Fallback to simplified address
|
||||
address = fmt.Sprintf("addr_%x", pubKeyBytes[:8])
|
||||
}
|
||||
|
||||
return didStr, address
|
||||
}
|
||||
@@ -1,302 +0,0 @@
|
||||
// Package ucan provides User-Controlled Authorization Networks (UCAN) implementation
|
||||
// for decentralized authorization and capability delegation in the Sonr network.
|
||||
// This package handles JWT-based tokens, cryptographic verification, and resource capabilities.
|
||||
package ucan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/sonr-io/crypto/keys"
|
||||
"github.com/sonr-io/crypto/mpc"
|
||||
"lukechampine.com/blake3"
|
||||
)
|
||||
|
||||
// KeyshareSource provides MPC-based UCAN token creation and validation
|
||||
type KeyshareSource interface {
|
||||
Address() string
|
||||
Issuer() string
|
||||
ChainCode() ([]byte, error)
|
||||
OriginToken() (*Token, error)
|
||||
SignData(data []byte) ([]byte, error)
|
||||
VerifyData(data []byte, sig []byte) (bool, error)
|
||||
Enclave() mpc.Enclave
|
||||
|
||||
// UCAN token creation methods
|
||||
NewOriginToken(
|
||||
audienceDID string,
|
||||
att []Attenuation,
|
||||
fct []Fact,
|
||||
notBefore, expires time.Time,
|
||||
) (*Token, error)
|
||||
NewAttenuatedToken(
|
||||
parent *Token,
|
||||
audienceDID string,
|
||||
att []Attenuation,
|
||||
fct []Fact,
|
||||
nbf, exp time.Time,
|
||||
) (*Token, error)
|
||||
}
|
||||
|
||||
// mpcKeyshareSource implements KeyshareSource using MPC enclave
|
||||
type mpcKeyshareSource struct {
|
||||
enclave mpc.Enclave
|
||||
issuerDID string
|
||||
addr string
|
||||
}
|
||||
|
||||
// NewMPCKeyshareSource creates a new MPC-based keyshare source from an enclave
|
||||
func NewMPCKeyshareSource(enclave mpc.Enclave) (KeyshareSource, error) {
|
||||
if !enclave.IsValid() {
|
||||
return nil, fmt.Errorf("invalid MPC enclave provided")
|
||||
}
|
||||
|
||||
pubKeyBytes := enclave.PubKeyBytes()
|
||||
issuerDID, addr, err := getIssuerDIDFromBytes(pubKeyBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to derive issuer DID: %w", err)
|
||||
}
|
||||
|
||||
return &mpcKeyshareSource{
|
||||
enclave: enclave,
|
||||
issuerDID: issuerDID,
|
||||
addr: addr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Address returns the address derived from the enclave public key
|
||||
func (k *mpcKeyshareSource) Address() string {
|
||||
return k.addr
|
||||
}
|
||||
|
||||
// Issuer returns the DID of the issuer derived from the enclave public key
|
||||
func (k *mpcKeyshareSource) Issuer() string {
|
||||
return k.issuerDID
|
||||
}
|
||||
|
||||
// Enclave returns the underlying MPC enclave
|
||||
func (k *mpcKeyshareSource) Enclave() mpc.Enclave {
|
||||
return k.enclave
|
||||
}
|
||||
|
||||
// ChainCode derives a deterministic chain code from the enclave
|
||||
func (k *mpcKeyshareSource) ChainCode() ([]byte, error) {
|
||||
// Sign the address to create a deterministic chain code
|
||||
sig, err := k.SignData([]byte(k.addr))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign address for chain code: %w", err)
|
||||
}
|
||||
|
||||
// Hash the signature to create a 32-byte chain code
|
||||
hash := blake3.Sum256(sig)
|
||||
return hash[:32], nil
|
||||
}
|
||||
|
||||
// OriginToken creates a default origin token with basic capabilities
|
||||
func (k *mpcKeyshareSource) OriginToken() (*Token, error) {
|
||||
// Create basic capability for the MPC keyshare
|
||||
resource := &SimpleResource{
|
||||
Scheme: "mpc",
|
||||
Value: k.addr,
|
||||
URI: fmt.Sprintf("mpc://%s", k.addr),
|
||||
}
|
||||
|
||||
capability := &SimpleCapability{Action: "sign"}
|
||||
|
||||
attenuation := Attenuation{
|
||||
Capability: capability,
|
||||
Resource: resource,
|
||||
}
|
||||
|
||||
// Create token with no expiration for origin token
|
||||
zero := time.Time{}
|
||||
return k.NewOriginToken(k.issuerDID, []Attenuation{attenuation}, nil, zero, zero)
|
||||
}
|
||||
|
||||
// SignData signs data using the MPC enclave
|
||||
func (k *mpcKeyshareSource) SignData(data []byte) ([]byte, error) {
|
||||
if !k.enclave.IsValid() {
|
||||
return nil, fmt.Errorf("enclave is not valid")
|
||||
}
|
||||
|
||||
return k.enclave.Sign(data)
|
||||
}
|
||||
|
||||
// VerifyData verifies a signature using the MPC enclave
|
||||
func (k *mpcKeyshareSource) VerifyData(data []byte, sig []byte) (bool, error) {
|
||||
if !k.enclave.IsValid() {
|
||||
return false, fmt.Errorf("enclave is not valid")
|
||||
}
|
||||
|
||||
return k.enclave.Verify(data, sig)
|
||||
}
|
||||
|
||||
// NewOriginToken creates a new UCAN origin token using MPC signing
|
||||
func (k *mpcKeyshareSource) NewOriginToken(
|
||||
audienceDID string,
|
||||
att []Attenuation,
|
||||
fct []Fact,
|
||||
notBefore, expires time.Time,
|
||||
) (*Token, error) {
|
||||
return k.newToken(audienceDID, nil, att, fct, notBefore, expires)
|
||||
}
|
||||
|
||||
// NewAttenuatedToken creates a new attenuated UCAN token using MPC signing
|
||||
func (k *mpcKeyshareSource) NewAttenuatedToken(
|
||||
parent *Token,
|
||||
audienceDID string,
|
||||
att []Attenuation,
|
||||
fct []Fact,
|
||||
nbf, exp time.Time,
|
||||
) (*Token, error) {
|
||||
// Validate that new attenuations are more restrictive than parent
|
||||
if !isAttenuationSubset(att, parent.Attenuations) {
|
||||
return nil, fmt.Errorf("scope of ucan attenuations must be less than its parent")
|
||||
}
|
||||
|
||||
// Add parent as proof
|
||||
proofs := []Proof{}
|
||||
if parent.Raw != "" {
|
||||
proofs = append(proofs, Proof(parent.Raw))
|
||||
}
|
||||
proofs = append(proofs, parent.Proofs...)
|
||||
|
||||
return k.newToken(audienceDID, proofs, att, fct, nbf, exp)
|
||||
}
|
||||
|
||||
// newToken creates a new UCAN token with MPC signing
|
||||
func (k *mpcKeyshareSource) newToken(
|
||||
audienceDID string,
|
||||
proofs []Proof,
|
||||
att []Attenuation,
|
||||
fct []Fact,
|
||||
nbf, exp time.Time,
|
||||
) (*Token, error) {
|
||||
// Validate audience DID
|
||||
if !isValidDID(audienceDID) {
|
||||
return nil, fmt.Errorf("invalid audience DID: %s", audienceDID)
|
||||
}
|
||||
|
||||
// Create JWT with MPC signing method
|
||||
signingMethod := NewMPCSigningMethod("MPC256", k.enclave)
|
||||
t := jwt.New(signingMethod)
|
||||
|
||||
// Set UCAN version header
|
||||
t.Header["ucv"] = "0.9.0"
|
||||
|
||||
var (
|
||||
nbfUnix int64
|
||||
expUnix int64
|
||||
)
|
||||
|
||||
if !nbf.IsZero() {
|
||||
nbfUnix = nbf.Unix()
|
||||
}
|
||||
if !exp.IsZero() {
|
||||
expUnix = exp.Unix()
|
||||
}
|
||||
|
||||
// Convert attenuations to claim format
|
||||
attClaims := make([]map[string]any, len(att))
|
||||
for i, a := range att {
|
||||
attClaims[i] = map[string]any{
|
||||
"can": a.Capability.GetActions(),
|
||||
"with": a.Resource.GetURI(),
|
||||
}
|
||||
}
|
||||
|
||||
// Convert proofs to strings
|
||||
proofStrings := make([]string, len(proofs))
|
||||
for i, proof := range proofs {
|
||||
proofStrings[i] = string(proof)
|
||||
}
|
||||
|
||||
// Convert facts to any slice
|
||||
factData := make([]any, len(fct))
|
||||
for i, fact := range fct {
|
||||
factData[i] = string(fact.Data)
|
||||
}
|
||||
|
||||
// Set claims
|
||||
claims := jwt.MapClaims{
|
||||
"iss": k.issuerDID,
|
||||
"aud": audienceDID,
|
||||
"att": attClaims,
|
||||
}
|
||||
|
||||
if nbfUnix > 0 {
|
||||
claims["nbf"] = nbfUnix
|
||||
}
|
||||
if expUnix > 0 {
|
||||
claims["exp"] = expUnix
|
||||
}
|
||||
if len(proofStrings) > 0 {
|
||||
claims["prf"] = proofStrings
|
||||
}
|
||||
if len(factData) > 0 {
|
||||
claims["fct"] = factData
|
||||
}
|
||||
|
||||
t.Claims = claims
|
||||
|
||||
// Sign the token using MPC enclave
|
||||
tokenString, err := t.SignedString(nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign token: %w", err)
|
||||
}
|
||||
|
||||
return &Token{
|
||||
Raw: tokenString,
|
||||
Issuer: k.issuerDID,
|
||||
Audience: audienceDID,
|
||||
ExpiresAt: expUnix,
|
||||
NotBefore: nbfUnix,
|
||||
Attenuations: att,
|
||||
Proofs: proofs,
|
||||
Facts: fct,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getIssuerDIDFromBytes creates an issuer DID and address from public key bytes
|
||||
func getIssuerDIDFromBytes(pubKeyBytes []byte) (string, string, error) {
|
||||
// Use the enhanced NewFromMPCPubKey method for proper MPC integration
|
||||
did, err := keys.NewFromMPCPubKey(pubKeyBytes)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to create DID from MPC public key: %w", err)
|
||||
}
|
||||
|
||||
didStr := did.String()
|
||||
|
||||
// Use the enhanced Address method for blockchain-compatible address derivation
|
||||
address, err := did.Address()
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to derive address from DID: %w", err)
|
||||
}
|
||||
|
||||
return didStr, address, nil
|
||||
}
|
||||
|
||||
// isAttenuationSubset checks if child attenuations are a subset of parent attenuations
|
||||
func isAttenuationSubset(child, parent []Attenuation) bool {
|
||||
for _, childAtt := range child {
|
||||
if !containsAttenuation(parent, childAtt) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// containsAttenuation checks if the parent list contains an equivalent attenuation
|
||||
func containsAttenuation(parent []Attenuation, att Attenuation) bool {
|
||||
for _, parentAtt := range parent {
|
||||
if parentAtt.Resource.Matches(att.Resource) &&
|
||||
parentAtt.Capability.Contains(att.Capability) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Note: MPC signing methods are already implemented in mpc.go
|
||||
// Note: isValidDID is already implemented in stubs.go
|
||||
@@ -1,87 +0,0 @@
|
||||
package ucan
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// TokenBuilderInterface defines token building methods
|
||||
type TokenBuilderInterface interface {
|
||||
CreateOriginToken(
|
||||
issuer string,
|
||||
capabilities []Attenuation,
|
||||
facts []Fact,
|
||||
start, expiry time.Time,
|
||||
) (*Token, error)
|
||||
CreateDelegatedToken(
|
||||
parentToken *Token,
|
||||
issuer string,
|
||||
capabilities []Attenuation,
|
||||
facts []Fact,
|
||||
start, expiry time.Time,
|
||||
) (*Token, error)
|
||||
}
|
||||
|
||||
// TokenBuilder implements token builder functionality
|
||||
type TokenBuilder struct {
|
||||
Capability Attenuation
|
||||
}
|
||||
|
||||
// CreateOriginToken creates a new origin token
|
||||
func (tb *TokenBuilder) CreateOriginToken(
|
||||
issuer string,
|
||||
capabilities []Attenuation,
|
||||
facts []Fact,
|
||||
start, expiry time.Time,
|
||||
) (*Token, error) {
|
||||
return &Token{
|
||||
Raw: "",
|
||||
Issuer: issuer,
|
||||
Audience: "",
|
||||
ExpiresAt: expiry.Unix(),
|
||||
NotBefore: start.Unix(),
|
||||
Attenuations: capabilities,
|
||||
Proofs: []Proof{},
|
||||
Facts: facts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateDelegatedToken creates a delegated token
|
||||
func (tb *TokenBuilder) CreateDelegatedToken(
|
||||
parentToken *Token,
|
||||
issuer string,
|
||||
capabilities []Attenuation,
|
||||
facts []Fact,
|
||||
start, expiry time.Time,
|
||||
) (*Token, error) {
|
||||
proofs := []Proof{}
|
||||
if parentToken.Raw != "" {
|
||||
proofs = append(proofs, Proof(parentToken.Raw))
|
||||
}
|
||||
|
||||
return &Token{
|
||||
Raw: "",
|
||||
Issuer: issuer,
|
||||
Audience: parentToken.Issuer,
|
||||
ExpiresAt: expiry.Unix(),
|
||||
NotBefore: start.Unix(),
|
||||
Attenuations: capabilities,
|
||||
Proofs: proofs,
|
||||
Facts: facts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Stub for DID validation
|
||||
func isValidDID(did string) bool {
|
||||
// Basic DID validation stub
|
||||
return did != "" && len(did) > 5 && did[:4] == "did:"
|
||||
}
|
||||
|
||||
// Stub for preparing delegation proofs
|
||||
func prepareDelegationProofs(token *Token, capabilities []Attenuation) ([]Proof, error) {
|
||||
// Minimal stub implementation
|
||||
proofs := []Proof{}
|
||||
if token.Raw != "" {
|
||||
proofs = append(proofs, Proof(token.Raw))
|
||||
}
|
||||
return proofs, nil
|
||||
}
|
||||
@@ -1,313 +0,0 @@
|
||||
package ucan
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/multiformats/go-multihash"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCapabilityCreation(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
actions []string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "Basic Capability Creation",
|
||||
actions: []string{"read", "write"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Empty Actions",
|
||||
actions: []string{},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Complex Actions",
|
||||
actions: []string{"create", "update", "delete", "admin"},
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
capability := &MultiCapability{Actions: tc.actions}
|
||||
|
||||
assert.NotNil(t, capability)
|
||||
assert.Equal(t, len(tc.actions), len(capability.Actions))
|
||||
|
||||
for _, action := range tc.actions {
|
||||
assert.Contains(t, capability.Actions, action)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapabilityValidation(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
actions []string
|
||||
resourceScheme string
|
||||
shouldPass bool
|
||||
}{
|
||||
{
|
||||
name: "Valid Standard Actions",
|
||||
actions: []string{"read", "write"},
|
||||
resourceScheme: "example",
|
||||
shouldPass: true,
|
||||
},
|
||||
{
|
||||
name: "Invalid Actions",
|
||||
actions: []string{"delete", "admin"},
|
||||
resourceScheme: "restricted",
|
||||
shouldPass: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
capability := &MultiCapability{Actions: tc.actions}
|
||||
resource := &SimpleResource{
|
||||
Scheme: tc.resourceScheme,
|
||||
Value: "test",
|
||||
URI: tc.resourceScheme + "://test",
|
||||
}
|
||||
|
||||
attenuation := Attenuation{
|
||||
Capability: capability,
|
||||
Resource: resource,
|
||||
}
|
||||
|
||||
StandardTemplate.AddAllowedActions(tc.resourceScheme, []string{"read", "write"})
|
||||
err := StandardTemplate.ValidateAttenuation(attenuation)
|
||||
|
||||
if tc.shouldPass {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJWTTokenLifecycle(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
actions []string
|
||||
resourceScheme string
|
||||
duration time.Duration
|
||||
shouldPass bool
|
||||
}{
|
||||
{
|
||||
name: "Valid Token Generation and Verification",
|
||||
actions: []string{"read", "write"},
|
||||
resourceScheme: "example",
|
||||
duration: time.Hour,
|
||||
shouldPass: true,
|
||||
},
|
||||
{
|
||||
name: "Expired Token",
|
||||
actions: []string{"read"},
|
||||
resourceScheme: "test",
|
||||
duration: -time.Hour, // Expired token
|
||||
shouldPass: false,
|
||||
},
|
||||
}
|
||||
|
||||
// Use standard service template for testing
|
||||
StandardTemplate := StandardServiceTemplate()
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
capability := &MultiCapability{Actions: tc.actions}
|
||||
resource := &SimpleResource{
|
||||
Scheme: tc.resourceScheme,
|
||||
Value: "test",
|
||||
URI: tc.resourceScheme + "://test",
|
||||
}
|
||||
|
||||
attenuation := Attenuation{
|
||||
Capability: capability,
|
||||
Resource: resource,
|
||||
}
|
||||
|
||||
// Validate attenuation against template
|
||||
err := StandardTemplate.ValidateAttenuation(attenuation)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Simulate JWT token generation and verification
|
||||
token := "test_token_" + time.Now().String()
|
||||
|
||||
if tc.shouldPass {
|
||||
// Simulate verification
|
||||
verifiedToken := &Token{
|
||||
Raw: token,
|
||||
Issuer: "did:sonr:local",
|
||||
Attenuations: []Attenuation{attenuation},
|
||||
ExpiresAt: time.Now().Add(tc.duration).Unix(),
|
||||
}
|
||||
|
||||
assert.NotNil(t, verifiedToken)
|
||||
assert.Equal(t, "did:sonr:local", verifiedToken.Issuer)
|
||||
assert.Len(t, verifiedToken.Attenuations, 1)
|
||||
assert.Equal(
|
||||
t,
|
||||
tc.resourceScheme+"://test",
|
||||
verifiedToken.Attenuations[0].Resource.GetURI(),
|
||||
)
|
||||
} else {
|
||||
// Simulate expired token verification
|
||||
assert.True(t, time.Now().Unix() > time.Now().Add(tc.duration).Unix())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapabilityRevocation(t *testing.T) {
|
||||
capability := &MultiCapability{Actions: []string{"read", "write"}}
|
||||
resource := &SimpleResource{
|
||||
Scheme: "example",
|
||||
Value: "test",
|
||||
URI: "example://test",
|
||||
}
|
||||
|
||||
attenuation := Attenuation{
|
||||
Capability: capability,
|
||||
Resource: resource,
|
||||
}
|
||||
|
||||
// Generate token
|
||||
token, err := GenerateJWTToken(attenuation, time.Hour)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Revoke capability
|
||||
err = RevokeCapability(attenuation)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Attempt to verify revoked token should fail
|
||||
_, err = VerifyJWTToken(token)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "token has been revoked")
|
||||
}
|
||||
|
||||
func TestResourceValidation(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
resourceScheme string
|
||||
resourceValue string
|
||||
resourceURI string
|
||||
expectValid bool
|
||||
}{
|
||||
{
|
||||
name: "Valid Resource",
|
||||
resourceScheme: "sonr",
|
||||
resourceValue: "test-resource",
|
||||
resourceURI: "sonr://test-resource",
|
||||
expectValid: true,
|
||||
},
|
||||
{
|
||||
name: "Invalid Resource URI",
|
||||
resourceScheme: "invalid",
|
||||
resourceValue: "test-resource",
|
||||
resourceURI: "invalid-malformed-uri",
|
||||
expectValid: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
resource := &SimpleResource{
|
||||
Scheme: tc.resourceScheme,
|
||||
Value: tc.resourceValue,
|
||||
URI: tc.resourceURI,
|
||||
}
|
||||
|
||||
// Simplified resource validation
|
||||
if tc.expectValid {
|
||||
assert.Regexp(t, `^[a-z]+://[a-z-]+$`, resource.URI)
|
||||
} else {
|
||||
assert.NotRegexp(t, `^[a-z]+://[a-z-]+$`, resource.URI)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateEnclaveDataCIDIntegrity(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
data []byte
|
||||
expectedCID string
|
||||
expectError bool
|
||||
errorContains string
|
||||
}{
|
||||
{
|
||||
name: "Empty CID",
|
||||
data: []byte("test data"),
|
||||
expectedCID: "",
|
||||
expectError: true,
|
||||
errorContains: "enclave data CID cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "Empty data",
|
||||
data: []byte{},
|
||||
expectedCID: "QmTest",
|
||||
expectError: true,
|
||||
errorContains: "enclave data cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "Invalid CID format",
|
||||
data: []byte("test data"),
|
||||
expectedCID: "invalid-cid",
|
||||
expectError: true,
|
||||
errorContains: "invalid IPFS CID format",
|
||||
},
|
||||
{
|
||||
name: "Valid CID verification - should pass",
|
||||
data: []byte("test data"),
|
||||
expectedCID: generateValidCIDForData([]byte("test data")),
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "Mismatched CID - should fail",
|
||||
data: []byte("test data"),
|
||||
expectedCID: generateValidCIDForData([]byte("different data")),
|
||||
expectError: true,
|
||||
errorContains: "CID verification failed",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := ValidateEnclaveDataCIDIntegrity(tc.expectedCID, tc.data)
|
||||
|
||||
if tc.expectError {
|
||||
assert.Error(t, err)
|
||||
if tc.errorContains != "" {
|
||||
assert.Contains(t, err.Error(), tc.errorContains)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to generate a valid CID for test data
|
||||
func generateValidCIDForData(data []byte) string {
|
||||
hasher := sha256.New()
|
||||
hasher.Write(data)
|
||||
digest := hasher.Sum(nil)
|
||||
|
||||
mhash, err := multihash.EncodeName(digest, "sha2-256")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
calculatedCID := cid.NewCidV1(cid.DagProtobuf, mhash)
|
||||
return calculatedCID.String()
|
||||
}
|
||||
@@ -1,485 +0,0 @@
|
||||
// Package ucan provides User-Controlled Authorization Networks (UCAN) implementation
|
||||
// for decentralized authorization and capability delegation in the Sonr network.
|
||||
// This package handles JWT-based tokens, cryptographic verification, and resource capabilities.
|
||||
package ucan
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
z "github.com/Oudwins/zog"
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/multiformats/go-multihash"
|
||||
)
|
||||
|
||||
// Constants for vault capability actions
|
||||
const (
|
||||
VaultAdminAction = "vault/admin"
|
||||
)
|
||||
|
||||
// VaultCapabilitySchema defines validation specifically for vault capabilities
|
||||
var VaultCapabilitySchema = z.Struct(z.Shape{
|
||||
"can": z.String().Required().OneOf(
|
||||
[]string{
|
||||
VaultAdminAction,
|
||||
"vault/read",
|
||||
"vault/write",
|
||||
"vault/sign",
|
||||
"vault/export",
|
||||
"vault/import",
|
||||
"vault/delete",
|
||||
},
|
||||
z.Message("Invalid vault capability"),
|
||||
),
|
||||
"with": z.String().
|
||||
Required().
|
||||
TestFunc(ValidateIPFSCID, z.Message("Vault resource must be IPFS CID in format 'ipfs://CID'")),
|
||||
"actions": z.Slice(z.String().OneOf(
|
||||
[]string{"read", "write", "sign", "export", "import", "delete"},
|
||||
z.Message("Invalid vault action"),
|
||||
)).Optional(),
|
||||
"vault": z.String().Required().Min(1, z.Message("Vault address cannot be empty")),
|
||||
"cavs": z.Slice(z.String()).Optional(), // Caveats as string array for vault capabilities
|
||||
})
|
||||
|
||||
// VaultCapability implements Capability for vault-specific operations
|
||||
// with support for admin permissions, actions, and enclave data management.
|
||||
type VaultCapability struct {
|
||||
Action string `json:"can"`
|
||||
Actions []string `json:"actions,omitempty"`
|
||||
VaultAddress string `json:"vault,omitempty"`
|
||||
Caveats []string `json:"cavs,omitempty"`
|
||||
EnclaveDataCID string `json:"enclave_data_cid,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// GetActions returns the actions this vault capability grants
|
||||
func (c *VaultCapability) GetActions() []string {
|
||||
if c.Action == VaultAdminAction {
|
||||
// Admin capability grants all vault actions
|
||||
return []string{"read", "write", "sign", "export", "import", "delete", VaultAdminAction}
|
||||
}
|
||||
|
||||
if len(c.Actions) > 0 {
|
||||
return c.Actions
|
||||
}
|
||||
|
||||
// Extract action from the main capability string
|
||||
if strings.HasPrefix(c.Action, "vault/") {
|
||||
return []string{c.Action[6:]} // Remove "vault/" prefix
|
||||
}
|
||||
|
||||
return []string{c.Action}
|
||||
}
|
||||
|
||||
// Grants checks if this capability grants the required abilities
|
||||
func (c *VaultCapability) Grants(abilities []string) bool {
|
||||
if c.Action == VaultAdminAction {
|
||||
// Admin capability grants everything
|
||||
return true
|
||||
}
|
||||
|
||||
grantedActions := make(map[string]bool)
|
||||
for _, action := range c.GetActions() {
|
||||
grantedActions[action] = true
|
||||
grantedActions["vault/"+action] = true // Support both formats
|
||||
}
|
||||
|
||||
// Check each required ability
|
||||
for _, ability := range abilities {
|
||||
if !grantedActions[ability] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Contains checks if this capability contains another capability
|
||||
func (c *VaultCapability) Contains(other Capability) bool {
|
||||
if c.Action == VaultAdminAction {
|
||||
// Admin contains all vault capabilities
|
||||
if otherVault, ok := other.(*VaultCapability); ok {
|
||||
return strings.HasPrefix(otherVault.Action, "vault/")
|
||||
}
|
||||
// Admin contains any action that starts with vault-related actions
|
||||
for _, action := range other.GetActions() {
|
||||
if strings.HasPrefix(action, "vault/") ||
|
||||
action == "read" || action == "write" || action == "sign" ||
|
||||
action == "export" || action == "import" || action == "delete" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if our actions contain all of the other capability's actions
|
||||
ourActions := make(map[string]bool)
|
||||
for _, action := range c.GetActions() {
|
||||
ourActions[action] = true
|
||||
ourActions["vault/"+action] = true
|
||||
}
|
||||
|
||||
for _, otherAction := range other.GetActions() {
|
||||
if !ourActions[otherAction] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// String returns string representation
|
||||
func (c *VaultCapability) String() string {
|
||||
return c.Action
|
||||
}
|
||||
|
||||
// VaultResourceExt represents an extended IPFS-based vault resource (to avoid redeclaration)
|
||||
type VaultResourceExt struct {
|
||||
SimpleResource
|
||||
VaultAddress string `json:"vault_address"`
|
||||
EnclaveDataCID string `json:"enclave_data_cid"`
|
||||
}
|
||||
|
||||
// ValidateIPFSCID validates IPFS CID format for vault resources
|
||||
func ValidateIPFSCID(value *string, ctx z.Ctx) bool {
|
||||
if !strings.HasPrefix(*value, "ipfs://") {
|
||||
return false
|
||||
}
|
||||
cidStr := (*value)[7:] // Remove "ipfs://" prefix
|
||||
|
||||
// Enhanced CID validation
|
||||
return validateCIDFormat(cidStr)
|
||||
}
|
||||
|
||||
// validateCIDFormat performs comprehensive IPFS CID format validation
|
||||
func validateCIDFormat(cidStr string) bool {
|
||||
if len(cidStr) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// CIDv0: Base58-encoded SHA-256 multihash (starts with 'Qm' and is 46 characters)
|
||||
if strings.HasPrefix(cidStr, "Qm") && len(cidStr) == 46 {
|
||||
return isValidBase58(cidStr)
|
||||
}
|
||||
|
||||
// CIDv1: Base32 or Base58 encoded (starts with 'b' for base32 or other prefixes)
|
||||
if len(cidStr) >= 59 {
|
||||
// CIDv1 in base32 typically starts with 'b' and is longer
|
||||
if strings.HasPrefix(cidStr, "b") {
|
||||
return isValidBase32(cidStr[1:]) // Remove 'b' prefix
|
||||
}
|
||||
// CIDv1 in base58 or other encodings
|
||||
return isValidBase58(cidStr)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// isValidBase58 checks if string contains valid base58 characters
|
||||
func isValidBase58(s string) bool {
|
||||
base58Chars := "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
||||
for _, char := range s {
|
||||
if !strings.Contains(base58Chars, string(char)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// isValidBase32 checks if string contains valid base32 characters
|
||||
func isValidBase32(s string) bool {
|
||||
base32Chars := "abcdefghijklmnopqrstuvwxyz234567"
|
||||
for _, char := range s {
|
||||
if !strings.Contains(base32Chars, string(char)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ValidateEnclaveDataCIDIntegrity validates enclave data against expected CID
|
||||
func ValidateEnclaveDataCIDIntegrity(enclaveDataCID string, enclaveData []byte) error {
|
||||
if enclaveDataCID == "" {
|
||||
return fmt.Errorf("enclave data CID cannot be empty")
|
||||
}
|
||||
|
||||
if len(enclaveData) == 0 {
|
||||
return fmt.Errorf("enclave data cannot be empty")
|
||||
}
|
||||
|
||||
// Validate CID format first
|
||||
if !validateCIDFormat(enclaveDataCID) {
|
||||
return fmt.Errorf("invalid IPFS CID format: %s", enclaveDataCID)
|
||||
}
|
||||
|
||||
// Implement actual CID verification by hashing enclave data
|
||||
// 1. Hash the enclave data using SHA-256
|
||||
hasher := sha256.New()
|
||||
hasher.Write(enclaveData)
|
||||
digest := hasher.Sum(nil)
|
||||
|
||||
// 2. Create multihash with SHA-256 prefix
|
||||
mhash, err := multihash.EncodeName(digest, "sha2-256")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create multihash: %w", err)
|
||||
}
|
||||
|
||||
// 3. Create CID and compare with expected
|
||||
expectedCID, err := cid.Parse(enclaveDataCID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse expected CID: %w", err)
|
||||
}
|
||||
|
||||
// Create CID v1 with dag-pb codec (IPFS default)
|
||||
calculatedCID := cid.NewCidV1(cid.DagProtobuf, mhash)
|
||||
|
||||
// Compare CIDs
|
||||
if !expectedCID.Equals(calculatedCID) {
|
||||
return fmt.Errorf(
|
||||
"CID verification failed: expected %s, calculated %s",
|
||||
expectedCID.String(),
|
||||
calculatedCID.String(),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateVaultCapability validates vault-specific capabilities
|
||||
func ValidateVaultCapability(att map[string]any) error {
|
||||
var validated struct {
|
||||
Can string `json:"can"`
|
||||
With string `json:"with"`
|
||||
Actions []string `json:"actions,omitempty"`
|
||||
Vault string `json:"vault"`
|
||||
Cavs []string `json:"cavs,omitempty"`
|
||||
}
|
||||
|
||||
errs := VaultCapabilitySchema.Parse(att, &validated)
|
||||
if errs != nil {
|
||||
return fmt.Errorf("vault capability validation failed: %v", errs)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VaultAttenuationConstructor creates vault-specific attenuations with enhanced validation
|
||||
func VaultAttenuationConstructor(m map[string]any) (Attenuation, error) {
|
||||
// First validate using vault-specific schema
|
||||
if err := ValidateVaultCapability(m); err != nil {
|
||||
return Attenuation{}, fmt.Errorf("vault attenuation validation failed: %w", err)
|
||||
}
|
||||
|
||||
capStr, withStr, err := extractRequiredFields(m)
|
||||
if err != nil {
|
||||
return Attenuation{}, err
|
||||
}
|
||||
|
||||
vaultCap := createVaultCapability(capStr, m)
|
||||
resource := createVaultResource(withStr, vaultCap.VaultAddress)
|
||||
|
||||
// Set enclave data CID if using IPFS resource
|
||||
if vaultRes, ok := resource.(*VaultResource); ok {
|
||||
vaultCap.EnclaveDataCID = vaultRes.EnclaveDataCID
|
||||
}
|
||||
|
||||
return Attenuation{
|
||||
Capability: vaultCap,
|
||||
Resource: resource,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// extractRequiredFields extracts and validates required 'can' and 'with' fields
|
||||
func extractRequiredFields(m map[string]any) (string, string, error) {
|
||||
capValue, exists := m["can"]
|
||||
if !exists {
|
||||
return "", "", fmt.Errorf("missing 'can' field in attenuation")
|
||||
}
|
||||
capStr, ok := capValue.(string)
|
||||
if !ok {
|
||||
return "", "", fmt.Errorf("'can' field must be a string")
|
||||
}
|
||||
|
||||
withValue, exists := m["with"]
|
||||
if !exists {
|
||||
return "", "", fmt.Errorf("missing 'with' field in attenuation")
|
||||
}
|
||||
withStr, ok := withValue.(string)
|
||||
if !ok {
|
||||
return "", "", fmt.Errorf("'with' field must be a string")
|
||||
}
|
||||
|
||||
return capStr, withStr, nil
|
||||
}
|
||||
|
||||
// createVaultCapability creates and populates a VaultCapability from the input map
|
||||
func createVaultCapability(action string, m map[string]any) *VaultCapability {
|
||||
vaultCap := &VaultCapability{Action: action}
|
||||
|
||||
if actions, exists := m["actions"]; exists {
|
||||
vaultCap.Actions = extractStringSlice(actions)
|
||||
}
|
||||
|
||||
if vault, exists := m["vault"]; exists {
|
||||
if vaultStr, ok := vault.(string); ok {
|
||||
vaultCap.VaultAddress = vaultStr
|
||||
}
|
||||
}
|
||||
|
||||
if cavs, exists := m["cavs"]; exists {
|
||||
vaultCap.Caveats = extractStringSlice(cavs)
|
||||
}
|
||||
|
||||
return vaultCap
|
||||
}
|
||||
|
||||
// extractStringSlice safely extracts a string slice from an any
|
||||
func extractStringSlice(value any) []string {
|
||||
if slice, ok := value.([]any); ok {
|
||||
result := make([]string, 0, len(slice))
|
||||
for _, item := range slice {
|
||||
if str, ok := item.(string); ok {
|
||||
result = append(result, str)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// createVaultResource creates appropriate Resource based on the URI scheme
|
||||
func createVaultResource(withStr, vaultAddress string) Resource {
|
||||
parts := strings.SplitN(withStr, "://", 2)
|
||||
if len(parts) == 2 && parts[0] == "ipfs" {
|
||||
return &VaultResource{
|
||||
SimpleResource: SimpleResource{
|
||||
Scheme: "ipfs",
|
||||
Value: parts[1],
|
||||
URI: withStr,
|
||||
},
|
||||
VaultAddress: vaultAddress,
|
||||
EnclaveDataCID: parts[1],
|
||||
}
|
||||
}
|
||||
|
||||
return &SimpleResource{
|
||||
Scheme: "ipfs",
|
||||
Value: withStr,
|
||||
URI: withStr,
|
||||
}
|
||||
}
|
||||
|
||||
// NewVaultAdminToken creates a new UCAN token with vault admin capabilities
|
||||
func NewVaultAdminToken(
|
||||
builder TokenBuilderInterface,
|
||||
vaultOwnerDID string,
|
||||
vaultAddress string,
|
||||
enclaveDataCID string,
|
||||
exp time.Time,
|
||||
) (*Token, error) {
|
||||
// Validate input parameters
|
||||
if !isValidDID(vaultOwnerDID) {
|
||||
return nil, fmt.Errorf("invalid vault owner DID: %s", vaultOwnerDID)
|
||||
}
|
||||
|
||||
// Create vault admin attenuation with full permissions
|
||||
vaultResource := &VaultResource{
|
||||
SimpleResource: SimpleResource{
|
||||
Scheme: "ipfs",
|
||||
Value: enclaveDataCID,
|
||||
URI: fmt.Sprintf("ipfs://%s", enclaveDataCID),
|
||||
},
|
||||
VaultAddress: vaultAddress,
|
||||
EnclaveDataCID: enclaveDataCID,
|
||||
}
|
||||
|
||||
vaultCap := &VaultCapability{
|
||||
Action: VaultAdminAction,
|
||||
Actions: []string{"read", "write", "sign", "export", "import", "delete"},
|
||||
VaultAddress: vaultAddress,
|
||||
EnclaveDataCID: enclaveDataCID,
|
||||
}
|
||||
|
||||
// Validate the vault capability using vault-specific schema
|
||||
capMap := map[string]any{
|
||||
"can": vaultCap.Action,
|
||||
"with": vaultResource.URI,
|
||||
"actions": vaultCap.Actions,
|
||||
"vault": vaultCap.VaultAddress,
|
||||
}
|
||||
if err := ValidateVaultCapability(capMap); err != nil {
|
||||
return nil, fmt.Errorf("invalid vault capability: %w", err)
|
||||
}
|
||||
|
||||
attenuation := Attenuation{
|
||||
Capability: vaultCap,
|
||||
Resource: vaultResource,
|
||||
}
|
||||
|
||||
// Create token with vault admin capabilities
|
||||
return builder.CreateOriginToken(
|
||||
vaultOwnerDID,
|
||||
[]Attenuation{attenuation},
|
||||
nil,
|
||||
time.Now(),
|
||||
exp,
|
||||
)
|
||||
}
|
||||
|
||||
// ValidateVaultTokenCapability validates a UCAN token for vault operations
|
||||
func ValidateVaultTokenCapability(token *Token, enclaveDataCID, requiredAction string) error {
|
||||
expectedResource := fmt.Sprintf("ipfs://%s", enclaveDataCID)
|
||||
|
||||
// Validate the required action parameter
|
||||
validActions := []string{"read", "write", "sign", "export", "import", "delete"}
|
||||
actionValid := slices.Contains(validActions, requiredAction)
|
||||
if !actionValid {
|
||||
return fmt.Errorf("invalid required action: %s", requiredAction)
|
||||
}
|
||||
|
||||
// Check if token contains the required vault capability
|
||||
for _, att := range token.Attenuations {
|
||||
if att.Resource.GetURI() == expectedResource {
|
||||
// Check if this is a vault capability
|
||||
if vaultCap, ok := att.Capability.(*VaultCapability); ok {
|
||||
// Validate using vault-specific schema
|
||||
validationMap := map[string]any{
|
||||
"can": vaultCap.Action,
|
||||
"with": att.Resource.GetURI(),
|
||||
"actions": vaultCap.Actions,
|
||||
"vault": vaultCap.VaultAddress,
|
||||
}
|
||||
|
||||
if err := ValidateVaultCapability(validationMap); err != nil {
|
||||
continue // Skip invalid capabilities
|
||||
}
|
||||
|
||||
// Check if capability grants the required action
|
||||
if vaultCap.Grants([]string{requiredAction}) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf(
|
||||
"insufficient vault capability: required action '%s' for enclave '%s'",
|
||||
requiredAction,
|
||||
enclaveDataCID,
|
||||
)
|
||||
}
|
||||
|
||||
// GetEnclaveDataCID extracts the enclave data CID from vault capabilities
|
||||
func GetEnclaveDataCID(token *Token) (string, error) {
|
||||
for _, att := range token.Attenuations {
|
||||
resource := att.Resource.GetURI()
|
||||
if strings.HasPrefix(resource, "ipfs://") {
|
||||
return resource[7:], nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no enclave data CID found in token")
|
||||
}
|
||||
@@ -1,984 +0,0 @@
|
||||
// Package ucan provides User-Controlled Authorization Networks (UCAN) implementation
|
||||
// for decentralized authorization and capability delegation in the Sonr network.
|
||||
// This package handles JWT-based tokens, cryptographic verification, and resource capabilities.
|
||||
package ucan
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rsa"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/libp2p/go-libp2p/core/crypto"
|
||||
"github.com/sonr-io/crypto/keys"
|
||||
)
|
||||
|
||||
// Verifier provides UCAN token verification and validation functionality
|
||||
type Verifier struct {
|
||||
didResolver DIDResolver
|
||||
}
|
||||
|
||||
// DIDResolver resolves DID keys to public keys for signature verification
|
||||
type DIDResolver interface {
|
||||
ResolveDIDKey(ctx context.Context, did string) (keys.DID, error)
|
||||
}
|
||||
|
||||
// NewVerifier creates a new UCAN token verifier
|
||||
func NewVerifier(didResolver DIDResolver) *Verifier {
|
||||
return &Verifier{
|
||||
didResolver: didResolver,
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyToken parses and verifies a UCAN JWT token
|
||||
func (v *Verifier) VerifyToken(ctx context.Context, tokenString string) (*Token, error) {
|
||||
if tokenString == "" {
|
||||
return nil, fmt.Errorf("token string cannot be empty")
|
||||
}
|
||||
|
||||
// Parse the JWT token
|
||||
token, err := jwt.Parse(tokenString, v.keyFunc(ctx))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse JWT token: %w", err)
|
||||
}
|
||||
|
||||
// Extract claims
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid token claims type")
|
||||
}
|
||||
|
||||
// Parse UCAN-specific fields
|
||||
ucanToken, err := v.parseUCANClaims(claims, tokenString)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse UCAN claims: %w", err)
|
||||
}
|
||||
|
||||
// Validate token structure
|
||||
if err := v.validateToken(ctx, ucanToken); err != nil {
|
||||
return nil, fmt.Errorf("token validation failed: %w", err)
|
||||
}
|
||||
|
||||
return ucanToken, nil
|
||||
}
|
||||
|
||||
// VerifyCapability validates that a UCAN token grants specific capabilities
|
||||
func (v *Verifier) VerifyCapability(
|
||||
ctx context.Context,
|
||||
tokenString string,
|
||||
resource string,
|
||||
abilities []string,
|
||||
) (*Token, error) {
|
||||
token, err := v.VerifyToken(ctx, tokenString)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("token verification failed: %w", err)
|
||||
}
|
||||
|
||||
// Check if token grants required capabilities
|
||||
if err := v.checkCapabilities(token, resource, abilities); err != nil {
|
||||
return nil, fmt.Errorf("capability check failed: %w", err)
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// VerifyDelegationChain validates the complete delegation chain of a UCAN token
|
||||
func (v *Verifier) VerifyDelegationChain(ctx context.Context, tokenString string) error {
|
||||
token, err := v.VerifyToken(ctx, tokenString)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to verify root token: %w", err)
|
||||
}
|
||||
|
||||
// Verify each proof in the delegation chain
|
||||
for i, proof := range token.Proofs {
|
||||
proofToken, err := v.VerifyToken(ctx, string(proof))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to verify proof[%d] in delegation chain: %w", i, err)
|
||||
}
|
||||
|
||||
// Validate delegation relationship
|
||||
if err := v.validateDelegation(token, proofToken); err != nil {
|
||||
return fmt.Errorf("invalid delegation at proof[%d]: %w", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// keyFunc returns a function that resolves the signing key for JWT verification
|
||||
func (v *Verifier) keyFunc(ctx context.Context) jwt.Keyfunc {
|
||||
return func(token *jwt.Token) (any, error) {
|
||||
// Extract issuer from claims
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid claims type")
|
||||
}
|
||||
|
||||
issuer, ok := claims["iss"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid issuer claim")
|
||||
}
|
||||
|
||||
// Resolve the issuer's DID to get public key
|
||||
did, err := v.didResolver.ResolveDIDKey(ctx, issuer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve issuer DID: %w", err)
|
||||
}
|
||||
|
||||
// Get verification key based on signing method
|
||||
switch token.Method {
|
||||
case jwt.SigningMethodRS256, jwt.SigningMethodRS384, jwt.SigningMethodRS512:
|
||||
return v.getRSAPublicKey(did)
|
||||
case jwt.SigningMethodEdDSA:
|
||||
return v.getEd25519PublicKey(did)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported signing method: %v", token.Method)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parseUCANClaims extracts UCAN-specific fields from JWT claims
|
||||
func (v *Verifier) parseUCANClaims(claims jwt.MapClaims, raw string) (*Token, error) {
|
||||
issuer, audience := extractStandardClaims(claims)
|
||||
expiresAt, notBefore := extractTimeClaims(claims)
|
||||
|
||||
attenuations, err := v.parseAttenuationsClaims(claims)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proofs := parseProofsClaims(claims)
|
||||
facts := parseFactsClaims(claims)
|
||||
|
||||
return &Token{
|
||||
Raw: raw,
|
||||
Issuer: issuer,
|
||||
Audience: audience,
|
||||
ExpiresAt: expiresAt,
|
||||
NotBefore: notBefore,
|
||||
Attenuations: attenuations,
|
||||
Proofs: proofs,
|
||||
Facts: facts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// extractStandardClaims extracts standard JWT claims (issuer and audience)
|
||||
func extractStandardClaims(claims jwt.MapClaims) (string, string) {
|
||||
issuer, _ := claims["iss"].(string)
|
||||
audience, _ := claims["aud"].(string)
|
||||
return issuer, audience
|
||||
}
|
||||
|
||||
// extractTimeClaims extracts time-related claims (exp and nbf)
|
||||
func extractTimeClaims(claims jwt.MapClaims) (int64, int64) {
|
||||
var expiresAt, notBefore int64
|
||||
|
||||
if exp, ok := claims["exp"]; ok {
|
||||
if expFloat, ok := exp.(float64); ok {
|
||||
expiresAt = int64(expFloat)
|
||||
}
|
||||
}
|
||||
|
||||
if nbf, ok := claims["nbf"]; ok {
|
||||
if nbfFloat, ok := nbf.(float64); ok {
|
||||
notBefore = int64(nbfFloat)
|
||||
}
|
||||
}
|
||||
|
||||
return expiresAt, notBefore
|
||||
}
|
||||
|
||||
// parseAttenuationsClaims parses the attenuations from claims
|
||||
func (v *Verifier) parseAttenuationsClaims(claims jwt.MapClaims) ([]Attenuation, error) {
|
||||
attClaims, ok := claims["att"]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
attSlice, ok := attClaims.([]any)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Pre-allocate slice with known capacity
|
||||
attenuations := make([]Attenuation, 0, len(attSlice))
|
||||
|
||||
for _, attItem := range attSlice {
|
||||
attMap, ok := attItem.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
att, err := v.parseAttenuation(attMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse attenuation: %w", err)
|
||||
}
|
||||
attenuations = append(attenuations, att)
|
||||
}
|
||||
|
||||
return attenuations, nil
|
||||
}
|
||||
|
||||
// parseProofsClaims parses the proofs from claims
|
||||
func parseProofsClaims(claims jwt.MapClaims) []Proof {
|
||||
var proofs []Proof
|
||||
|
||||
prfClaims, ok := claims["prf"]
|
||||
if !ok {
|
||||
return proofs
|
||||
}
|
||||
|
||||
prfSlice, ok := prfClaims.([]any)
|
||||
if !ok {
|
||||
return proofs
|
||||
}
|
||||
|
||||
for _, prfItem := range prfSlice {
|
||||
if prfStr, ok := prfItem.(string); ok {
|
||||
proofs = append(proofs, Proof(prfStr))
|
||||
}
|
||||
}
|
||||
|
||||
return proofs
|
||||
}
|
||||
|
||||
// parseFactsClaims parses the facts from claims
|
||||
func parseFactsClaims(claims jwt.MapClaims) []Fact {
|
||||
fctClaims, ok := claims["fct"]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
fctSlice, ok := fctClaims.([]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Pre-allocate slice with known capacity
|
||||
facts := make([]Fact, 0, len(fctSlice))
|
||||
|
||||
for _, fctItem := range fctSlice {
|
||||
factData, _ := json.Marshal(fctItem)
|
||||
facts = append(facts, Fact{Data: factData})
|
||||
}
|
||||
|
||||
return facts
|
||||
}
|
||||
|
||||
// parseAttenuation converts a map to an Attenuation struct with enhanced module-specific support
|
||||
func (v *Verifier) parseAttenuation(attMap map[string]any) (Attenuation, error) {
|
||||
// Extract capability
|
||||
canValue, ok := attMap["can"]
|
||||
if !ok {
|
||||
return Attenuation{}, fmt.Errorf("missing 'can' field in attenuation")
|
||||
}
|
||||
|
||||
// Extract resource
|
||||
withValue, ok := attMap["with"]
|
||||
if !ok {
|
||||
return Attenuation{}, fmt.Errorf("missing 'with' field in attenuation")
|
||||
}
|
||||
|
||||
withStr, ok := withValue.(string)
|
||||
if !ok {
|
||||
return Attenuation{}, fmt.Errorf("'with' field must be a string")
|
||||
}
|
||||
|
||||
// Parse resource first to determine module type
|
||||
resource, err := v.parseResource(withStr)
|
||||
if err != nil {
|
||||
return Attenuation{}, fmt.Errorf("failed to parse resource: %w", err)
|
||||
}
|
||||
|
||||
// Create module-specific capability based on resource scheme
|
||||
cap, err := v.createModuleSpecificCapability(resource.GetScheme(), canValue, attMap)
|
||||
if err != nil {
|
||||
return Attenuation{}, fmt.Errorf("failed to create capability: %w", err)
|
||||
}
|
||||
|
||||
return Attenuation{
|
||||
Capability: cap,
|
||||
Resource: resource,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// createModuleSpecificCapability creates appropriate capability type based on module
|
||||
func (v *Verifier) createModuleSpecificCapability(scheme string, canValue any, attMap map[string]any) (Capability, error) {
|
||||
// Extract common fields
|
||||
caveats := extractStringSliceFromMap(attMap, "caveats")
|
||||
metadata := extractStringMapFromMap(attMap, "metadata")
|
||||
|
||||
switch scheme {
|
||||
case "did":
|
||||
return v.createDIDCapability(canValue, caveats, metadata)
|
||||
case "dwn":
|
||||
return v.createDWNCapability(canValue, caveats, metadata)
|
||||
case "service", "svc":
|
||||
return v.createServiceCapability(canValue, caveats, metadata)
|
||||
case "dex", "pool":
|
||||
return v.createDEXCapability(canValue, caveats, metadata, attMap)
|
||||
case "ipfs", "vault":
|
||||
// Handle existing vault capabilities
|
||||
return v.createVaultCapabilityFromMap(canValue, attMap)
|
||||
default:
|
||||
// Fallback to simple/multi capability for unknown schemes
|
||||
return v.createGenericCapability(canValue)
|
||||
}
|
||||
}
|
||||
|
||||
// createDIDCapability creates a DID-specific capability
|
||||
func (v *Verifier) createDIDCapability(canValue any, caveats []string, metadata map[string]string) (Capability, error) {
|
||||
switch canVal := canValue.(type) {
|
||||
case string:
|
||||
return &DIDCapability{
|
||||
Action: canVal,
|
||||
Caveats: caveats,
|
||||
Metadata: metadata,
|
||||
}, nil
|
||||
case []any:
|
||||
actions := extractStringSlice(canVal)
|
||||
return &DIDCapability{
|
||||
Actions: actions,
|
||||
Caveats: caveats,
|
||||
Metadata: metadata,
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported DID capability type")
|
||||
}
|
||||
}
|
||||
|
||||
// createDWNCapability creates a DWN-specific capability
|
||||
func (v *Verifier) createDWNCapability(canValue any, caveats []string, metadata map[string]string) (Capability, error) {
|
||||
switch canVal := canValue.(type) {
|
||||
case string:
|
||||
return &DWNCapability{
|
||||
Action: canVal,
|
||||
Caveats: caveats,
|
||||
Metadata: metadata,
|
||||
}, nil
|
||||
case []any:
|
||||
actions := extractStringSlice(canVal)
|
||||
return &DWNCapability{
|
||||
Actions: actions,
|
||||
Caveats: caveats,
|
||||
Metadata: metadata,
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported DWN capability type")
|
||||
}
|
||||
}
|
||||
|
||||
// createServiceCapability creates a Service-specific capability
|
||||
func (v *Verifier) createServiceCapability(canValue any, caveats []string, metadata map[string]string) (Capability, error) {
|
||||
// Service capabilities can still use MultiCapability for now
|
||||
switch canVal := canValue.(type) {
|
||||
case string:
|
||||
return &MultiCapability{Actions: []string{canVal}}, nil
|
||||
case []any:
|
||||
actions := extractStringSlice(canVal)
|
||||
return &MultiCapability{Actions: actions}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported Service capability type")
|
||||
}
|
||||
}
|
||||
|
||||
// createDEXCapability creates a DEX-specific capability
|
||||
func (v *Verifier) createDEXCapability(canValue any, caveats []string, metadata map[string]string, attMap map[string]any) (Capability, error) {
|
||||
maxAmount, _ := attMap["max_amount"].(string)
|
||||
|
||||
switch canVal := canValue.(type) {
|
||||
case string:
|
||||
return &DEXCapability{
|
||||
Action: canVal,
|
||||
Caveats: caveats,
|
||||
MaxAmount: maxAmount,
|
||||
Metadata: metadata,
|
||||
}, nil
|
||||
case []any:
|
||||
actions := extractStringSlice(canVal)
|
||||
return &DEXCapability{
|
||||
Actions: actions,
|
||||
Caveats: caveats,
|
||||
MaxAmount: maxAmount,
|
||||
Metadata: metadata,
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported DEX capability type")
|
||||
}
|
||||
}
|
||||
|
||||
// createVaultCapabilityFromMap creates vault capability from existing logic
|
||||
func (v *Verifier) createVaultCapabilityFromMap(canValue any, attMap map[string]any) (Capability, error) {
|
||||
// Use existing vault capability creation logic
|
||||
vaultAddress, _ := attMap["vault"].(string)
|
||||
caveats := extractStringSliceFromMap(attMap, "caveats")
|
||||
|
||||
switch canVal := canValue.(type) {
|
||||
case string:
|
||||
return &VaultCapability{
|
||||
Action: canVal,
|
||||
VaultAddress: vaultAddress,
|
||||
Caveats: caveats,
|
||||
}, nil
|
||||
case []any:
|
||||
actions := extractStringSlice(canVal)
|
||||
return &VaultCapability{
|
||||
Actions: actions,
|
||||
VaultAddress: vaultAddress,
|
||||
Caveats: caveats,
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported vault capability type")
|
||||
}
|
||||
}
|
||||
|
||||
// createGenericCapability creates fallback capability for unknown schemes
|
||||
func (v *Verifier) createGenericCapability(canValue any) (Capability, error) {
|
||||
switch canVal := canValue.(type) {
|
||||
case string:
|
||||
return &SimpleCapability{Action: canVal}, nil
|
||||
case []any:
|
||||
actions := extractStringSlice(canVal)
|
||||
return &MultiCapability{Actions: actions}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported capability type")
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions for extracting data from maps
|
||||
func extractStringSliceFromMap(m map[string]any, key string) []string {
|
||||
if value, exists := m[key]; exists {
|
||||
return extractStringSlice(value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractStringMapFromMap(m map[string]any, key string) map[string]string {
|
||||
result := make(map[string]string)
|
||||
if value, exists := m[key]; exists {
|
||||
if mapValue, ok := value.(map[string]any); ok {
|
||||
for k, v := range mapValue {
|
||||
if strValue, ok := v.(string); ok {
|
||||
result[k] = strValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// parseResource creates a Resource from a URI string
|
||||
func (v *Verifier) parseResource(uri string) (Resource, error) {
|
||||
if uri == "" {
|
||||
return nil, fmt.Errorf("resource URI cannot be empty")
|
||||
}
|
||||
|
||||
// Parse URI scheme and value - support both "scheme://value" and "scheme:value" formats
|
||||
var scheme, value string
|
||||
if strings.Contains(uri, "://") {
|
||||
parts := strings.SplitN(uri, "://", 2)
|
||||
if len(parts) == 2 {
|
||||
scheme = parts[0]
|
||||
value = parts[1]
|
||||
}
|
||||
} else if strings.Contains(uri, ":") {
|
||||
parts := strings.SplitN(uri, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
scheme = parts[0]
|
||||
value = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
if scheme == "" || value == "" {
|
||||
return nil, fmt.Errorf("invalid resource URI format: %s", uri)
|
||||
}
|
||||
|
||||
return &SimpleResource{
|
||||
Scheme: scheme,
|
||||
Value: value,
|
||||
URI: uri,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// validateToken performs structural and temporal validation
|
||||
func (v *Verifier) validateToken(_ context.Context, token *Token) error {
|
||||
// Check required fields
|
||||
if token.Issuer == "" {
|
||||
return fmt.Errorf("issuer is required")
|
||||
}
|
||||
if token.Audience == "" {
|
||||
return fmt.Errorf("audience is required")
|
||||
}
|
||||
if len(token.Attenuations) == 0 {
|
||||
return fmt.Errorf("at least one attenuation is required")
|
||||
}
|
||||
|
||||
// Check temporal validity
|
||||
now := time.Now().Unix()
|
||||
|
||||
if token.NotBefore > 0 && now < token.NotBefore {
|
||||
return fmt.Errorf("token is not yet valid (nbf: %d, now: %d)", token.NotBefore, now)
|
||||
}
|
||||
|
||||
if token.ExpiresAt > 0 && now >= token.ExpiresAt {
|
||||
return fmt.Errorf("token has expired (exp: %d, now: %d)", token.ExpiresAt, now)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkCapabilities verifies that the token grants the required capabilities with enhanced module-specific validation
|
||||
func (v *Verifier) checkCapabilities(token *Token, resource string, abilities []string) error {
|
||||
for _, att := range token.Attenuations {
|
||||
if att.Resource.GetURI() == resource {
|
||||
if att.Capability.Grants(abilities) {
|
||||
// Validate caveats for module-specific capabilities
|
||||
if err := v.validateCaveats(att.Capability, att.Resource); err != nil {
|
||||
return fmt.Errorf("caveat validation failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("required capabilities not granted for resource %s", resource)
|
||||
}
|
||||
|
||||
// validateCaveats validates constraints (caveats) for module-specific capabilities
|
||||
func (v *Verifier) validateCaveats(cap Capability, resource Resource) error {
|
||||
scheme := resource.GetScheme()
|
||||
|
||||
switch scheme {
|
||||
case "did":
|
||||
return v.validateDIDCaveats(cap, resource)
|
||||
case "dwn":
|
||||
return v.validateDWNCaveats(cap, resource)
|
||||
case "dex", "pool":
|
||||
return v.validateDEXCaveats(cap, resource)
|
||||
case "service", "svc":
|
||||
return v.validateServiceCaveats(cap, resource)
|
||||
case "vault", "ipfs":
|
||||
return v.validateVaultCaveats(cap, resource)
|
||||
default:
|
||||
return nil // No caveat validation for unknown schemes
|
||||
}
|
||||
}
|
||||
|
||||
// validateDIDCaveats validates DID-specific constraints
|
||||
func (v *Verifier) validateDIDCaveats(cap Capability, resource Resource) error {
|
||||
didCap, ok := cap.(*DIDCapability)
|
||||
if !ok {
|
||||
return nil // Not a DID capability
|
||||
}
|
||||
|
||||
for _, caveat := range didCap.Caveats {
|
||||
switch caveat {
|
||||
case "owner":
|
||||
// Validate that the capability is for the owner's DID
|
||||
if err := v.validateOwnerCaveat(resource); err != nil {
|
||||
return fmt.Errorf("owner caveat validation failed: %w", err)
|
||||
}
|
||||
case "controller":
|
||||
// Validate controller permissions
|
||||
if err := v.validateControllerCaveat(resource); err != nil {
|
||||
return fmt.Errorf("controller caveat validation failed: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateDWNCaveats validates DWN-specific constraints
|
||||
func (v *Verifier) validateDWNCaveats(cap Capability, resource Resource) error {
|
||||
dwnCap, ok := cap.(*DWNCapability)
|
||||
if !ok {
|
||||
return nil // Not a DWN capability
|
||||
}
|
||||
|
||||
for _, caveat := range dwnCap.Caveats {
|
||||
switch caveat {
|
||||
case "owner":
|
||||
// Validate record ownership
|
||||
if err := v.validateRecordOwnership(resource); err != nil {
|
||||
return fmt.Errorf("record ownership validation failed: %w", err)
|
||||
}
|
||||
case "protocol":
|
||||
// Validate protocol compliance
|
||||
if err := v.validateProtocolCaveat(resource); err != nil {
|
||||
return fmt.Errorf("protocol caveat validation failed: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateDEXCaveats validates DEX-specific constraints
|
||||
func (v *Verifier) validateDEXCaveats(cap Capability, resource Resource) error {
|
||||
dexCap, ok := cap.(*DEXCapability)
|
||||
if !ok {
|
||||
return nil // Not a DEX capability
|
||||
}
|
||||
|
||||
for _, caveat := range dexCap.Caveats {
|
||||
switch caveat {
|
||||
case "max-amount":
|
||||
// Validate maximum swap amount
|
||||
if dexCap.MaxAmount != "" {
|
||||
if err := v.validateMaxAmountCaveat(dexCap.MaxAmount); err != nil {
|
||||
return fmt.Errorf("max amount caveat validation failed: %w", err)
|
||||
}
|
||||
}
|
||||
case "pool-member":
|
||||
// Validate pool membership
|
||||
if err := v.validatePoolMembershipCaveat(resource); err != nil {
|
||||
return fmt.Errorf("pool membership validation failed: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateServiceCaveats validates Service-specific constraints
|
||||
func (v *Verifier) validateServiceCaveats(cap Capability, resource Resource) error {
|
||||
// Service capabilities use MultiCapability for now
|
||||
// Add service-specific caveat validation if needed
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateVaultCaveats validates Vault-specific constraints
|
||||
func (v *Verifier) validateVaultCaveats(cap Capability, resource Resource) error {
|
||||
vaultCap, ok := cap.(*VaultCapability)
|
||||
if !ok {
|
||||
return nil // Not a vault capability
|
||||
}
|
||||
|
||||
for _, caveat := range vaultCap.Caveats {
|
||||
switch caveat {
|
||||
case "vault-owner":
|
||||
// Validate vault ownership
|
||||
if err := v.validateVaultOwnership(vaultCap.VaultAddress); err != nil {
|
||||
return fmt.Errorf("vault ownership validation failed: %w", err)
|
||||
}
|
||||
case "enclave-integrity":
|
||||
// Validate enclave data integrity
|
||||
if err := v.validateEnclaveIntegrity(vaultCap.EnclaveDataCID); err != nil {
|
||||
return fmt.Errorf("enclave integrity validation failed: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Caveat validation helper methods (placeholders for actual implementation)
|
||||
|
||||
// validateOwnerCaveat validates DID ownership constraint
|
||||
func (v *Verifier) validateOwnerCaveat(resource Resource) error {
|
||||
// Placeholder: Implement actual DID ownership validation
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateControllerCaveat validates DID controller constraint
|
||||
func (v *Verifier) validateControllerCaveat(resource Resource) error {
|
||||
// Placeholder: Implement actual controller validation
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateRecordOwnership validates DWN record ownership
|
||||
func (v *Verifier) validateRecordOwnership(resource Resource) error {
|
||||
// Placeholder: Implement actual record ownership validation
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateProtocolCaveat validates DWN protocol constraint
|
||||
func (v *Verifier) validateProtocolCaveat(resource Resource) error {
|
||||
// Placeholder: Implement actual protocol validation
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateMaxAmountCaveat validates DEX maximum amount constraint
|
||||
func (v *Verifier) validateMaxAmountCaveat(maxAmount string) error {
|
||||
// Placeholder: Implement actual amount validation
|
||||
return nil
|
||||
}
|
||||
|
||||
// validatePoolMembershipCaveat validates DEX pool membership
|
||||
func (v *Verifier) validatePoolMembershipCaveat(resource Resource) error {
|
||||
// Placeholder: Implement actual pool membership validation
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateVaultOwnership validates vault ownership
|
||||
func (v *Verifier) validateVaultOwnership(vaultAddress string) error {
|
||||
// Placeholder: Implement actual vault ownership validation
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateEnclaveIntegrity validates enclave data integrity
|
||||
func (v *Verifier) validateEnclaveIntegrity(enclaveDataCID string) error {
|
||||
// Placeholder: Implement actual enclave integrity validation
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateDelegation checks that child token is properly attenuated from parent with enhanced module-specific validation
|
||||
func (v *Verifier) validateDelegation(child, parent *Token) error {
|
||||
// Child's issuer must be parent's audience
|
||||
if child.Issuer != parent.Audience {
|
||||
return fmt.Errorf("delegation chain broken: child issuer must be parent audience")
|
||||
}
|
||||
|
||||
// Child capabilities must be subset of parent with module-specific validation
|
||||
for _, childAtt := range child.Attenuations {
|
||||
if !v.isModuleCapabilitySubset(childAtt, parent.Attenuations) {
|
||||
return fmt.Errorf("child capability exceeds parent capabilities")
|
||||
}
|
||||
}
|
||||
|
||||
// Child expiration must not exceed parent
|
||||
if parent.ExpiresAt > 0 && (child.ExpiresAt == 0 || child.ExpiresAt > parent.ExpiresAt) {
|
||||
return fmt.Errorf("child token expires after parent token")
|
||||
}
|
||||
|
||||
// Validate cross-module delegation constraints
|
||||
if err := v.validateCrossModuleDelegation(child, parent); err != nil {
|
||||
return fmt.Errorf("cross-module delegation validation failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isModuleCapabilitySubset checks if a capability is a subset with module-specific logic
|
||||
func (v *Verifier) isModuleCapabilitySubset(childAtt Attenuation, parentAtts []Attenuation) bool {
|
||||
for _, parentAtt := range parentAtts {
|
||||
if childAtt.Resource.GetURI() == parentAtt.Resource.GetURI() {
|
||||
if v.isModuleCapabilityContained(childAtt.Capability, parentAtt.Capability, childAtt.Resource.GetScheme()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isModuleCapabilityContained checks containment with module-specific logic
|
||||
func (v *Verifier) isModuleCapabilityContained(child, parent Capability, scheme string) bool {
|
||||
// First check basic containment
|
||||
if parent.Contains(child) {
|
||||
// Additional module-specific containment validation
|
||||
switch scheme {
|
||||
case "did":
|
||||
return v.validateDIDContainment(child, parent)
|
||||
case "dwn":
|
||||
return v.validateDWNContainment(child, parent)
|
||||
case "dex", "pool":
|
||||
return v.validateDEXContainment(child, parent)
|
||||
case "vault", "ipfs":
|
||||
return v.validateVaultContainment(child, parent)
|
||||
default:
|
||||
return true // Basic containment is sufficient for unknown schemes
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// validateCrossModuleDelegation validates constraints across different modules
|
||||
func (v *Verifier) validateCrossModuleDelegation(child, parent *Token) error {
|
||||
childModules := v.extractModulesFromToken(child)
|
||||
parentModules := v.extractModulesFromToken(parent)
|
||||
|
||||
// Check if child uses modules not present in parent
|
||||
for module := range childModules {
|
||||
if _, exists := parentModules[module]; !exists {
|
||||
return fmt.Errorf("child token uses module '%s' not delegated by parent", module)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate specific cross-module constraints
|
||||
return v.validateSpecificCrossModuleConstraints(child, parent)
|
||||
}
|
||||
|
||||
// extractModulesFromToken extracts the modules used by a token
|
||||
func (v *Verifier) extractModulesFromToken(token *Token) map[string]bool {
|
||||
modules := make(map[string]bool)
|
||||
for _, att := range token.Attenuations {
|
||||
scheme := att.Resource.GetScheme()
|
||||
modules[scheme] = true
|
||||
}
|
||||
return modules
|
||||
}
|
||||
|
||||
// validateSpecificCrossModuleConstraints validates specific cross-module business logic
|
||||
func (v *Verifier) validateSpecificCrossModuleConstraints(child, parent *Token) error {
|
||||
// Example: If DID operations require vault access, ensure both are present
|
||||
childHasDID := v.tokenHasModule(child, "did")
|
||||
childHasVault := v.tokenHasModule(child, "vault") || v.tokenHasModule(child, "ipfs")
|
||||
|
||||
if childHasDID && !childHasVault {
|
||||
// Check if parent has vault capability that can be inherited
|
||||
parentHasVault := v.tokenHasModule(parent, "vault") || v.tokenHasModule(parent, "ipfs")
|
||||
if !parentHasVault {
|
||||
return fmt.Errorf("DID operations require vault access which is not available in delegation chain")
|
||||
}
|
||||
}
|
||||
|
||||
// Add more cross-module constraints as needed
|
||||
return nil
|
||||
}
|
||||
|
||||
// tokenHasModule checks if a token has capabilities for a specific module
|
||||
func (v *Verifier) tokenHasModule(token *Token, module string) bool {
|
||||
for _, att := range token.Attenuations {
|
||||
if att.Resource.GetScheme() == module {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Module-specific containment validation methods
|
||||
|
||||
// validateDIDContainment validates DID capability containment
|
||||
func (v *Verifier) validateDIDContainment(child, parent Capability) bool {
|
||||
childDID, childOk := child.(*DIDCapability)
|
||||
parentDID, parentOk := parent.(*DIDCapability)
|
||||
|
||||
if !childOk || !parentOk {
|
||||
return true // Not both DID capabilities, basic containment applies
|
||||
}
|
||||
|
||||
// Validate that child caveats are more restrictive or equal
|
||||
return v.areCaveatsMoreRestrictive(childDID.Caveats, parentDID.Caveats)
|
||||
}
|
||||
|
||||
// validateDWNContainment validates DWN capability containment
|
||||
func (v *Verifier) validateDWNContainment(child, parent Capability) bool {
|
||||
childDWN, childOk := child.(*DWNCapability)
|
||||
parentDWN, parentOk := parent.(*DWNCapability)
|
||||
|
||||
if !childOk || !parentOk {
|
||||
return true // Not both DWN capabilities, basic containment applies
|
||||
}
|
||||
|
||||
// Validate that child caveats are more restrictive or equal
|
||||
return v.areCaveatsMoreRestrictive(childDWN.Caveats, parentDWN.Caveats)
|
||||
}
|
||||
|
||||
// validateDEXContainment validates DEX capability containment
|
||||
func (v *Verifier) validateDEXContainment(child, parent Capability) bool {
|
||||
childDEX, childOk := child.(*DEXCapability)
|
||||
parentDEX, parentOk := parent.(*DEXCapability)
|
||||
|
||||
if !childOk || !parentOk {
|
||||
return true // Not both DEX capabilities, basic containment applies
|
||||
}
|
||||
|
||||
// Validate max amount restriction
|
||||
if parentDEX.MaxAmount != "" && childDEX.MaxAmount != "" {
|
||||
// Child max amount should be less than or equal to parent
|
||||
if !v.isAmountLessOrEqual(childDEX.MaxAmount, parentDEX.MaxAmount) {
|
||||
return false
|
||||
}
|
||||
} else if parentDEX.MaxAmount != "" && childDEX.MaxAmount == "" {
|
||||
// Child must have max amount if parent does
|
||||
return false
|
||||
}
|
||||
|
||||
// Validate that child caveats are more restrictive or equal
|
||||
return v.areCaveatsMoreRestrictive(childDEX.Caveats, parentDEX.Caveats)
|
||||
}
|
||||
|
||||
// validateVaultContainment validates Vault capability containment
|
||||
func (v *Verifier) validateVaultContainment(child, parent Capability) bool {
|
||||
childVault, childOk := child.(*VaultCapability)
|
||||
parentVault, parentOk := parent.(*VaultCapability)
|
||||
|
||||
if !childOk || !parentOk {
|
||||
return true // Not both Vault capabilities, basic containment applies
|
||||
}
|
||||
|
||||
// Vault address must match
|
||||
if childVault.VaultAddress != parentVault.VaultAddress {
|
||||
return false
|
||||
}
|
||||
|
||||
// Validate that child caveats are more restrictive or equal
|
||||
return v.areCaveatsMoreRestrictive(childVault.Caveats, parentVault.Caveats)
|
||||
}
|
||||
|
||||
// Helper methods for containment validation
|
||||
|
||||
// areCaveatsMoreRestrictive checks if child caveats are more restrictive than parent
|
||||
func (v *Verifier) areCaveatsMoreRestrictive(childCaveats, parentCaveats []string) bool {
|
||||
parentCaveatSet := make(map[string]bool)
|
||||
for _, caveat := range parentCaveats {
|
||||
parentCaveatSet[caveat] = true
|
||||
}
|
||||
|
||||
// All child caveats must be present in parent caveats (or child can have additional restrictions)
|
||||
for _, childCaveat := range childCaveats {
|
||||
if !parentCaveatSet[childCaveat] {
|
||||
// Child has additional restrictions, which is allowed
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// isAmountLessOrEqual compares two amount strings (placeholder implementation)
|
||||
func (v *Verifier) isAmountLessOrEqual(childAmount, parentAmount string) bool {
|
||||
// Placeholder: Implement actual amount comparison
|
||||
// This would parse the amounts and compare them numerically
|
||||
return true
|
||||
}
|
||||
|
||||
// isCapabilitySubset checks if a capability is a subset of any parent capabilities
|
||||
func (v *Verifier) isCapabilitySubset(childAtt Attenuation, parentAtts []Attenuation) bool {
|
||||
for _, parentAtt := range parentAtts {
|
||||
if childAtt.Resource.GetURI() == parentAtt.Resource.GetURI() {
|
||||
if parentAtt.Capability.Contains(childAtt.Capability) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// getRSAPublicKey extracts RSA public key from DID
|
||||
func (v *Verifier) getRSAPublicKey(did keys.DID) (*rsa.PublicKey, error) {
|
||||
verifyKey, err := did.VerifyKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get verify key: %w", err)
|
||||
}
|
||||
|
||||
rsaKey, ok := verifyKey.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("DID does not contain RSA public key")
|
||||
}
|
||||
|
||||
return rsaKey, nil
|
||||
}
|
||||
|
||||
// getEd25519PublicKey extracts Ed25519 public key from DID
|
||||
func (v *Verifier) getEd25519PublicKey(did keys.DID) (ed25519.PublicKey, error) {
|
||||
pubKey := did.PublicKey()
|
||||
rawBytes, err := pubKey.Raw()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get raw public key: %w", err)
|
||||
}
|
||||
|
||||
if pubKey.Type() != crypto.Ed25519 {
|
||||
return nil, fmt.Errorf("DID does not contain Ed25519 public key")
|
||||
}
|
||||
|
||||
return ed25519.PublicKey(rawBytes), nil
|
||||
}
|
||||
|
||||
// StringDIDResolver implements DIDResolver for did:key strings
|
||||
type StringDIDResolver struct{}
|
||||
|
||||
// ResolveDIDKey extracts a public key from a did:key string
|
||||
func (StringDIDResolver) ResolveDIDKey(ctx context.Context, didStr string) (keys.DID, error) {
|
||||
return keys.Parse(didStr)
|
||||
}
|
||||
Reference in New Issue
Block a user