From f66f6f5533505e7a1ae3cce5b10771c3c6429e02 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Wed, 7 Jan 2026 23:39:40 -0500 Subject: [PATCH 01/35] chore(deps): update dependencies for enclave module --- TODO.md | 15 ++++++++++ go.mod | 40 +++++++++++++++++++++++-- go.sum | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 144 insertions(+), 2 deletions(-) diff --git a/TODO.md b/TODO.md index 47dfcd5..cba3c3b 100644 --- a/TODO.md +++ b/TODO.md @@ -199,6 +199,7 @@ Remaining tasks from [MIGRATION.md](./MIGRATION.md) for the Nebula Key Enclave. > Reference: `main.go` ### 6.1 Extend `exec` Resource Handlers + - [ ] Add `key_shares` resource handler - [ ] Add `ucans` resource handler - [ ] Add `delegations` resource handler @@ -207,6 +208,7 @@ Remaining tasks from [MIGRATION.md](./MIGRATION.md) for the Nebula Key Enclave. - [ ] Add `sync_checkpoints` resource handler ### 6.2 Extend `generate` Function + - [ ] Parse WebAuthn credential properly (CBOR/COSE format) - [ ] Extract public key from credential - [ ] Create initial verification method @@ -214,6 +216,7 @@ Remaining tasks from [MIGRATION.md](./MIGRATION.md) for the Nebula Key Enclave. - [ ] Generate initial account (if key share provided) ### 6.3 Signing Function + - [ ] Implement `sign` wasmexport function - [ ] Support signing with MPC key shares - [ ] Return signature in appropriate format @@ -226,12 +229,14 @@ Remaining tasks from [MIGRATION.md](./MIGRATION.md) for the Nebula Key Enclave. > Reference: MIGRATION.md lines 826-827 ### 7.1 Delegation Chain Management + - [ ] Enforce maximum delegation depth (prevent infinite chains) - [ ] Validate delegator has capability to delegate - [ ] Ensure proper capability attenuation - [ ] Track parent-child relationships ### 7.2 Delegation Status + - [ ] Implement expiration checking - [ ] Handle revocation cascades (revoke chain) - [ ] Update status on expiry @@ -243,6 +248,7 @@ Remaining tasks from [MIGRATION.md](./MIGRATION.md) for the Nebula Key Enclave. > Reference: MIGRATION.md line 827 ### 8.1 Sync Infrastructure + - [ ] Create `internal/enclave/sync.go` - DID state sync logic - [ ] Implement checkpoint tracking - [ ] Store last synced block height @@ -261,6 +267,7 @@ Remaining tasks from [MIGRATION.md](./MIGRATION.md) for the Nebula Key Enclave. > Reference: README.md, `src/` directory ### 9.1 Core SDK + - [ ] Implement `createEnclave(wasmPath)` factory - [ ] Implement `generate(credential)` wrapper - [ ] Implement `load(database)` wrapper @@ -268,11 +275,13 @@ Remaining tasks from [MIGRATION.md](./MIGRATION.md) for the Nebula Key Enclave. - [ ] Implement `query(did?)` wrapper ### 9.2 Type Definitions + - [ ] Generate TypeScript types from Go structs - [ ] Export type definitions for consumers - [ ] Add JSDoc documentation ### 9.3 WebAuthn Integration + - [ ] Helper for credential creation - [ ] Helper for PRF extension output - [ ] Proper encoding/decoding utilities @@ -282,18 +291,21 @@ Remaining tasks from [MIGRATION.md](./MIGRATION.md) for the Nebula Key Enclave. ## 10. Testing ### 10.1 Unit Tests + - [ ] Test all ActionManager methods - [ ] Test serialization/deserialization roundtrip - [ ] Test encryption/decryption - [ ] Test UCAN validation logic ### 10.2 Integration Tests + - [ ] Test full generate → load → exec flow - [ ] Test credential lifecycle - [ ] Test session management - [ ] Test grant management ### 10.3 Plugin Tests + - [ ] Extend `make test-plugin` with all functions - [ ] Add error case testing - [ ] Test with various input formats @@ -303,18 +315,21 @@ Remaining tasks from [MIGRATION.md](./MIGRATION.md) for the Nebula Key Enclave. ## 11. Security Hardening ### 11.1 Input Validation + - [ ] Validate all JSON inputs against schemas - [ ] Sanitize SQL-sensitive characters in serialization - [ ] Validate DID format on all inputs - [ ] Validate base64 encoding ### 11.2 Cryptographic Security + - [ ] Use constant-time comparison for sensitive data - [ ] Clear sensitive data from memory after use - [ ] Validate key sizes and formats - [ ] Implement proper nonce generation ### 11.3 Access Control + - [ ] Enforce DID ownership on all mutations - [ ] Validate session before sensitive operations - [ ] Check grant scopes before data access diff --git a/go.mod b/go.mod index de29d03..b029fcd 100644 --- a/go.mod +++ b/go.mod @@ -2,11 +2,47 @@ module enclave go 1.25.5 -require github.com/extism/go-pdk v1.1.3 +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/ncruces/go-sqlite3 v0.30.4 + github.com/sonr-io/crypto v1.0.1 + github.com/stretchr/testify v1.11.1 + golang.org/x/crypto v0.46.0 + lukechampine.com/blake3 v1.4.1 +) require ( - github.com/ncruces/go-sqlite3 v0.30.4 // indirect + filippo.io/edwards25519 v1.1.0 // 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/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 + github.com/mr-tron/base58 v1.2.0 // indirect + 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-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/spaolacci/murmur3 v1.1.0 // indirect github.com/tetratelabs/wazero v1.11.0 // indirect + golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // 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 ) diff --git a/go.sum b/go.sum index 4e755c3..541838d 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,101 @@ +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/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= +github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/bwesterb/go-ristretto v1.2.3 h1:1w53tCkGhCQ5djbat3+MH0BAQ5Kfgbt56UZQ/JMzngw= +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/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/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/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= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +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= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= +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-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= +github.com/multiformats/go-varint v0.1.0/go.mod h1:5KVAVXegtfmNQQm/lCY+ATvDzvJJhSkUlGQV9wgObdI= github.com/ncruces/go-sqlite3 v0.30.4 h1:j9hEoOL7f9ZoXl8uqXVniaq1VNwlWAXihZbTvhqPPjA= github.com/ncruces/go-sqlite3 v0.30.4/go.mod h1:7WR20VSC5IZusKhUdiR9y1NsUqnZgqIYCmKKoMEYg68= github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M= 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.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +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/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= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +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= +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/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +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= +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.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= +lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= -- 2.43.0 From 5ed451d09b7b978dc250c9bd011d97748276cedf Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Wed, 7 Jan 2026 23:39:42 -0500 Subject: [PATCH 02/35] docs(mpc): add README documentation for MPC library --- internal/crypto/mpc/README.md | 499 ++++++++++++++ internal/crypto/mpc/codec.go | 110 ++++ internal/crypto/mpc/codec_test.go | 178 +++++ internal/crypto/mpc/enclave.go | 158 +++++ internal/crypto/mpc/enclave_test.go | 307 +++++++++ internal/crypto/mpc/import.go | 140 ++++ internal/crypto/mpc/protocol.go | 91 +++ internal/crypto/mpc/spec/jwt.go | 116 ++++ internal/crypto/mpc/spec/source.go | 305 +++++++++ internal/crypto/mpc/spec/ucan.go | 125 ++++ internal/crypto/mpc/utils.go | 160 +++++ internal/crypto/mpc/verify.go | 29 + internal/crypto/ucan/capability.go | 860 ++++++++++++++++++++++++ internal/crypto/ucan/crypto.go | 352 ++++++++++ internal/crypto/ucan/jwt.go | 595 +++++++++++++++++ internal/crypto/ucan/mpc.go | 625 ++++++++++++++++++ internal/crypto/ucan/source.go | 302 +++++++++ internal/crypto/ucan/stubs.go | 87 +++ internal/crypto/ucan/ucan_test.go | 313 +++++++++ internal/crypto/ucan/vault.go | 485 ++++++++++++++ internal/crypto/ucan/verifier.go | 984 ++++++++++++++++++++++++++++ 21 files changed, 6821 insertions(+) create mode 100644 internal/crypto/mpc/README.md create mode 100644 internal/crypto/mpc/codec.go create mode 100644 internal/crypto/mpc/codec_test.go create mode 100644 internal/crypto/mpc/enclave.go create mode 100644 internal/crypto/mpc/enclave_test.go create mode 100644 internal/crypto/mpc/import.go create mode 100644 internal/crypto/mpc/protocol.go create mode 100644 internal/crypto/mpc/spec/jwt.go create mode 100644 internal/crypto/mpc/spec/source.go create mode 100644 internal/crypto/mpc/spec/ucan.go create mode 100644 internal/crypto/mpc/utils.go create mode 100644 internal/crypto/mpc/verify.go create mode 100644 internal/crypto/ucan/capability.go create mode 100644 internal/crypto/ucan/crypto.go create mode 100644 internal/crypto/ucan/jwt.go create mode 100644 internal/crypto/ucan/mpc.go create mode 100644 internal/crypto/ucan/source.go create mode 100644 internal/crypto/ucan/stubs.go create mode 100644 internal/crypto/ucan/ucan_test.go create mode 100644 internal/crypto/ucan/vault.go create mode 100644 internal/crypto/ucan/verifier.go diff --git a/internal/crypto/mpc/README.md b/internal/crypto/mpc/README.md new file mode 100644 index 0000000..deafe34 --- /dev/null +++ b/internal/crypto/mpc/README.md @@ -0,0 +1,499 @@ +# MPC (Multi-Party Computation) Cryptographic Library + +![Go](https://img.shields.io/badge/Go-1.24+-green) +![MPC](https://img.shields.io/badge/MPC-Threshold_Signing-blue) +![Encryption](https://img.shields.io/badge/Encryption-AES--GCM-red) +![ECDSA](https://img.shields.io/badge/Curve-secp256k1-yellow) + +A comprehensive Go implementation of Multi-Party Computation (MPC) primitives for secure distributed cryptography. This package provides threshold signing, encrypted key management, and secure keyshare operations for decentralized applications. + +## Features + +- ✅ **Threshold Cryptography** - 2-of-2 MPC key generation and signing +- ✅ **Secure Enclaves** - Encrypted keyshare storage and management +- ✅ **Multiple Curves** - Support for secp256k1, P-256, Ed25519, BLS12-381, and more +- ✅ **Key Refresh** - Proactive security through keyshare rotation +- ✅ **ECDSA Signing** - Distributed signature generation with SHA3-256 +- ✅ **Encrypted Export/Import** - Secure enclave serialization with AES-GCM +- ✅ **UCAN Integration** - MPC-based JWT signing for User-Controlled Authorization Networks + +## Architecture + +The package is built around the concept of secure **Enclaves** that manage distributed keyshares: + +``` +┌─────────────────────────────────────────────────────────┐ +│ MPC Enclave │ +├─────────────────────────────────────────────────────────┤ +│ ┌─────────────┐ ┌─────────────┐ │ +│ │ Alice Share │ │ Bob Share │ ←── Threshold 2/2 │ +│ │ (Validator) │ │ (User) │ │ +│ └─────────────┘ └─────────────┘ │ +├─────────────────────────────────────────────────────────┤ +│ • Distributed Key Generation (DKG) │ +│ • Threshold Signing (2-of-2) │ +│ • Key Refresh (Proactive Security) │ +│ • Encrypted Storage (AES-GCM) │ +└─────────────────────────────────────────────────────────┘ +``` + +## Quick Start + +### Installation + +```bash +go get github.com/sonr-io/crypto/mpc +``` + +### Basic Usage + +#### Creating a New MPC Enclave + +```go +package main + +import ( + "fmt" + "github.com/sonr-io/crypto/mpc" +) + +func main() { + // Generate a new MPC enclave with distributed keyshares + enclave, err := mpc.NewEnclave() + if err != nil { + panic(err) + } + + // Get the public key + pubKeyHex := enclave.PubKeyHex() + fmt.Printf("Public Key: %s\n", pubKeyHex) + + // Verify the enclave is valid + if enclave.IsValid() { + fmt.Println("✅ Enclave successfully created!") + } +} +``` + +#### Signing and Verification + +```go +// Sign data using distributed MPC protocol +message := []byte("Hello, distributed world!") +signature, err := enclave.Sign(message) +if err != nil { + panic(err) +} + +// Verify the signature +isValid, err := enclave.Verify(message, signature) +if err != nil { + panic(err) +} + +fmt.Printf("Signature valid: %t\n", isValid) +``` + +#### Secure Export and Import + +```go +// Export enclave with encryption +secretKey := []byte("my-super-secret-key-32-bytes-long") +encryptedData, err := enclave.Encrypt(secretKey) +if err != nil { + panic(err) +} + +// Import from encrypted data +restoredEnclave, err := mpc.ImportEnclave( + mpc.WithEncryptedData(encryptedData, secretKey), +) +if err != nil { + panic(err) +} + +fmt.Printf("Restored public key: %s\n", restoredEnclave.PubKeyHex()) +``` + +## Core Concepts + +### Enclaves + +An **Enclave** represents a secure MPC keyshare environment that manages distributed cryptographic operations: + +```go +type Enclave interface { + // Key Management + PubKeyHex() string // Get public key as hex string + PubKeyBytes() []byte // Get public key as bytes + IsValid() bool // Check if enclave has valid keyshares + + // Cryptographic Operations + Sign(data []byte) ([]byte, error) // Threshold signing + Verify(data []byte, sig []byte) (bool, error) // Signature verification + Refresh() (Enclave, error) // Proactive key refresh + + // Secure Storage + Encrypt(key []byte) ([]byte, error) // Export encrypted + Decrypt(key []byte, data []byte) ([]byte, error) // Import encrypted + + // Serialization + Marshal() ([]byte, error) // JSON serialization + Unmarshal(data []byte) error // JSON deserialization + + // Data Access + GetData() *EnclaveData // Access enclave internals + GetEnclave() Enclave // Self-reference +} +``` + +### Multi-Party Computation Protocol + +The package implements a 2-of-2 threshold scheme: + +1. **Alice (Validator)** - Server-side keyshare +2. **Bob (User)** - Client-side keyshare + +Both parties must participate in: +- **Distributed Key Generation (DKG)** - Creates shared public key +- **Threshold Signing** - Generates valid signatures cooperatively +- **Key Refresh** - Rotates keyshares while preserving public key + +### Supported Curves + +The package supports multiple elliptic curves: + +```go +type CurveName string + +const ( + K256Name CurveName = "secp256k1" // Bitcoin/Ethereum + P256Name CurveName = "P-256" // NIST P-256 + ED25519Name CurveName = "ed25519" // EdDSA + BLS12381G1Name CurveName = "BLS12381G1" // BLS12-381 G1 + BLS12381G2Name CurveName = "BLS12381G2" // BLS12-381 G2 + // ... more curves supported +) +``` + +## Advanced Usage + +### Custom Import Options + +The package provides flexible import mechanisms: + +```go +// Import from initial keyshares (after DKG) +enclave, err := mpc.ImportEnclave( + mpc.WithInitialShares(validatorShare, userShare, mpc.K256Name), +) + +// Import from existing enclave data +enclave, err := mpc.ImportEnclave( + mpc.WithEnclaveData(enclaveData), +) + +// Import from encrypted backup +enclave, err := mpc.ImportEnclave( + mpc.WithEncryptedData(encryptedBytes, secretKey), +) +``` + +### Key Refresh for Proactive Security + +```go +// Refresh keyshares while keeping the same public key +refreshedEnclave, err := enclave.Refresh() +if err != nil { + panic(err) +} + +// Public key remains the same +fmt.Printf("Original: %s\n", enclave.PubKeyHex()) +fmt.Printf("Refreshed: %s\n", refreshedEnclave.PubKeyHex()) +// Both should be identical! + +// But the enclave now has fresh keyshares +// This provides forward secrecy against key compromise +``` + +### Standalone Verification + +```go +// Verify signatures without the full enclave +pubKeyBytes := enclave.PubKeyBytes() +isValid, err := mpc.VerifyWithPubKey(pubKeyBytes, message, signature) +if err != nil { + panic(err) +} +``` + +### UCAN Integration + +The package includes MPC-based JWT signing for UCAN tokens: + +```go +import "github.com/sonr-io/crypto/mpc/spec" + +// Create MPC-backed UCAN token source +// (Implementation details in spec package) +keyshareSource := spec.KeyshareSource{ + // ... MPC enclave integration +} + +// Use with UCAN token creation +token, err := keyshareSource.NewOriginToken( + "did:key:audience", + attenuations, + facts, + notBefore, + expires, +) +``` + +## Security Features + +### Encryption + +All encrypted operations use **AES-GCM** with **SHA3-256** key derivation: + +```go +// Secure key derivation +func GetHashKey(key []byte) []byte { + hash := sha3.New256() + hash.Write(key) + return hash.Sum(nil)[:32] // 256-bit key +} +``` + +### Threshold Security + +- **2-of-2 threshold** - Both parties required for operations +- **No single point of failure** - Neither party alone can sign +- **Proactive refresh** - Regular keyshare rotation without changing public key +- **Forward secrecy** - Old keyshares cannot be used after refresh + +### Cryptographic Primitives + +- **ECDSA Signing** with **SHA3-256** message hashing +- **AES-GCM** encryption with 12-byte nonces +- **Secure random nonce generation** +- **Multiple curve support** for different use cases + +## Public API Reference + +### Core Functions + +```go +// Generate new MPC enclave +func NewEnclave() (Enclave, error) + +// Import enclave from various sources +func ImportEnclave(options ...ImportOption) (Enclave, error) + +// Execute distributed signing protocol +func ExecuteSigning(signFuncVal SignFunc, signFuncUser SignFunc) ([]byte, error) + +// Execute keyshare refresh protocol +func ExecuteRefresh(refreshFuncVal RefreshFunc, refreshFuncUser RefreshFunc, + curve CurveName) (Enclave, error) + +// Standalone signature verification +func VerifyWithPubKey(pubKeyCompressed []byte, data []byte, sig []byte) (bool, error) +``` + +### Import Options + +```go +type ImportOption func(Options) Options + +// Create from initial DKG results +func WithInitialShares(valKeyshare Message, userKeyshare Message, + curve CurveName) ImportOption + +// Create from encrypted backup +func WithEncryptedData(data []byte, key []byte) ImportOption + +// Create from existing data structure +func WithEnclaveData(data *EnclaveData) ImportOption +``` + +### EnclaveData Structure + +```go +type EnclaveData struct { + PubHex string `json:"pub_hex"` // Compressed public key (hex) + PubBytes []byte `json:"pub_bytes"` // Uncompressed public key + ValShare Message `json:"val_share"` // Alice (validator) keyshare + UserShare Message `json:"user_share"`// Bob (user) keyshare + Nonce []byte `json:"nonce"` // Encryption nonce + Curve CurveName `json:"curve"` // Elliptic curve name +} +``` + +### Protocol Types + +```go +type Message *protocol.Message // MPC protocol message +type Signature *curves.EcdsaSignature // ECDSA signature +type RefreshFunc interface{ protocol.Iterator } // Key refresh protocol +type SignFunc interface{ protocol.Iterator } // Signing protocol +type Point curves.Point // Elliptic curve point +``` + +### Utility Functions + +```go +// Cryptographic utilities +func GetHashKey(key []byte) []byte +func SerializeSignature(sig *curves.EcdsaSignature) ([]byte, error) +func DeserializeSignature(sigBytes []byte) (*curves.EcdsaSignature, error) + +// Key conversion utilities +func GetECDSAPoint(pubKey []byte) (*curves.EcPoint, error) + +// Protocol error handling +func CheckIteratedErrors(aErr, bErr error) error +``` + +## Error Handling + +The package provides comprehensive error handling: + +```go +// Common error patterns +enclave, err := mpc.NewEnclave() +if err != nil { + // Handle DKG failure + log.Fatalf("Failed to generate enclave: %v", err) +} + +signature, err := enclave.Sign(data) +if err != nil { + // Handle signing protocol failure + log.Fatalf("Failed to sign: %v", err) +} + +// Validation errors +if !enclave.IsValid() { + log.Fatal("Enclave has invalid keyshares") +} +``` + +## Performance Considerations + +### Memory Usage + +- **Minimal footprint** - Only active keyshares kept in memory +- **Efficient serialization** - JSON-based with compression +- **Secure cleanup** - Sensitive data cleared after use + +### Network Communication + +- **Minimal rounds** - Optimized protocol with few message exchanges +- **Small messages** - Compact protocol message format +- **Stateless operations** - No persistent connections required + +### Cryptographic Performance + +- **Hardware acceleration** - Leverages optimized curve implementations +- **Efficient hashing** - SHA3-256 with minimal overhead +- **Fast verification** - Public key operations optimized + +## Testing + +The package includes comprehensive tests: + +```bash +# Run all tests +go test -v ./crypto/mpc + +# Run specific test suites +go test -v ./crypto/mpc -run TestEnclaveData +go test -v ./crypto/mpc -run TestKeyShareGeneration +go test -v ./crypto/mpc -run TestEnclaveOperations + +# Run with race detection +go test -race ./crypto/mpc + +# Generate coverage report +go test -cover ./crypto/mpc +``` + +## Use Cases + +### Decentralized Identity + +- **DID key management** - Secure distributed identity keys +- **Threshold signing** - Multi-party authorization for identity operations +- **Key recovery** - Distributed backup and restore mechanisms + +### Cryptocurrency Wallets + +- **Multi-signature wallets** - True threshold custody solutions +- **Exchange security** - Hot wallet protection with distributed keys +- **Institutional custody** - Compliance-friendly key management + +### Blockchain Infrastructure + +- **Validator signing** - Secure consensus participation +- **Cross-chain bridges** - Multi-party custody of bridged assets +- **DAO governance** - Distributed decision-making mechanisms + +### Enterprise Applications + +- **Document signing** - Distributed digital signatures +- **API authentication** - Threshold-based service authentication +- **Secure communication** - End-to-end encrypted messaging + +## Dependencies + +- **Core Cryptography**: `github.com/sonr-io/crypto/core/curves` +- **Protocol Framework**: `github.com/sonr-io/crypto/core/protocol` +- **Threshold ECDSA**: `github.com/sonr-io/crypto/tecdsa/dklsv1` +- **UCAN Integration**: `github.com/sonr-io/crypto/ucan` +- **Standard Crypto**: `golang.org/x/crypto/sha3` +- **JWT Support**: `github.com/golang-jwt/jwt` + +## Security Considerations + +### Threat Model + +The package is designed to protect against: + +- **Key compromise** - Distributed keyshares prevent single points of failure +- **Insider threats** - No single party can perform operations alone +- **Network attacks** - Protocol messages are cryptographically protected +- **Side-channel attacks** - Secure implementations of cryptographic primitives + +### Best Practices + +1. **Regular key refresh** - Rotate keyshares periodically +2. **Secure communication** - Use TLS for protocol message exchange +3. **Access controls** - Implement proper authentication for MPC operations +4. **Audit logging** - Log all cryptographic operations +5. **Backup strategies** - Securely store encrypted enclave exports + +### Limitations + +- **2-of-2 threshold only** - Currently supports only 2-party protocols +- **Network dependency** - Requires communication between parties +- **No byzantine fault tolerance** - Assumes honest-but-curious adversaries + +## Contributing + +We welcome contributions! Please ensure: + +1. **Security first** - All cryptographic code must be carefully reviewed +2. **Comprehensive testing** - Include unit tests and integration tests +3. **Documentation** - Document all public APIs and security assumptions +4. **Performance** - Benchmark critical cryptographic operations +5. **Compatibility** - Maintain backward compatibility with existing enclaves + +## License + +This project follows the same license as the main Sonr project. + +--- + +**⚠️ Security Notice**: This is cryptographic software. While extensively tested, it should be used with appropriate security measures and understanding of the underlying protocols. For production deployments, consider additional security audits and operational security measures. \ No newline at end of file diff --git a/internal/crypto/mpc/codec.go b/internal/crypto/mpc/codec.go new file mode 100644 index 0000000..305ac2d --- /dev/null +++ b/internal/crypto/mpc/codec.go @@ -0,0 +1,110 @@ +// Package mpc implements the Sonr MPC protocol +package mpc + +import ( + "crypto/rand" + + "github.com/sonr-io/crypto/core/curves" + "github.com/sonr-io/crypto/core/protocol" + "github.com/sonr-io/crypto/tecdsa/dklsv1/dkg" +) + +type CurveName string + +const ( + K256Name CurveName = "secp256k1" + BLS12381G1Name CurveName = "BLS12381G1" + BLS12381G2Name CurveName = "BLS12381G2" + BLS12831Name CurveName = "BLS12831" + P256Name CurveName = "P-256" + ED25519Name CurveName = "ed25519" + PallasName CurveName = "pallas" + BLS12377G1Name CurveName = "BLS12377G1" + BLS12377G2Name CurveName = "BLS12377G2" + BLS12377Name CurveName = "BLS12377" +) + +func (c CurveName) String() string { + return string(c) +} + +func (c CurveName) Curve() *curves.Curve { + switch c { + case K256Name: + return curves.K256() + case BLS12381G1Name: + return curves.BLS12381G1() + case BLS12381G2Name: + return curves.BLS12381G2() + case BLS12831Name: + return curves.BLS12381G1() + case P256Name: + return curves.P256() + case ED25519Name: + return curves.ED25519() + case PallasName: + return curves.PALLAS() + case BLS12377G1Name: + return curves.BLS12377G1() + case BLS12377G2Name: + return curves.BLS12377G2() + case BLS12377Name: + return curves.BLS12377G1() + default: + return curves.K256() + } +} + +// ╭───────────────────────────────────────────────────────────╮ +// │ Exported Generics │ +// ╰───────────────────────────────────────────────────────────╯ + +type ( + AliceOut *dkg.AliceOutput + BobOut *dkg.BobOutput + Point curves.Point + Role string // Role is the type for the role + Message *protocol.Message // Message is the protocol.Message that is used for MPC + Signature *curves.EcdsaSignature // Signature is the type for the signature + RefreshFunc interface{ protocol.Iterator } // RefreshFunc is the type for the refresh function + SignFunc interface{ protocol.Iterator } // SignFunc is the type for the sign function +) + +const ( + RoleVal = "validator" + RoleUser = "user" +) + +func randNonce() []byte { + nonce := make([]byte, 12) + rand.Read(nonce) + return nonce +} + +// Enclave defines the interface for key management operations +type Enclave interface { + GetData() *EnclaveData // GetData returns the data of the keyEnclave + GetEnclave() Enclave // GetEnclave returns the enclave of the keyEnclave + Decrypt( + key []byte, + encryptedData []byte, + ) ([]byte, error) // Decrypt returns decrypted enclave data + Encrypt( + key []byte, + ) ([]byte, error) // Encrypt returns encrypted enclave data + IsValid() bool // IsValid returns true if the keyEnclave is valid + PubKeyBytes() []byte // PubKeyBytes returns the public key of the keyEnclave + PubKeyHex() string // PubKeyHex returns the public key of the keyEnclave + Refresh() (Enclave, error) // Refresh returns a new keyEnclave + Marshal() ([]byte, error) // Serialize returns the serialized keyEnclave + Sign( + data []byte, + ) ([]byte, error) // Sign returns the signature of the data + Unmarshal( + data []byte, + ) error // Verify returns true if the signature is valid + Verify( + data []byte, + sig []byte, + ) (bool, error) // Verify returns true if the signature is valid +} diff --git a/internal/crypto/mpc/codec_test.go b/internal/crypto/mpc/codec_test.go new file mode 100644 index 0000000..933b93a --- /dev/null +++ b/internal/crypto/mpc/codec_test.go @@ -0,0 +1,178 @@ +package mpc + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestKeyShareGeneration(t *testing.T) { + t.Run("Generate Valid Enclave", func(t *testing.T) { + // Generate enclave + enclave, err := NewEnclave() + require.NoError(t, err) + require.NotNil(t, enclave) + + // Validate enclave contents + assert.True(t, enclave.IsValid()) + }) + + t.Run("Export and Import", func(t *testing.T) { + // Generate original enclave + original, err := NewEnclave() + require.NoError(t, err) + + // Test key for encryption/decryption (32 bytes) + testKey := []byte("test-key-12345678-test-key-123456") + + // Test Export/Import + t.Run("Full Enclave", func(t *testing.T) { + // Export enclave + data, err := original.Encrypt(testKey) + require.NoError(t, err) + require.NotEmpty(t, data) + + // Create new empty enclave + newEnclave, err := NewEnclave() + require.NoError(t, err) + + // Verify the imported enclave works by signing + testData := []byte("test message") + sig, err := newEnclave.Sign(testData) + require.NoError(t, err) + valid, err := newEnclave.Verify(testData, sig) + require.NoError(t, err) + assert.True(t, valid) + }) + }) + + t.Run("Encrypt and Decrypt", func(t *testing.T) { + // Generate original enclave + original, err := NewEnclave() + require.NoError(t, err) + require.NotNil(t, original) + + // Test key for encryption/decryption (32 bytes) + testKey := []byte("test-key-12345678-test-key-123456") + + // Test Encrypt + encrypted, err := original.Encrypt(testKey) + require.NoError(t, err) + require.NotEmpty(t, encrypted) + + // Test Decrypt + decrypted, err := original.Decrypt(testKey, encrypted) + require.NoError(t, err) + require.NotEmpty(t, decrypted) + + // Verify decrypted data matches original + originalData, err := original.Marshal() + require.NoError(t, err) + assert.Equal(t, originalData, decrypted) + + // Test with wrong key should fail + wrongKey := []byte("wrong-key-12345678-wrong-key-123456") + _, err = original.Decrypt(wrongKey, encrypted) + assert.Error(t, err, "Decryption with wrong key should fail") + }) +} + +func TestEnclaveOperations(t *testing.T) { + t.Run("Signing and Verification", func(t *testing.T) { + // Generate valid enclave + enclave, err := NewEnclave() + require.NoError(t, err) + + // Test signing + testData := []byte("test message") + signature, err := enclave.Sign(testData) + require.NoError(t, err) + require.NotNil(t, signature) + + // Verify the signature + valid, err := enclave.Verify(testData, signature) + require.NoError(t, err) + assert.True(t, valid) + + // Test invalid data verification + invalidData := []byte("wrong message") + valid, err = enclave.Verify(invalidData, signature) + require.NoError(t, err) + assert.False(t, valid) + }) + + t.Run("Refresh Operation", func(t *testing.T) { + enclave, err := NewEnclave() + require.NoError(t, err) + + // Test refresh + refreshedEnclave, err := enclave.Refresh() + require.NoError(t, err) + require.NotNil(t, refreshedEnclave) + + // Verify refreshed enclave is valid + assert.True(t, refreshedEnclave.IsValid()) + }) +} + +func TestEnclaveDataAccess(t *testing.T) { + t.Run("GetData", func(t *testing.T) { + // Generate enclave + enclave, err := NewEnclave() + require.NoError(t, err) + require.NotNil(t, enclave) + + // Get the enclave data + data := enclave.GetData() + require.NotNil(t, data, "GetData should return non-nil value") + + // Verify the data is valid + assert.True(t, data.IsValid(), "Enclave data should be valid") + + // Verify the public key in the data matches the enclave's public key + assert.Equal(t, enclave.PubKeyHex(), data.PubKeyHex(), "Public keys should match") + }) + + t.Run("PubKeyHex", func(t *testing.T) { + // Generate enclave + enclave, err := NewEnclave() + require.NoError(t, err) + require.NotNil(t, enclave) + + // Get the public key hex + pubKeyHex := enclave.PubKeyHex() + require.NotEmpty(t, pubKeyHex, "PubKeyHex should return non-empty string") + + // Check that it's a valid hex string (should be 66 chars for compressed point: 0x02/0x03 + 32 bytes) + assert.GreaterOrEqual( + t, + len(pubKeyHex), + 66, + "Public key hex should be at least 66 characters", + ) + assert.True(t, len(pubKeyHex)%2 == 0, "Hex string should have even length") + + // Compare with the enclave data's public key + data := enclave.GetData() + assert.Equal( + t, + data.PubKeyHex(), + pubKeyHex, + "Public key hex should match the one from GetData", + ) + + // Verify that two different enclaves have different public keys + enclave2, err := NewEnclave() + require.NoError(t, err) + require.NotNil(t, enclave2) + + pubKeyHex2 := enclave2.PubKeyHex() + assert.NotEqual( + t, + pubKeyHex, + pubKeyHex2, + "Different enclaves should have different public keys", + ) + }) +} diff --git a/internal/crypto/mpc/enclave.go b/internal/crypto/mpc/enclave.go new file mode 100644 index 0000000..be7c941 --- /dev/null +++ b/internal/crypto/mpc/enclave.go @@ -0,0 +1,158 @@ +package mpc + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/ecdsa" + "encoding/json" + "fmt" + + "github.com/sonr-io/crypto/core/curves" + "golang.org/x/crypto/sha3" +) + +// EnclaveData implements the Enclave interface +type EnclaveData struct { + PubHex string `json:"pub_hex"` // PubHex is the hex-encoded compressed public key + PubBytes []byte `json:"pub_bytes"` // PubBytes is the uncompressed public key + ValShare Message `json:"val_share"` + UserShare Message `json:"user_share"` + Nonce []byte `json:"nonce"` + Curve CurveName `json:"curve"` +} + +// GetData returns the data of the keyEnclave +func (k *EnclaveData) GetData() *EnclaveData { + return k +} + +// GetEnclave returns the enclave of the keyEnclave +func (k *EnclaveData) GetEnclave() Enclave { + return k +} + +// GetPubPoint returns the public point of the keyEnclave +func (k *EnclaveData) GetPubPoint() (curves.Point, error) { + curve := k.Curve.Curve() + return curve.NewIdentityPoint().FromAffineUncompressed(k.PubBytes) +} + +// PubKeyHex returns the public key of the keyEnclave +func (k *EnclaveData) PubKeyHex() string { + return k.PubHex +} + +// PubKeyBytes returns the public key of the keyEnclave +func (k *EnclaveData) PubKeyBytes() []byte { + return k.PubBytes +} + +// Decrypt returns decrypted enclave data +func (k *EnclaveData) Decrypt(key []byte, encryptedData []byte) ([]byte, error) { + hashedKey := GetHashKey(key) + block, err := aes.NewCipher(hashedKey) + if err != nil { + return nil, err + } + + aesgcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + // Decrypt the data using AES-GCM + plaintext, err := aesgcm.Open(nil, k.Nonce, encryptedData, nil) + if err != nil { + return nil, fmt.Errorf("decryption failed: %w", err) + } + return plaintext, nil +} + +// Encrypt returns encrypted enclave data +func (k *EnclaveData) Encrypt(key []byte) ([]byte, error) { + data, err := k.Marshal() + if err != nil { + return nil, fmt.Errorf("failed to serialize enclave: %w", err) + } + + hashedKey := GetHashKey(key) + block, err := aes.NewCipher(hashedKey) + if err != nil { + return nil, err + } + + aesgcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + return aesgcm.Seal(nil, k.Nonce, data, nil), nil +} + +// IsValid returns true if the keyEnclave is valid +func (k *EnclaveData) IsValid() bool { + return k.ValShare != nil && k.UserShare != nil +} + +// Refresh returns a new keyEnclave +func (k *EnclaveData) Refresh() (Enclave, error) { + refreshFuncVal, err := GetAliceRefreshFunc(k) + if err != nil { + return nil, err + } + refreshFuncUser, err := GetBobRefreshFunc(k) + if err != nil { + return nil, err + } + return ExecuteRefresh(refreshFuncVal, refreshFuncUser, k.Curve) +} + +// Sign returns the signature of the data +func (k *EnclaveData) Sign(data []byte) ([]byte, error) { + userSign, err := GetBobSignFunc(k, data) + if err != nil { + return nil, err + } + valSign, err := GetAliceSignFunc(k, data) + if err != nil { + return nil, err + } + return ExecuteSigning(valSign, userSign) +} + +// Verify returns true if the signature is valid +func (k *EnclaveData) Verify(data []byte, sig []byte) (bool, error) { + edSig, err := DeserializeSignature(sig) + if err != nil { + return false, err + } + ePub, err := GetECDSAPoint(k.PubBytes) + if err != nil { + return false, err + } + pk := &ecdsa.PublicKey{ + Curve: ePub.Curve, + X: ePub.X, + Y: ePub.Y, + } + + // Hash the message using SHA3-256 + hash := sha3.New256() + hash.Write(data) + digest := hash.Sum(nil) + + return ecdsa.Verify(pk, digest, edSig.R, edSig.S), nil +} + +// Marshal returns the JSON encoding of keyEnclave +func (k *EnclaveData) Marshal() ([]byte, error) { + return json.Marshal(k) +} + +// Unmarshal unmarshals the JSON encoding of keyEnclave +func (k *EnclaveData) Unmarshal(data []byte) error { + if err := json.Unmarshal(data, k); err != nil { + return err + } + return nil +} diff --git a/internal/crypto/mpc/enclave_test.go b/internal/crypto/mpc/enclave_test.go new file mode 100644 index 0000000..39dc115 --- /dev/null +++ b/internal/crypto/mpc/enclave_test.go @@ -0,0 +1,307 @@ +package mpc + +import ( + "bytes" + "encoding/hex" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestEnclaveData_GetData(t *testing.T) { + // Create a new enclave + enclave, err := NewEnclave() + require.NoError(t, err) + require.NotNil(t, enclave) + + // Get the data + data := enclave.GetData() + require.NotNil(t, data) + + // Ensure the data is the same instance + assert.Equal(t, enclave, data.GetEnclave()) + + // Ensure the data is valid + assert.True(t, data.IsValid()) +} + +func TestEnclaveData_GetEnclave(t *testing.T) { + // Create a new enclave + enclave, err := NewEnclave() + require.NoError(t, err) + require.NotNil(t, enclave) + + // Get the enclave data + data := enclave.GetData() + require.NotNil(t, data) + + // Get the enclave back + returnedEnclave := data.GetEnclave() + require.NotNil(t, returnedEnclave) + + // Verify the returned enclave is the same + assert.Equal(t, enclave, returnedEnclave) +} + +func TestEnclaveData_GetPubPoint(t *testing.T) { + // Create a new enclave + enclave, err := NewEnclave() + require.NoError(t, err) + require.NotNil(t, enclave) + + // Get the enclave data + data := enclave.GetData() + require.NotNil(t, data) + + // Get the public point + pubPoint, err := data.GetPubPoint() + require.NoError(t, err) + require.NotNil(t, pubPoint) + + // Verify the public point's serialization matches the stored public bytes + pointBytes := pubPoint.ToAffineUncompressed() + assert.Equal(t, data.PubBytes, pointBytes) +} + +func TestEnclaveData_PubKeyHex(t *testing.T) { + // Create a new enclave + enclave, err := NewEnclave() + require.NoError(t, err) + require.NotNil(t, enclave) + + // Get the enclave data + data := enclave.GetData() + require.NotNil(t, data) + + // Get the public key hex + pubKeyHex := data.PubKeyHex() + require.NotEmpty(t, pubKeyHex) + + // Verify it's a valid hex string + _, err = hex.DecodeString(pubKeyHex) + require.NoError(t, err) + + // Verify it matches the stored PubHex + assert.Equal(t, data.PubHex, pubKeyHex) +} + +func TestEnclaveData_PubKeyBytes(t *testing.T) { + // Create a new enclave + enclave, err := NewEnclave() + require.NoError(t, err) + require.NotNil(t, enclave) + + // Get the enclave data + data := enclave.GetData() + require.NotNil(t, data) + + // Get the public key bytes + pubKeyBytes := data.PubKeyBytes() + require.NotEmpty(t, pubKeyBytes) + + // Verify it matches the stored PubBytes + assert.Equal(t, data.PubBytes, pubKeyBytes) +} + +func TestEnclaveData_EncryptDecrypt(t *testing.T) { + // Create a new enclave + enclave, err := NewEnclave() + require.NoError(t, err) + require.NotNil(t, enclave) + + // Get the enclave data + data := enclave.GetData() + require.NotNil(t, data) + + // Test key for encryption/decryption + testKey := []byte("test-key-12345678-test-key-123456") + + // Test encryption + encrypted, err := data.Encrypt(testKey) + require.NoError(t, err) + require.NotEmpty(t, encrypted) + + // Test decryption + decrypted, err := data.Decrypt(testKey, encrypted) + require.NoError(t, err) + require.NotEmpty(t, decrypted) + + // Serialize the original data for comparison + originalData, err := data.Marshal() + require.NoError(t, err) + + // Verify the decrypted data matches the original + assert.Equal(t, originalData, decrypted) + + // Test decryption with wrong key (should fail) + wrongKey := []byte("wrong-key-12345678-wrong-key-123456") + _, err = data.Decrypt(wrongKey, encrypted) + assert.Error(t, err, "Decryption with wrong key should fail") +} + +func TestEnclaveData_IsValid(t *testing.T) { + // Create a new enclave + enclave, err := NewEnclave() + require.NoError(t, err) + require.NotNil(t, enclave) + + // Get the enclave data + data := enclave.GetData() + require.NotNil(t, data) + + // Verify it's valid + assert.True(t, data.IsValid()) + + // Create an invalid enclave + invalidEnclave := &EnclaveData{ + PubHex: "invalid", + PubBytes: []byte("invalid"), + Nonce: []byte("nonce"), + Curve: K256Name, + } + + // Verify it's invalid + assert.False(t, invalidEnclave.IsValid()) +} + +func TestEnclaveData_RefreshAndSign(t *testing.T) { + // Create a new enclave + enclave, err := NewEnclave() + require.NoError(t, err) + require.NotNil(t, enclave) + + // Get the original public key + originalPubKeyHex := enclave.PubKeyHex() + originalPubKeyBytes := enclave.PubKeyBytes() + require.NotEmpty(t, originalPubKeyHex) + require.NotEmpty(t, originalPubKeyBytes) + + // Sign a message with the original enclave to verify it works + testMessage := []byte("test message before refresh") + originalSignature, err := enclave.Sign(testMessage) + require.NoError(t, err) + require.NotEmpty(t, originalSignature) + + // Verify the original signature + valid, err := enclave.Verify(testMessage, originalSignature) + require.NoError(t, err) + assert.True(t, valid, "Original signature should be valid") + + // Refresh the enclave + refreshedEnclave, err := enclave.Refresh() + require.NoError(t, err) + require.NotNil(t, refreshedEnclave) + + // CRITICAL TEST: The public key should remain the same after refresh + refreshedPubKeyHex := refreshedEnclave.PubKeyHex() + refreshedPubKeyBytes := refreshedEnclave.PubKeyBytes() + + assert.Equal(t, originalPubKeyHex, refreshedPubKeyHex, + "Public key hex should not change after refresh") + assert.Equal(t, originalPubKeyBytes, refreshedPubKeyBytes, + "Public key bytes should not change after refresh") + + // Verify the refreshed enclave is valid + assert.True(t, refreshedEnclave.IsValid(), "Refreshed enclave should be valid") + + // Test that the refreshed enclave can still sign messages + testMessage2 := []byte("test message after refresh") + refreshedSignature, err := refreshedEnclave.Sign(testMessage2) + require.NoError(t, err) + require.NotEmpty(t, refreshedSignature) + + // Verify the signature from the refreshed enclave with its own key + valid, err = refreshedEnclave.Verify(testMessage2, refreshedSignature) + require.NoError(t, err) + assert.True(t, valid, "Signature from refreshed enclave should be valid") + + // CRITICAL TEST: The original enclave should be able to verify the signature + // from the refreshed enclave since they have the same public key + valid, err = enclave.Verify(testMessage2, refreshedSignature) + require.NoError(t, err) + assert.True(t, valid, "Original enclave should be able to verify refreshed enclave's signature") + + // CRITICAL TEST: The refreshed enclave should be able to verify the signature + // from the original enclave since they have the same public key + valid, err = refreshedEnclave.Verify(testMessage, originalSignature) + require.NoError(t, err) + assert.True(t, valid, "Refreshed enclave should be able to verify original enclave's signature") + + // Test with wrong message (should fail) + wrongMessage := []byte("wrong message") + valid, err = refreshedEnclave.Verify(wrongMessage, refreshedSignature) + require.NoError(t, err) + assert.False(t, valid, "Wrong message verification should fail") +} + +func TestEnclaveData_MarshalUnmarshal(t *testing.T) { + // Create a new enclave + enclave, err := NewEnclave() + require.NoError(t, err) + require.NotNil(t, enclave) + + // Get the enclave data + data := enclave.GetData() + require.NotNil(t, data) + + // Marshal the enclave + encoded, err := data.Marshal() + require.NoError(t, err) + require.NotEmpty(t, encoded) + + // Create a new empty enclave + newEnclave := &EnclaveData{} + + // Unmarshal the encoded data + err = newEnclave.Unmarshal(encoded) + require.NoError(t, err) + + // Verify the unmarshaled enclave matches the original + assert.Equal(t, data.PubHex, newEnclave.PubHex) + assert.Equal(t, data.Curve, newEnclave.Curve) + assert.True(t, bytes.Equal(data.PubBytes, newEnclave.PubBytes)) + assert.True(t, bytes.Equal(data.Nonce, newEnclave.Nonce)) + assert.True(t, newEnclave.IsValid()) + + // Verify the public key matches + assert.Equal(t, data.PubKeyHex(), newEnclave.PubKeyHex()) +} + +func TestEnclaveData_Verify(t *testing.T) { + // Create a new enclave + enclave, err := NewEnclave() + require.NoError(t, err) + require.NotNil(t, enclave) + + // Sign a message + testMessage := []byte("test message") + signature, err := enclave.Sign(testMessage) + require.NoError(t, err) + require.NotEmpty(t, signature) + + // Verify the signature + valid, err := enclave.Verify(testMessage, signature) + require.NoError(t, err) + assert.True(t, valid) + + // Verify with wrong message + wrongMessage := []byte("wrong message") + valid, err = enclave.Verify(wrongMessage, signature) + require.NoError(t, err) + assert.False(t, valid) + + // Corrupt the signature + corruptedSig := make([]byte, len(signature)) + copy(corruptedSig, signature) + corruptedSig[0] ^= 0x01 // flip a bit + + // Verify with corrupted signature (should fail) + valid, err = enclave.Verify(testMessage, corruptedSig) + require.NoError(t, err) + assert.False(t, valid) + + // We don't need to manually create ECDSA signatures here + // as we already verified the Sign and Verify functions work together. + // This completes the verification of the enclave's signature functionality. +} diff --git a/internal/crypto/mpc/import.go b/internal/crypto/mpc/import.go new file mode 100644 index 0000000..ccc116d --- /dev/null +++ b/internal/crypto/mpc/import.go @@ -0,0 +1,140 @@ +package mpc + +import ( + "encoding/hex" + "errors" + "fmt" +) + +// ImportEnclave creates an Enclave instance from various import options. +// It prioritizes enclave bytes over keyshares if both are provided. +func ImportEnclave(options ...ImportOption) (Enclave, error) { + if len(options) == 0 { + return nil, errors.New("no import options provided") + } + + opts := Options{} + for _, opt := range options { + opts = opt(opts) + } + return opts.Apply() +} + +// Options is a struct that holds the import options +type Options struct { + valKeyshare Message + userKeyshare Message + enclaveBytes []byte + enclaveData *EnclaveData + initialShares bool + isEncrypted bool + secretKey []byte + curve CurveName +} + +// ImportOption is a function that modifies the import options +type ImportOption func(Options) Options + +// WithInitialShares creates an option to import an enclave from validator and user keyshares. +func WithInitialShares(valKeyshare Message, userKeyshare Message, curve CurveName) ImportOption { + return func(opts Options) Options { + opts.valKeyshare = valKeyshare + opts.userKeyshare = userKeyshare + opts.initialShares = true + opts.curve = curve + return opts + } +} + +// WithEncryptedData creates an option to import an enclave from encrypted data. +func WithEncryptedData(data []byte, key []byte) ImportOption { + return func(opts Options) Options { + opts.enclaveBytes = data + opts.initialShares = false + opts.isEncrypted = true + opts.secretKey = key + return opts + } +} + +// WithEnclaveData creates an option to import an enclave from a data struct. +func WithEnclaveData(data *EnclaveData) ImportOption { + return func(opts Options) Options { + opts.enclaveData = data + opts.initialShares = false + return opts + } +} + +// Apply applies the import options to create an Enclave instance. +func (opts Options) Apply() (Enclave, error) { + // Load from encrypted data if provided + if opts.isEncrypted { + if len(opts.enclaveBytes) == 0 { + return nil, errors.New("enclave bytes cannot be empty") + } + return RestoreEncryptedEnclave(opts.enclaveBytes, opts.secretKey) + } + // Generate from keyshares if provided + if opts.initialShares { + // Then try to build from keyshares + if opts.valKeyshare == nil { + return nil, errors.New("validator share cannot be nil") + } + if opts.userKeyshare == nil { + return nil, errors.New("user share cannot be nil") + } + return BuildEnclave(opts.valKeyshare, opts.userKeyshare, opts) + } + // Load from enclave data if provided + return RestoreEnclaveFromData(opts.enclaveData) +} + +// BuildEnclave creates a new enclave from validator and user keyshares. +func BuildEnclave(valShare, userShare Message, options Options) (Enclave, error) { + if valShare == nil { + return nil, errors.New("validator share cannot be nil") + } + if userShare == nil { + return nil, errors.New("user share cannot be nil") + } + + pubPoint, err := GetAlicePublicPoint(valShare) + if err != nil { + return nil, fmt.Errorf("failed to get public point: %w", err) + } + return &EnclaveData{ + PubBytes: pubPoint.ToAffineUncompressed(), + PubHex: hex.EncodeToString(pubPoint.ToAffineCompressed()), + ValShare: valShare, + UserShare: userShare, + Nonce: randNonce(), + Curve: options.curve, + }, nil +} + +// RestoreEnclaveFromData deserializes an enclave from its data struct. +func RestoreEnclaveFromData(data *EnclaveData) (Enclave, error) { + if data == nil { + return nil, errors.New("enclave data cannot be nil") + } + return data, nil +} + +// RestoreEncryptedEnclave decrypts an enclave from its binary representation. and key +func RestoreEncryptedEnclave(data []byte, key []byte) (Enclave, error) { + keyclave := &EnclaveData{} + err := keyclave.Unmarshal(data) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal enclave: %w", err) + } + decryptedData, err := keyclave.Decrypt(key, data) + if err != nil { + return nil, fmt.Errorf("failed to decrypt enclave: %w", err) + } + err = keyclave.Unmarshal(decryptedData) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal decrypted enclave: %w", err) + } + return keyclave, nil +} diff --git a/internal/crypto/mpc/protocol.go b/internal/crypto/mpc/protocol.go new file mode 100644 index 0000000..3ff91df --- /dev/null +++ b/internal/crypto/mpc/protocol.go @@ -0,0 +1,91 @@ +package mpc + +import ( + "github.com/sonr-io/crypto/core/protocol" + "github.com/sonr-io/crypto/tecdsa/dklsv1" +) + +// NewEnclave generates a new MPC keyshare +func NewEnclave() (Enclave, error) { + curve := K256Name.Curve() + valKs := dklsv1.NewAliceDkg(curve, protocol.Version1) + userKs := dklsv1.NewBobDkg(curve, protocol.Version1) + aErr, bErr := RunProtocol(userKs, valKs) + if err := CheckIteratedErrors(aErr, bErr); err != nil { + return nil, err + } + valRes, err := valKs.Result(protocol.Version1) + if err != nil { + return nil, err + } + userRes, err := userKs.Result(protocol.Version1) + if err != nil { + return nil, err + } + return ImportEnclave(WithInitialShares(valRes, userRes, K256Name)) +} + +// ExecuteSigning runs the MPC signing protocol +func ExecuteSigning(signFuncVal SignFunc, signFuncUser SignFunc) ([]byte, error) { + aErr, bErr := RunProtocol(signFuncVal, signFuncUser) + if err := CheckIteratedErrors(aErr, bErr); err != nil { + return nil, err + } + out, err := signFuncUser.Result(protocol.Version1) + if err != nil { + return nil, err + } + s, err := dklsv1.DecodeSignature(out) + if err != nil { + return nil, err + } + sig, err := SerializeSignature(s) + if err != nil { + return nil, err + } + return sig, nil +} + +// ExecuteRefresh runs the MPC refresh protocol +func ExecuteRefresh( + refreshFuncVal RefreshFunc, + refreshFuncUser RefreshFunc, + curve CurveName, +) (Enclave, error) { + aErr, bErr := RunProtocol(refreshFuncVal, refreshFuncUser) + if err := CheckIteratedErrors(aErr, bErr); err != nil { + return nil, err + } + valRefreshResult, err := refreshFuncVal.Result(protocol.Version1) + if err != nil { + return nil, err + } + userRefreshResult, err := refreshFuncUser.Result(protocol.Version1) + if err != nil { + return nil, err + } + return ImportEnclave(WithInitialShares(valRefreshResult, userRefreshResult, curve)) +} + +// RunProtocol runs the MPC protocol +func RunProtocol(firstParty protocol.Iterator, secondParty protocol.Iterator) (error, error) { + var ( + message *protocol.Message + aErr error + bErr error + ) + + for aErr != protocol.ErrProtocolFinished || bErr != protocol.ErrProtocolFinished { + // Crank each protocol forward one iteration + message, bErr = firstParty.Next(message) + if bErr != nil && bErr != protocol.ErrProtocolFinished { + return nil, bErr + } + + message, aErr = secondParty.Next(message) + if aErr != nil && aErr != protocol.ErrProtocolFinished { + return aErr, nil + } + } + return aErr, bErr +} diff --git a/internal/crypto/mpc/spec/jwt.go b/internal/crypto/mpc/spec/jwt.go new file mode 100644 index 0000000..7b82bb8 --- /dev/null +++ b/internal/crypto/mpc/spec/jwt.go @@ -0,0 +1,116 @@ +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, + } + }) +} diff --git a/internal/crypto/mpc/spec/source.go b/internal/crypto/mpc/spec/source.go new file mode 100644 index 0000000..647e991 --- /dev/null +++ b/internal/crypto/mpc/spec/source.go @@ -0,0 +1,305 @@ +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 +} diff --git a/internal/crypto/mpc/spec/ucan.go b/internal/crypto/mpc/spec/ucan.go new file mode 100644 index 0000000..01d9463 --- /dev/null +++ b/internal/crypto/mpc/spec/ucan.go @@ -0,0 +1,125 @@ +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 +} diff --git a/internal/crypto/mpc/utils.go b/internal/crypto/mpc/utils.go new file mode 100644 index 0000000..ef0c188 --- /dev/null +++ b/internal/crypto/mpc/utils.go @@ -0,0 +1,160 @@ +package mpc + +import ( + "crypto/aes" + "crypto/cipher" + "errors" + "fmt" + "math/big" + + "github.com/sonr-io/crypto/core/curves" + "github.com/sonr-io/crypto/core/protocol" + "github.com/sonr-io/crypto/tecdsa/dklsv1" + "golang.org/x/crypto/sha3" +) + +func CheckIteratedErrors(aErr, bErr error) error { + if aErr == protocol.ErrProtocolFinished && bErr == protocol.ErrProtocolFinished { + return nil + } + if aErr != protocol.ErrProtocolFinished { + return aErr + } + if bErr != protocol.ErrProtocolFinished { + return bErr + } + return nil +} + +func GetHashKey(key []byte) []byte { + hash := sha3.New256() + hash.Write(key) + return hash.Sum(nil)[:32] // Use first 32 bytes of hash +} + +func DecryptKeyshare(msg []byte, key []byte, nonce []byte) ([]byte, error) { + hashedKey := GetHashKey(key) + block, err := aes.NewCipher(hashedKey) + if err != nil { + return nil, err + } + aesgcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + plaintext, err := aesgcm.Open(nil, nonce, msg, nil) + if err != nil { + return nil, err + } + return plaintext, nil +} + +func EncryptKeyshare(msg Message, key []byte, nonce []byte) ([]byte, error) { + hashedKey := GetHashKey(key) + msgBytes, err := protocol.EncodeMessage(msg) + if err != nil { + return nil, err + } + block, err := aes.NewCipher(hashedKey) + if err != nil { + return nil, err + } + aesgcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + ciphertext := aesgcm.Seal(nil, nonce, []byte(msgBytes), nil) + return ciphertext, nil +} + +func GetAliceOut(msg *protocol.Message) (AliceOut, error) { + return dklsv1.DecodeAliceDkgResult(msg) +} + +func GetAlicePublicPoint(msg *protocol.Message) (Point, error) { + out, err := dklsv1.DecodeAliceDkgResult(msg) + if err != nil { + return nil, err + } + return out.PublicKey, nil +} + +func GetBobOut(msg *protocol.Message) (BobOut, error) { + return dklsv1.DecodeBobDkgResult(msg) +} + +func GetBobPubPoint(msg *protocol.Message) (Point, error) { + out, err := dklsv1.DecodeBobDkgResult(msg) + if err != nil { + return nil, err + } + return out.PublicKey, nil +} + +// GetECDSAPoint builds an elliptic curve point from a compressed byte slice +func GetECDSAPoint(pubKey []byte) (*curves.EcPoint, error) { + crv := curves.K256() + x := new(big.Int).SetBytes(pubKey[1:33]) + y := new(big.Int).SetBytes(pubKey[33:]) + ecCurve, err := crv.ToEllipticCurve() + if err != nil { + return nil, fmt.Errorf("error converting curve: %v", err) + } + return &curves.EcPoint{X: x, Y: y, Curve: ecCurve}, nil +} + +func SerializeSignature(sig *curves.EcdsaSignature) ([]byte, error) { + if sig == nil { + return nil, errors.New("nil signature") + } + + rBytes := sig.R.Bytes() + sBytes := sig.S.Bytes() + + // Ensure both components are 32 bytes + rPadded := make([]byte, 32) + sPadded := make([]byte, 32) + copy(rPadded[32-len(rBytes):], rBytes) + copy(sPadded[32-len(sBytes):], sBytes) + + // Concatenate R and S + result := make([]byte, 64) + copy(result[0:32], rPadded) + copy(result[32:64], sPadded) + + return result, nil +} + +func DeserializeSignature(sigBytes []byte) (*curves.EcdsaSignature, error) { + if len(sigBytes) != 64 { + return nil, fmt.Errorf("invalid signature length: expected 64 bytes, got %d", len(sigBytes)) + } + + r := new(big.Int).SetBytes(sigBytes[:32]) + s := new(big.Int).SetBytes(sigBytes[32:]) + + return &curves.EcdsaSignature{ + R: r, + S: s, + }, nil +} + +func GetAliceSignFunc(k *EnclaveData, bz []byte) (SignFunc, error) { + curve := k.Curve.Curve() + return dklsv1.NewAliceSign(curve, sha3.New256(), bz, k.ValShare, protocol.Version1) +} + +func GetAliceRefreshFunc(k *EnclaveData) (RefreshFunc, error) { + curve := k.Curve.Curve() + return dklsv1.NewAliceRefresh(curve, k.ValShare, protocol.Version1) +} + +func GetBobSignFunc(k *EnclaveData, bz []byte) (SignFunc, error) { + curve := curves.K256() + return dklsv1.NewBobSign(curve, sha3.New256(), bz, k.UserShare, protocol.Version1) +} + +func GetBobRefreshFunc(k *EnclaveData) (RefreshFunc, error) { + curve := curves.K256() + return dklsv1.NewBobRefresh(curve, k.UserShare, protocol.Version1) +} diff --git a/internal/crypto/mpc/verify.go b/internal/crypto/mpc/verify.go new file mode 100644 index 0000000..4163759 --- /dev/null +++ b/internal/crypto/mpc/verify.go @@ -0,0 +1,29 @@ +package mpc + +import ( + "crypto/ecdsa" + + "golang.org/x/crypto/sha3" +) + +func VerifyWithPubKey(pubKeyCompressed []byte, data []byte, sig []byte) (bool, error) { + edSig, err := DeserializeSignature(sig) + if err != nil { + return false, err + } + ePub, err := GetECDSAPoint(pubKeyCompressed) + if err != nil { + return false, err + } + pk := &ecdsa.PublicKey{ + Curve: ePub.Curve, + X: ePub.X, + Y: ePub.Y, + } + + // Hash the message using SHA3-256 + hash := sha3.New256() + hash.Write(data) + digest := hash.Sum(nil) + return ecdsa.Verify(pk, digest, edSig.R, edSig.S), nil +} diff --git a/internal/crypto/ucan/capability.go b/internal/crypto/ucan/capability.go new file mode 100644 index 0000000..b8d2e32 --- /dev/null +++ b/internal/crypto/ucan/capability.go @@ -0,0 +1,860 @@ +// 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"` +} + +// 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 +} diff --git a/internal/crypto/ucan/crypto.go b/internal/crypto/ucan/crypto.go new file mode 100644 index 0000000..f69b3b3 --- /dev/null +++ b/internal/crypto/ucan/crypto.go @@ -0,0 +1,352 @@ +// 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 +} diff --git a/internal/crypto/ucan/jwt.go b/internal/crypto/ucan/jwt.go new file mode 100644 index 0000000..95ce860 --- /dev/null +++ b/internal/crypto/ucan/jwt.go @@ -0,0 +1,595 @@ +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) +} diff --git a/internal/crypto/ucan/mpc.go b/internal/crypto/ucan/mpc.go new file mode 100644 index 0000000..3d5a8b6 --- /dev/null +++ b/internal/crypto/ucan/mpc.go @@ -0,0 +1,625 @@ +// 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 +} diff --git a/internal/crypto/ucan/source.go b/internal/crypto/ucan/source.go new file mode 100644 index 0000000..ee9fbff --- /dev/null +++ b/internal/crypto/ucan/source.go @@ -0,0 +1,302 @@ +// 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 diff --git a/internal/crypto/ucan/stubs.go b/internal/crypto/ucan/stubs.go new file mode 100644 index 0000000..c50600d --- /dev/null +++ b/internal/crypto/ucan/stubs.go @@ -0,0 +1,87 @@ +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 +} diff --git a/internal/crypto/ucan/ucan_test.go b/internal/crypto/ucan/ucan_test.go new file mode 100644 index 0000000..3a38d4f --- /dev/null +++ b/internal/crypto/ucan/ucan_test.go @@ -0,0 +1,313 @@ +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() +} diff --git a/internal/crypto/ucan/vault.go b/internal/crypto/ucan/vault.go new file mode 100644 index 0000000..579f533 --- /dev/null +++ b/internal/crypto/ucan/vault.go @@ -0,0 +1,485 @@ +// 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") +} diff --git a/internal/crypto/ucan/verifier.go b/internal/crypto/ucan/verifier.go new file mode 100644 index 0000000..a8a065c --- /dev/null +++ b/internal/crypto/ucan/verifier.go @@ -0,0 +1,984 @@ +// 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) +} -- 2.43.0 From 96991231d66f505ca0dba458a0ba69eb0978f888 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Thu, 8 Jan 2026 00:28:19 -0500 Subject: [PATCH 03/35] refactor(keybase): improve database export and restore functionality --- .gitignore | 1 + internal/crypto/ucan/capability.go | 2 +- internal/keybase/conn.go | 105 ++++++-- main.go | 368 +++++++++++++++++++++++++++++ 4 files changed, 460 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index e070b11..2c5a954 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ src/dist src/node_modules dist node_modules +.osgrep diff --git a/internal/crypto/ucan/capability.go b/internal/crypto/ucan/capability.go index b8d2e32..3833e04 100644 --- a/internal/crypto/ucan/capability.go +++ b/internal/crypto/ucan/capability.go @@ -650,7 +650,7 @@ type DEXResource struct { Metadata map[string]string `json:"metadata,omitempty"` } -// Enhanced ServiceResource adds delegation capabilities +// SupportsDelegate Enhanced ServiceResource adds delegation capabilities func (r *ServiceResource) SupportsDelegate() bool { return r.Metadata != nil && r.Metadata["supports_delegation"] == "true" } diff --git a/internal/keybase/conn.go b/internal/keybase/conn.go index dbd0f93..fdccd93 100644 --- a/internal/keybase/conn.go +++ b/internal/keybase/conn.go @@ -202,7 +202,6 @@ func (k *Keybase) Serialize() ([]byte, error) { return k.exportDump() } -// exportDump creates a SQL dump of the database. func (k *Keybase) exportDump() ([]byte, error) { var dump strings.Builder dump.WriteString(migrations.SchemaSQL + "\n") @@ -214,33 +213,109 @@ func (k *Keybase) exportDump() ([]byte, error) { } for _, table := range tables { - rows, err := k.db.Query(fmt.Sprintf("SELECT * FROM %s", table)) - if err != nil { + if err := k.exportTable(&dump, table); err != nil { continue } + } - cols, err := rows.Columns() - if err != nil { - rows.Close() - continue - } + return []byte(dump.String()), nil +} +func (k *Keybase) exportTable(dump *strings.Builder, table string) error { + rows, err := k.db.Query(fmt.Sprintf("SELECT * FROM %s", table)) + if err != nil { + return err + } + defer rows.Close() + + cols, err := rows.Columns() + if err != nil { + return err + } + + for rows.Next() { values := make([]any, len(cols)) valuePtrs := make([]any, len(cols)) for i := range values { valuePtrs[i] = &values[i] } - for rows.Next() { - if err := rows.Scan(valuePtrs...); err != nil { - continue - } - fmt.Fprintf(&dump, "-- Row from %s\n", table) + if err := rows.Scan(valuePtrs...); err != nil { + continue } - rows.Close() + + dump.WriteString(fmt.Sprintf("INSERT INTO %s (", table)) + dump.WriteString(strings.Join(cols, ", ")) + dump.WriteString(") VALUES (") + + for i, val := range values { + if i > 0 { + dump.WriteString(", ") + } + dump.WriteString(formatSQLValue(val)) + } + dump.WriteString(");\n") } - return []byte(dump.String()), nil + return rows.Err() +} + +func formatSQLValue(val any) string { + if val == nil { + return "NULL" + } + + switch v := val.(type) { + case int64: + return fmt.Sprintf("%d", v) + case float64: + return fmt.Sprintf("%f", v) + case bool: + if v { + return "1" + } + return "0" + case []byte: + return fmt.Sprintf("'%s'", escapeSQLString(string(v))) + case string: + return fmt.Sprintf("'%s'", escapeSQLString(v)) + default: + return fmt.Sprintf("'%s'", escapeSQLString(fmt.Sprintf("%v", v))) + } +} + +func escapeSQLString(s string) string { + return strings.ReplaceAll(s, "'", "''") +} + +func (k *Keybase) RestoreFromDump(data []byte) error { + k.mu.Lock() + defer k.mu.Unlock() + + statements := strings.Split(string(data), ";\n") + for _, stmt := range statements { + stmt = strings.TrimSpace(stmt) + if stmt == "" || strings.HasPrefix(stmt, "--") { + continue + } + if strings.HasPrefix(stmt, "INSERT INTO") { + if _, err := k.db.Exec(stmt); err != nil { + return fmt.Errorf("keybase: failed to execute statement: %w", err) + } + } + } + + docs, err := k.queries.ListAllDIDs(context.Background()) + if err != nil { + return fmt.Errorf("keybase: failed to list DIDs: %w", err) + } + + if len(docs) > 0 { + k.did = docs[0].Did + k.didID = docs[0].ID + } + + return nil } // WithTx executes a function within a database transaction. diff --git a/main.go b/main.go index a7fb20b..d60fcb7 100644 --- a/main.go +++ b/main.go @@ -318,10 +318,119 @@ func validateUCAN(token string, params *types.FilterParams) error { return errors.New("token is required") } + parts := strings.Split(token, ".") + if len(parts) != 3 { + return errors.New("invalid token format: expected JWT with 3 parts") + } + + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + return fmt.Errorf("invalid token payload: %w", err) + } + + var claims map[string]any + if err := json.Unmarshal(payload, &claims); err != nil { + return fmt.Errorf("invalid token claims: %w", err) + } + + if exp, ok := claims["exp"].(float64); ok { + if int64(exp) < currentUnixTime() { + return errors.New("token has expired") + } + } + + if nbf, ok := claims["nbf"].(float64); ok { + if int64(nbf) > currentUnixTime() { + return errors.New("token is not yet valid") + } + } + + if aud, ok := claims["aud"].(string); ok { + currentDID := state.GetDID() + if currentDID != "" && aud != currentDID { + pdk.Log(pdk.LogDebug, fmt.Sprintf("validateUCAN: audience mismatch, expected %s got %s", currentDID, aud)) + } + } + + if att, ok := claims["att"].([]any); ok { + if !checkAttenuations(att, params.Resource, params.Action) { + return fmt.Errorf("token does not grant capability for %s:%s", params.Resource, params.Action) + } + } + + am, err := keybase.NewActionManager() + if err == nil { + if cid, ok := claims["cid"].(string); ok { + ctx := context.Background() + revoked, err := am.IsUCANRevoked(ctx, cid) + if err == nil && revoked { + return errors.New("token has been revoked") + } + } + } + pdk.Log(pdk.LogDebug, fmt.Sprintf("validateUCAN: validated token for %s:%s", params.Resource, params.Action)) return nil } +func currentUnixTime() int64 { + return 0 +} + +func checkAttenuations(attenuations []any, resource, action string) bool { + for _, att := range attenuations { + attMap, ok := att.(map[string]any) + if !ok { + continue + } + + with, ok := attMap["with"].(string) + if !ok { + continue + } + + if !matchResource(with, resource) { + continue + } + + can := attMap["can"] + if canStr, ok := can.(string); ok { + if canStr == "*" || canStr == action { + return true + } + } else if canSlice, ok := can.([]any); ok { + for _, c := range canSlice { + if cStr, ok := c.(string); ok { + if cStr == "*" || cStr == action { + return true + } + } + } + } + } + return false +} + +func matchResource(pattern, resource string) bool { + if pattern == resource { + return true + } + + if strings.HasSuffix(pattern, "/*") { + prefix := strings.TrimSuffix(pattern, "/*") + return strings.HasPrefix(resource, prefix) + } + + if strings.Contains(pattern, "://") { + parts := strings.SplitN(pattern, "://", 2) + if len(parts) == 2 && parts[1] == resource { + return true + } + } + + return false +} + func executeAction(params *types.FilterParams) (json.RawMessage, error) { switch params.Resource { case "accounts": @@ -332,6 +441,16 @@ func executeAction(params *types.FilterParams) (json.RawMessage, error) { return executeSessionAction(params) case "grants": return executeGrantAction(params) + case "key_shares": + return executeKeyShareAction(params) + case "ucans": + return executeUCANAction(params) + case "delegations": + return executeDelegationAction(params) + case "verification_methods": + return executeVerificationMethodAction(params) + case "services": + return executeServiceAction(params) default: return nil, fmt.Errorf("unknown resource: %s", params.Resource) } @@ -493,6 +612,176 @@ func executeGrantAction(params *types.FilterParams) (json.RawMessage, error) { } } +func executeKeyShareAction(params *types.FilterParams) (json.RawMessage, error) { + am, err := keybase.NewActionManager() + if err != nil { + return nil, fmt.Errorf("action manager: %w", err) + } + + ctx := context.Background() + + switch params.Action { + case "list": + shares, err := am.ListKeyShares(ctx) + if err != nil { + return nil, fmt.Errorf("list key shares: %w", err) + } + return json.Marshal(shares) + case "get": + if params.Subject == "" { + return nil, errors.New("subject (share_id) required for get action") + } + share, err := am.GetKeyShareByID(ctx, params.Subject) + if err != nil { + return nil, fmt.Errorf("get key share: %w", err) + } + return json.Marshal(share) + case "rotate": + if params.Subject == "" { + return nil, errors.New("subject (share_id) required for rotate action") + } + if err := am.RotateKeyShare(ctx, params.Subject); err != nil { + return nil, fmt.Errorf("rotate key share: %w", err) + } + return json.Marshal(map[string]bool{"rotated": true}) + case "archive": + if params.Subject == "" { + return nil, errors.New("subject (share_id) required for archive action") + } + if err := am.ArchiveKeyShare(ctx, params.Subject); err != nil { + return nil, fmt.Errorf("archive key share: %w", err) + } + return json.Marshal(map[string]bool{"archived": true}) + case "delete": + if params.Subject == "" { + return nil, errors.New("subject (share_id) required for delete action") + } + if err := am.DeleteKeyShare(ctx, params.Subject); err != nil { + return nil, fmt.Errorf("delete key share: %w", err) + } + return json.Marshal(map[string]bool{"deleted": true}) + default: + return nil, fmt.Errorf("unknown action for key_shares: %s", params.Action) + } +} + +func executeUCANAction(params *types.FilterParams) (json.RawMessage, error) { + am, err := keybase.NewActionManager() + if err != nil { + return nil, fmt.Errorf("action manager: %w", err) + } + + ctx := context.Background() + + switch params.Action { + case "list": + ucans, err := am.ListUCANs(ctx) + if err != nil { + return nil, fmt.Errorf("list ucans: %w", err) + } + return json.Marshal(ucans) + case "get": + if params.Subject == "" { + return nil, errors.New("subject (cid) required for get action") + } + ucan, err := am.GetUCANByCID(ctx, params.Subject) + if err != nil { + return nil, fmt.Errorf("get ucan: %w", err) + } + return json.Marshal(ucan) + case "revoke": + if params.Subject == "" { + return nil, errors.New("subject (cid) required for revoke action") + } + if err := am.RevokeUCAN(ctx, params.Subject); err != nil { + return nil, fmt.Errorf("revoke ucan: %w", err) + } + return json.Marshal(map[string]bool{"revoked": true}) + case "verify": + if params.Subject == "" { + return nil, errors.New("subject (cid) required for verify action") + } + revoked, err := am.IsUCANRevoked(ctx, params.Subject) + if err != nil { + return nil, fmt.Errorf("check ucan: %w", err) + } + return json.Marshal(map[string]bool{"valid": !revoked, "revoked": revoked}) + case "cleanup": + if err := am.CleanExpiredUCANs(ctx); err != nil { + return nil, fmt.Errorf("cleanup ucans: %w", err) + } + return json.Marshal(map[string]bool{"cleaned": true}) + default: + return nil, fmt.Errorf("unknown action for ucans: %s", params.Action) + } +} + +func executeDelegationAction(params *types.FilterParams) (json.RawMessage, error) { + am, err := keybase.NewActionManager() + if err != nil { + return nil, fmt.Errorf("action manager: %w", err) + } + + ctx := context.Background() + + switch params.Action { + case "list": + if params.Subject == "" { + return nil, errors.New("subject (delegator or delegate DID) required for list action") + } + delegations, err := am.ListDelegationsByDelegator(ctx, params.Subject) + if err != nil { + return nil, fmt.Errorf("list delegations: %w", err) + } + return json.Marshal(delegations) + case "list_received": + if params.Subject == "" { + return nil, errors.New("subject (delegate DID) required for list_received action") + } + delegations, err := am.ListDelegationsByDelegate(ctx, params.Subject) + if err != nil { + return nil, fmt.Errorf("list received delegations: %w", err) + } + return json.Marshal(delegations) + case "list_resource": + if params.Subject == "" { + return nil, errors.New("subject (resource) required for list_resource action") + } + delegations, err := am.ListDelegationsForResource(ctx, params.Subject) + if err != nil { + return nil, fmt.Errorf("list delegations for resource: %w", err) + } + return json.Marshal(delegations) + case "chain": + if params.Subject == "" { + return nil, errors.New("subject (delegation_id) required for chain action") + } + var delegationID int64 + if _, err := fmt.Sscanf(params.Subject, "%d", &delegationID); err != nil { + return nil, fmt.Errorf("invalid delegation_id: %w", err) + } + chain, err := am.GetDelegationChain(ctx, delegationID) + if err != nil { + return nil, fmt.Errorf("get delegation chain: %w", err) + } + return json.Marshal(chain) + case "revoke": + if params.Subject == "" { + return nil, errors.New("subject (delegation_id) required for revoke action") + } + var delegationID int64 + if _, err := fmt.Sscanf(params.Subject, "%d", &delegationID); err != nil { + return nil, fmt.Errorf("invalid delegation_id: %w", err) + } + if err := am.RevokeDelegation(ctx, delegationID); err != nil { + return nil, fmt.Errorf("revoke delegation: %w", err) + } + return json.Marshal(map[string]bool{"revoked": true}) + default: + return nil, fmt.Errorf("unknown action for delegations: %s", params.Action) + } +} + func resolveDID(did string) (*types.QueryOutput, error) { am, err := keybase.NewActionManager() if err != nil { @@ -550,3 +839,82 @@ func resolveDID(did string) (*types.QueryOutput, error) { Credentials: credentials, }, nil } + +func executeVerificationMethodAction(params *types.FilterParams) (json.RawMessage, error) { + am, err := keybase.NewActionManager() + if err != nil { + return nil, fmt.Errorf("action manager: %w", err) + } + + ctx := context.Background() + + switch params.Action { + case "list": + vms, err := am.ListVerificationMethodsFull(ctx) + if err != nil { + return nil, fmt.Errorf("list verification methods: %w", err) + } + return json.Marshal(vms) + case "get": + if params.Subject == "" { + return nil, errors.New("subject (method_id) required for get action") + } + vm, err := am.GetVerificationMethod(ctx, params.Subject) + if err != nil { + return nil, fmt.Errorf("get verification method: %w", err) + } + return json.Marshal(vm) + case "delete": + if params.Subject == "" { + return nil, errors.New("subject (method_id) required for delete action") + } + if err := am.DeleteVerificationMethod(ctx, params.Subject); err != nil { + return nil, fmt.Errorf("delete verification method: %w", err) + } + return json.Marshal(map[string]bool{"deleted": true}) + default: + return nil, fmt.Errorf("unknown action for verification_methods: %s", params.Action) + } +} + +func executeServiceAction(params *types.FilterParams) (json.RawMessage, error) { + am, err := keybase.NewActionManager() + if err != nil { + return nil, fmt.Errorf("action manager: %w", err) + } + + ctx := context.Background() + + switch params.Action { + case "list": + services, err := am.ListVerifiedServices(ctx) + if err != nil { + return nil, fmt.Errorf("list verified services: %w", err) + } + return json.Marshal(services) + case "get": + if params.Subject == "" { + return nil, errors.New("subject (origin) required for get action") + } + svc, err := am.GetServiceByOrigin(ctx, params.Subject) + if err != nil { + return nil, fmt.Errorf("get service: %w", err) + } + return json.Marshal(svc) + case "get_by_id": + if params.Subject == "" { + return nil, errors.New("subject (service_id) required for get_by_id action") + } + var serviceID int64 + if _, err := fmt.Sscanf(params.Subject, "%d", &serviceID); err != nil { + return nil, fmt.Errorf("invalid service_id: %w", err) + } + svc, err := am.GetServiceByID(ctx, serviceID) + if err != nil { + return nil, fmt.Errorf("get service by ID: %w", err) + } + return json.Marshal(svc) + default: + return nil, fmt.Errorf("unknown action for services: %s", params.Action) + } +} -- 2.43.0 From f6dde77e607b4505dc5bd2bb542f788ed1f88a8b Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Thu, 8 Jan 2026 00:28:23 -0500 Subject: [PATCH 04/35] feat(enclave): add WebAuthn PRF key derivation and encryption APIs --- internal/enclave/crypto.go | 219 +++++++++++++++++++++++ internal/enclave/enclave.go | 184 +++++++++++++++++++ internal/keybase/actions_account.go | 177 ++++++++++++++++++ internal/keybase/actions_credential.go | 155 ++++++++++++++++ internal/keybase/actions_delegation.go | 181 +++++++++++++++++++ internal/keybase/actions_grant.go | 181 +++++++++++++++++++ internal/keybase/actions_keyshare.go | 206 +++++++++++++++++++++ internal/keybase/actions_service.go | 172 ++++++++++++++++++ internal/keybase/actions_session.go | 84 +++++++++ internal/keybase/actions_ucan.go | 205 +++++++++++++++++++++ internal/keybase/actions_verification.go | 114 ++++++++++++ 11 files changed, 1878 insertions(+) create mode 100644 internal/enclave/crypto.go create mode 100644 internal/enclave/enclave.go create mode 100644 internal/keybase/actions_account.go create mode 100644 internal/keybase/actions_credential.go create mode 100644 internal/keybase/actions_delegation.go create mode 100644 internal/keybase/actions_grant.go create mode 100644 internal/keybase/actions_keyshare.go create mode 100644 internal/keybase/actions_service.go create mode 100644 internal/keybase/actions_session.go create mode 100644 internal/keybase/actions_ucan.go create mode 100644 internal/keybase/actions_verification.go diff --git a/internal/enclave/crypto.go b/internal/enclave/crypto.go new file mode 100644 index 0000000..18ca7f9 --- /dev/null +++ b/internal/enclave/crypto.go @@ -0,0 +1,219 @@ +// Package enclave provides encrypted database operations with WebAuthn PRF key derivation. +package enclave + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/sha256" + "fmt" + "io" + + "golang.org/x/crypto/hkdf" +) + +const ( + // EnclaveSalt is the salt used for HKDF key derivation + EnclaveSalt = "nebula-enclave-v1" + + // KeySize is the size of the derived encryption key (256 bits) + KeySize = 32 + + // NonceSize is the size of the GCM nonce (96 bits) + NonceSize = 12 + + // AuthTagSize is the size of the GCM authentication tag (128 bits) + AuthTagSize = 16 +) + +// DeriveEncryptionKey derives a 256-bit encryption key from WebAuthn PRF output using HKDF. +// +// Parameters: +// - prfOutput: The raw PRF output from WebAuthn (typically 32 bytes) +// +// Returns: +// - A 32-byte key suitable for AES-256-GCM encryption +// - An error if key derivation fails +func DeriveEncryptionKey(prfOutput []byte) ([]byte, error) { + if len(prfOutput) == 0 { + return nil, fmt.Errorf("enclave: PRF output cannot be empty") + } + + salt := []byte(EnclaveSalt) + info := []byte("database-encryption") + + hkdfReader := hkdf.New(sha256.New, prfOutput, salt, info) + + key := make([]byte, KeySize) + if _, err := io.ReadFull(hkdfReader, key); err != nil { + return nil, fmt.Errorf("enclave: failed to derive key: %w", err) + } + + return key, nil +} + +// DeriveKeyWithContext derives an encryption key with additional context binding. +// This allows deriving different keys for different purposes from the same PRF output. +// +// Parameters: +// - prfOutput: The raw PRF output from WebAuthn +// - context: Additional context to bind the key to (e.g., "database", "mpc-share") +func DeriveKeyWithContext(prfOutput []byte, context string) ([]byte, error) { + if len(prfOutput) == 0 { + return nil, fmt.Errorf("enclave: PRF output cannot be empty") + } + if context == "" { + return nil, fmt.Errorf("enclave: context cannot be empty") + } + + salt := []byte(EnclaveSalt) + info := []byte(context) + + hkdfReader := hkdf.New(sha256.New, prfOutput, salt, info) + + key := make([]byte, KeySize) + if _, err := io.ReadFull(hkdfReader, key); err != nil { + return nil, fmt.Errorf("enclave: failed to derive key: %w", err) + } + + return key, nil +} + +// EncryptedData represents encrypted data with its metadata. +type EncryptedData struct { + // Nonce is the unique nonce used for this encryption (12 bytes) + Nonce []byte `json:"nonce"` + // Ciphertext is the encrypted data including the GCM authentication tag + Ciphertext []byte `json:"ciphertext"` + // Version indicates the encryption scheme version + Version int `json:"version"` +} + +// Encrypt encrypts plaintext using AES-256-GCM with the provided key. +// +// Parameters: +// - key: 32-byte encryption key (from DeriveEncryptionKey) +// - plaintext: The data to encrypt +// +// Returns: +// - EncryptedData containing nonce and ciphertext +// - An error if encryption fails +func Encrypt(key, plaintext []byte) (*EncryptedData, error) { + if len(key) != KeySize { + return nil, fmt.Errorf("enclave: invalid key size: got %d, want %d", len(key), KeySize) + } + + block, err := aes.NewCipher(key) + if err != nil { + return nil, fmt.Errorf("enclave: failed to create cipher: %w", err) + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, fmt.Errorf("enclave: failed to create GCM: %w", err) + } + + nonce := make([]byte, NonceSize) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, fmt.Errorf("enclave: failed to generate nonce: %w", err) + } + + ciphertext := gcm.Seal(nil, nonce, plaintext, nil) + + return &EncryptedData{ + Nonce: nonce, + Ciphertext: ciphertext, + Version: 1, + }, nil +} + +// Decrypt decrypts ciphertext using AES-256-GCM with the provided key. +// +// Parameters: +// - key: 32-byte encryption key (from DeriveEncryptionKey) +// - data: The EncryptedData to decrypt +// +// Returns: +// - The decrypted plaintext +// - An error if decryption fails (including authentication failure) +func Decrypt(key []byte, data *EncryptedData) ([]byte, error) { + if len(key) != KeySize { + return nil, fmt.Errorf("enclave: invalid key size: got %d, want %d", len(key), KeySize) + } + if data == nil { + return nil, fmt.Errorf("enclave: encrypted data cannot be nil") + } + if len(data.Nonce) != NonceSize { + return nil, fmt.Errorf("enclave: invalid nonce size: got %d, want %d", len(data.Nonce), NonceSize) + } + + block, err := aes.NewCipher(key) + if err != nil { + return nil, fmt.Errorf("enclave: failed to create cipher: %w", err) + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, fmt.Errorf("enclave: failed to create GCM: %w", err) + } + + plaintext, err := gcm.Open(nil, data.Nonce, data.Ciphertext, nil) + if err != nil { + return nil, fmt.Errorf("enclave: decryption failed (authentication error): %w", err) + } + + return plaintext, nil +} + +// EncryptBytes is a convenience function that encrypts and returns serialized bytes. +// The format is: version (1 byte) + nonce (12 bytes) + ciphertext (variable) +func EncryptBytes(key, plaintext []byte) ([]byte, error) { + data, err := Encrypt(key, plaintext) + if err != nil { + return nil, err + } + + result := make([]byte, 1+NonceSize+len(data.Ciphertext)) + result[0] = byte(data.Version) + copy(result[1:1+NonceSize], data.Nonce) + copy(result[1+NonceSize:], data.Ciphertext) + + return result, nil +} + +// DecryptBytes is a convenience function that decrypts serialized encrypted bytes. +// Expected format: version (1 byte) + nonce (12 bytes) + ciphertext (variable) +func DecryptBytes(key, encryptedBytes []byte) ([]byte, error) { + if len(encryptedBytes) < 1+NonceSize+AuthTagSize { + return nil, fmt.Errorf("enclave: encrypted data too short") + } + + version := int(encryptedBytes[0]) + if version != 1 { + return nil, fmt.Errorf("enclave: unsupported encryption version: %d", version) + } + + data := &EncryptedData{ + Version: version, + Nonce: encryptedBytes[1 : 1+NonceSize], + Ciphertext: encryptedBytes[1+NonceSize:], + } + + return Decrypt(key, data) +} + +// GenerateNonce generates a cryptographically secure random nonce. +func GenerateNonce() ([]byte, error) { + nonce := make([]byte, NonceSize) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, fmt.Errorf("enclave: failed to generate nonce: %w", err) + } + return nonce, nil +} + +// SecureZero zeros out a byte slice to prevent sensitive data from remaining in memory. +func SecureZero(b []byte) { + for i := range b { + b[i] = 0 + } +} diff --git a/internal/enclave/enclave.go b/internal/enclave/enclave.go new file mode 100644 index 0000000..f013b15 --- /dev/null +++ b/internal/enclave/enclave.go @@ -0,0 +1,184 @@ +package enclave + +import ( + "encoding/json" + "fmt" + + "enclave/internal/keybase" +) + +// Enclave wraps a Keybase with encryption capabilities using WebAuthn PRF-derived keys. +type Enclave struct { + keybase *keybase.Keybase + encryptionKey []byte +} + +// Config holds enclave configuration options. +type Config struct { + PRFOutput []byte +} + +// New creates a new Enclave with the given PRF output for key derivation. +func New(prfOutput []byte) (*Enclave, error) { + if len(prfOutput) == 0 { + return nil, fmt.Errorf("enclave: PRF output required") + } + + key, err := DeriveEncryptionKey(prfOutput) + if err != nil { + return nil, fmt.Errorf("enclave: key derivation failed: %w", err) + } + + kb, err := keybase.Open() + if err != nil { + SecureZero(key) + return nil, fmt.Errorf("enclave: failed to open keybase: %w", err) + } + + return &Enclave{ + keybase: kb, + encryptionKey: key, + }, nil +} + +// Keybase returns the underlying keybase instance. +func (e *Enclave) Keybase() *keybase.Keybase { + return e.keybase +} + +// DID returns the current DID. +func (e *Enclave) DID() string { + return e.keybase.DID() +} + +// IsInitialized returns true if the enclave has been initialized with a DID. +func (e *Enclave) IsInitialized() bool { + return e.keybase.IsInitialized() +} + +// SerializeEncrypted exports the database state as encrypted bytes. +func (e *Enclave) SerializeEncrypted() ([]byte, error) { + plaintext, err := e.keybase.Serialize() + if err != nil { + return nil, fmt.Errorf("enclave: serialization failed: %w", err) + } + + encrypted, err := EncryptBytes(e.encryptionKey, plaintext) + if err != nil { + SecureZero(plaintext) + return nil, fmt.Errorf("enclave: encryption failed: %w", err) + } + + SecureZero(plaintext) + return encrypted, nil +} + +// LoadEncrypted loads the database state from encrypted bytes. +func (e *Enclave) LoadEncrypted(encryptedData []byte) error { + plaintext, err := DecryptBytes(e.encryptionKey, encryptedData) + if err != nil { + return fmt.Errorf("enclave: decryption failed: %w", err) + } + defer SecureZero(plaintext) + + return e.loadFromPlaintext(plaintext) +} + +// loadFromPlaintext parses and executes the SQL statements to restore database state. +func (e *Enclave) loadFromPlaintext(data []byte) error { + if len(data) == 0 { + return fmt.Errorf("enclave: empty data") + } + + return e.keybase.RestoreFromDump(data) +} + +// Close securely closes the enclave and zeros out sensitive data. +func (e *Enclave) Close() error { + SecureZero(e.encryptionKey) + return keybase.Close() +} + +// EncryptedBundle represents a complete encrypted database export. +type EncryptedBundle struct { + Version int `json:"version"` + DID string `json:"did"` + Ciphertext []byte `json:"ciphertext"` + Nonce []byte `json:"nonce"` +} + +// Export creates a complete encrypted bundle for storage. +func (e *Enclave) Export() (*EncryptedBundle, error) { + plaintext, err := e.keybase.Serialize() + if err != nil { + return nil, fmt.Errorf("enclave: serialization failed: %w", err) + } + defer SecureZero(plaintext) + + encData, err := Encrypt(e.encryptionKey, plaintext) + if err != nil { + return nil, fmt.Errorf("enclave: encryption failed: %w", err) + } + + return &EncryptedBundle{ + Version: encData.Version, + DID: e.keybase.DID(), + Ciphertext: encData.Ciphertext, + Nonce: encData.Nonce, + }, nil +} + +// Import loads an encrypted bundle. +func (e *Enclave) Import(bundle *EncryptedBundle) error { + if bundle == nil { + return fmt.Errorf("enclave: bundle cannot be nil") + } + + encData := &EncryptedData{ + Version: bundle.Version, + Ciphertext: bundle.Ciphertext, + Nonce: bundle.Nonce, + } + + plaintext, err := Decrypt(e.encryptionKey, encData) + if err != nil { + return fmt.Errorf("enclave: decryption failed: %w", err) + } + defer SecureZero(plaintext) + + return e.loadFromPlaintext(plaintext) +} + +// MarshalBundle serializes an encrypted bundle to JSON. +func (b *EncryptedBundle) Marshal() ([]byte, error) { + return json.Marshal(b) +} + +// UnmarshalBundle deserializes an encrypted bundle from JSON. +func UnmarshalBundle(data []byte) (*EncryptedBundle, error) { + var bundle EncryptedBundle + if err := json.Unmarshal(data, &bundle); err != nil { + return nil, fmt.Errorf("enclave: failed to unmarshal bundle: %w", err) + } + return &bundle, nil +} + +// FromExisting wraps an existing keybase with encryption capabilities. +func FromExisting(kb *keybase.Keybase, prfOutput []byte) (*Enclave, error) { + if kb == nil { + return nil, fmt.Errorf("enclave: keybase cannot be nil") + } + if len(prfOutput) == 0 { + return nil, fmt.Errorf("enclave: PRF output required") + } + + key, err := DeriveEncryptionKey(prfOutput) + if err != nil { + return nil, fmt.Errorf("enclave: key derivation failed: %w", err) + } + + return &Enclave{ + keybase: kb, + encryptionKey: key, + }, nil +} diff --git a/internal/keybase/actions_account.go b/internal/keybase/actions_account.go new file mode 100644 index 0000000..8c139c7 --- /dev/null +++ b/internal/keybase/actions_account.go @@ -0,0 +1,177 @@ +package keybase + +import ( + "context" + "fmt" +) + +type NewAccountInput struct { + KeyShareID int64 `json:"key_share_id"` + Address string `json:"address"` + ChainID string `json:"chain_id"` + CoinType int64 `json:"coin_type"` + AccountIndex int64 `json:"account_index"` + AddressIndex int64 `json:"address_index"` + Label string `json:"label,omitempty"` +} + +func (am *ActionManager) CreateAccount(ctx context.Context, params NewAccountInput) (*AccountResult, error) { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + if am.kb.didID == 0 { + return nil, fmt.Errorf("DID not initialized") + } + + var label *string + if params.Label != "" { + label = ¶ms.Label + } + + acc, err := am.kb.queries.CreateAccount(ctx, CreateAccountParams{ + DidID: am.kb.didID, + KeyShareID: params.KeyShareID, + Address: params.Address, + ChainID: params.ChainID, + CoinType: params.CoinType, + AccountIndex: params.AccountIndex, + AddressIndex: params.AddressIndex, + Label: label, + }) + if err != nil { + return nil, fmt.Errorf("create account: %w", err) + } + + labelStr := "" + if acc.Label != nil { + labelStr = *acc.Label + } + + return &AccountResult{ + ID: acc.ID, + Address: acc.Address, + ChainID: acc.ChainID, + CoinType: acc.CoinType, + AccountIndex: acc.AccountIndex, + AddressIndex: acc.AddressIndex, + Label: labelStr, + IsDefault: acc.IsDefault == 1, + CreatedAt: acc.CreatedAt, + }, nil +} + +func (am *ActionManager) ListAccountsByChain(ctx context.Context, chainID string) ([]AccountResult, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + if am.kb.didID == 0 { + return []AccountResult{}, nil + } + + accounts, err := am.kb.queries.ListAccountsByChain(ctx, ListAccountsByChainParams{ + DidID: am.kb.didID, + ChainID: chainID, + }) + if err != nil { + return nil, fmt.Errorf("list accounts by chain: %w", err) + } + + results := make([]AccountResult, len(accounts)) + for i, acc := range accounts { + label := "" + if acc.Label != nil { + label = *acc.Label + } + results[i] = AccountResult{ + ID: acc.ID, + Address: acc.Address, + ChainID: acc.ChainID, + CoinType: acc.CoinType, + AccountIndex: acc.AccountIndex, + AddressIndex: acc.AddressIndex, + Label: label, + IsDefault: acc.IsDefault == 1, + CreatedAt: acc.CreatedAt, + } + } + + return results, nil +} + +func (am *ActionManager) GetDefaultAccount(ctx context.Context, chainID string) (*AccountResult, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + if am.kb.didID == 0 { + return nil, fmt.Errorf("DID not initialized") + } + + acc, err := am.kb.queries.GetDefaultAccount(ctx, GetDefaultAccountParams{ + DidID: am.kb.didID, + ChainID: chainID, + }) + if err != nil { + return nil, fmt.Errorf("get default account: %w", err) + } + + label := "" + if acc.Label != nil { + label = *acc.Label + } + + return &AccountResult{ + ID: acc.ID, + Address: acc.Address, + ChainID: acc.ChainID, + CoinType: acc.CoinType, + AccountIndex: acc.AccountIndex, + AddressIndex: acc.AddressIndex, + Label: label, + IsDefault: acc.IsDefault == 1, + CreatedAt: acc.CreatedAt, + }, nil +} + +func (am *ActionManager) SetDefaultAccount(ctx context.Context, accountID int64, chainID string) error { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + if am.kb.didID == 0 { + return fmt.Errorf("DID not initialized") + } + + return am.kb.queries.SetDefaultAccount(ctx, SetDefaultAccountParams{ + ID: accountID, + DidID: am.kb.didID, + ChainID: chainID, + }) +} + +func (am *ActionManager) UpdateAccountLabel(ctx context.Context, accountID int64, label string) error { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + var labelPtr *string + if label != "" { + labelPtr = &label + } + + return am.kb.queries.UpdateAccountLabel(ctx, UpdateAccountLabelParams{ + Label: labelPtr, + ID: accountID, + }) +} + +func (am *ActionManager) DeleteAccount(ctx context.Context, accountID int64) error { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + if am.kb.didID == 0 { + return fmt.Errorf("DID not initialized") + } + + return am.kb.queries.DeleteAccount(ctx, DeleteAccountParams{ + ID: accountID, + DidID: am.kb.didID, + }) +} diff --git a/internal/keybase/actions_credential.go b/internal/keybase/actions_credential.go new file mode 100644 index 0000000..f3994d8 --- /dev/null +++ b/internal/keybase/actions_credential.go @@ -0,0 +1,155 @@ +package keybase + +import ( + "context" + "encoding/json" + "fmt" +) + +type NewCredentialInput struct { + CredentialID string `json:"credential_id"` + PublicKey string `json:"public_key"` + PublicKeyAlg int64 `json:"public_key_alg"` + AAGUID string `json:"aaguid,omitempty"` + Transports []string `json:"transports"` + DeviceName string `json:"device_name"` + DeviceType string `json:"device_type"` + Authenticator string `json:"authenticator,omitempty"` + IsDiscoverable bool `json:"is_discoverable"` + BackedUp bool `json:"backed_up"` +} + +func (am *ActionManager) CreateCredential(ctx context.Context, params NewCredentialInput) (*CredentialResult, error) { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + if am.kb.didID == 0 { + return nil, fmt.Errorf("DID not initialized") + } + + var aaguid, authenticator *string + if params.AAGUID != "" { + aaguid = ¶ms.AAGUID + } + if params.Authenticator != "" { + authenticator = ¶ms.Authenticator + } + + transports, err := json.Marshal(params.Transports) + if err != nil { + return nil, fmt.Errorf("marshal transports: %w", err) + } + + var isDiscoverable, backedUp int64 + if params.IsDiscoverable { + isDiscoverable = 1 + } + if params.BackedUp { + backedUp = 1 + } + + cred, err := am.kb.queries.CreateCredential(ctx, CreateCredentialParams{ + DidID: am.kb.didID, + CredentialID: params.CredentialID, + PublicKey: params.PublicKey, + PublicKeyAlg: params.PublicKeyAlg, + Aaguid: aaguid, + Transports: transports, + DeviceName: params.DeviceName, + DeviceType: params.DeviceType, + Authenticator: authenticator, + IsDiscoverable: isDiscoverable, + BackedUp: backedUp, + }) + if err != nil { + return nil, fmt.Errorf("create credential: %w", err) + } + + return credentialToResult(&cred), nil +} + +func (am *ActionManager) UpdateCredentialCounter(ctx context.Context, credentialID string, signCount int64) error { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + cred, err := am.kb.queries.GetCredentialByID(ctx, credentialID) + if err != nil { + return fmt.Errorf("get credential: %w", err) + } + + return am.kb.queries.UpdateCredentialCounter(ctx, UpdateCredentialCounterParams{ + SignCount: signCount, + ID: cred.ID, + }) +} + +func (am *ActionManager) RenameCredential(ctx context.Context, credentialID string, newName string) error { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + cred, err := am.kb.queries.GetCredentialByID(ctx, credentialID) + if err != nil { + return fmt.Errorf("get credential: %w", err) + } + + return am.kb.queries.RenameCredential(ctx, RenameCredentialParams{ + DeviceName: newName, + ID: cred.ID, + }) +} + +func (am *ActionManager) DeleteCredential(ctx context.Context, credentialID string) error { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + if am.kb.didID == 0 { + return fmt.Errorf("DID not initialized") + } + + cred, err := am.kb.queries.GetCredentialByID(ctx, credentialID) + if err != nil { + return fmt.Errorf("get credential: %w", err) + } + + return am.kb.queries.DeleteCredential(ctx, DeleteCredentialParams{ + ID: cred.ID, + DidID: am.kb.didID, + }) +} + +func (am *ActionManager) CountCredentialsByDID(ctx context.Context) (int64, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + if am.kb.didID == 0 { + return 0, nil + } + + return am.kb.queries.CountCredentialsByDID(ctx, am.kb.didID) +} + +func credentialToResult(cred *Credential) *CredentialResult { + var transports []string + if err := json.Unmarshal(cred.Transports, &transports); err != nil { + transports = []string{} + } + + authenticator := "" + if cred.Authenticator != nil { + authenticator = *cred.Authenticator + } + + return &CredentialResult{ + ID: cred.ID, + CredentialID: cred.CredentialID, + DeviceName: cred.DeviceName, + DeviceType: cred.DeviceType, + Authenticator: authenticator, + Transports: transports, + SignCount: cred.SignCount, + IsDiscoverable: cred.IsDiscoverable == 1, + BackedUp: cred.BackedUp == 1, + CreatedAt: cred.CreatedAt, + LastUsed: cred.LastUsed, + } +} diff --git a/internal/keybase/actions_delegation.go b/internal/keybase/actions_delegation.go new file mode 100644 index 0000000..eab2434 --- /dev/null +++ b/internal/keybase/actions_delegation.go @@ -0,0 +1,181 @@ +package keybase + +import ( + "context" + "encoding/json" + "fmt" +) + +type DelegationResult struct { + ID int64 `json:"id"` + UcanID int64 `json:"ucan_id"` + Delegator string `json:"delegator"` + Delegate string `json:"delegate"` + Resource string `json:"resource"` + Action string `json:"action"` + Caveats json.RawMessage `json:"caveats"` + Depth int64 `json:"depth"` + Status string `json:"status"` + CreatedAt string `json:"created_at"` + ExpiresAt string `json:"expires_at,omitempty"` +} + +type NewDelegationInput struct { + UcanID int64 `json:"ucan_id"` + Delegator string `json:"delegator"` + Delegate string `json:"delegate"` + Resource string `json:"resource"` + Action string `json:"action"` + Caveats json.RawMessage `json:"caveats,omitempty"` + ParentID int64 `json:"parent_id,omitempty"` + Depth int64 `json:"depth"` + ExpiresAt string `json:"expires_at,omitempty"` +} + +func (am *ActionManager) CreateDelegation(ctx context.Context, params NewDelegationInput) (*DelegationResult, error) { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + if am.kb.didID == 0 { + return nil, fmt.Errorf("DID not initialized") + } + + var parentID *int64 + if params.ParentID != 0 { + parentID = ¶ms.ParentID + } + + var expiresAt *string + if params.ExpiresAt != "" { + expiresAt = ¶ms.ExpiresAt + } + + caveats := params.Caveats + if caveats == nil { + caveats = json.RawMessage(`{}`) + } + + d, err := am.kb.queries.CreateDelegation(ctx, CreateDelegationParams{ + DidID: am.kb.didID, + UcanID: params.UcanID, + Delegator: params.Delegator, + Delegate: params.Delegate, + Resource: params.Resource, + Action: params.Action, + Caveats: caveats, + ParentID: parentID, + Depth: params.Depth, + ExpiresAt: expiresAt, + }) + if err != nil { + return nil, fmt.Errorf("create delegation: %w", err) + } + + return delegationToResult(&d), nil +} + +func (am *ActionManager) ListDelegationsByDelegator(ctx context.Context, delegator string) ([]DelegationResult, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + delegations, err := am.kb.queries.ListDelegationsByDelegator(ctx, delegator) + if err != nil { + return nil, fmt.Errorf("list delegations by delegator: %w", err) + } + + results := make([]DelegationResult, len(delegations)) + for i, d := range delegations { + results[i] = *delegationToResult(&d) + } + + return results, nil +} + +func (am *ActionManager) ListDelegationsByDelegate(ctx context.Context, delegate string) ([]DelegationResult, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + delegations, err := am.kb.queries.ListDelegationsByDelegate(ctx, delegate) + if err != nil { + return nil, fmt.Errorf("list delegations by delegate: %w", err) + } + + results := make([]DelegationResult, len(delegations)) + for i, d := range delegations { + results[i] = *delegationToResult(&d) + } + + return results, nil +} + +func (am *ActionManager) ListDelegationsForResource(ctx context.Context, resource string) ([]DelegationResult, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + if am.kb.didID == 0 { + return []DelegationResult{}, nil + } + + delegations, err := am.kb.queries.ListDelegationsForResource(ctx, ListDelegationsForResourceParams{ + DidID: am.kb.didID, + Resource: resource, + }) + if err != nil { + return nil, fmt.Errorf("list delegations for resource: %w", err) + } + + results := make([]DelegationResult, len(delegations)) + for i, d := range delegations { + results[i] = *delegationToResult(&d) + } + + return results, nil +} + +func (am *ActionManager) GetDelegationChain(ctx context.Context, delegationID int64) ([]DelegationResult, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + delegations, err := am.kb.queries.GetDelegationChain(ctx, GetDelegationChainParams{ + ID: delegationID, + ParentID: &delegationID, + }) + if err != nil { + return nil, fmt.Errorf("get delegation chain: %w", err) + } + + results := make([]DelegationResult, len(delegations)) + for i, d := range delegations { + results[i] = *delegationToResult(&d) + } + + return results, nil +} + +func (am *ActionManager) RevokeDelegation(ctx context.Context, delegationID int64) error { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + return am.kb.queries.RevokeDelegation(ctx, delegationID) +} + +func delegationToResult(d *Delegation) *DelegationResult { + expiresAt := "" + if d.ExpiresAt != nil { + expiresAt = *d.ExpiresAt + } + + return &DelegationResult{ + ID: d.ID, + UcanID: d.UcanID, + Delegator: d.Delegator, + Delegate: d.Delegate, + Resource: d.Resource, + Action: d.Action, + Caveats: d.Caveats, + Depth: d.Depth, + Status: d.Status, + CreatedAt: d.CreatedAt, + ExpiresAt: expiresAt, + } +} diff --git a/internal/keybase/actions_grant.go b/internal/keybase/actions_grant.go new file mode 100644 index 0000000..dbf9949 --- /dev/null +++ b/internal/keybase/actions_grant.go @@ -0,0 +1,181 @@ +package keybase + +import ( + "context" + "encoding/json" + "fmt" +) + +type NewGrantInput struct { + ServiceID int64 `json:"service_id"` + UcanID int64 `json:"ucan_id,omitempty"` + Scopes json.RawMessage `json:"scopes"` + Accounts json.RawMessage `json:"accounts"` + ExpiresAt string `json:"expires_at,omitempty"` +} + +func (am *ActionManager) CreateGrant(ctx context.Context, params NewGrantInput) (*GrantResult, error) { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + if am.kb.didID == 0 { + return nil, fmt.Errorf("DID not initialized") + } + + var ucanID *int64 + if params.UcanID != 0 { + ucanID = ¶ms.UcanID + } + + var expiresAt *string + if params.ExpiresAt != "" { + expiresAt = ¶ms.ExpiresAt + } + + scopes := params.Scopes + if scopes == nil { + scopes = json.RawMessage(`[]`) + } + accounts := params.Accounts + if accounts == nil { + accounts = json.RawMessage(`[]`) + } + + g, err := am.kb.queries.CreateGrant(ctx, CreateGrantParams{ + DidID: am.kb.didID, + ServiceID: params.ServiceID, + UcanID: ucanID, + Scopes: scopes, + Accounts: accounts, + ExpiresAt: expiresAt, + }) + if err != nil { + return nil, fmt.Errorf("create grant: %w", err) + } + + svc, err := am.kb.queries.GetServiceByID(ctx, g.ServiceID) + if err != nil { + return nil, fmt.Errorf("get service: %w", err) + } + + serviceLogo := "" + if svc.LogoUrl != nil { + serviceLogo = *svc.LogoUrl + } + + lastUsed := "" + if g.LastUsed != nil { + lastUsed = *g.LastUsed + } + + expires := "" + if g.ExpiresAt != nil { + expires = *g.ExpiresAt + } + + return &GrantResult{ + ID: g.ID, + ServiceName: svc.Name, + ServiceOrigin: svc.Origin, + ServiceLogo: serviceLogo, + Scopes: g.Scopes, + Accounts: g.Accounts, + Status: g.Status, + GrantedAt: g.GrantedAt, + LastUsed: lastUsed, + ExpiresAt: expires, + }, nil +} + +func (am *ActionManager) GetGrantByService(ctx context.Context, serviceID int64) (*GrantResult, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + if am.kb.didID == 0 { + return nil, fmt.Errorf("DID not initialized") + } + + g, err := am.kb.queries.GetGrantByService(ctx, GetGrantByServiceParams{ + DidID: am.kb.didID, + ServiceID: serviceID, + }) + if err != nil { + return nil, fmt.Errorf("get grant by service: %w", err) + } + + svc, err := am.kb.queries.GetServiceByID(ctx, g.ServiceID) + if err != nil { + return nil, fmt.Errorf("get service: %w", err) + } + + serviceLogo := "" + if svc.LogoUrl != nil { + serviceLogo = *svc.LogoUrl + } + + lastUsed := "" + if g.LastUsed != nil { + lastUsed = *g.LastUsed + } + + expires := "" + if g.ExpiresAt != nil { + expires = *g.ExpiresAt + } + + return &GrantResult{ + ID: g.ID, + ServiceName: svc.Name, + ServiceOrigin: svc.Origin, + ServiceLogo: serviceLogo, + Scopes: g.Scopes, + Accounts: g.Accounts, + Status: g.Status, + GrantedAt: g.GrantedAt, + LastUsed: lastUsed, + ExpiresAt: expires, + }, nil +} + +func (am *ActionManager) UpdateGrantScopes(ctx context.Context, grantID int64, scopes, accounts json.RawMessage) error { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + return am.kb.queries.UpdateGrantScopes(ctx, UpdateGrantScopesParams{ + Scopes: scopes, + Accounts: accounts, + ID: grantID, + }) +} + +func (am *ActionManager) UpdateGrantLastUsed(ctx context.Context, grantID int64) error { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + return am.kb.queries.UpdateGrantLastUsed(ctx, grantID) +} + +func (am *ActionManager) SuspendGrant(ctx context.Context, grantID int64) error { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + return am.kb.queries.SuspendGrant(ctx, grantID) +} + +func (am *ActionManager) ReactivateGrant(ctx context.Context, grantID int64) error { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + return am.kb.queries.ReactivateGrant(ctx, grantID) +} + +func (am *ActionManager) CountActiveGrants(ctx context.Context) (int64, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + if am.kb.didID == 0 { + return 0, nil + } + + return am.kb.queries.CountActiveGrants(ctx, am.kb.didID) +} diff --git a/internal/keybase/actions_keyshare.go b/internal/keybase/actions_keyshare.go new file mode 100644 index 0000000..974df80 --- /dev/null +++ b/internal/keybase/actions_keyshare.go @@ -0,0 +1,206 @@ +package keybase + +import ( + "context" + "crypto/rand" + "encoding/hex" + "fmt" +) + +type KeyShareResult struct { + ID int64 `json:"id"` + ShareID string `json:"share_id"` + KeyID string `json:"key_id"` + PartyIndex int64 `json:"party_index"` + Threshold int64 `json:"threshold"` + TotalParties int64 `json:"total_parties"` + Curve string `json:"curve"` + PublicKey string `json:"public_key"` + ChainCode string `json:"chain_code,omitempty"` + DerivationPath string `json:"derivation_path,omitempty"` + Status string `json:"status"` + CreatedAt string `json:"created_at"` + RotatedAt string `json:"rotated_at,omitempty"` +} + +type NewKeyShareInput struct { + KeyID string `json:"key_id"` + PartyIndex int64 `json:"party_index"` + Threshold int64 `json:"threshold"` + TotalParties int64 `json:"total_parties"` + Curve string `json:"curve"` + ShareData string `json:"share_data"` + PublicKey string `json:"public_key"` + ChainCode string `json:"chain_code,omitempty"` + DerivationPath string `json:"derivation_path,omitempty"` +} + +func (am *ActionManager) CreateKeyShare(ctx context.Context, params NewKeyShareInput) (*KeyShareResult, error) { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + if am.kb.didID == 0 { + return nil, fmt.Errorf("DID not initialized") + } + + shareID := generateShareID() + + var chainCode, derivationPath *string + if params.ChainCode != "" { + chainCode = ¶ms.ChainCode + } + if params.DerivationPath != "" { + derivationPath = ¶ms.DerivationPath + } + + ks, err := am.kb.queries.CreateKeyShare(ctx, CreateKeyShareParams{ + DidID: am.kb.didID, + ShareID: shareID, + KeyID: params.KeyID, + PartyIndex: params.PartyIndex, + Threshold: params.Threshold, + TotalParties: params.TotalParties, + Curve: params.Curve, + ShareData: params.ShareData, + PublicKey: params.PublicKey, + ChainCode: chainCode, + DerivationPath: derivationPath, + }) + if err != nil { + return nil, fmt.Errorf("create key share: %w", err) + } + + return keyShareToResult(&ks), nil +} + +func (am *ActionManager) ListKeyShares(ctx context.Context) ([]KeyShareResult, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + if am.kb.didID == 0 { + return []KeyShareResult{}, nil + } + + shares, err := am.kb.queries.ListKeySharesByDID(ctx, am.kb.didID) + if err != nil { + return nil, fmt.Errorf("list key shares: %w", err) + } + + results := make([]KeyShareResult, len(shares)) + for i, ks := range shares { + results[i] = *keyShareToResult(&ks) + } + + return results, nil +} + +func (am *ActionManager) GetKeyShareByID(ctx context.Context, shareID string) (*KeyShareResult, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + ks, err := am.kb.queries.GetKeyShareByID(ctx, shareID) + if err != nil { + return nil, fmt.Errorf("get key share: %w", err) + } + + return keyShareToResult(&ks), nil +} + +func (am *ActionManager) GetKeyShareByKeyID(ctx context.Context, keyID string) (*KeyShareResult, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + if am.kb.didID == 0 { + return nil, fmt.Errorf("DID not initialized") + } + + ks, err := am.kb.queries.GetKeyShareByKeyID(ctx, GetKeyShareByKeyIDParams{ + DidID: am.kb.didID, + KeyID: keyID, + }) + if err != nil { + return nil, fmt.Errorf("get key share by key ID: %w", err) + } + + return keyShareToResult(&ks), nil +} + +func (am *ActionManager) RotateKeyShare(ctx context.Context, shareID string) error { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + ks, err := am.kb.queries.GetKeyShareByID(ctx, shareID) + if err != nil { + return fmt.Errorf("get key share: %w", err) + } + + return am.kb.queries.RotateKeyShare(ctx, ks.ID) +} + +func (am *ActionManager) ArchiveKeyShare(ctx context.Context, shareID string) error { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + ks, err := am.kb.queries.GetKeyShareByID(ctx, shareID) + if err != nil { + return fmt.Errorf("get key share: %w", err) + } + + return am.kb.queries.ArchiveKeyShare(ctx, ks.ID) +} + +func (am *ActionManager) DeleteKeyShare(ctx context.Context, shareID string) error { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + if am.kb.didID == 0 { + return fmt.Errorf("DID not initialized") + } + + ks, err := am.kb.queries.GetKeyShareByID(ctx, shareID) + if err != nil { + return fmt.Errorf("get key share: %w", err) + } + + return am.kb.queries.DeleteKeyShare(ctx, DeleteKeyShareParams{ + ID: ks.ID, + DidID: am.kb.didID, + }) +} + +func generateShareID() string { + b := make([]byte, 16) + rand.Read(b) + return "ks_" + hex.EncodeToString(b) +} + +func keyShareToResult(ks *KeyShare) *KeyShareResult { + chainCode := "" + if ks.ChainCode != nil { + chainCode = *ks.ChainCode + } + derivationPath := "" + if ks.DerivationPath != nil { + derivationPath = *ks.DerivationPath + } + rotatedAt := "" + if ks.RotatedAt != nil { + rotatedAt = *ks.RotatedAt + } + + return &KeyShareResult{ + ID: ks.ID, + ShareID: ks.ShareID, + KeyID: ks.KeyID, + PartyIndex: ks.PartyIndex, + Threshold: ks.Threshold, + TotalParties: ks.TotalParties, + Curve: ks.Curve, + PublicKey: ks.PublicKey, + ChainCode: chainCode, + DerivationPath: derivationPath, + Status: ks.Status, + CreatedAt: ks.CreatedAt, + RotatedAt: rotatedAt, + } +} diff --git a/internal/keybase/actions_service.go b/internal/keybase/actions_service.go new file mode 100644 index 0000000..9bafb37 --- /dev/null +++ b/internal/keybase/actions_service.go @@ -0,0 +1,172 @@ +package keybase + +import ( + "context" + "encoding/json" + "fmt" +) + +type ServiceResult struct { + ID int64 `json:"id"` + Origin string `json:"origin"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + LogoURL string `json:"logo_url,omitempty"` + DID string `json:"did,omitempty"` + IsVerified bool `json:"is_verified"` + Metadata json.RawMessage `json:"metadata,omitempty"` + CreatedAt string `json:"created_at"` +} + +type NewServiceInput struct { + Origin string `json:"origin"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + LogoURL string `json:"logo_url,omitempty"` + DID string `json:"did,omitempty"` + IsVerified bool `json:"is_verified"` + Metadata json.RawMessage `json:"metadata,omitempty"` +} + +type UpdateServiceInput struct { + ID int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + LogoURL string `json:"logo_url,omitempty"` + Metadata json.RawMessage `json:"metadata,omitempty"` +} + +func (am *ActionManager) CreateService(ctx context.Context, params NewServiceInput) (*ServiceResult, error) { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + var description, logoURL, did *string + if params.Description != "" { + description = ¶ms.Description + } + if params.LogoURL != "" { + logoURL = ¶ms.LogoURL + } + if params.DID != "" { + did = ¶ms.DID + } + + metadata := params.Metadata + if metadata == nil { + metadata = json.RawMessage(`{}`) + } + + var isVerified int64 + if params.IsVerified { + isVerified = 1 + } + + svc, err := am.kb.queries.CreateService(ctx, CreateServiceParams{ + Origin: params.Origin, + Name: params.Name, + Description: description, + LogoUrl: logoURL, + Did: did, + IsVerified: isVerified, + Metadata: metadata, + }) + if err != nil { + return nil, fmt.Errorf("create service: %w", err) + } + + return serviceToResult(&svc), nil +} + +func (am *ActionManager) GetServiceByOrigin(ctx context.Context, origin string) (*ServiceResult, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + svc, err := am.kb.queries.GetServiceByOrigin(ctx, origin) + if err != nil { + return nil, fmt.Errorf("get service by origin: %w", err) + } + + return serviceToResult(&svc), nil +} + +func (am *ActionManager) GetServiceByID(ctx context.Context, serviceID int64) (*ServiceResult, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + svc, err := am.kb.queries.GetServiceByID(ctx, serviceID) + if err != nil { + return nil, fmt.Errorf("get service by ID: %w", err) + } + + return serviceToResult(&svc), nil +} + +func (am *ActionManager) UpdateService(ctx context.Context, params UpdateServiceInput) error { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + var description, logoURL *string + if params.Description != "" { + description = ¶ms.Description + } + if params.LogoURL != "" { + logoURL = ¶ms.LogoURL + } + + metadata := params.Metadata + if metadata == nil { + metadata = json.RawMessage(`{}`) + } + + return am.kb.queries.UpdateService(ctx, UpdateServiceParams{ + Name: params.Name, + Description: description, + LogoUrl: logoURL, + Metadata: metadata, + ID: params.ID, + }) +} + +func (am *ActionManager) ListVerifiedServices(ctx context.Context) ([]ServiceResult, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + services, err := am.kb.queries.ListVerifiedServices(ctx) + if err != nil { + return nil, fmt.Errorf("list verified services: %w", err) + } + + results := make([]ServiceResult, len(services)) + for i, svc := range services { + results[i] = *serviceToResult(&svc) + } + + return results, nil +} + +func serviceToResult(svc *Service) *ServiceResult { + description := "" + if svc.Description != nil { + description = *svc.Description + } + logoURL := "" + if svc.LogoUrl != nil { + logoURL = *svc.LogoUrl + } + did := "" + if svc.Did != nil { + did = *svc.Did + } + + return &ServiceResult{ + ID: svc.ID, + Origin: svc.Origin, + Name: svc.Name, + Description: description, + LogoURL: logoURL, + DID: did, + IsVerified: svc.IsVerified == 1, + Metadata: svc.Metadata, + CreatedAt: svc.CreatedAt, + } +} diff --git a/internal/keybase/actions_session.go b/internal/keybase/actions_session.go new file mode 100644 index 0000000..1bbacb7 --- /dev/null +++ b/internal/keybase/actions_session.go @@ -0,0 +1,84 @@ +package keybase + +import ( + "context" + "fmt" +) + +func (am *ActionManager) GetSessionByID(ctx context.Context, sessionID string) (*SessionResult, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + sess, err := am.kb.queries.GetSessionByID(ctx, sessionID) + if err != nil { + return nil, fmt.Errorf("get session: %w", err) + } + + return sessionToResult(&sess), nil +} + +func (am *ActionManager) GetCurrentSession(ctx context.Context) (*SessionResult, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + if am.kb.didID == 0 { + return nil, fmt.Errorf("DID not initialized") + } + + sess, err := am.kb.queries.GetCurrentSession(ctx, am.kb.didID) + if err != nil { + return nil, fmt.Errorf("get current session: %w", err) + } + + return sessionToResult(&sess), nil +} + +func (am *ActionManager) UpdateSessionActivity(ctx context.Context, sessionID string) error { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + sess, err := am.kb.queries.GetSessionByID(ctx, sessionID) + if err != nil { + return fmt.Errorf("get session: %w", err) + } + + return am.kb.queries.UpdateSessionActivity(ctx, sess.ID) +} + +func (am *ActionManager) SetCurrentSession(ctx context.Context, sessionID string) error { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + if am.kb.didID == 0 { + return fmt.Errorf("DID not initialized") + } + + sess, err := am.kb.queries.GetSessionByID(ctx, sessionID) + if err != nil { + return fmt.Errorf("get session: %w", err) + } + + return am.kb.queries.SetCurrentSession(ctx, SetCurrentSessionParams{ + ID: sess.ID, + DidID: am.kb.didID, + }) +} + +func (am *ActionManager) DeleteExpiredSessions(ctx context.Context) error { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + return am.kb.queries.DeleteExpiredSessions(ctx) +} + +func sessionToResult(sess *Session) *SessionResult { + return &SessionResult{ + ID: sess.ID, + SessionID: sess.SessionID, + DeviceInfo: sess.DeviceInfo, + IsCurrent: sess.IsCurrent == 1, + LastActivity: sess.LastActivity, + ExpiresAt: sess.ExpiresAt, + CreatedAt: sess.CreatedAt, + } +} diff --git a/internal/keybase/actions_ucan.go b/internal/keybase/actions_ucan.go new file mode 100644 index 0000000..eb367e9 --- /dev/null +++ b/internal/keybase/actions_ucan.go @@ -0,0 +1,205 @@ +package keybase + +import ( + "context" + "encoding/json" + "fmt" +) + +type UCANResult struct { + ID int64 `json:"id"` + CID string `json:"cid"` + Issuer string `json:"issuer"` + Audience string `json:"audience"` + Subject string `json:"subject,omitempty"` + Capabilities json.RawMessage `json:"capabilities"` + NotBefore string `json:"not_before,omitempty"` + ExpiresAt string `json:"expires_at"` + IsRevoked bool `json:"is_revoked"` + CreatedAt string `json:"created_at"` +} + +type NewUCANInput struct { + CID string `json:"cid"` + Issuer string `json:"issuer"` + Audience string `json:"audience"` + Subject string `json:"subject,omitempty"` + Capabilities json.RawMessage `json:"capabilities"` + ProofChain json.RawMessage `json:"proof_chain,omitempty"` + NotBefore string `json:"not_before,omitempty"` + ExpiresAt string `json:"expires_at"` + Nonce string `json:"nonce,omitempty"` + Facts json.RawMessage `json:"facts,omitempty"` + Signature string `json:"signature"` + RawToken string `json:"raw_token"` +} + +func (am *ActionManager) CreateUCAN(ctx context.Context, params NewUCANInput) (*UCANResult, error) { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + if am.kb.didID == 0 { + return nil, fmt.Errorf("DID not initialized") + } + + var subject, notBefore, nonce *string + if params.Subject != "" { + subject = ¶ms.Subject + } + if params.NotBefore != "" { + notBefore = ¶ms.NotBefore + } + if params.Nonce != "" { + nonce = ¶ms.Nonce + } + + proofChain := params.ProofChain + if proofChain == nil { + proofChain = json.RawMessage(`[]`) + } + facts := params.Facts + if facts == nil { + facts = json.RawMessage(`{}`) + } + + ucan, err := am.kb.queries.CreateUCAN(ctx, CreateUCANParams{ + DidID: am.kb.didID, + Cid: params.CID, + Issuer: params.Issuer, + Audience: params.Audience, + Subject: subject, + Capabilities: params.Capabilities, + ProofChain: proofChain, + NotBefore: notBefore, + ExpiresAt: params.ExpiresAt, + Nonce: nonce, + Facts: facts, + Signature: params.Signature, + RawToken: params.RawToken, + }) + if err != nil { + return nil, fmt.Errorf("create ucan: %w", err) + } + + return ucanToResult(&ucan), nil +} + +func (am *ActionManager) ListUCANs(ctx context.Context) ([]UCANResult, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + if am.kb.didID == 0 { + return []UCANResult{}, nil + } + + ucans, err := am.kb.queries.ListUCANsByDID(ctx, am.kb.didID) + if err != nil { + return nil, fmt.Errorf("list ucans: %w", err) + } + + results := make([]UCANResult, len(ucans)) + for i, u := range ucans { + results[i] = *ucanToResult(&u) + } + + return results, nil +} + +func (am *ActionManager) GetUCANByCID(ctx context.Context, cid string) (*UCANResult, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + ucan, err := am.kb.queries.GetUCANByCID(ctx, cid) + if err != nil { + return nil, fmt.Errorf("get ucan: %w", err) + } + + return ucanToResult(&ucan), nil +} + +func (am *ActionManager) ListUCANsByAudience(ctx context.Context, audience string) ([]UCANResult, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + ucans, err := am.kb.queries.ListUCANsByAudience(ctx, audience) + if err != nil { + return nil, fmt.Errorf("list ucans by audience: %w", err) + } + + results := make([]UCANResult, len(ucans)) + for i, u := range ucans { + results[i] = *ucanToResult(&u) + } + + return results, nil +} + +func (am *ActionManager) RevokeUCAN(ctx context.Context, cid string) error { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + return am.kb.queries.RevokeUCAN(ctx, cid) +} + +func (am *ActionManager) IsUCANRevoked(ctx context.Context, cid string) (bool, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + revoked, err := am.kb.queries.IsUCANRevoked(ctx, cid) + if err != nil { + return false, fmt.Errorf("check ucan revocation: %w", err) + } + + return revoked == 1, nil +} + +func (am *ActionManager) CreateRevocation(ctx context.Context, ucanCID string, revokedBy string, reason string) error { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + var reasonPtr *string + if reason != "" { + reasonPtr = &reason + } + + if err := am.kb.queries.RevokeUCAN(ctx, ucanCID); err != nil { + return fmt.Errorf("revoke ucan token: %w", err) + } + + return am.kb.queries.CreateRevocation(ctx, CreateRevocationParams{ + UcanCid: ucanCID, + RevokedBy: revokedBy, + Reason: reasonPtr, + }) +} + +func (am *ActionManager) CleanExpiredUCANs(ctx context.Context) error { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + return am.kb.queries.CleanExpiredUCANs(ctx) +} + +func ucanToResult(u *UcanToken) *UCANResult { + subject := "" + if u.Subject != nil { + subject = *u.Subject + } + notBefore := "" + if u.NotBefore != nil { + notBefore = *u.NotBefore + } + + return &UCANResult{ + ID: u.ID, + CID: u.Cid, + Issuer: u.Issuer, + Audience: u.Audience, + Subject: subject, + Capabilities: u.Capabilities, + NotBefore: notBefore, + ExpiresAt: u.ExpiresAt, + IsRevoked: u.IsRevoked == 1, + CreatedAt: u.CreatedAt, + } +} diff --git a/internal/keybase/actions_verification.go b/internal/keybase/actions_verification.go new file mode 100644 index 0000000..5d38990 --- /dev/null +++ b/internal/keybase/actions_verification.go @@ -0,0 +1,114 @@ +package keybase + +import ( + "context" + "fmt" +) + +type NewVerificationMethodInput struct { + MethodID string `json:"method_id"` + MethodType string `json:"method_type"` + Controller string `json:"controller"` + PublicKey string `json:"public_key"` + Purpose string `json:"purpose"` +} + +func (am *ActionManager) CreateVerificationMethod(ctx context.Context, params NewVerificationMethodInput) (*VerificationMethodResult, error) { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + if am.kb.didID == 0 { + return nil, fmt.Errorf("DID not initialized") + } + + vm, err := am.kb.queries.CreateVerificationMethod(ctx, CreateVerificationMethodParams{ + DidID: am.kb.didID, + MethodID: params.MethodID, + MethodType: params.MethodType, + Controller: params.Controller, + PublicKey: params.PublicKey, + Purpose: params.Purpose, + }) + if err != nil { + return nil, fmt.Errorf("create verification method: %w", err) + } + + return &VerificationMethodResult{ + ID: vm.MethodID, + Type: vm.MethodType, + Controller: vm.Controller, + PublicKey: vm.PublicKey, + Purpose: vm.Purpose, + }, nil +} + +func (am *ActionManager) ListVerificationMethodsFull(ctx context.Context) ([]VerificationMethodResult, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + if am.kb.didID == 0 { + return []VerificationMethodResult{}, nil + } + + vms, err := am.kb.queries.ListVerificationMethods(ctx, am.kb.didID) + if err != nil { + return nil, fmt.Errorf("list verification methods: %w", err) + } + + results := make([]VerificationMethodResult, len(vms)) + for i, vm := range vms { + results[i] = VerificationMethodResult{ + ID: vm.MethodID, + Type: vm.MethodType, + Controller: vm.Controller, + PublicKey: vm.PublicKey, + Purpose: vm.Purpose, + } + } + + return results, nil +} + +func (am *ActionManager) GetVerificationMethod(ctx context.Context, methodID string) (*VerificationMethodResult, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + if am.kb.didID == 0 { + return nil, fmt.Errorf("DID not initialized") + } + + vm, err := am.kb.queries.GetVerificationMethod(ctx, GetVerificationMethodParams{ + DidID: am.kb.didID, + MethodID: methodID, + }) + if err != nil { + return nil, fmt.Errorf("get verification method: %w", err) + } + + return &VerificationMethodResult{ + ID: vm.MethodID, + Type: vm.MethodType, + Controller: vm.Controller, + PublicKey: vm.PublicKey, + Purpose: vm.Purpose, + }, nil +} + +func (am *ActionManager) DeleteVerificationMethod(ctx context.Context, methodID string) error { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + if am.kb.didID == 0 { + return fmt.Errorf("DID not initialized") + } + + vm, err := am.kb.queries.GetVerificationMethod(ctx, GetVerificationMethodParams{ + DidID: am.kb.didID, + MethodID: methodID, + }) + if err != nil { + return fmt.Errorf("get verification method: %w", err) + } + + return am.kb.queries.DeleteVerificationMethod(ctx, vm.ID) +} -- 2.43.0 From e4943509c97038e55eecc76ae231a1c5ddf37011 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Thu, 8 Jan 2026 14:54:40 -0500 Subject: [PATCH 05/35] feat(ucan): migrate to UCAN v1.0.0-rc.1 envelope format --- TODO.md | 317 +++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 231 insertions(+), 86 deletions(-) diff --git a/TODO.md b/TODO.md index cba3c3b..ed5d4d2 100644 --- a/TODO.md +++ b/TODO.md @@ -11,45 +11,187 @@ Remaining tasks from [MIGRATION.md](./MIGRATION.md) for the Nebula Key Enclave. | Generated Code | Complete | `internal/keybase/*.go` | | Basic Plugin Functions | Complete | `generate`, `load`, `exec`, `query`, `ping` | | Encryption | Not Started | WebAuthn PRF key derivation needed | -| UCAN Authorization | Placeholder | Validation logic not implemented | +| **UCAN v1.0.0-rc.1** | **CRITICAL** | Go implementation uses deprecated JWT format | | MPC Key Shares | Not Started | Key share management missing | | Database Serialization | Incomplete | Export dumps comments only | --- -## 1. Encryption Strategy +## 1. UCAN v1.0.0-rc.1 Migration (CRITICAL PRIORITY) + +> **Breaking Change**: Current Go implementation (`internal/crypto/ucan/`) uses deprecated JWT-based UCAN format. Must migrate to envelope format per v1.0.0-rc.1 spec. + +### Current State (DEPRECATED - Must Replace) + +The following files use the **old JWT-based format** and must be rewritten: + +| File | Status | Issue | +|------|--------|-------| +| `jwt.go` | DEPRECATED | Uses `github.com/golang-jwt/jwt/v5`, old `can`+`with` format | +| `capability.go` | DEPRECATED | Old Attenuation/Resource/Capability model | +| `verifier.go` | DEPRECATED | JWT parsing, old proof chain format | +| `source.go` | DEPRECATED | JWT token creation with MPC | +| `vault.go` | PARTIAL | VaultCapability needs Policy migration | + +### Reference Implementation (Already Compliant) + +These files are already aligned with v1.0.0-rc.1: +- `src/ucan.ts` - TypeScript types with envelope format +- `internal/codec/ucan-schemas.json` - JSON Schema definitions + +### 1.1 Core Data Structures + +- [ ] Create `internal/crypto/ucan/types.go` - v1.0.0-rc.1 types + - [ ] `DelegationPayload` struct (iss, aud, sub, cmd, pol, nonce, meta, nbf, exp) + - [ ] `InvocationPayload` struct (iss, sub, aud, cmd, args, prf, meta, nonce, exp, iat, cause) + - [ ] `Delegation` type as `[Signature, DelegationSigPayload]` tuple + - [ ] `Invocation` type as `[Signature, InvocationSigPayload]` tuple + - [ ] `Task` struct (sub, cmd, args, nonce) + - [ ] `ReceiptPayload` struct (iss, ran, out, fx, meta, iat) + - [ ] `RevocationPayload` struct + +- [ ] Create `internal/crypto/ucan/policy.go` - Policy Language + - [ ] `PolicyStatement` union type + - [ ] `EqualityStatement` - `["==", selector, value]` / `["!=", selector, value]` + - [ ] `InequalityStatement` - `[">", selector, number]` etc. + - [ ] `LikeStatement` - `["like", selector, glob]` + - [ ] `NotStatement` - `["not", statement]` + - [ ] `AndStatement` / `OrStatement` - logical connectives + - [ ] `AllStatement` / `AnyStatement` - quantifiers + - [ ] `Selector` parser (jq-inspired: `.foo`, `.bar[0]`, `.items[-1]`) + - [ ] `GlobPattern` matcher + +- [ ] Create `internal/crypto/ucan/command.go` - Command types + - [ ] `Command` type with validation (must start with `/`, lowercase, no trailing slash) + - [ ] Standard commands: `/crud/*`, `/msg/*`, `/ucan/revoke`, `/wasm/run` + - [ ] Custom Sonr commands: `/vault/*`, `/did/*`, `/dwn/*` + +### 1.2 Envelope Format & Encoding + +- [ ] Create `internal/crypto/ucan/envelope.go` - Envelope handling + - [ ] `UCANEnvelope[P]` generic type as `[Signature, {h: VarsigHeader} & P]` + - [ ] Encode envelope to CBOR (requires `github.com/fxamacker/cbor/v2`) + - [ ] Decode envelope from CBOR + - [ ] DAG-JSON encoding for interop + - [ ] CID computation (DAG-CBOR codec, SHA-256 multihash, base58btc) + +- [ ] Create `internal/crypto/ucan/varsig.go` - Varsig v1 headers + - [ ] Varsig header encoding/decoding + - [ ] Algorithm metadata extraction + - [ ] Support Ed25519, P-256, secp256k1 + +### 1.3 Delegation Operations + +- [ ] Create `internal/crypto/ucan/delegation.go` - Delegation creation/validation + - [ ] `NewDelegation(issuer, audience, subject, cmd, policy, exp, meta)` builder + - [ ] Sign delegation with issuer private key + - [ ] Validate delegation signature + - [ ] Validate delegation payload (temporal, structural) + - [ ] Extract `Capability` from delegation (sub + cmd + pol) + +### 1.4 Invocation Operations + +- [ ] Create `internal/crypto/ucan/invocation.go` - Invocation creation/validation + - [ ] `NewInvocation(issuer, subject, cmd, args, proofs, exp)` builder + - [ ] Sign invocation with invoker private key + - [ ] Validate invocation signature + - [ ] Validate proof chain (CID references to delegations) + - [ ] Evaluate policies against invocation args + +### 1.5 Policy Evaluation Engine + +- [ ] Create `internal/crypto/ucan/eval.go` - Policy evaluation + - [ ] `EvaluatePolicy(policy Policy, args Arguments) (bool, error)` + - [ ] Selector resolution against IPLD data + - [ ] Equality comparison (deep IPLD equality) + - [ ] Numeric comparisons + - [ ] Glob pattern matching for `like` operator + - [ ] Logical connectives (`and`, `or`, `not`) + - [ ] Quantifiers (`all`, `any`) over collections + +### 1.6 Proof Chain Validation + +- [ ] Create `internal/crypto/ucan/chain.go` - Chain validation + - [ ] Resolve CID to Delegation (requires delegation store) + - [ ] Validate chain continuity (child.iss == parent.aud) + - [ ] Validate capability attenuation (child.cmd subsumes parent.cmd) + - [ ] Validate policy attenuation (child.pol more restrictive than parent.pol) + - [ ] Validate temporal bounds (child.exp <= parent.exp) + - [ ] Check revocation status for all chain members + +### 1.7 Revocation + +- [ ] Create `internal/crypto/ucan/revocation.go` - Revocation handling + - [ ] `NewRevocation(revoker, delegation_cid)` builder + - [ ] Validate revoker is in delegation's issuer chain + - [ ] Store revocation in database + - [ ] Query revocation status by CID + +### 1.8 Database Integration + +- [ ] Update `internal/migrations/schema.sql` for v1.0.0-rc.1 + - [ ] `ucan_delegations` table (cid, envelope_cbor, iss, aud, sub, cmd, exp, created_at) + - [ ] `ucan_invocations` table (cid, envelope_cbor, iss, sub, cmd, exp, created_at) + - [ ] `ucan_revocations` table (cid, delegation_cid, revoker, created_at) + - [ ] Indexes on iss, aud, sub, cmd for efficient queries + +- [ ] Update `internal/migrations/query.sql` for v1.0.0-rc.1 + - [ ] `InsertDelegation`, `GetDelegationByCID`, `ListDelegationsByAudience` + - [ ] `InsertInvocation`, `GetInvocationByCID` + - [ ] `InsertRevocation`, `IsRevoked`, `GetRevocationsByDelegation` + +### 1.9 Migration from Old Format + +- [ ] Create migration script for existing UCAN data (if any) +- [ ] Remove deprecated files after migration complete: + - [ ] `jwt.go` - Remove entirely + - [ ] `capability.go` - Replace with policy-based capabilities + - [ ] `verifier.go` - Replace with envelope-based verification + - [ ] `source.go` - Replace with envelope-based token creation + +### 1.10 Testing + +- [ ] Unit tests for policy evaluation +- [ ] Unit tests for envelope encoding/decoding +- [ ] Unit tests for chain validation +- [ ] Interoperability tests against TypeScript implementation +- [ ] Test vectors from UCAN spec + +--- + +## 2. Encryption Strategy > Reference: MIGRATION.md lines 770-814 -### 1.1 WebAuthn PRF Key Derivation +### 2.1 WebAuthn PRF Key Derivation - [ ] Implement `DeriveEncryptionKey(prfOutput []byte) ([]byte, error)` - [ ] Use HKDF with SHA-256 to derive 256-bit encryption key - [ ] Salt with `"nebula-enclave-v1"` as info parameter -### 1.2 Database Encryption +### 2.2 Database Encryption - [ ] Implement application-level AES-GCM encryption for serialized pages - [ ] Add encryption wrapper around `Serialize()` output - [ ] Add decryption wrapper for `Load()` input - [ ] Store encryption metadata (IV, auth tag) with serialized data -### 1.3 Encrypted Database Wrapper +### 2.3 Encrypted Database Wrapper - [ ] Create `internal/enclave/enclave.go` - Encrypted database wrapper - [ ] Create `internal/enclave/crypto.go` - WebAuthn PRF key derivation - [ ] Integrate with existing `internal/keybase` package --- -## 2. Database Serialization +## 3. Database Serialization > Current implementation in `conn.go:exportDump()` only outputs comments -### 2.1 Proper Serialization +### 3.1 Proper Serialization - [ ] Implement full row export with proper SQL INSERT statements - [ ] Handle JSON columns correctly (escape special characters) - [ ] Include table creation order for foreign key constraints - [ ] Add version header for migration compatibility -### 2.2 Proper Deserialization +### 3.2 Proper Deserialization - [ ] Parse serialized SQL dump in `Load()` - [ ] Execute INSERT statements to restore data - [ ] Validate data integrity after restore @@ -57,11 +199,11 @@ Remaining tasks from [MIGRATION.md](./MIGRATION.md) for the Nebula Key Enclave. --- -## 3. Action Manager Extensions +## 4. Action Manager Extensions > Reference: `internal/keybase/actions.go` -### 3.1 Key Share Actions +### 4.1 Key Share Actions - [ ] `CreateKeyShare(ctx, params) (*KeyShareResult, error)` - [ ] `ListKeyShares(ctx) ([]KeyShareResult, error)` - [ ] `GetKeyShareByID(ctx, shareID) (*KeyShareResult, error)` @@ -70,39 +212,31 @@ Remaining tasks from [MIGRATION.md](./MIGRATION.md) for the Nebula Key Enclave. - [ ] `ArchiveKeyShare(ctx, shareID) error` - [ ] `DeleteKeyShare(ctx, shareID) error` -### 3.2 UCAN Token Actions -- [ ] `CreateUCAN(ctx, params) (*UCANResult, error)` -- [ ] `ListUCANs(ctx) ([]UCANResult, error)` -- [ ] `GetUCANByCID(ctx, cid) (*UCANResult, error)` -- [ ] `ListUCANsByAudience(ctx, audience) ([]UCANResult, error)` +### 4.2 UCAN Token Actions (v1.0.0-rc.1) +- [ ] `CreateDelegation(ctx, params) (*DelegationResult, error)` +- [ ] `ListDelegations(ctx) ([]DelegationResult, error)` +- [ ] `GetDelegationByCID(ctx, cid) (*DelegationResult, error)` +- [ ] `ListDelegationsByAudience(ctx, audience) ([]DelegationResult, error)` +- [ ] `CreateInvocation(ctx, params) (*InvocationResult, error)` +- [ ] `ValidateInvocation(ctx, invocation) (*ValidationResult, error)` - [ ] `RevokeUCAN(ctx, cid) error` - [ ] `IsUCANRevoked(ctx, cid) (bool, error)` -- [ ] `CreateRevocation(ctx, params) error` - [ ] `CleanExpiredUCANs(ctx) error` -### 3.3 Delegation Actions -- [ ] `CreateDelegation(ctx, params) (*DelegationResult, error)` -- [ ] `ListDelegationsByDelegator(ctx, delegator) ([]DelegationResult, error)` -- [ ] `ListDelegationsByDelegate(ctx, delegate) ([]DelegationResult, error)` -- [ ] `ListDelegationsForResource(ctx, resource) ([]DelegationResult, error)` -- [ ] `GetDelegationChain(ctx, delegationID) ([]DelegationResult, error)` -- [ ] `RevokeDelegation(ctx, delegationID) error` -- [ ] `RevokeDelegationChain(ctx, delegationID) error` - -### 3.4 Verification Method Actions +### 4.3 Verification Method Actions - [ ] `CreateVerificationMethod(ctx, params) (*VerificationMethodResult, error)` - [ ] `ListVerificationMethods(ctx) ([]VerificationMethodResult, error)` - [ ] `GetVerificationMethod(ctx, methodID) (*VerificationMethodResult, error)` - [ ] `DeleteVerificationMethod(ctx, methodID) error` -### 3.5 Service Actions +### 4.4 Service Actions - [ ] `CreateService(ctx, params) (*ServiceResult, error)` - [ ] `GetServiceByOrigin(ctx, origin) (*ServiceResult, error)` - [ ] `GetServiceByID(ctx, serviceID) (*ServiceResult, error)` - [ ] `UpdateService(ctx, params) error` - [ ] `ListVerifiedServices(ctx) ([]ServiceResult, error)` -### 3.6 Grant Actions (Extend Existing) +### 4.5 Grant Actions (Extend Existing) - [ ] `CreateGrant(ctx, params) (*GrantResult, error)` - [ ] `GetGrantByService(ctx, serviceID) (*GrantResult, error)` - [ ] `UpdateGrantScopes(ctx, grantID, scopes, accounts) error` @@ -111,7 +245,7 @@ Remaining tasks from [MIGRATION.md](./MIGRATION.md) for the Nebula Key Enclave. - [ ] `ReactivateGrant(ctx, grantID) error` - [ ] `CountActiveGrants(ctx) (int64, error)` -### 3.7 Account Actions (Extend Existing) +### 4.6 Account Actions (Extend Existing) - [ ] `CreateAccount(ctx, params) (*AccountResult, error)` - [ ] `ListAccountsByChain(ctx, chainID) ([]AccountResult, error)` - [ ] `GetDefaultAccount(ctx, chainID) (*AccountResult, error)` @@ -119,57 +253,27 @@ Remaining tasks from [MIGRATION.md](./MIGRATION.md) for the Nebula Key Enclave. - [ ] `UpdateAccountLabel(ctx, accountID, label) error` - [ ] `DeleteAccount(ctx, accountID) error` -### 3.8 Credential Actions (Extend Existing) +### 4.7 Credential Actions (Extend Existing) - [ ] `CreateCredential(ctx, params) (*CredentialResult, error)` - [ ] `UpdateCredentialCounter(ctx, credentialID, signCount) error` - [ ] `RenameCredential(ctx, credentialID, name) error` - [ ] `DeleteCredential(ctx, credentialID) error` - [ ] `CountCredentialsByDID(ctx) (int64, error)` -### 3.9 Session Actions (Extend Existing) +### 4.8 Session Actions (Extend Existing) - [ ] `GetSessionByID(ctx, sessionID) (*SessionResult, error)` - [ ] `GetCurrentSession(ctx) (*SessionResult, error)` - [ ] `UpdateSessionActivity(ctx, sessionID) error` - [ ] `SetCurrentSession(ctx, sessionID) error` - [ ] `DeleteExpiredSessions(ctx) error` -### 3.10 Sync Checkpoint Actions +### 4.9 Sync Checkpoint Actions - [ ] `GetSyncCheckpoint(ctx, resourceType) (*SyncCheckpointResult, error)` - [ ] `UpsertSyncCheckpoint(ctx, params) error` - [ ] `ListSyncCheckpoints(ctx) ([]SyncCheckpointResult, error)` --- -## 4. UCAN Authorization - -> Reference: MIGRATION.md lines 820-821 - -### 4.1 Token Validation -- [ ] Implement proper UCAN token parsing (JWT-like structure) -- [ ] Validate token signature against issuer's public key -- [ ] Check token expiration (`exp` claim) -- [ ] Check token not-before (`nbf` claim) -- [ ] Validate audience matches expected DID - -### 4.2 Capability Verification -- [ ] Parse capabilities array from token -- [ ] Match requested action against granted capabilities -- [ ] Implement resource pattern matching (e.g., `sonr://vault/*`) -- [ ] Respect action restrictions (e.g., `sign`, `read`, `write`) - -### 4.3 Proof Chain Validation -- [ ] Follow proof chain to root UCAN -- [ ] Validate each link in the chain -- [ ] Ensure capability attenuation (child can't exceed parent) -- [ ] Check revocation status for all tokens in chain - -### 4.4 Revocation Checking -- [ ] Query `ucan_revocations` table -- [ ] Check all tokens in proof chain -- [ ] Cache revocation status for performance - ---- - ## 5. MPC Key Share Management > Reference: MIGRATION.md lines 823-824 @@ -201,8 +305,8 @@ Remaining tasks from [MIGRATION.md](./MIGRATION.md) for the Nebula Key Enclave. ### 6.1 Extend `exec` Resource Handlers - [ ] Add `key_shares` resource handler -- [ ] Add `ucans` resource handler -- [ ] Add `delegations` resource handler +- [ ] Add `delegations` resource handler (v1.0.0-rc.1) +- [ ] Add `invocations` resource handler (v1.0.0-rc.1) - [ ] Add `verification_methods` resource handler - [ ] Add `services` resource handler - [ ] Add `sync_checkpoints` resource handler @@ -224,18 +328,24 @@ Remaining tasks from [MIGRATION.md](./MIGRATION.md) for the Nebula Key Enclave. --- -## 7. Capability Delegation +## 7. Capability Delegation (v1.0.0-rc.1) -> Reference: MIGRATION.md lines 826-827 +> Reference: UCAN Delegation specification ### 7.1 Delegation Chain Management - [ ] Enforce maximum delegation depth (prevent infinite chains) -- [ ] Validate delegator has capability to delegate -- [ ] Ensure proper capability attenuation -- [ ] Track parent-child relationships +- [ ] Validate delegator has capability to delegate (sub field) +- [ ] Ensure proper capability attenuation (cmd + pol) +- [ ] Track parent-child relationships via CID references -### 7.2 Delegation Status +### 7.2 Policy Attenuation + +- [ ] Child policy must be more restrictive than parent +- [ ] Implement policy subsumption checking +- [ ] Command hierarchy validation (`/crud/*` subsumes `/crud/read`) + +### 7.3 Delegation Status - [ ] Implement expiration checking - [ ] Handle revocation cascades (revoke chain) @@ -274,11 +384,13 @@ Remaining tasks from [MIGRATION.md](./MIGRATION.md) for the Nebula Key Enclave. - [ ] Implement `exec(filter, token?)` wrapper - [ ] Implement `query(did?)` wrapper -### 9.2 Type Definitions +### 9.2 UCAN SDK (v1.0.0-rc.1) -- [ ] Generate TypeScript types from Go structs -- [ ] Export type definitions for consumers -- [ ] Add JSDoc documentation +- [ ] Delegation builder using `src/ucan.ts` types +- [ ] Invocation builder +- [ ] Policy builder helpers +- [ ] Envelope encoding/decoding (DAG-CBOR) +- [ ] CID computation ### 9.3 WebAuthn Integration @@ -295,14 +407,16 @@ Remaining tasks from [MIGRATION.md](./MIGRATION.md) for the Nebula Key Enclave. - [ ] Test all ActionManager methods - [ ] Test serialization/deserialization roundtrip - [ ] Test encryption/decryption -- [ ] Test UCAN validation logic +- [ ] Test UCAN policy evaluation +- [ ] Test UCAN envelope encoding ### 10.2 Integration Tests -- [ ] Test full generate → load → exec flow +- [ ] Test full generate -> load -> exec flow - [ ] Test credential lifecycle - [ ] Test session management - [ ] Test grant management +- [ ] Test UCAN delegation chain ### 10.3 Plugin Tests @@ -310,6 +424,12 @@ Remaining tasks from [MIGRATION.md](./MIGRATION.md) for the Nebula Key Enclave. - [ ] Add error case testing - [ ] Test with various input formats +### 10.4 Interoperability Tests + +- [ ] Go <-> TypeScript UCAN envelope compatibility +- [ ] CID computation consistency +- [ ] Policy evaluation consistency + --- ## 11. Security Hardening @@ -339,20 +459,45 @@ Remaining tasks from [MIGRATION.md](./MIGRATION.md) for the Nebula Key Enclave. ## Priority Order -1. **High Priority** (Core Functionality) - - Database Serialization (2.1, 2.2) - - Credential Creation (6.2, 3.8) - - Key Share Actions (3.1) - - Account Actions (3.7) +1. **CRITICAL (Spec Compliance)** + - UCAN v1.0.0-rc.1 Migration (Section 1) + - Core data structures (1.1) + - Envelope format (1.2) + - Delegation operations (1.3) + - Policy evaluation (1.5) -2. **Medium Priority** (Authorization) - - UCAN Validation (4.1, 4.2) - - Delegation Management (7.1, 7.2) - - Encryption Strategy (1.1, 1.2) +2. **High Priority (Core Functionality)** + - Database Serialization (3.1, 3.2) + - Credential Creation (6.2, 4.7) + - Key Share Actions (4.1) + - Account Actions (4.6) -3. **Lower Priority** (Enhancement) +3. **Medium Priority (Authorization)** + - Invocation operations (1.4) + - Proof chain validation (1.6) + - Revocation (1.7) + - Encryption Strategy (2.1, 2.2) + +4. **Lower Priority (Enhancement)** - TypeScript SDK (9.x) - DID State Sync (8.x) - Additional exec handlers (6.1) - Testing (10.x) - Security Hardening (11.x) + +--- + +## Deprecated Items (Removed) + +The following items from the previous TODO have been removed as they reference the **deprecated JWT-based UCAN format**: + +- ~~Section 4.1 "Token Validation" - JWT parsing~~ -> Replaced by envelope validation (1.2, 1.3) +- ~~Section 4.2 "Capability Verification" - `can`/`with` format~~ -> Replaced by policy evaluation (1.5) +- ~~Section 4.3 "Proof Chain Validation" - JWT proof strings~~ -> Replaced by CID-based chain (1.6) +- ~~Section 3.2 "UCAN Token Actions" - Old format~~ -> Replaced by v1.0.0-rc.1 actions (4.2) +- ~~Section 3.3 "Delegation Actions" - Old delegation model~~ -> Merged into Section 1 and 4.2 + +The old capability model (`Attenuation`, `Resource`, `Capability` interfaces) is replaced by: +- `sub` (DID) - Subject of the capability +- `cmd` (Command) - Action being delegated +- `pol` (Policy) - Constraints on invocation arguments -- 2.43.0 From 8a953e641bd37e904deac6f27fcc598338492166 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Thu, 8 Jan 2026 14:54:41 -0500 Subject: [PATCH 06/35] feat(codec): add json schemas for policy language and primitives --- internal/codec/policy.json | 305 ++++++++ internal/codec/primitives.json | 118 +++ internal/codec/ucan-schemas.json | 1160 ++++++++++++++++++++++++++++++ src/ucan.ts | 592 +++++++++++++++ 4 files changed, 2175 insertions(+) create mode 100644 internal/codec/policy.json create mode 100644 internal/codec/primitives.json create mode 100644 internal/codec/ucan-schemas.json create mode 100644 src/ucan.ts diff --git a/internal/codec/policy.json b/internal/codec/policy.json new file mode 100644 index 0000000..c5da6f6 --- /dev/null +++ b/internal/codec/policy.json @@ -0,0 +1,305 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucan.xyz/schemas/policy.json", + "title": "UCAN Policy Language", + "description": "Policy statements and selectors for constraining invocation arguments", + "$defs": { + "Selector": { + "title": "Policy Selector", + "description": "jq-inspired selector for navigating IPLD data structures", + "type": "string", + "pattern": "^\\.(([a-zA-Z_][a-zA-Z0-9_]*)|(\\[[^\\]]+\\])|(\\[\\])|(\\[-?\\d+\\])|(\\[\\d*:\\d*\\]))?((\\.[a-zA-Z_][a-zA-Z0-9_]*)|(\\[[^\\]]+\\])|(\\[\\])|(\\[-?\\d+\\])|(\\[\\d*:\\d*\\]))*\\??$", + "examples": [ + ".", + ".foo", + ".bar[0]", + ".items[-1]", + ".data[2:5]", + ".optional?" + ] + }, + "GlobPattern": { + "title": "Glob Pattern", + "description": "Pattern for 'like' operator. * = wildcard, \\* = literal", + "type": "string", + "examples": [ + "*@example.com", + "prefix*suffix" + ] + }, + "EqualityOperator": { + "title": "Equality Operator", + "type": "string", + "enum": [ + "==", + "!=" + ] + }, + "InequalityOperator": { + "title": "Inequality Operator", + "type": "string", + "enum": [ + ">", + ">=", + "<", + "<=" + ] + }, + "EqualityStatement": { + "title": "Equality Statement", + "description": "Deep comparison: [operator, selector, value]", + "type": "array", + "prefixItems": [ + { + "$ref": "#/$defs/EqualityOperator" + }, + { + "$ref": "#/$defs/Selector" + }, + {} + ], + "minItems": 3, + "maxItems": 3, + "examples": [ + [ + "==", + ".status", + "draft" + ], + [ + "!=", + ".deleted", + true + ] + ] + }, + "InequalityStatement": { + "title": "Inequality Statement", + "description": "Numeric comparison: [operator, selector, number]", + "type": "array", + "prefixItems": [ + { + "$ref": "#/$defs/InequalityOperator" + }, + { + "$ref": "#/$defs/Selector" + }, + { + "type": "number" + } + ], + "minItems": 3, + "maxItems": 3, + "examples": [ + [ + ">", + ".age", + 18 + ], + [ + "<=", + ".price", + 100.50 + ] + ] + }, + "LikeStatement": { + "title": "Like Statement", + "description": "Glob pattern matching: ['like', selector, pattern]", + "type": "array", + "prefixItems": [ + { + "const": "like" + }, + { + "$ref": "#/$defs/Selector" + }, + { + "$ref": "#/$defs/GlobPattern" + } + ], + "minItems": 3, + "maxItems": 3, + "examples": [ + [ + "like", + ".email", + "*@example.com" + ] + ] + }, + "NotStatement": { + "title": "Not Statement", + "description": "Logical negation: ['not', statement]", + "type": "array", + "prefixItems": [ + { + "const": "not" + }, + { + "$ref": "#/$defs/PolicyStatement" + } + ], + "minItems": 2, + "maxItems": 2 + }, + "AndStatement": { + "title": "And Statement", + "description": "Logical AND: ['and', [statements...]]. Empty array = true", + "type": "array", + "prefixItems": [ + { + "const": "and" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/PolicyStatement" + } + } + ], + "minItems": 2, + "maxItems": 2 + }, + "OrStatement": { + "title": "Or Statement", + "description": "Logical OR: ['or', [statements...]]. Empty array = true", + "type": "array", + "prefixItems": [ + { + "const": "or" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/PolicyStatement" + } + } + ], + "minItems": 2, + "maxItems": 2 + }, + "AllStatement": { + "title": "All Statement", + "description": "Universal quantifier: ['all', selector, statement]", + "type": "array", + "prefixItems": [ + { + "const": "all" + }, + { + "$ref": "#/$defs/Selector" + }, + { + "$ref": "#/$defs/PolicyStatement" + } + ], + "minItems": 3, + "maxItems": 3, + "examples": [ + [ + "all", + ".reviewers", + [ + "like", + ".email", + "*@example.com" + ] + ] + ] + }, + "AnyStatement": { + "title": "Any Statement", + "description": "Existential quantifier: ['any', selector, statement]", + "type": "array", + "prefixItems": [ + { + "const": "any" + }, + { + "$ref": "#/$defs/Selector" + }, + { + "$ref": "#/$defs/PolicyStatement" + } + ], + "minItems": 3, + "maxItems": 3, + "examples": [ + [ + "any", + ".tags", + [ + "==", + ".", + "urgent" + ] + ] + ] + }, + "PolicyStatement": { + "title": "Policy Statement", + "description": "A single policy predicate expression", + "oneOf": [ + { + "$ref": "#/$defs/EqualityStatement" + }, + { + "$ref": "#/$defs/InequalityStatement" + }, + { + "$ref": "#/$defs/LikeStatement" + }, + { + "$ref": "#/$defs/NotStatement" + }, + { + "$ref": "#/$defs/AndStatement" + }, + { + "$ref": "#/$defs/OrStatement" + }, + { + "$ref": "#/$defs/AllStatement" + }, + { + "$ref": "#/$defs/AnyStatement" + } + ] + }, + "Policy": { + "title": "UCAN Policy", + "description": "Array of statements forming implicit AND. Constrains invocation args.", + "type": "array", + "items": { + "$ref": "#/$defs/PolicyStatement" + }, + "examples": [ + [], + [ + [ + "==", + ".from", + "alice@example.com" + ] + ], + [ + [ + "==", + ".status", + "draft" + ], + [ + "all", + ".reviewer", + [ + "like", + ".email", + "*@example.com" + ] + ] + ] + ] + } + } +} diff --git a/internal/codec/primitives.json b/internal/codec/primitives.json new file mode 100644 index 0000000..819b8bc --- /dev/null +++ b/internal/codec/primitives.json @@ -0,0 +1,118 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucan.xyz/schemas/primitives.json", + "title": "UCAN Primitive Types", + "description": "Core primitive types used across all UCAN specifications", + "$defs": { + "DID": { + "title": "Decentralized Identifier", + "description": "A W3C Decentralized Identifier (DID) string", + "type": "string", + "pattern": "^did:[a-z0-9]+:[a-zA-Z0-9._%-]+(:[a-zA-Z0-9._%-]+)*([/?#].*)?$", + "examples": [ + "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp", + "did:web:example.com" + ] + }, + "CID": { + "title": "Content Identifier", + "description": "IPLD CIDv1 with DAG-CBOR codec and SHA-256 multihash (base58btc)", + "type": "object", + "properties": { + "/": { + "type": "string", + "pattern": "^zdpu[a-km-zA-HJ-NP-Z1-9]+$" + } + }, + "required": [ + "/" + ], + "additionalProperties": false + }, + "Bytes": { + "title": "Binary Data", + "description": "Binary data in DAG-JSON format (base64 encoded)", + "type": "object", + "properties": { + "/": { + "type": "object", + "properties": { + "bytes": { + "type": "string", + "contentEncoding": "base64" + } + }, + "required": [ + "bytes" + ], + "additionalProperties": false + } + }, + "required": [ + "/" + ], + "additionalProperties": false + }, + "Timestamp": { + "title": "Unix Timestamp", + "description": "Unix timestamp in seconds (53-bit integer for JS compatibility)", + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "NullableTimestamp": { + "title": "Nullable Timestamp", + "description": "Timestamp or null for non-expiring tokens", + "oneOf": [ + { + "$ref": "#/$defs/Timestamp" + }, + { + "type": "null" + } + ] + }, + "VarsigHeader": { + "title": "Varsig Header", + "description": "Variable signature header with algorithm metadata", + "allOf": [ + { + "$ref": "#/$defs/Bytes" + } + ] + }, + "Signature": { + "title": "Cryptographic Signature", + "description": "Raw signature bytes", + "allOf": [ + { + "$ref": "#/$defs/Bytes" + } + ] + }, + "Command": { + "title": "UCAN Command", + "description": "Slash-delimited path describing an action (lowercase, starts with /)", + "type": "string", + "pattern": "^/([a-z0-9_\\u00C0-\\u024F]+(/[a-z0-9_\\u00C0-\\u024F]+)*)?$", + "examples": [ + "/", + "/crud/create", + "/msg/send", + "/ucan/revoke" + ] + }, + "Metadata": { + "title": "Metadata", + "description": "Arbitrary metadata map", + "type": "object", + "additionalProperties": true + }, + "Arguments": { + "title": "Command Arguments", + "description": "Map of command arguments", + "type": "object", + "additionalProperties": true + } + } +} diff --git a/internal/codec/ucan-schemas.json b/internal/codec/ucan-schemas.json new file mode 100644 index 0000000..e191bc8 --- /dev/null +++ b/internal/codec/ucan-schemas.json @@ -0,0 +1,1160 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucan.xyz/schemas/ucan-complete.json", + "title": "UCAN Complete Schema Definitions", + "description": "Comprehensive JSON Schema definitions for User-Controlled Authorization Network (UCAN) v1.0.0-rc.1 including Tokens, Delegation, and Invocation specifications", + "$defs": { + "DID": { + "$id": "#DID", + "title": "Decentralized Identifier", + "description": "A W3C Decentralized Identifier (DID) string. Must be a valid DID URL.", + "type": "string", + "pattern": "^did:[a-z0-9]+:[a-zA-Z0-9._%-]+(:[a-zA-Z0-9._%-]+)*([/?#].*)?$", + "examples": [ + "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp", + "did:key:zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169", + "did:web:example.com", + "did:plc:abc123" + ] + }, + "CID": { + "$id": "#CID", + "title": "Content Identifier", + "description": "IPLD Content Identifier (CIDv1) with DAG-CBOR codec and SHA-256 multihash. Encoded in base58btc, starting with 'zdpu'.", + "type": "object", + "properties": { + "/": { + "type": "string", + "pattern": "^zdpu[a-km-zA-HJ-NP-Z1-9]+$" + } + }, + "required": [ + "/" + ], + "additionalProperties": false, + "examples": [ + { + "/": "zdpuAzx4sBrBCabrZZqXgvK3NDzh7Mf5mKbG11aBkkMCdLtCp" + } + ] + }, + "Bytes": { + "$id": "#Bytes", + "title": "Binary Data", + "description": "Binary data encoded as base64 in DAG-JSON format", + "type": "object", + "properties": { + "/": { + "type": "object", + "properties": { + "bytes": { + "type": "string", + "contentEncoding": "base64" + } + }, + "required": [ + "bytes" + ], + "additionalProperties": false + } + }, + "required": [ + "/" + ], + "additionalProperties": false, + "examples": [ + { + "/": { + "bytes": "bGlnaHQgd29yay4" + } + }, + { + "/": { + "bytes": "TWFueSBopvcs" + } + } + ] + }, + "Timestamp": { + "$id": "#Timestamp", + "title": "Unix Timestamp", + "description": "Unix timestamp in seconds since epoch (UTC). Must be within 53-bit integer range for JavaScript compatibility.", + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "examples": [ + 1529496683, + 1575606941, + 1697409438 + ] + }, + "NullableTimestamp": { + "$id": "#NullableTimestamp", + "title": "Nullable Unix Timestamp", + "description": "Unix timestamp or null for non-expiring tokens", + "oneOf": [ + { + "$ref": "#/$defs/Timestamp" + }, + { + "type": "null" + } + ] + }, + "VarsigHeader": { + "$id": "#VarsigHeader", + "title": "Varsig Header", + "description": "Variable signature header containing cryptographic algorithm metadata", + "allOf": [ + { + "$ref": "#/$defs/Bytes" + } + ], + "examples": [ + { + "/": { + "bytes": "NBIFEgEAcQ" + } + } + ] + }, + "Signature": { + "$id": "#Signature", + "title": "Cryptographic Signature", + "description": "Raw signature bytes over the signed payload", + "allOf": [ + { + "$ref": "#/$defs/Bytes" + } + ], + "examples": [ + { + "/": { + "bytes": "7aEDQLYvb3lygk9yvAbk0OZD0q+iF9c3+wpZC4YlFThkiNShcVriobPFr/wl3akjM18VvIv/Zw2LtA4uUmB5m8PWEAU" + } + } + ] + }, + "Command": { + "$id": "#Command", + "title": "UCAN Command", + "description": "A slash-delimited path describing the action to perform. Must be lowercase, start with '/', and not have a trailing slash.", + "type": "string", + "pattern": "^/([a-z0-9_\\u00C0-\\u024F]+(/[a-z0-9_\\u00C0-\\u024F]+)*)?$", + "examples": [ + "/", + "/crud", + "/crud/create", + "/crud/read", + "/crud/update", + "/crud/delete", + "/msg/send", + "/msg/receive", + "/ucan/revoke", + "/wasm/run", + "/crypto/sign", + "/blog/post/create" + ] + }, + "Selector": { + "$id": "#Selector", + "title": "Policy Selector", + "description": "jq-inspired selector for navigating IPLD data structures in policy evaluation", + "type": "string", + "pattern": "^\\.(([a-zA-Z_][a-zA-Z0-9_]*)|(\\[[^\\]]+\\])|(\\[\\])|(\\[-?\\d+\\])|(\\[\\d*:\\d*\\]))?((\\.[a-zA-Z_][a-zA-Z0-9_]*)|(\\[[^\\]]+\\])|(\\[\\])|(\\[-?\\d+\\])|(\\[\\d*:\\d*\\]))*\\??$", + "examples": [ + ".", + ".foo", + ".bar.baz", + ".items[0]", + ".items[-1]", + ".data[2:5]", + ".optional?", + "[\"special-key\"]", + ".array[]" + ] + }, + "GlobPattern": { + "$id": "#GlobPattern", + "title": "Glob Pattern", + "description": "Pattern string for 'like' operator. Use * for wildcard, \\* for literal asterisk.", + "type": "string", + "examples": [ + "*@example.com", + "prefix*suffix", + "exact-match", + "escaped\\*asterisk" + ] + }, + "EqualityOperator": { + "$id": "#EqualityOperator", + "title": "Equality Operator", + "type": "string", + "enum": [ + "==", + "!=" + ] + }, + "InequalityOperator": { + "$id": "#InequalityOperator", + "title": "Inequality Operator", + "type": "string", + "enum": [ + ">", + ">=", + "<", + "<=" + ] + }, + "ConnectiveOperator": { + "$id": "#ConnectiveOperator", + "title": "Connective Operator", + "type": "string", + "enum": [ + "and", + "or" + ] + }, + "QuantifierOperator": { + "$id": "#QuantifierOperator", + "title": "Quantifier Operator", + "type": "string", + "enum": [ + "all", + "any" + ] + }, + "EqualityStatement": { + "$id": "#EqualityStatement", + "title": "Equality Statement", + "description": "Deep comparison of selector result against any IPLD value", + "type": "array", + "prefixItems": [ + { + "$ref": "#/$defs/EqualityOperator" + }, + { + "$ref": "#/$defs/Selector" + }, + {} + ], + "minItems": 3, + "maxItems": 3, + "examples": [ + [ + "==", + ".status", + "draft" + ], + [ + "!=", + ".deleted", + true + ], + [ + "==", + ".tags", + [ + "news", + "press" + ] + ] + ] + }, + "InequalityStatement": { + "$id": "#InequalityStatement", + "title": "Inequality Statement", + "description": "Numeric comparison of selector result", + "type": "array", + "prefixItems": [ + { + "$ref": "#/$defs/InequalityOperator" + }, + { + "$ref": "#/$defs/Selector" + }, + { + "type": "number" + } + ], + "minItems": 3, + "maxItems": 3, + "examples": [ + [ + ">", + ".age", + 18 + ], + [ + "<=", + ".price", + 100.50 + ], + [ + ">=", + ".count", + 0 + ] + ] + }, + "LikeStatement": { + "$id": "#LikeStatement", + "title": "Like Statement", + "description": "Glob pattern matching on string values", + "type": "array", + "prefixItems": [ + { + "const": "like" + }, + { + "$ref": "#/$defs/Selector" + }, + { + "$ref": "#/$defs/GlobPattern" + } + ], + "minItems": 3, + "maxItems": 3, + "examples": [ + [ + "like", + ".email", + "*@example.com" + ], + [ + "like", + ".path", + "/users/*" + ] + ] + }, + "NotStatement": { + "$id": "#NotStatement", + "title": "Not Statement", + "description": "Logical negation of a statement", + "type": "array", + "prefixItems": [ + { + "const": "not" + }, + { + "$ref": "#/$defs/PolicyStatement" + } + ], + "minItems": 2, + "maxItems": 2, + "examples": [ + [ + "not", + [ + "==", + ".deleted", + true + ] + ] + ] + }, + "AndStatement": { + "$id": "#AndStatement", + "title": "And Statement", + "description": "Logical AND of multiple statements. Empty array evaluates to true.", + "type": "array", + "prefixItems": [ + { + "const": "and" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/PolicyStatement" + } + } + ], + "minItems": 2, + "maxItems": 2, + "examples": [ + [ + "and", + [ + [ + "==", + ".name", + "Katie" + ], + [ + ">=", + ".age", + 21 + ] + ] + ] + ] + }, + "OrStatement": { + "$id": "#OrStatement", + "title": "Or Statement", + "description": "Logical OR of multiple statements. Empty array evaluates to true.", + "type": "array", + "prefixItems": [ + { + "const": "or" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/PolicyStatement" + } + } + ], + "minItems": 2, + "maxItems": 2, + "examples": [ + [ + "or", + [ + [ + "==", + ".status", + "active" + ], + [ + "==", + ".status", + "pending" + ] + ] + ] + ] + }, + "AllStatement": { + "$id": "#AllStatement", + "title": "All Statement", + "description": "Universal quantifier - statement must hold for all elements in collection", + "type": "array", + "prefixItems": [ + { + "const": "all" + }, + { + "$ref": "#/$defs/Selector" + }, + { + "$ref": "#/$defs/PolicyStatement" + } + ], + "minItems": 3, + "maxItems": 3, + "examples": [ + [ + "all", + ".reviewers", + [ + "like", + ".email", + "*@example.com" + ] + ] + ] + }, + "AnyStatement": { + "$id": "#AnyStatement", + "title": "Any Statement", + "description": "Existential quantifier - statement must hold for at least one element in collection", + "type": "array", + "prefixItems": [ + { + "const": "any" + }, + { + "$ref": "#/$defs/Selector" + }, + { + "$ref": "#/$defs/PolicyStatement" + } + ], + "minItems": 3, + "maxItems": 3, + "examples": [ + [ + "any", + ".tags", + [ + "==", + ".", + "urgent" + ] + ] + ] + }, + "PolicyStatement": { + "$id": "#PolicyStatement", + "title": "Policy Statement", + "description": "A single policy predicate expression", + "oneOf": [ + { + "$ref": "#/$defs/EqualityStatement" + }, + { + "$ref": "#/$defs/InequalityStatement" + }, + { + "$ref": "#/$defs/LikeStatement" + }, + { + "$ref": "#/$defs/NotStatement" + }, + { + "$ref": "#/$defs/AndStatement" + }, + { + "$ref": "#/$defs/OrStatement" + }, + { + "$ref": "#/$defs/AllStatement" + }, + { + "$ref": "#/$defs/AnyStatement" + } + ] + }, + "Policy": { + "$id": "#Policy", + "title": "UCAN Policy", + "description": "Array of policy statements forming an implicit AND conjunction. Constrains the args field of eventual invocations.", + "type": "array", + "items": { + "$ref": "#/$defs/PolicyStatement" + }, + "examples": [ + [], + [ + [ + "==", + ".from", + "alice@example.com" + ] + ], + [ + [ + "==", + ".status", + "draft" + ], + [ + "all", + ".reviewer", + [ + "like", + ".email", + "*@example.com" + ] + ], + [ + "any", + ".tags", + [ + "or", + [ + [ + "==", + ".", + "news" + ], + [ + "==", + ".", + "press" + ] + ] + ] + ] + ] + ] + }, + "Metadata": { + "$id": "#Metadata", + "title": "UCAN Metadata", + "description": "Optional map of arbitrary metadata, facts, and proofs of knowledge. Must be self-evident and externally verifiable.", + "type": "object", + "additionalProperties": true, + "examples": [ + { + "challenges": { + "example.com": "abcdef" + } + }, + { + "env": "development", + "tags": [ + "blog", + "post" + ] + } + ] + }, + "Arguments": { + "$id": "#Arguments", + "title": "Command Arguments", + "description": "Map of arguments for a command. Shape is defined by the command type.", + "type": "object", + "additionalProperties": true, + "examples": [ + { + "from": "mailto:alice@example.com", + "to": [ + "bob@example.com" + ], + "subject": "Hello", + "body": "World" + }, + { + "uri": "https://example.com/resource", + "payload": { + "key": "value" + } + } + ] + }, + "Capability": { + "$id": "#Capability", + "title": "UCAN Capability", + "description": "The semantically-relevant claim of a delegation: subject × command × policy", + "type": "object", + "properties": { + "sub": { + "description": "The Subject DID this capability is about, or null for powerline delegation", + "oneOf": [ + { + "$ref": "#/$defs/DID" + }, + { + "type": "null" + } + ] + }, + "cmd": { + "$ref": "#/$defs/Command", + "description": "The command being delegated" + }, + "pol": { + "$ref": "#/$defs/Policy", + "description": "Constraints on eventual invocation arguments" + } + }, + "required": [ + "sub", + "cmd", + "pol" + ], + "additionalProperties": false, + "examples": [ + { + "sub": "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp", + "cmd": "/crud/read", + "pol": [] + }, + { + "sub": null, + "cmd": "/", + "pol": [] + } + ] + }, + "DelegationPayload": { + "$id": "#DelegationPayload", + "title": "UCAN Delegation Payload", + "description": "The payload of a UCAN Delegation token (ucan/dlg@1.0.0-rc.1)", + "type": "object", + "properties": { + "iss": { + "$ref": "#/$defs/DID", + "description": "Issuer DID - the principal delegating authority" + }, + "aud": { + "$ref": "#/$defs/DID", + "description": "Audience DID - the principal receiving delegated authority" + }, + "sub": { + "description": "Subject DID - the principal the chain is about, or null for powerline", + "oneOf": [ + { + "$ref": "#/$defs/DID" + }, + { + "type": "null" + } + ] + }, + "cmd": { + "$ref": "#/$defs/Command", + "description": "The command being delegated" + }, + "pol": { + "$ref": "#/$defs/Policy", + "description": "Policy constraints on invocation arguments" + }, + "nonce": { + "$ref": "#/$defs/Bytes", + "description": "Random nonce ensuring unique CID" + }, + "meta": { + "$ref": "#/$defs/Metadata", + "description": "Optional arbitrary metadata" + }, + "nbf": { + "$ref": "#/$defs/Timestamp", + "description": "Not-before timestamp (optional)" + }, + "exp": { + "$ref": "#/$defs/NullableTimestamp", + "description": "Expiration timestamp, or null for non-expiring" + } + }, + "required": [ + "iss", + "aud", + "sub", + "cmd", + "pol", + "nonce", + "exp" + ], + "additionalProperties": false + }, + "DelegationSigPayload": { + "$id": "#DelegationSigPayload", + "title": "Delegation Signature Payload", + "description": "The content signed in a UCAN Delegation", + "type": "object", + "properties": { + "h": { + "$ref": "#/$defs/VarsigHeader", + "description": "Varsig v1 header with algorithm metadata" + }, + "ucan/dlg@1.0.0-rc.1": { + "$ref": "#/$defs/DelegationPayload", + "description": "The delegation payload" + } + }, + "required": [ + "h", + "ucan/dlg@1.0.0-rc.1" + ], + "additionalProperties": false + }, + "Delegation": { + "$id": "#Delegation", + "title": "UCAN Delegation", + "description": "Complete UCAN Delegation envelope with signature", + "type": "array", + "prefixItems": [ + { + "$ref": "#/$defs/Signature", + "description": "Signature over the SigPayload by the issuer" + }, + { + "$ref": "#/$defs/DelegationSigPayload", + "description": "The signed content" + } + ], + "minItems": 2, + "maxItems": 2 + }, + "ProofChain": { + "$id": "#ProofChain", + "title": "Proof Chain", + "description": "Ordered array of CID references to Delegations forming the authority chain from Subject to Invoker", + "type": "array", + "items": { + "$ref": "#/$defs/CID" + }, + "examples": [ + [ + { + "/": "zdpuAzx4sBrBCabrZZqXgvK3NDzh7Mf5mKbG11aBkkMCdLtCp" + }, + { + "/": "zdpuApTCXfoKh2sB1KaUaVSGofCBNPUnXoBb6WiCeitXEibZy" + } + ] + ] + }, + "InvocationPayload": { + "$id": "#InvocationPayload", + "title": "UCAN Invocation Payload", + "description": "The payload of a UCAN Invocation token (ucan/inv@1.0.0-rc.1)", + "type": "object", + "properties": { + "iss": { + "$ref": "#/$defs/DID", + "description": "Issuer DID - the invoker requesting execution" + }, + "sub": { + "$ref": "#/$defs/DID", + "description": "Subject DID - the principal being invoked" + }, + "aud": { + "$ref": "#/$defs/DID", + "description": "Optional audience DID if executor differs from subject" + }, + "cmd": { + "$ref": "#/$defs/Command", + "description": "The command to execute" + }, + "args": { + "$ref": "#/$defs/Arguments", + "description": "Command arguments" + }, + "prf": { + "$ref": "#/$defs/ProofChain", + "description": "Proof chain of delegations" + }, + "meta": { + "$ref": "#/$defs/Metadata", + "description": "Optional metadata" + }, + "nonce": { + "$ref": "#/$defs/Bytes", + "description": "Optional nonce for non-idempotent invocations" + }, + "exp": { + "$ref": "#/$defs/NullableTimestamp", + "description": "Expiration timestamp" + }, + "iat": { + "$ref": "#/$defs/Timestamp", + "description": "Optional issuance timestamp" + }, + "cause": { + "$ref": "#/$defs/CID", + "description": "Optional CID of Receipt that enqueued this task" + } + }, + "required": [ + "iss", + "sub", + "cmd", + "args", + "prf", + "exp" + ], + "additionalProperties": false + }, + "InvocationSigPayload": { + "$id": "#InvocationSigPayload", + "title": "Invocation Signature Payload", + "description": "The content signed in a UCAN Invocation", + "type": "object", + "properties": { + "h": { + "$ref": "#/$defs/VarsigHeader", + "description": "Varsig v1 header" + }, + "ucan/inv@1.0.0-rc.1": { + "$ref": "#/$defs/InvocationPayload", + "description": "The invocation payload" + } + }, + "required": [ + "h", + "ucan/inv@1.0.0-rc.1" + ], + "additionalProperties": false + }, + "Invocation": { + "$id": "#Invocation", + "title": "UCAN Invocation", + "description": "Complete UCAN Invocation envelope with signature", + "type": "array", + "prefixItems": [ + { + "$ref": "#/$defs/Signature", + "description": "Signature over the SigPayload by the invoker" + }, + { + "$ref": "#/$defs/InvocationSigPayload", + "description": "The signed content" + } + ], + "minItems": 2, + "maxItems": 2 + }, + "Task": { + "$id": "#Task", + "title": "UCAN Task", + "description": "The subset of Invocation fields uniquely determining work to perform. Task ID is the CID of these fields.", + "type": "object", + "properties": { + "sub": { + "$ref": "#/$defs/DID", + "description": "Subject DID" + }, + "cmd": { + "$ref": "#/$defs/Command", + "description": "Command to execute" + }, + "args": { + "$ref": "#/$defs/Arguments", + "description": "Command arguments" + }, + "nonce": { + "$ref": "#/$defs/Bytes", + "description": "Nonce for uniqueness" + } + }, + "required": [ + "sub", + "cmd", + "args", + "nonce" + ], + "additionalProperties": false + }, + "ReceiptPayload": { + "$id": "#ReceiptPayload", + "title": "UCAN Receipt Payload", + "description": "The payload of a UCAN Receipt (execution result)", + "type": "object", + "properties": { + "iss": { + "$ref": "#/$defs/DID", + "description": "Executor DID that produced this receipt" + }, + "ran": { + "$ref": "#/$defs/CID", + "description": "CID of the Invocation that was executed" + }, + "out": { + "description": "Result of execution - either success or error", + "oneOf": [ + { + "type": "object", + "properties": { + "ok": { + "description": "Success value" + } + }, + "required": [ + "ok" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "err": { + "description": "Error value" + } + }, + "required": [ + "err" + ], + "additionalProperties": false + } + ] + }, + "fx": { + "type": "array", + "items": { + "$ref": "#/$defs/CID" + }, + "description": "Effects - CIDs of Tasks to enqueue" + }, + "meta": { + "$ref": "#/$defs/Metadata", + "description": "Optional metadata" + }, + "iat": { + "$ref": "#/$defs/Timestamp", + "description": "Issuance timestamp" + } + }, + "required": [ + "iss", + "ran", + "out" + ], + "additionalProperties": false + }, + "RevocationPayload": { + "$id": "#RevocationPayload", + "title": "UCAN Revocation Payload", + "description": "Payload for revoking a previously issued delegation", + "type": "object", + "properties": { + "iss": { + "$ref": "#/$defs/DID", + "description": "Revoker DID - must be an issuer in the delegation chain" + }, + "sub": { + "$ref": "#/$defs/DID", + "description": "Subject of the delegation being revoked" + }, + "cmd": { + "const": "/ucan/revoke", + "description": "Revocation command" + }, + "args": { + "type": "object", + "properties": { + "ucan": { + "$ref": "#/$defs/CID", + "description": "CID of the delegation to revoke" + } + }, + "required": [ + "ucan" + ], + "additionalProperties": false + }, + "prf": { + "$ref": "#/$defs/ProofChain", + "description": "Proof chain showing revoker's authority" + }, + "nonce": { + "$ref": "#/$defs/Bytes" + }, + "exp": { + "$ref": "#/$defs/NullableTimestamp" + } + }, + "required": [ + "iss", + "sub", + "cmd", + "args", + "prf", + "nonce", + "exp" + ], + "additionalProperties": false + }, + "UCANEnvelope": { + "$id": "#UCANEnvelope", + "title": "UCAN Envelope", + "description": "Generic UCAN envelope format used by all UCAN token types", + "type": "array", + "prefixItems": [ + { + "$ref": "#/$defs/Signature", + "description": "Signature by payload's issuer over the SigPayload" + }, + { + "type": "object", + "properties": { + "h": { + "$ref": "#/$defs/VarsigHeader", + "description": "Varsig v1 header" + } + }, + "required": [ + "h" + ], + "additionalProperties": true + } + ], + "minItems": 2, + "maxItems": 2 + }, + "CryptoAlgorithm": { + "$id": "#CryptoAlgorithm", + "title": "Supported Cryptographic Algorithm", + "description": "Signature algorithms supported by UCAN", + "type": "string", + "enum": [ + "Ed25519", + "P-256", + "secp256k1" + ], + "default": "Ed25519" + }, + "HashAlgorithm": { + "$id": "#HashAlgorithm", + "title": "Hash Algorithm", + "description": "Hash algorithm for content addressing", + "type": "string", + "enum": [ + "sha2-256" + ], + "default": "sha2-256" + }, + "ValidationError": { + "$id": "#ValidationError", + "title": "Validation Error", + "description": "Error returned when UCAN validation fails", + "type": "object", + "properties": { + "code": { + "type": "string", + "enum": [ + "EXPIRED", + "NOT_YET_VALID", + "INVALID_SIGNATURE", + "PRINCIPAL_MISALIGNMENT", + "POLICY_VIOLATION", + "REVOKED", + "INVALID_PROOF_CHAIN", + "UNKNOWN_COMMAND", + "MALFORMED_TOKEN" + ] + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "code", + "message" + ] + }, + "ValidationResult": { + "$id": "#ValidationResult", + "title": "Validation Result", + "description": "Result of UCAN validation", + "oneOf": [ + { + "type": "object", + "properties": { + "valid": { + "const": true + }, + "capability": { + "$ref": "#/$defs/Capability" + } + }, + "required": [ + "valid", + "capability" + ] + }, + { + "type": "object", + "properties": { + "valid": { + "const": false + }, + "error": { + "$ref": "#/$defs/ValidationError" + } + }, + "required": [ + "valid", + "error" + ] + } + ] + } + }, + "oneOf": [ + { + "$ref": "#/$defs/Delegation" + }, + { + "$ref": "#/$defs/Invocation" + } + ] +} diff --git a/src/ucan.ts b/src/ucan.ts new file mode 100644 index 0000000..02debb2 --- /dev/null +++ b/src/ucan.ts @@ -0,0 +1,592 @@ +/** + * UCAN Complete TypeScript Type Definitions + * User-Controlled Authorization Network v1.0.0-rc.1 + * + * Comprehensive types for Tokens, Delegation, and Invocation specifications + */ + +// ============================================================================= +// PRIMITIVES +// ============================================================================= + +/** + * Decentralized Identifier (DID) string + * @see https://www.w3.org/TR/did-core/ + */ +export type DID = `did:${string}:${string}`; + +/** + * IPLD Content Identifier in DAG-JSON format + * CIDv1 with DAG-CBOR codec and SHA-256 multihash, base58btc encoded + */ +export interface CID { + "/": string; +} + +/** + * Binary data in DAG-JSON format (base64 encoded) + */ +export interface Bytes { + "/": { + bytes: string; + }; +} + +/** + * Unix timestamp in seconds (53-bit integer for JS compatibility) + * Range: -9007199254740991 to 9007199254740991 + */ +export type Timestamp = number; + +/** + * Nullable timestamp - null indicates non-expiring + */ +export type NullableTimestamp = Timestamp | null; + +/** + * Varsig v1 header containing cryptographic algorithm metadata + */ +export type VarsigHeader = Bytes; + +/** + * Raw signature bytes + */ +export type Signature = Bytes; + +// ============================================================================= +// COMMANDS +// ============================================================================= + +/** + * UCAN Command - slash-delimited path describing the action + * Must be lowercase, start with '/', no trailing slash + */ +export type Command = + | "/" + | `/${string}` + | `/crud/${CRUDAction}` + | `/msg/${MessageAction}` + | `/ucan/${UCANAction}` + | `/wasm/${WasmAction}`; + +export type CRUDAction = "create" | "read" | "update" | "delete" | "mutate"; +export type MessageAction = "send" | "receive"; +export type UCANAction = "revoke"; +export type WasmAction = "run"; + +// ============================================================================= +// POLICY LANGUAGE +// ============================================================================= + +/** + * jq-inspired selector for navigating IPLD data + * @example "." | ".foo" | ".bar[0]" | ".items[-1]" | ".optional?" + */ +export type Selector = string; + +/** + * Glob pattern for 'like' operator + * Use * for wildcard, \* for literal asterisk + */ +export type GlobPattern = string; + +// Policy Operators +export type EqualityOperator = "==" | "!="; +export type InequalityOperator = ">" | ">=" | "<" | "<="; +export type ConnectiveOperator = "and" | "or"; +export type QuantifierOperator = "all" | "any"; + +// Policy Statements +export type EqualityStatement = [EqualityOperator, Selector, unknown]; +export type InequalityStatement = [InequalityOperator, Selector, number]; +export type LikeStatement = ["like", Selector, GlobPattern]; +export type NotStatement = ["not", PolicyStatement]; +export type AndStatement = ["and", PolicyStatement[]]; +export type OrStatement = ["or", PolicyStatement[]]; +export type AllStatement = ["all", Selector, PolicyStatement]; +export type AnyStatement = ["any", Selector, PolicyStatement]; + +/** + * Union of all policy statement types + */ +export type PolicyStatement = + | EqualityStatement + | InequalityStatement + | LikeStatement + | NotStatement + | AndStatement + | OrStatement + | AllStatement + | AnyStatement; + +/** + * UCAN Policy - array of statements forming implicit AND + */ +export type Policy = PolicyStatement[]; + +// ============================================================================= +// METADATA & ARGUMENTS +// ============================================================================= + +/** + * Arbitrary metadata map + */ +export type Metadata = Record; + +/** + * Command arguments - shape defined by command type + */ +export type Arguments = Record; + +// ============================================================================= +// CAPABILITY +// ============================================================================= + +/** + * UCAN Capability - the semantically-relevant claim of a delegation + */ +export interface Capability { + /** Subject DID or null for powerline delegation */ + sub: DID | null; + /** Command being delegated */ + cmd: Command; + /** Policy constraints on invocation arguments */ + pol: Policy; +} + +// ============================================================================= +// DELEGATION +// ============================================================================= + +/** + * UCAN Delegation Payload (ucan/dlg@1.0.0-rc.1) + */ +export interface DelegationPayload { + /** Issuer DID - the delegator */ + iss: DID; + /** Audience DID - the delegate */ + aud: DID; + /** Subject DID or null for powerline */ + sub: DID | null; + /** Command being delegated */ + cmd: Command; + /** Policy constraints */ + pol: Policy; + /** Random nonce for unique CID */ + nonce: Bytes; + /** Optional metadata */ + meta?: Metadata; + /** Not-before timestamp (optional) */ + nbf?: Timestamp; + /** Expiration timestamp or null */ + exp: NullableTimestamp; +} + +/** + * Delegation signature payload structure + */ +export interface DelegationSigPayload { + /** Varsig header */ + h: VarsigHeader; + /** Delegation payload with version tag */ + "ucan/dlg@1.0.0-rc.1": DelegationPayload; +} + +/** + * Complete UCAN Delegation envelope + */ +export type Delegation = [Signature, DelegationSigPayload]; + +// ============================================================================= +// INVOCATION +// ============================================================================= + +/** + * Proof chain - ordered CIDs of delegations from Subject to Invoker + */ +export type ProofChain = CID[]; + +/** + * UCAN Invocation Payload (ucan/inv@1.0.0-rc.1) + */ +export interface InvocationPayload { + /** Issuer DID - the invoker */ + iss: DID; + /** Subject DID being invoked */ + sub: DID; + /** Optional audience DID if executor differs from subject */ + aud?: DID; + /** Command to execute */ + cmd: Command; + /** Command arguments */ + args: Arguments; + /** Proof chain of delegations */ + prf: ProofChain; + /** Optional metadata */ + meta?: Metadata; + /** Optional nonce for non-idempotent invocations */ + nonce?: Bytes; + /** Expiration timestamp */ + exp: NullableTimestamp; + /** Optional issuance timestamp */ + iat?: Timestamp; + /** Optional CID of Receipt that enqueued this task */ + cause?: CID; +} + +/** + * Invocation signature payload structure + */ +export interface InvocationSigPayload { + /** Varsig header */ + h: VarsigHeader; + /** Invocation payload with version tag */ + "ucan/inv@1.0.0-rc.1": InvocationPayload; +} + +/** + * Complete UCAN Invocation envelope + */ +export type Invocation = [Signature, InvocationSigPayload]; + +// ============================================================================= +// TASK +// ============================================================================= + +/** + * UCAN Task - subset of Invocation fields uniquely determining work + * Task ID is the CID of these fields + */ +export interface Task { + /** Subject DID */ + sub: DID; + /** Command to execute */ + cmd: Command; + /** Command arguments */ + args: Arguments; + /** Nonce for uniqueness */ + nonce: Bytes; +} + +// ============================================================================= +// RECEIPT +// ============================================================================= + +/** + * Successful execution result + */ +export interface SuccessResult { + ok: T; +} + +/** + * Failed execution result + */ +export interface ErrorResult { + err: E; +} + +/** + * Execution outcome + */ +export type ExecutionResult = SuccessResult | ErrorResult; + +/** + * UCAN Receipt Payload - execution result + */ +export interface ReceiptPayload { + /** Executor DID */ + iss: DID; + /** CID of executed Invocation */ + ran: CID; + /** Execution result */ + out: ExecutionResult; + /** Effects - CIDs of Tasks to enqueue */ + fx?: CID[]; + /** Optional metadata */ + meta?: Metadata; + /** Issuance timestamp */ + iat?: Timestamp; +} + +// ============================================================================= +// REVOCATION +// ============================================================================= + +/** + * Revocation arguments + */ +export interface RevocationArgs { + /** CID of delegation to revoke */ + ucan: CID; +} + +/** + * UCAN Revocation Payload + */ +export interface RevocationPayload { + /** Revoker DID - must be issuer in delegation chain */ + iss: DID; + /** Subject of delegation being revoked */ + sub: DID; + /** Revocation command */ + cmd: "/ucan/revoke"; + /** Revocation arguments */ + args: RevocationArgs; + /** Proof chain */ + prf: ProofChain; + /** Nonce */ + nonce: Bytes; + /** Expiration */ + exp: NullableTimestamp; +} + +// ============================================================================= +// ENVELOPE +// ============================================================================= + +/** + * Generic UCAN Envelope format + */ +export type UCANEnvelope

= [ + Signature, + { + h: VarsigHeader; + } & P +]; + +// ============================================================================= +// CRYPTOGRAPHY +// ============================================================================= + +/** + * Supported signature algorithms + */ +export type CryptoAlgorithm = "Ed25519" | "P-256" | "secp256k1"; + +/** + * Supported hash algorithms + */ +export type HashAlgorithm = "sha2-256"; + +// ============================================================================= +// VALIDATION +// ============================================================================= + +/** + * Validation error codes + */ +export type ValidationErrorCode = + | "EXPIRED" + | "NOT_YET_VALID" + | "INVALID_SIGNATURE" + | "PRINCIPAL_MISALIGNMENT" + | "POLICY_VIOLATION" + | "REVOKED" + | "INVALID_PROOF_CHAIN" + | "UNKNOWN_COMMAND" + | "MALFORMED_TOKEN"; + +/** + * Validation error structure + */ +export interface ValidationError { + code: ValidationErrorCode; + message: string; + details?: Record; +} + +/** + * Successful validation result + */ +export interface ValidationSuccess { + valid: true; + capability: Capability; +} + +/** + * Failed validation result + */ +export interface ValidationFailure { + valid: false; + error: ValidationError; +} + +/** + * Validation result union + */ +export type ValidationResult = ValidationSuccess | ValidationFailure; + +// ============================================================================= +// ROLES +// ============================================================================= + +/** + * UCAN Agent Roles + */ +export interface Roles { + /** General class of entities interacting with UCAN */ + Agent: DID; + /** Principal delegated to in current UCAN (aud field) */ + Audience: DID; + /** Agent that performs invocation action */ + Executor: DID; + /** Principal requesting execution */ + Invoker: DID; + /** Principal of current UCAN (iss field) */ + Issuer: DID; + /** Subject controlling external resource */ + Owner: DID; + /** Agent identified by DID */ + Principal: DID; + /** Issuer in proof chain that revokes */ + Revoker: DID; + /** Principal whose authority is delegated */ + Subject: DID; + /** Agent interpreting UCAN validity */ + Validator: DID; +} + +// ============================================================================= +// COMMON COMMANDS (for convenience) +// ============================================================================= + +/** + * Standard CRUD command arguments + */ +export namespace CRUDCommands { + export interface CreateArgs { + uri: string; + payload: unknown; + headers?: Record; + } + + export interface ReadArgs { + uri: string; + query?: Record; + } + + export interface UpdateArgs { + uri: string; + payload: unknown; + headers?: Record; + } + + export interface DeleteArgs { + uri: string; + } +} + +/** + * Standard messaging command arguments + */ +export namespace MessageCommands { + export interface SendArgs { + from: string; + to: string[]; + subject?: string; + body: string; + cc?: string[]; + bcc?: string[]; + } + + export interface ReceiveArgs { + mailbox: string; + since?: Timestamp; + limit?: number; + } +} + +/** + * WebAssembly execution command arguments + */ +export namespace WasmCommands { + export interface RunArgs { + /** Wasm module as data URI or CID */ + mod: string | CID; + /** Function to execute */ + fun: string; + /** Function parameters */ + params: unknown[]; + } +} + +// ============================================================================= +// TYPE GUARDS +// ============================================================================= + +export function isDID(value: unknown): value is DID { + return typeof value === "string" && value.startsWith("did:"); +} + +export function isCID(value: unknown): value is CID { + return ( + typeof value === "object" && + value !== null && + "/" in value && + typeof (value as CID)["/"] === "string" + ); +} + +export function isBytes(value: unknown): value is Bytes { + return ( + typeof value === "object" && + value !== null && + "/" in value && + typeof (value as Bytes)["/"] === "object" && + "bytes" in (value as Bytes)["/"] + ); +} + +export function isSuccessResult(result: ExecutionResult): result is SuccessResult { + return "ok" in result; +} + +export function isErrorResult(result: ExecutionResult): result is ErrorResult { + return "err" in result; +} + +export function isDelegation(token: Delegation | Invocation): token is Delegation { + return "ucan/dlg@1.0.0-rc.1" in token[1]; +} + +export function isInvocation(token: Delegation | Invocation): token is Invocation { + return "ucan/inv@1.0.0-rc.1" in token[1]; +} + +// ============================================================================= +// UTILITY TYPES +// ============================================================================= + +/** + * Extract the payload type from a UCAN envelope + */ +export type ExtractPayload> = + E extends UCANEnvelope ? P : never; + +/** + * Create a typed invocation for a specific command + */ +export type TypedInvocation = [ + Signature, + { + h: VarsigHeader; + "ucan/inv@1.0.0-rc.1": Omit & { + cmd: C; + args: A; + }; + } +]; + +/** + * Create a typed delegation for a specific command + */ +export type TypedDelegation = [ + Signature, + { + h: VarsigHeader; + "ucan/dlg@1.0.0-rc.1": Omit & { + cmd: C; + }; + } +]; -- 2.43.0 From 46b072668255b5fbf7805c60a060dd4b56dde678 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Thu, 8 Jan 2026 15:21:03 -0500 Subject: [PATCH 07/35] refactor(ucan): migrate away from MPC-based signing in favor of @sonr.io/crypto/mpc --- go.mod | 20 +- go.sum | 69 +- internal/crypto/mpc/spec/jwt.go | 116 ---- internal/crypto/mpc/spec/source.go | 305 --------- internal/crypto/mpc/spec/ucan.go | 125 ---- internal/crypto/ucan/capability.go | 860 ------------------------- internal/crypto/ucan/crypto.go | 352 ----------- internal/crypto/ucan/jwt.go | 595 ----------------- internal/crypto/ucan/mpc.go | 625 ------------------ internal/crypto/ucan/source.go | 302 --------- internal/crypto/ucan/stubs.go | 87 --- internal/crypto/ucan/ucan_test.go | 313 --------- internal/crypto/ucan/vault.go | 485 -------------- internal/crypto/ucan/verifier.go | 984 ----------------------------- 14 files changed, 53 insertions(+), 5185 deletions(-) delete mode 100644 internal/crypto/mpc/spec/jwt.go delete mode 100644 internal/crypto/mpc/spec/source.go delete mode 100644 internal/crypto/mpc/spec/ucan.go delete mode 100644 internal/crypto/ucan/capability.go delete mode 100644 internal/crypto/ucan/crypto.go delete mode 100644 internal/crypto/ucan/jwt.go delete mode 100644 internal/crypto/ucan/mpc.go delete mode 100644 internal/crypto/ucan/source.go delete mode 100644 internal/crypto/ucan/stubs.go delete mode 100644 internal/crypto/ucan/ucan_test.go delete mode 100644 internal/crypto/ucan/vault.go delete mode 100644 internal/crypto/ucan/verifier.go diff --git a/go.mod b/go.mod index b029fcd..febadd3 100644 --- a/go.mod +++ b/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 ) diff --git a/go.sum b/go.sum index 541838d..2dd919f 100644 --- a/go.sum +++ b/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= diff --git a/internal/crypto/mpc/spec/jwt.go b/internal/crypto/mpc/spec/jwt.go deleted file mode 100644 index 7b82bb8..0000000 --- a/internal/crypto/mpc/spec/jwt.go +++ /dev/null @@ -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, - } - }) -} diff --git a/internal/crypto/mpc/spec/source.go b/internal/crypto/mpc/spec/source.go deleted file mode 100644 index 647e991..0000000 --- a/internal/crypto/mpc/spec/source.go +++ /dev/null @@ -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 -} diff --git a/internal/crypto/mpc/spec/ucan.go b/internal/crypto/mpc/spec/ucan.go deleted file mode 100644 index 01d9463..0000000 --- a/internal/crypto/mpc/spec/ucan.go +++ /dev/null @@ -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 -} diff --git a/internal/crypto/ucan/capability.go b/internal/crypto/ucan/capability.go deleted file mode 100644 index 3833e04..0000000 --- a/internal/crypto/ucan/capability.go +++ /dev/null @@ -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 -} diff --git a/internal/crypto/ucan/crypto.go b/internal/crypto/ucan/crypto.go deleted file mode 100644 index f69b3b3..0000000 --- a/internal/crypto/ucan/crypto.go +++ /dev/null @@ -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 -} diff --git a/internal/crypto/ucan/jwt.go b/internal/crypto/ucan/jwt.go deleted file mode 100644 index 95ce860..0000000 --- a/internal/crypto/ucan/jwt.go +++ /dev/null @@ -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) -} diff --git a/internal/crypto/ucan/mpc.go b/internal/crypto/ucan/mpc.go deleted file mode 100644 index 3d5a8b6..0000000 --- a/internal/crypto/ucan/mpc.go +++ /dev/null @@ -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 -} diff --git a/internal/crypto/ucan/source.go b/internal/crypto/ucan/source.go deleted file mode 100644 index ee9fbff..0000000 --- a/internal/crypto/ucan/source.go +++ /dev/null @@ -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 diff --git a/internal/crypto/ucan/stubs.go b/internal/crypto/ucan/stubs.go deleted file mode 100644 index c50600d..0000000 --- a/internal/crypto/ucan/stubs.go +++ /dev/null @@ -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 -} diff --git a/internal/crypto/ucan/ucan_test.go b/internal/crypto/ucan/ucan_test.go deleted file mode 100644 index 3a38d4f..0000000 --- a/internal/crypto/ucan/ucan_test.go +++ /dev/null @@ -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() -} diff --git a/internal/crypto/ucan/vault.go b/internal/crypto/ucan/vault.go deleted file mode 100644 index 579f533..0000000 --- a/internal/crypto/ucan/vault.go +++ /dev/null @@ -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") -} diff --git a/internal/crypto/ucan/verifier.go b/internal/crypto/ucan/verifier.go deleted file mode 100644 index a8a065c..0000000 --- a/internal/crypto/ucan/verifier.go +++ /dev/null @@ -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) -} -- 2.43.0 From ec87d579aa3da30dd67609ee682ac95410e5c7a3 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Thu, 8 Jan 2026 15:21:07 -0500 Subject: [PATCH 08/35] feat(ucan): add delegation and invocation builders --- internal/crypto/ucan/delegation.go | 271 +++++++++++++++++++++++++++++ internal/crypto/ucan/invocation.go | 259 +++++++++++++++++++++++++++ internal/crypto/ucan/policy.go | 213 +++++++++++++++++++++++ internal/crypto/ucan/types.go | 261 +++++++++++++++++++++++++++ internal/crypto/ucan/ucan.go | 195 +++++++++++++++++++++ 5 files changed, 1199 insertions(+) create mode 100644 internal/crypto/ucan/delegation.go create mode 100644 internal/crypto/ucan/invocation.go create mode 100644 internal/crypto/ucan/policy.go create mode 100644 internal/crypto/ucan/types.go create mode 100644 internal/crypto/ucan/ucan.go diff --git a/internal/crypto/ucan/delegation.go b/internal/crypto/ucan/delegation.go new file mode 100644 index 0000000..100162d --- /dev/null +++ b/internal/crypto/ucan/delegation.go @@ -0,0 +1,271 @@ +package ucan + +import ( + "fmt" + "time" + + "github.com/MetaMask/go-did-it" + "github.com/MetaMask/go-did-it/crypto" + "github.com/ipfs/go-cid" + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/pkg/policy" + "github.com/ucan-wg/go-ucan/token/delegation" +) + +// DelegationBuilder provides a fluent API for creating UCAN delegations. +// A delegation grants authority from issuer to audience to perform a command on a subject. +type DelegationBuilder struct { + issuer did.DID + audience did.DID + subject did.DID + cmd command.Command + pol policy.Policy + opts []delegation.Option + err error + isPowerline bool +} + +// NewDelegationBuilder creates a builder for constructing UCAN delegations. +func NewDelegationBuilder() *DelegationBuilder { + return &DelegationBuilder{ + pol: policy.Policy{}, + opts: make([]delegation.Option, 0), + } +} + +// Issuer sets the delegation issuer (the entity granting authority). +func (b *DelegationBuilder) Issuer(iss did.DID) *DelegationBuilder { + b.issuer = iss + return b +} + +// IssuerString parses and sets the issuer from a DID string. +func (b *DelegationBuilder) IssuerString(issStr string) *DelegationBuilder { + iss, err := did.Parse(issStr) + if err != nil { + b.err = fmt.Errorf("invalid issuer DID: %w", err) + return b + } + b.issuer = iss + return b +} + +// Audience sets the delegation audience (the entity receiving authority). +func (b *DelegationBuilder) Audience(aud did.DID) *DelegationBuilder { + b.audience = aud + return b +} + +// AudienceString parses and sets the audience from a DID string. +func (b *DelegationBuilder) AudienceString(audStr string) *DelegationBuilder { + aud, err := did.Parse(audStr) + if err != nil { + b.err = fmt.Errorf("invalid audience DID: %w", err) + return b + } + b.audience = aud + return b +} + +// Subject sets the delegation subject (the resource being delegated). +// For root delegations, subject should equal issuer. +// For powerline delegations, use Powerline() instead. +func (b *DelegationBuilder) Subject(sub did.DID) *DelegationBuilder { + b.subject = sub + b.isPowerline = false + return b +} + +// SubjectString parses and sets the subject from a DID string. +func (b *DelegationBuilder) SubjectString(subStr string) *DelegationBuilder { + sub, err := did.Parse(subStr) + if err != nil { + b.err = fmt.Errorf("invalid subject DID: %w", err) + return b + } + b.subject = sub + b.isPowerline = false + return b +} + +// Powerline creates a powerline delegation (subject = nil). +// Powerline automatically delegates all future delegations regardless of subject. +// Use with caution - this is a very powerful pattern. +func (b *DelegationBuilder) Powerline() *DelegationBuilder { + b.subject = nil + b.isPowerline = true + return b +} + +// AsRoot sets the subject equal to the issuer (root delegation). +// Root delegations are typically used to create the initial grant of authority. +func (b *DelegationBuilder) AsRoot() *DelegationBuilder { + b.subject = b.issuer + b.isPowerline = false + return b +} + +// Command sets the command being delegated. +func (b *DelegationBuilder) Command(cmd command.Command) *DelegationBuilder { + b.cmd = cmd + return b +} + +// CommandString parses and sets the command from a string. +func (b *DelegationBuilder) CommandString(cmdStr string) *DelegationBuilder { + cmd, err := command.Parse(cmdStr) + if err != nil { + b.err = fmt.Errorf("invalid command: %w", err) + return b + } + b.cmd = cmd + return b +} + +// Policy sets the policy constraints on the delegation. +func (b *DelegationBuilder) Policy(pol policy.Policy) *DelegationBuilder { + b.pol = pol + return b +} + +// ExpiresAt sets when the delegation expires. +func (b *DelegationBuilder) ExpiresAt(exp time.Time) *DelegationBuilder { + b.opts = append(b.opts, delegation.WithExpiration(exp)) + return b +} + +// ExpiresIn sets the delegation to expire after a duration from now. +func (b *DelegationBuilder) ExpiresIn(d time.Duration) *DelegationBuilder { + b.opts = append(b.opts, delegation.WithExpirationIn(d)) + return b +} + +// NotBefore sets when the delegation becomes valid. +func (b *DelegationBuilder) NotBefore(nbf time.Time) *DelegationBuilder { + b.opts = append(b.opts, delegation.WithNotBefore(nbf)) + return b +} + +// NotBeforeIn sets the delegation to become valid after a duration from now. +func (b *DelegationBuilder) NotBeforeIn(d time.Duration) *DelegationBuilder { + b.opts = append(b.opts, delegation.WithNotBeforeIn(d)) + return b +} + +// Meta adds metadata to the delegation. +func (b *DelegationBuilder) Meta(key string, value any) *DelegationBuilder { + b.opts = append(b.opts, delegation.WithMeta(key, value)) + return b +} + +// Nonce sets a custom nonce. If not called, a random 12-byte nonce is generated. +func (b *DelegationBuilder) Nonce(nonce []byte) *DelegationBuilder { + b.opts = append(b.opts, delegation.WithNonce(nonce)) + return b +} + +// Build creates the delegation token. +func (b *DelegationBuilder) Build() (*delegation.Token, error) { + if b.err != nil { + return nil, b.err + } + + if b.issuer == nil { + return nil, fmt.Errorf("issuer is required") + } + if b.audience == nil { + return nil, fmt.Errorf("audience is required") + } + if b.cmd == "" { + return nil, fmt.Errorf("command is required") + } + + if b.isPowerline { + return delegation.Powerline(b.issuer, b.audience, b.cmd, b.pol, b.opts...) + } + + // If subject not set and not powerline, default to root delegation + if b.subject == nil { + b.subject = b.issuer + } + + return delegation.New(b.issuer, b.audience, b.cmd, b.pol, b.subject, b.opts...) +} + +// BuildSealed creates and signs the delegation, returning DAG-CBOR bytes and CID. +func (b *DelegationBuilder) BuildSealed(privKey crypto.PrivateKeySigningBytes) ([]byte, cid.Cid, error) { + tkn, err := b.Build() + if err != nil { + return nil, cid.Cid{}, err + } + return tkn.ToSealed(privKey) +} + +// --- Sonr-specific Delegation Builders --- + +// NewVaultDelegation creates a delegation for vault operations. +// cmd should be one of: VaultRead, VaultWrite, VaultSign, VaultExport, VaultImport, VaultDelete, VaultAdmin, or Vault (all). +func NewVaultDelegation(issuer, audience did.DID, cmd command.Command, vaultCID string, opts ...delegation.Option) (*delegation.Token, error) { + pol := VaultPolicy(vaultCID) + return delegation.Root(issuer, audience, cmd, pol, opts...) +} + +// NewVaultReadDelegation creates a delegation to read from a specific vault. +func NewVaultReadDelegation(issuer, audience did.DID, vaultCID string, opts ...delegation.Option) (*delegation.Token, error) { + return NewVaultDelegation(issuer, audience, VaultRead, vaultCID, opts...) +} + +// NewVaultWriteDelegation creates a delegation to write to a specific vault. +func NewVaultWriteDelegation(issuer, audience did.DID, vaultCID string, opts ...delegation.Option) (*delegation.Token, error) { + return NewVaultDelegation(issuer, audience, VaultWrite, vaultCID, opts...) +} + +// NewVaultSignDelegation creates a delegation to sign with keys in a specific vault. +func NewVaultSignDelegation(issuer, audience did.DID, vaultCID string, opts ...delegation.Option) (*delegation.Token, error) { + return NewVaultDelegation(issuer, audience, VaultSign, vaultCID, opts...) +} + +// NewVaultFullAccessDelegation creates a delegation for all vault operations on a specific vault. +func NewVaultFullAccessDelegation(issuer, audience did.DID, vaultCID string, opts ...delegation.Option) (*delegation.Token, error) { + return NewVaultDelegation(issuer, audience, Vault, vaultCID, opts...) +} + +// NewDIDDelegation creates a delegation for DID operations. +// cmd should be one of: DIDCreate, DIDUpdate, DIDDeactivate, or DID (all). +func NewDIDDelegation(issuer, audience did.DID, cmd command.Command, didPattern string, opts ...delegation.Option) (*delegation.Token, error) { + pol := DIDPolicy(didPattern) + return delegation.Root(issuer, audience, cmd, pol, opts...) +} + +// NewDIDUpdateDelegation creates a delegation to update DIDs matching a pattern. +func NewDIDUpdateDelegation(issuer, audience did.DID, didPattern string, opts ...delegation.Option) (*delegation.Token, error) { + return NewDIDDelegation(issuer, audience, DIDUpdate, didPattern, opts...) +} + +// NewDIDFullAccessDelegation creates a delegation for all DID operations on DIDs matching a pattern. +func NewDIDFullAccessDelegation(issuer, audience did.DID, didPattern string, opts ...delegation.Option) (*delegation.Token, error) { + return NewDIDDelegation(issuer, audience, DID, didPattern, opts...) +} + +// NewDWNDelegation creates a delegation for DWN (Decentralized Web Node) operations. +// cmd should be one of: DWNRecordsWrite, DWNRecordsRead, DWNRecordsDelete, or DWN (all). +func NewDWNDelegation(issuer, audience did.DID, cmd command.Command, recordType string, opts ...delegation.Option) (*delegation.Token, error) { + pol := RecordTypePolicy(recordType) + return delegation.Root(issuer, audience, cmd, pol, opts...) +} + +// NewDWNReadDelegation creates a delegation to read DWN records of a specific type. +func NewDWNReadDelegation(issuer, audience did.DID, recordType string, opts ...delegation.Option) (*delegation.Token, error) { + return NewDWNDelegation(issuer, audience, DWNRecordsRead, recordType, opts...) +} + +// NewDWNWriteDelegation creates a delegation to write DWN records of a specific type. +func NewDWNWriteDelegation(issuer, audience did.DID, recordType string, opts ...delegation.Option) (*delegation.Token, error) { + return NewDWNDelegation(issuer, audience, DWNRecordsWrite, recordType, opts...) +} + +// NewRootCapabilityDelegation creates a root delegation granting all capabilities. +// Use with extreme caution - this grants full authority. +func NewRootCapabilityDelegation(issuer, audience did.DID, opts ...delegation.Option) (*delegation.Token, error) { + return delegation.Root(issuer, audience, Root, EmptyPolicy(), opts...) +} diff --git a/internal/crypto/ucan/invocation.go b/internal/crypto/ucan/invocation.go new file mode 100644 index 0000000..fa0018c --- /dev/null +++ b/internal/crypto/ucan/invocation.go @@ -0,0 +1,259 @@ +package ucan + +import ( + "fmt" + "time" + + "github.com/MetaMask/go-did-it" + "github.com/MetaMask/go-did-it/crypto" + "github.com/ipfs/go-cid" + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/token/invocation" +) + +// InvocationBuilder provides a fluent API for creating UCAN invocations. +// An invocation exercises a delegated capability to perform an action. +type InvocationBuilder struct { + issuer did.DID + subject did.DID + cmd command.Command + proofs []cid.Cid + opts []invocation.Option + err error +} + +// NewInvocationBuilder creates a builder for constructing UCAN invocations. +func NewInvocationBuilder() *InvocationBuilder { + return &InvocationBuilder{ + proofs: make([]cid.Cid, 0), + opts: make([]invocation.Option, 0), + } +} + +// Issuer sets the invocation issuer (the entity invoking the capability). +func (b *InvocationBuilder) Issuer(iss did.DID) *InvocationBuilder { + b.issuer = iss + return b +} + +// IssuerString parses and sets the issuer from a DID string. +func (b *InvocationBuilder) IssuerString(issStr string) *InvocationBuilder { + iss, err := did.Parse(issStr) + if err != nil { + b.err = fmt.Errorf("invalid issuer DID: %w", err) + return b + } + b.issuer = iss + return b +} + +// Subject sets the invocation subject (the resource being acted upon). +func (b *InvocationBuilder) Subject(sub did.DID) *InvocationBuilder { + b.subject = sub + return b +} + +// SubjectString parses and sets the subject from a DID string. +func (b *InvocationBuilder) SubjectString(subStr string) *InvocationBuilder { + sub, err := did.Parse(subStr) + if err != nil { + b.err = fmt.Errorf("invalid subject DID: %w", err) + return b + } + b.subject = sub + return b +} + +// Command sets the command being invoked. +func (b *InvocationBuilder) Command(cmd command.Command) *InvocationBuilder { + b.cmd = cmd + return b +} + +// CommandString parses and sets the command from a string. +func (b *InvocationBuilder) CommandString(cmdStr string) *InvocationBuilder { + cmd, err := command.Parse(cmdStr) + if err != nil { + b.err = fmt.Errorf("invalid command: %w", err) + return b + } + b.cmd = cmd + return b +} + +// Proof adds a delegation CID to the proof chain. +// Proofs should be ordered from leaf (matching invocation's issuer as audience) +// to root delegation. +func (b *InvocationBuilder) Proof(delegationCID cid.Cid) *InvocationBuilder { + b.proofs = append(b.proofs, delegationCID) + return b +} + +// ProofString parses and adds a delegation CID from a string. +func (b *InvocationBuilder) ProofString(cidStr string) *InvocationBuilder { + c, err := cid.Parse(cidStr) + if err != nil { + b.err = fmt.Errorf("invalid proof CID: %w", err) + return b + } + b.proofs = append(b.proofs, c) + return b +} + +// Proofs sets all proofs at once, replacing any previously added. +func (b *InvocationBuilder) Proofs(cids []cid.Cid) *InvocationBuilder { + b.proofs = cids + return b +} + +// Arg adds an argument to the invocation. +func (b *InvocationBuilder) Arg(key string, value any) *InvocationBuilder { + b.opts = append(b.opts, invocation.WithArgument(key, value)) + return b +} + +// Audience sets the invocation's audience (executor if different from subject). +func (b *InvocationBuilder) Audience(aud did.DID) *InvocationBuilder { + b.opts = append(b.opts, invocation.WithAudience(aud)) + return b +} + +// AudienceString parses and sets the audience from a DID string. +func (b *InvocationBuilder) AudienceString(audStr string) *InvocationBuilder { + aud, err := did.Parse(audStr) + if err != nil { + b.err = fmt.Errorf("invalid audience DID: %w", err) + return b + } + b.opts = append(b.opts, invocation.WithAudience(aud)) + return b +} + +// ExpiresAt sets when the invocation expires. +func (b *InvocationBuilder) ExpiresAt(exp time.Time) *InvocationBuilder { + b.opts = append(b.opts, invocation.WithExpiration(exp)) + return b +} + +// ExpiresIn sets the invocation to expire after a duration from now. +func (b *InvocationBuilder) ExpiresIn(d time.Duration) *InvocationBuilder { + b.opts = append(b.opts, invocation.WithExpirationIn(d)) + return b +} + +// Meta adds metadata to the invocation. +func (b *InvocationBuilder) Meta(key string, value any) *InvocationBuilder { + b.opts = append(b.opts, invocation.WithMeta(key, value)) + return b +} + +// Nonce sets a custom nonce. If not called, a random 12-byte nonce is generated. +func (b *InvocationBuilder) Nonce(nonce []byte) *InvocationBuilder { + b.opts = append(b.opts, invocation.WithNonce(nonce)) + return b +} + +// EmptyNonce sets an empty nonce for idempotent operations. +func (b *InvocationBuilder) EmptyNonce() *InvocationBuilder { + b.opts = append(b.opts, invocation.WithEmptyNonce()) + return b +} + +// Cause sets the receipt CID that enqueued this invocation. +func (b *InvocationBuilder) Cause(receiptCID cid.Cid) *InvocationBuilder { + b.opts = append(b.opts, invocation.WithCause(&receiptCID)) + return b +} + +// Build creates the invocation token. +func (b *InvocationBuilder) Build() (*invocation.Token, error) { + if b.err != nil { + return nil, b.err + } + + if b.issuer == nil { + return nil, fmt.Errorf("issuer is required") + } + if b.subject == nil { + return nil, fmt.Errorf("subject is required") + } + if b.cmd == "" { + return nil, fmt.Errorf("command is required") + } + + return invocation.New(b.issuer, b.cmd, b.subject, b.proofs, b.opts...) +} + +// BuildSealed creates and signs the invocation, returning DAG-CBOR bytes and CID. +func (b *InvocationBuilder) BuildSealed(privKey crypto.PrivateKeySigningBytes) ([]byte, cid.Cid, error) { + tkn, err := b.Build() + if err != nil { + return nil, cid.Cid{}, err + } + return tkn.ToSealed(privKey) +} + +// --- Sonr-specific Invocation Builders --- + +// VaultReadInvocation creates an invocation to read from a vault. +func VaultReadInvocation(issuer, subject did.DID, proofs []cid.Cid, vaultCID string, opts ...invocation.Option) (*invocation.Token, error) { + allOpts := append([]invocation.Option{invocation.WithArgument("vault", vaultCID)}, opts...) + return invocation.New(issuer, VaultRead, subject, proofs, allOpts...) +} + +// VaultWriteInvocation creates an invocation to write to a vault. +func VaultWriteInvocation(issuer, subject did.DID, proofs []cid.Cid, vaultCID string, data any, opts ...invocation.Option) (*invocation.Token, error) { + allOpts := append([]invocation.Option{ + invocation.WithArgument("vault", vaultCID), + invocation.WithArgument("data", data), + }, opts...) + return invocation.New(issuer, VaultWrite, subject, proofs, allOpts...) +} + +// VaultSignInvocation creates an invocation to sign with a key in a vault. +func VaultSignInvocation(issuer, subject did.DID, proofs []cid.Cid, vaultCID string, keyID string, payload []byte, opts ...invocation.Option) (*invocation.Token, error) { + allOpts := append([]invocation.Option{ + invocation.WithArgument("vault", vaultCID), + invocation.WithArgument("key_id", keyID), + invocation.WithArgument("payload", payload), + }, opts...) + return invocation.New(issuer, VaultSign, subject, proofs, allOpts...) +} + +// DIDUpdateInvocation creates an invocation to update a DID document. +func DIDUpdateInvocation(issuer, subject did.DID, proofs []cid.Cid, targetDID string, updates map[string]any, opts ...invocation.Option) (*invocation.Token, error) { + allOpts := append([]invocation.Option{ + invocation.WithArgument("did", targetDID), + invocation.WithArgument("updates", updates), + }, opts...) + return invocation.New(issuer, DIDUpdate, subject, proofs, allOpts...) +} + +// DWNRecordsWriteInvocation creates an invocation to write a DWN record. +func DWNRecordsWriteInvocation(issuer, subject did.DID, proofs []cid.Cid, recordType string, data any, opts ...invocation.Option) (*invocation.Token, error) { + allOpts := append([]invocation.Option{ + invocation.WithArgument("record_type", recordType), + invocation.WithArgument("data", data), + }, opts...) + return invocation.New(issuer, DWNRecordsWrite, subject, proofs, allOpts...) +} + +// DWNRecordsReadInvocation creates an invocation to read DWN records. +func DWNRecordsReadInvocation(issuer, subject did.DID, proofs []cid.Cid, recordType string, filter map[string]any, opts ...invocation.Option) (*invocation.Token, error) { + allOpts := []invocation.Option{ + invocation.WithArgument("record_type", recordType), + } + if filter != nil { + allOpts = append(allOpts, invocation.WithArgument("filter", filter)) + } + allOpts = append(allOpts, opts...) + return invocation.New(issuer, DWNRecordsRead, subject, proofs, allOpts...) +} + +// RevocationInvocation creates an invocation to revoke a delegation. +func RevocationInvocation(issuer, subject did.DID, proofs []cid.Cid, delegationCID cid.Cid, opts ...invocation.Option) (*invocation.Token, error) { + allOpts := append([]invocation.Option{ + invocation.WithArgument("ucan", delegationCID), + }, opts...) + return invocation.New(issuer, UCANRevoke, subject, proofs, allOpts...) +} diff --git a/internal/crypto/ucan/policy.go b/internal/crypto/ucan/policy.go new file mode 100644 index 0000000..5b7092e --- /dev/null +++ b/internal/crypto/ucan/policy.go @@ -0,0 +1,213 @@ +package ucan + +import ( + "github.com/ipld/go-ipld-prime" + "github.com/ucan-wg/go-ucan/pkg/policy" + "github.com/ucan-wg/go-ucan/pkg/policy/literal" +) + +// PolicyBuilder provides a fluent API for constructing UCAN policies. +// Policies are arrays of statements that form an implicit AND. +type PolicyBuilder struct { + constructors []policy.Constructor +} + +// NewPolicy creates a new PolicyBuilder. +func NewPolicy() *PolicyBuilder { + return &PolicyBuilder{ + constructors: make([]policy.Constructor, 0), + } +} + +// Build constructs the final Policy from all added statements. +func (b *PolicyBuilder) Build() (Policy, error) { + return policy.Construct(b.constructors...) +} + +// MustBuild constructs the final Policy, panicking on error. +func (b *PolicyBuilder) MustBuild() Policy { + return policy.MustConstruct(b.constructors...) +} + +// Equal adds an equality constraint: selector == value +func (b *PolicyBuilder) Equal(selector string, value any) *PolicyBuilder { + node, err := toIPLDNode(value) + if err != nil { + // Store error for Build() to return + b.constructors = append(b.constructors, func() (policy.Statement, error) { + return nil, err + }) + return b + } + b.constructors = append(b.constructors, policy.Equal(selector, node)) + return b +} + +// NotEqual adds an inequality constraint: selector != value +func (b *PolicyBuilder) NotEqual(selector string, value any) *PolicyBuilder { + node, err := toIPLDNode(value) + if err != nil { + b.constructors = append(b.constructors, func() (policy.Statement, error) { + return nil, err + }) + return b + } + b.constructors = append(b.constructors, policy.NotEqual(selector, node)) + return b +} + +// GreaterThan adds a comparison constraint: selector > value +func (b *PolicyBuilder) GreaterThan(selector string, value int64) *PolicyBuilder { + b.constructors = append(b.constructors, policy.GreaterThan(selector, literal.Int(value))) + return b +} + +// GreaterThanOrEqual adds a comparison constraint: selector >= value +func (b *PolicyBuilder) GreaterThanOrEqual(selector string, value int64) *PolicyBuilder { + b.constructors = append(b.constructors, policy.GreaterThanOrEqual(selector, literal.Int(value))) + return b +} + +// LessThan adds a comparison constraint: selector < value +func (b *PolicyBuilder) LessThan(selector string, value int64) *PolicyBuilder { + b.constructors = append(b.constructors, policy.LessThan(selector, literal.Int(value))) + return b +} + +// LessThanOrEqual adds a comparison constraint: selector <= value +func (b *PolicyBuilder) LessThanOrEqual(selector string, value int64) *PolicyBuilder { + b.constructors = append(b.constructors, policy.LessThanOrEqual(selector, literal.Int(value))) + return b +} + +// Like adds a glob pattern constraint: selector matches pattern +// Use * for wildcard, \* for literal asterisk. +func (b *PolicyBuilder) Like(selector, pattern string) *PolicyBuilder { + b.constructors = append(b.constructors, policy.Like(selector, pattern)) + return b +} + +// Not negates a statement +func (b *PolicyBuilder) Not(stmt policy.Constructor) *PolicyBuilder { + b.constructors = append(b.constructors, policy.Not(stmt)) + return b +} + +// And adds a logical AND of multiple statements +func (b *PolicyBuilder) And(stmts ...policy.Constructor) *PolicyBuilder { + b.constructors = append(b.constructors, policy.And(stmts...)) + return b +} + +// Or adds a logical OR of multiple statements +func (b *PolicyBuilder) Or(stmts ...policy.Constructor) *PolicyBuilder { + b.constructors = append(b.constructors, policy.Or(stmts...)) + return b +} + +// All adds a universal quantifier: all elements at selector must satisfy statement +func (b *PolicyBuilder) All(selector string, stmt policy.Constructor) *PolicyBuilder { + b.constructors = append(b.constructors, policy.All(selector, stmt)) + return b +} + +// Any adds an existential quantifier: at least one element at selector must satisfy statement +func (b *PolicyBuilder) Any(selector string, stmt policy.Constructor) *PolicyBuilder { + b.constructors = append(b.constructors, policy.Any(selector, stmt)) + return b +} + +// toIPLDNode converts a Go value to an IPLD node for policy evaluation. +func toIPLDNode(value any) (ipld.Node, error) { + // Handle IPLD nodes directly + if node, ok := value.(ipld.Node); ok { + return node, nil + } + + // Use literal package for conversion + return literal.Any(value) +} + +// --- Sonr-specific Policy Helpers --- + +// VaultPolicy creates a policy that restricts operations to a specific vault. +// The vault is identified by its CID. +func VaultPolicy(vaultCID string) Policy { + return NewPolicy().Equal(".vault", vaultCID).MustBuild() +} + +// DIDPolicy creates a policy that restricts operations to DIDs matching a pattern. +// Use glob patterns: "did:sonr:*" matches all Sonr DIDs. +func DIDPolicy(didPattern string) Policy { + return NewPolicy().Like(".did", didPattern).MustBuild() +} + +// ChainPolicy creates a policy that restricts operations to a specific chain. +func ChainPolicy(chainID string) Policy { + return NewPolicy().Equal(".chain_id", chainID).MustBuild() +} + +// AccountPolicy creates a policy that restricts operations to a specific account address. +func AccountPolicy(address string) Policy { + return NewPolicy().Equal(".address", address).MustBuild() +} + +// RecordTypePolicy creates a policy for DWN operations on specific record types. +func RecordTypePolicy(recordType string) Policy { + return NewPolicy().Equal(".record_type", recordType).MustBuild() +} + +// CombinePolicies merges multiple policies into one (implicit AND). +func CombinePolicies(policies ...Policy) Policy { + combined := make(Policy, 0) + for _, p := range policies { + combined = append(combined, p...) + } + return combined +} + +// EmptyPolicy returns an empty policy (no constraints). +func EmptyPolicy() Policy { + return Policy{} +} + +// --- Policy Constructor Helpers --- + +// These return policy.Constructor for use with And/Or/Not/All/Any + +// EqualTo creates an equality constructor for nested policy building. +func EqualTo(selector string, value any) policy.Constructor { + return func() (policy.Statement, error) { + node, err := toIPLDNode(value) + if err != nil { + return nil, err + } + return policy.Equal(selector, node)() + } +} + +// NotEqualTo creates an inequality constructor for nested policy building. +func NotEqualTo(selector string, value any) policy.Constructor { + return func() (policy.Statement, error) { + node, err := toIPLDNode(value) + if err != nil { + return nil, err + } + return policy.NotEqual(selector, node)() + } +} + +// Matches creates a glob pattern constructor for nested policy building. +func Matches(selector, pattern string) policy.Constructor { + return policy.Like(selector, pattern) +} + +// GreaterThanValue creates a comparison constructor. +func GreaterThanValue(selector string, value int64) policy.Constructor { + return policy.GreaterThan(selector, literal.Int(value)) +} + +// LessThanValue creates a comparison constructor. +func LessThanValue(selector string, value int64) policy.Constructor { + return policy.LessThan(selector, literal.Int(value)) +} diff --git a/internal/crypto/ucan/types.go b/internal/crypto/ucan/types.go new file mode 100644 index 0000000..47670d1 --- /dev/null +++ b/internal/crypto/ucan/types.go @@ -0,0 +1,261 @@ +package ucan + +import ( + "github.com/ipfs/go-cid" + "github.com/ucan-wg/go-ucan/pkg/policy" +) + +// ValidationErrorCode represents UCAN validation error types. +// These codes match the TypeScript ValidationErrorCode type in src/ucan.ts. +type ValidationErrorCode string + +const ( + ErrCodeExpired ValidationErrorCode = "EXPIRED" + ErrCodeNotYetValid ValidationErrorCode = "NOT_YET_VALID" + ErrCodeInvalidSignature ValidationErrorCode = "INVALID_SIGNATURE" + ErrCodePrincipalMisaligned ValidationErrorCode = "PRINCIPAL_MISALIGNMENT" + ErrCodePolicyViolation ValidationErrorCode = "POLICY_VIOLATION" + ErrCodeRevoked ValidationErrorCode = "REVOKED" + ErrCodeInvalidProofChain ValidationErrorCode = "INVALID_PROOF_CHAIN" + ErrCodeUnknownCommand ValidationErrorCode = "UNKNOWN_COMMAND" + ErrCodeMalformedToken ValidationErrorCode = "MALFORMED_TOKEN" +) + +// ValidationError represents a UCAN validation failure. +type ValidationError struct { + Code ValidationErrorCode `json:"code"` + Message string `json:"message"` + Details map[string]any `json:"details,omitempty"` +} + +func (e *ValidationError) Error() string { + return e.Message +} + +// NewValidationError creates a validation error with the given code and message. +func NewValidationError(code ValidationErrorCode, message string) *ValidationError { + return &ValidationError{ + Code: code, + Message: message, + } +} + +// NewValidationErrorWithDetails creates a validation error with additional details. +func NewValidationErrorWithDetails(code ValidationErrorCode, message string, details map[string]any) *ValidationError { + return &ValidationError{ + Code: code, + Message: message, + Details: details, + } +} + +// Capability represents the semantically-relevant claim of a delegation. +// It combines subject, command, and policy into a single authorization unit. +type Capability struct { + // Subject DID (resource owner) - nil for powerline delegations + Subject string `json:"sub"` + // Command being delegated + Command string `json:"cmd"` + // Policy constraints on invocation arguments + Policy policy.Policy `json:"pol"` +} + +// ValidationResult represents the outcome of UCAN validation. +type ValidationResult struct { + Valid bool `json:"valid"` + Capability *Capability `json:"capability,omitempty"` + Error *ValidationError `json:"error,omitempty"` +} + +// ValidationSuccess creates a successful validation result. +func ValidationSuccess(cap *Capability) *ValidationResult { + return &ValidationResult{ + Valid: true, + Capability: cap, + } +} + +// ValidationFailure creates a failed validation result. +func ValidationFailure(err *ValidationError) *ValidationResult { + return &ValidationResult{ + Valid: false, + Error: err, + } +} + +// CryptoAlgorithm represents supported signature algorithms. +type CryptoAlgorithm string + +const ( + AlgorithmEd25519 CryptoAlgorithm = "Ed25519" + AlgorithmP256 CryptoAlgorithm = "P-256" + AlgorithmSecp256k1 CryptoAlgorithm = "secp256k1" +) + +// ExecutionResult represents the outcome of an invocation execution. +type ExecutionResult[T any, E any] struct { + Ok *T `json:"ok,omitempty"` + Err *E `json:"err,omitempty"` +} + +// IsSuccess returns true if the result is successful. +func (r *ExecutionResult[T, E]) IsSuccess() bool { + return r.Ok != nil +} + +// IsError returns true if the result is an error. +func (r *ExecutionResult[T, E]) IsError() bool { + return r.Err != nil +} + +// Success creates a successful execution result. +func Success[T any, E any](value T) *ExecutionResult[T, E] { + return &ExecutionResult[T, E]{Ok: &value} +} + +// Failure creates a failed execution result. +func Failure[T any, E any](err E) *ExecutionResult[T, E] { + return &ExecutionResult[T, E]{Err: &err} +} + +// ReceiptPayload represents the result of an invocation execution. +// This matches the TypeScript ReceiptPayload in src/ucan.ts. +type ReceiptPayload struct { + // Executor DID + Issuer string `json:"iss"` + // CID of executed invocation + Ran cid.Cid `json:"ran"` + // Execution result + Out *ExecutionResult[any, any] `json:"out"` + // Effects - CIDs of Tasks to enqueue + Effects []cid.Cid `json:"fx,omitempty"` + // Optional metadata + Meta map[string]any `json:"meta,omitempty"` + // Issuance timestamp (Unix seconds) + IssuedAt *int64 `json:"iat,omitempty"` +} + +// RevocationPayload represents a UCAN revocation request. +// This matches the TypeScript RevocationPayload in src/ucan.ts. +type RevocationPayload struct { + // Revoker DID - must be issuer in delegation chain + Issuer string `json:"iss"` + // Subject of delegation being revoked + Subject string `json:"sub"` + // Revocation command (always "/ucan/revoke") + Command string `json:"cmd"` + // Revocation arguments + Args RevocationArgs `json:"args"` + // Proof chain + Proof []cid.Cid `json:"prf"` + // Nonce + Nonce []byte `json:"nonce"` + // Expiration (Unix seconds or null) + Expiration *int64 `json:"exp"` +} + +// RevocationArgs contains the arguments for a revocation invocation. +type RevocationArgs struct { + // CID of delegation to revoke + UCAN cid.Cid `json:"ucan"` +} + +// Task represents a subset of Invocation fields that uniquely determine work. +// The Task ID is the CID of these fields. +type Task struct { + // Subject DID + Subject string `json:"sub"` + // Command to execute + Command string `json:"cmd"` + // Command arguments + Args map[string]any `json:"args"` + // Nonce for uniqueness + Nonce []byte `json:"nonce"` +} + +// --- Sonr-specific Types --- + +// VaultCapability represents authorization for vault operations. +type VaultCapability struct { + // VaultCID identifies the specific vault + VaultCID string `json:"vault_cid"` + // Operations allowed (read, write, sign, export, import, delete, admin) + Operations []string `json:"operations"` +} + +// DIDCapability represents authorization for DID operations. +type DIDCapability struct { + // DIDPattern is a glob pattern matching allowed DIDs + DIDPattern string `json:"did_pattern"` + // Operations allowed (create, update, deactivate) + Operations []string `json:"operations"` +} + +// DWNCapability represents authorization for DWN operations. +type DWNCapability struct { + // RecordType specifies the allowed record type + RecordType string `json:"record_type"` + // Operations allowed (read, write, delete) + Operations []string `json:"operations"` +} + +// AccountCapability represents authorization for account operations. +type AccountCapability struct { + // ChainID specifies the blockchain + ChainID string `json:"chain_id"` + // Address specifies the account (or "*" for all) + Address string `json:"address"` + // Operations allowed + Operations []string `json:"operations"` +} + +// SealedToken represents a signed UCAN token with its CID and raw bytes. +type SealedToken struct { + // CID is the content identifier of the sealed token + CID cid.Cid `json:"cid"` + // Data is the DAG-CBOR encoded envelope + Data []byte `json:"data"` + // Type indicates if this is a delegation or invocation + Type string `json:"type"` // "delegation" or "invocation" +} + +// SealedDelegation is a type alias for a sealed delegation token. +type SealedDelegation = SealedToken + +// SealedInvocation is a type alias for a sealed invocation token. +type SealedInvocation = SealedToken + +// ProofChain represents an ordered list of delegation CIDs. +// Ordered from leaf (matching invocation issuer) to root delegation. +type ProofChain []cid.Cid + +// NewProofChain creates a new proof chain from CIDs. +func NewProofChain(cids ...cid.Cid) ProofChain { + return ProofChain(cids) +} + +// Add appends a CID to the proof chain. +func (p *ProofChain) Add(c cid.Cid) { + *p = append(*p, c) +} + +// IsEmpty returns true if the proof chain is empty. +func (p ProofChain) IsEmpty() bool { + return len(p) == 0 +} + +// Root returns the root delegation CID (last in chain). +func (p ProofChain) Root() (cid.Cid, bool) { + if len(p) == 0 { + return cid.Cid{}, false + } + return p[len(p)-1], true +} + +// Leaf returns the leaf delegation CID (first in chain). +func (p ProofChain) Leaf() (cid.Cid, bool) { + if len(p) == 0 { + return cid.Cid{}, false + } + return p[0], true +} diff --git a/internal/crypto/ucan/ucan.go b/internal/crypto/ucan/ucan.go new file mode 100644 index 0000000..31112b8 --- /dev/null +++ b/internal/crypto/ucan/ucan.go @@ -0,0 +1,195 @@ +// Package ucan provides UCAN v1.0.0-rc.1 compliant authorization +// for the Sonr network using the official go-ucan library. +// +// This package wraps github.com/ucan-wg/go-ucan to provide: +// - Delegation creation and validation +// - Invocation creation and validation +// - Policy evaluation +// - Sonr-specific capability types (vault, did, dwn) +// +// UCAN Envelope Format (DAG-CBOR): +// +// [ +// Signature, // Varsig-encoded signature +// { +// "h": VarsigHeader, // Algorithm metadata +// "ucan/dlg@1.0.0-rc.1": DelegationPayload // or "ucan/inv@1.0.0-rc.1" +// } +// ] +package ucan + +import ( + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/pkg/policy" + "github.com/ucan-wg/go-ucan/token/delegation" + "github.com/ucan-wg/go-ucan/token/invocation" +) + +// Re-export key types from go-ucan for convenience. +// Users should import this package instead of go-ucan directly +// for Sonr-specific functionality. +type ( + // Delegation is an immutable UCAN delegation token. + Delegation = delegation.Token + + // Invocation is an immutable UCAN invocation token. + Invocation = invocation.Token + + // Command is a validated UCAN command string (e.g., "/vault/read"). + Command = command.Command + + // Policy is a list of policy statements that constrain invocation arguments. + Policy = policy.Policy + + // Statement is a single policy statement (equality, like, and, or, etc.). + Statement = policy.Statement + + // DelegationOption configures optional fields when creating a delegation. + DelegationOption = delegation.Option + + // InvocationOption configures optional fields when creating an invocation. + InvocationOption = invocation.Option +) + +// Re-export constructors +var ( + // NewDelegation creates a delegation: "(issuer) allows (audience) to perform (cmd+pol) on (subject)". + NewDelegation = delegation.New + + // NewRootDelegation creates a root delegation where subject == issuer. + NewRootDelegation = delegation.Root + + // NewPowerlineDelegation creates a powerline delegation (subject = nil). + // Powerline automatically delegates all future delegations regardless of subject. + NewPowerlineDelegation = delegation.Powerline + + // NewInvocation creates an invocation: "(issuer) executes (command) on (subject)". + NewInvocation = invocation.New + + // ParseCommand validates and parses a command string. + ParseCommand = command.Parse + + // MustParseCommand parses a command string, panicking on error. + MustParseCommand = command.MustParse + + // TopCommand returns "/" - the most powerful capability (grants everything). + TopCommand = command.Top + + // NewCommand creates a command from segments (e.g., NewCommand("vault", "read") -> "/vault/read"). + NewCommand = command.New +) + +// Re-export delegation options +var ( + // WithExpiration sets the delegation's expiration time. + WithExpiration = delegation.WithExpiration + + // WithExpirationIn sets expiration to now + duration. + WithExpirationIn = delegation.WithExpirationIn + + // WithNotBefore sets when the delegation becomes valid. + WithNotBefore = delegation.WithNotBefore + + // WithNotBeforeIn sets not-before to now + duration. + WithNotBeforeIn = delegation.WithNotBeforeIn + + // WithDelegationMeta adds metadata to the delegation. + WithDelegationMeta = delegation.WithMeta + + // WithDelegationNonce sets a custom nonce (default: random 12 bytes). + WithDelegationNonce = delegation.WithNonce +) + +// Re-export invocation options +var ( + // WithArgument adds a single argument to the invocation. + WithArgument = invocation.WithArgument + + // WithAudience sets the invocation's audience (executor if different from subject). + WithAudience = invocation.WithAudience + + // WithInvocationMeta adds metadata to the invocation. + WithInvocationMeta = invocation.WithMeta + + // WithInvocationNonce sets a custom nonce. + WithInvocationNonce = invocation.WithNonce + + // WithEmptyNonce sets an empty nonce for idempotent operations. + WithEmptyNonce = invocation.WithEmptyNonce + + // WithInvocationExpiration sets the invocation's expiration time. + WithInvocationExpiration = invocation.WithExpiration + + // WithInvocationExpirationIn sets expiration to now + duration. + WithInvocationExpirationIn = invocation.WithExpirationIn + + // WithIssuedAt sets when the invocation was created. + WithIssuedAt = invocation.WithIssuedAt + + // WithCause sets the receipt CID that enqueued this task. + WithCause = invocation.WithCause +) + +// Standard Sonr commands following UCAN v1.0.0-rc.1 command format. +// Commands must be lowercase, start with '/', and have no trailing slash. +const ( + // Vault commands + CmdVaultRead = "/vault/read" + CmdVaultWrite = "/vault/write" + CmdVaultSign = "/vault/sign" + CmdVaultExport = "/vault/export" + CmdVaultImport = "/vault/import" + CmdVaultDelete = "/vault/delete" + CmdVaultAdmin = "/vault/admin" + CmdVault = "/vault" // Superuser - grants all vault commands + + // DID commands + CmdDIDCreate = "/did/create" + CmdDIDUpdate = "/did/update" + CmdDIDDeactivate = "/did/deactivate" + CmdDID = "/did" // Superuser - grants all DID commands + + // DWN commands + CmdDWNRecordsWrite = "/dwn/records/write" + CmdDWNRecordsRead = "/dwn/records/read" + CmdDWNRecordsDelete = "/dwn/records/delete" + CmdDWN = "/dwn" // Superuser - grants all DWN commands + + // UCAN meta commands + CmdUCANRevoke = "/ucan/revoke" + + // Root command - grants everything + CmdRoot = "/" +) + +// Pre-parsed Sonr commands for convenience +var ( + VaultRead = command.MustParse(CmdVaultRead) + VaultWrite = command.MustParse(CmdVaultWrite) + VaultSign = command.MustParse(CmdVaultSign) + VaultExport = command.MustParse(CmdVaultExport) + VaultImport = command.MustParse(CmdVaultImport) + VaultDelete = command.MustParse(CmdVaultDelete) + VaultAdmin = command.MustParse(CmdVaultAdmin) + Vault = command.MustParse(CmdVault) + + DIDCreate = command.MustParse(CmdDIDCreate) + DIDUpdate = command.MustParse(CmdDIDUpdate) + DIDDeactivate = command.MustParse(CmdDIDDeactivate) + DID = command.MustParse(CmdDID) + + DWNRecordsWrite = command.MustParse(CmdDWNRecordsWrite) + DWNRecordsRead = command.MustParse(CmdDWNRecordsRead) + DWNRecordsDelete = command.MustParse(CmdDWNRecordsDelete) + DWN = command.MustParse(CmdDWN) + + UCANRevoke = command.MustParse(CmdUCANRevoke) + Root = command.Top() +) + +// CommandSubsumes checks if parent command subsumes child command. +// A command subsumes another if the child is a path extension of parent. +// Example: "/vault" subsumes "/vault/read" and "/vault/write" +func CommandSubsumes(parent, child Command) bool { + return parent.Covers(child) +} -- 2.43.0 From c1e7d772a18bdffc2b0c6f0e742e993891a10ecf Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Thu, 8 Jan 2026 15:25:54 -0500 Subject: [PATCH 09/35] docs(todo): update migration tasks and references --- TODO.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/TODO.md b/TODO.md index ed5d4d2..bfc8b93 100644 --- a/TODO.md +++ b/TODO.md @@ -36,6 +36,7 @@ The following files use the **old JWT-based format** and must be rewritten: ### Reference Implementation (Already Compliant) These files are already aligned with v1.0.0-rc.1: + - `src/ucan.ts` - TypeScript types with envelope format - `internal/codec/ucan-schemas.json` - JSON Schema definitions @@ -164,17 +165,20 @@ These files are already aligned with v1.0.0-rc.1: > Reference: MIGRATION.md lines 770-814 ### 2.1 WebAuthn PRF Key Derivation + - [ ] Implement `DeriveEncryptionKey(prfOutput []byte) ([]byte, error)` - [ ] Use HKDF with SHA-256 to derive 256-bit encryption key - [ ] Salt with `"nebula-enclave-v1"` as info parameter ### 2.2 Database Encryption + - [ ] Implement application-level AES-GCM encryption for serialized pages - [ ] Add encryption wrapper around `Serialize()` output - [ ] Add decryption wrapper for `Load()` input - [ ] Store encryption metadata (IV, auth tag) with serialized data ### 2.3 Encrypted Database Wrapper + - [ ] Create `internal/enclave/enclave.go` - Encrypted database wrapper - [ ] Create `internal/enclave/crypto.go` - WebAuthn PRF key derivation - [ ] Integrate with existing `internal/keybase` package @@ -186,12 +190,14 @@ These files are already aligned with v1.0.0-rc.1: > Current implementation in `conn.go:exportDump()` only outputs comments ### 3.1 Proper Serialization + - [ ] Implement full row export with proper SQL INSERT statements - [ ] Handle JSON columns correctly (escape special characters) - [ ] Include table creation order for foreign key constraints - [ ] Add version header for migration compatibility ### 3.2 Proper Deserialization + - [ ] Parse serialized SQL dump in `Load()` - [ ] Execute INSERT statements to restore data - [ ] Validate data integrity after restore @@ -204,6 +210,7 @@ These files are already aligned with v1.0.0-rc.1: > Reference: `internal/keybase/actions.go` ### 4.1 Key Share Actions + - [ ] `CreateKeyShare(ctx, params) (*KeyShareResult, error)` - [ ] `ListKeyShares(ctx) ([]KeyShareResult, error)` - [ ] `GetKeyShareByID(ctx, shareID) (*KeyShareResult, error)` @@ -213,6 +220,7 @@ These files are already aligned with v1.0.0-rc.1: - [ ] `DeleteKeyShare(ctx, shareID) error` ### 4.2 UCAN Token Actions (v1.0.0-rc.1) + - [ ] `CreateDelegation(ctx, params) (*DelegationResult, error)` - [ ] `ListDelegations(ctx) ([]DelegationResult, error)` - [ ] `GetDelegationByCID(ctx, cid) (*DelegationResult, error)` @@ -224,12 +232,14 @@ These files are already aligned with v1.0.0-rc.1: - [ ] `CleanExpiredUCANs(ctx) error` ### 4.3 Verification Method Actions + - [ ] `CreateVerificationMethod(ctx, params) (*VerificationMethodResult, error)` - [ ] `ListVerificationMethods(ctx) ([]VerificationMethodResult, error)` - [ ] `GetVerificationMethod(ctx, methodID) (*VerificationMethodResult, error)` - [ ] `DeleteVerificationMethod(ctx, methodID) error` ### 4.4 Service Actions + - [ ] `CreateService(ctx, params) (*ServiceResult, error)` - [ ] `GetServiceByOrigin(ctx, origin) (*ServiceResult, error)` - [ ] `GetServiceByID(ctx, serviceID) (*ServiceResult, error)` @@ -237,6 +247,7 @@ These files are already aligned with v1.0.0-rc.1: - [ ] `ListVerifiedServices(ctx) ([]ServiceResult, error)` ### 4.5 Grant Actions (Extend Existing) + - [ ] `CreateGrant(ctx, params) (*GrantResult, error)` - [ ] `GetGrantByService(ctx, serviceID) (*GrantResult, error)` - [ ] `UpdateGrantScopes(ctx, grantID, scopes, accounts) error` @@ -246,6 +257,7 @@ These files are already aligned with v1.0.0-rc.1: - [ ] `CountActiveGrants(ctx) (int64, error)` ### 4.6 Account Actions (Extend Existing) + - [ ] `CreateAccount(ctx, params) (*AccountResult, error)` - [ ] `ListAccountsByChain(ctx, chainID) ([]AccountResult, error)` - [ ] `GetDefaultAccount(ctx, chainID) (*AccountResult, error)` @@ -254,6 +266,7 @@ These files are already aligned with v1.0.0-rc.1: - [ ] `DeleteAccount(ctx, accountID) error` ### 4.7 Credential Actions (Extend Existing) + - [ ] `CreateCredential(ctx, params) (*CredentialResult, error)` - [ ] `UpdateCredentialCounter(ctx, credentialID, signCount) error` - [ ] `RenameCredential(ctx, credentialID, name) error` @@ -261,6 +274,7 @@ These files are already aligned with v1.0.0-rc.1: - [ ] `CountCredentialsByDID(ctx) (int64, error)` ### 4.8 Session Actions (Extend Existing) + - [ ] `GetSessionByID(ctx, sessionID) (*SessionResult, error)` - [ ] `GetCurrentSession(ctx) (*SessionResult, error)` - [ ] `UpdateSessionActivity(ctx, sessionID) error` @@ -268,6 +282,7 @@ These files are already aligned with v1.0.0-rc.1: - [ ] `DeleteExpiredSessions(ctx) error` ### 4.9 Sync Checkpoint Actions + - [ ] `GetSyncCheckpoint(ctx, resourceType) (*SyncCheckpointResult, error)` - [ ] `UpsertSyncCheckpoint(ctx, params) error` - [ ] `ListSyncCheckpoints(ctx) ([]SyncCheckpointResult, error)` @@ -279,18 +294,21 @@ These files are already aligned with v1.0.0-rc.1: > Reference: MIGRATION.md lines 823-824 ### 5.1 Key Share Storage + - [ ] Parse key share data from MPC protocol - [ ] Encrypt share data before storage - [ ] Store public key and chain code - [ ] Track party index and threshold ### 5.2 Account Derivation + - [ ] Implement BIP44 derivation path parsing - [ ] Derive addresses from public keys - [ ] Support multiple chains (Cosmos 118, Ethereum 60) - [ ] Generate proper address encoding per chain ### 5.3 Key Rotation + - [ ] Implement key rotation workflow - [ ] Archive old shares - [ ] Update status transitions @@ -365,6 +383,7 @@ These files are already aligned with v1.0.0-rc.1: - [ ] Track last processed transaction hash ### 8.2 Sync Operations + - [ ] Fetch DID document updates from chain - [ ] Validate on-chain document hash - [ ] Update local state on changes @@ -498,6 +517,7 @@ The following items from the previous TODO have been removed as they reference t - ~~Section 3.3 "Delegation Actions" - Old delegation model~~ -> Merged into Section 1 and 4.2 The old capability model (`Attenuation`, `Resource`, `Capability` interfaces) is replaced by: + - `sub` (DID) - Subject of the capability - `cmd` (Command) - Action being delegated - `pol` (Policy) - Constraints on invocation arguments -- 2.43.0 From 1b2a57ca98b189c0cbea346423ba497a06744d75 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Thu, 8 Jan 2026 15:51:17 -0500 Subject: [PATCH 10/35] refactor(ucan): migrate from ucan-wg/go-ucan to code.sonr.org/go/ucan --- go.mod | 6 +++--- go.sum | 8 ++++---- internal/crypto/ucan/delegation.go | 10 +++++----- internal/crypto/ucan/invocation.go | 8 ++++---- internal/crypto/ucan/policy.go | 4 ++-- internal/crypto/ucan/types.go | 2 +- internal/crypto/ucan/ucan.go | 10 +++++----- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/go.mod b/go.mod index febadd3..272588a 100644 --- a/go.mod +++ b/go.mod @@ -3,18 +3,19 @@ module enclave go 1.25.5 require ( + code.sonr.org/go/did-it v1.0.0 + code.sonr.org/go/ucan v1.1.0 github.com/extism/go-pdk v1.1.3 + github.com/ipfs/go-cid v0.5.0 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 ) 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 @@ -23,7 +24,6 @@ require ( 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 diff --git a/go.sum b/go.sum index 2dd919f..fd744c6 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,10 @@ +code.sonr.org/go/did-it v1.0.0 h1:Wh8igUkD6cuf0Ul3gawi27z2/M1YfdnQ/mD9gBq/2EU= +code.sonr.org/go/did-it v1.0.0/go.mod h1:PFK6ItvNyB2xbnVqipBbkN9BK1Sq+E2lf1YfOyCA0Og= +code.sonr.org/go/ucan v1.1.0 h1:0VuJCGzDPbzcTrjBBgQAmLu+2ARp1eYeXiRl+2A86R8= +code.sonr.org/go/ucan v1.1.0/go.mod h1:QUGrUW93T2yVPiSiADLxj1RNpTJ/9RHaKoHGMGEC63A= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 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= @@ -88,8 +90,6 @@ 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= diff --git a/internal/crypto/ucan/delegation.go b/internal/crypto/ucan/delegation.go index 100162d..7b42050 100644 --- a/internal/crypto/ucan/delegation.go +++ b/internal/crypto/ucan/delegation.go @@ -4,12 +4,12 @@ import ( "fmt" "time" - "github.com/MetaMask/go-did-it" - "github.com/MetaMask/go-did-it/crypto" + "code.sonr.org/go/did-it" + "code.sonr.org/go/did-it/crypto" "github.com/ipfs/go-cid" - "github.com/ucan-wg/go-ucan/pkg/command" - "github.com/ucan-wg/go-ucan/pkg/policy" - "github.com/ucan-wg/go-ucan/token/delegation" + "code.sonr.org/go/ucan/pkg/command" + "code.sonr.org/go/ucan/pkg/policy" + "code.sonr.org/go/ucan/token/delegation" ) // DelegationBuilder provides a fluent API for creating UCAN delegations. diff --git a/internal/crypto/ucan/invocation.go b/internal/crypto/ucan/invocation.go index fa0018c..d925fd6 100644 --- a/internal/crypto/ucan/invocation.go +++ b/internal/crypto/ucan/invocation.go @@ -4,11 +4,11 @@ import ( "fmt" "time" - "github.com/MetaMask/go-did-it" - "github.com/MetaMask/go-did-it/crypto" + "code.sonr.org/go/did-it" + "code.sonr.org/go/did-it/crypto" "github.com/ipfs/go-cid" - "github.com/ucan-wg/go-ucan/pkg/command" - "github.com/ucan-wg/go-ucan/token/invocation" + "code.sonr.org/go/ucan/pkg/command" + "code.sonr.org/go/ucan/token/invocation" ) // InvocationBuilder provides a fluent API for creating UCAN invocations. diff --git a/internal/crypto/ucan/policy.go b/internal/crypto/ucan/policy.go index 5b7092e..e8ed4d9 100644 --- a/internal/crypto/ucan/policy.go +++ b/internal/crypto/ucan/policy.go @@ -2,8 +2,8 @@ package ucan import ( "github.com/ipld/go-ipld-prime" - "github.com/ucan-wg/go-ucan/pkg/policy" - "github.com/ucan-wg/go-ucan/pkg/policy/literal" + "code.sonr.org/go/ucan/pkg/policy" + "code.sonr.org/go/ucan/pkg/policy/literal" ) // PolicyBuilder provides a fluent API for constructing UCAN policies. diff --git a/internal/crypto/ucan/types.go b/internal/crypto/ucan/types.go index 47670d1..2589309 100644 --- a/internal/crypto/ucan/types.go +++ b/internal/crypto/ucan/types.go @@ -2,7 +2,7 @@ package ucan import ( "github.com/ipfs/go-cid" - "github.com/ucan-wg/go-ucan/pkg/policy" + "code.sonr.org/go/ucan/pkg/policy" ) // ValidationErrorCode represents UCAN validation error types. diff --git a/internal/crypto/ucan/ucan.go b/internal/crypto/ucan/ucan.go index 31112b8..a8167fb 100644 --- a/internal/crypto/ucan/ucan.go +++ b/internal/crypto/ucan/ucan.go @@ -1,7 +1,7 @@ // Package ucan provides UCAN v1.0.0-rc.1 compliant authorization // for the Sonr network using the official go-ucan library. // -// This package wraps github.com/ucan-wg/go-ucan to provide: +// This package wraps code.sonr.org/go/ucan to provide: // - Delegation creation and validation // - Invocation creation and validation // - Policy evaluation @@ -19,10 +19,10 @@ package ucan import ( - "github.com/ucan-wg/go-ucan/pkg/command" - "github.com/ucan-wg/go-ucan/pkg/policy" - "github.com/ucan-wg/go-ucan/token/delegation" - "github.com/ucan-wg/go-ucan/token/invocation" + "code.sonr.org/go/ucan/pkg/command" + "code.sonr.org/go/ucan/pkg/policy" + "code.sonr.org/go/ucan/token/delegation" + "code.sonr.org/go/ucan/token/invocation" ) // Re-export key types from go-ucan for convenience. -- 2.43.0 From 4fbfdf1e4db2fce54c5a2d6e2cd6ea552fbe1c04 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Thu, 8 Jan 2026 16:37:36 -0500 Subject: [PATCH 11/35] refactor(keybase): migrate to UCAN v1.0.0-rc.1 envelope format --- TODO.md | 229 +++-- internal/keybase/actions.go | 4 +- internal/keybase/actions_delegation.go | 323 +++++-- internal/keybase/actions_grant.go | 28 +- internal/keybase/actions_ucan.go | 205 ---- internal/keybase/db.go | 6 +- internal/keybase/models.go | 98 +- internal/keybase/querier.go | 54 +- internal/keybase/query.sql.go | 1200 ++++++++++++++++-------- internal/migrations/query.sql | 184 ++-- internal/migrations/schema.sql | 133 ++- 11 files changed, 1465 insertions(+), 999 deletions(-) delete mode 100644 internal/keybase/actions_ucan.go diff --git a/TODO.md b/TODO.md index bfc8b93..512c4b6 100644 --- a/TODO.md +++ b/TODO.md @@ -11,7 +11,7 @@ Remaining tasks from [MIGRATION.md](./MIGRATION.md) for the Nebula Key Enclave. | Generated Code | Complete | `internal/keybase/*.go` | | Basic Plugin Functions | Complete | `generate`, `load`, `exec`, `query`, `ping` | | Encryption | Not Started | WebAuthn PRF key derivation needed | -| **UCAN v1.0.0-rc.1** | **CRITICAL** | Go implementation uses deprecated JWT format | +| **UCAN v1.0.0-rc.1** | **In Progress** | Core types, builders, and policies complete. Uses `go-ucan v1.1.0` | | MPC Key Shares | Not Started | Key share management missing | | Database Serialization | Incomplete | Export dumps comments only | @@ -19,114 +19,116 @@ Remaining tasks from [MIGRATION.md](./MIGRATION.md) for the Nebula Key Enclave. ## 1. UCAN v1.0.0-rc.1 Migration (CRITICAL PRIORITY) -> **Breaking Change**: Current Go implementation (`internal/crypto/ucan/`) uses deprecated JWT-based UCAN format. Must migrate to envelope format per v1.0.0-rc.1 spec. +> **Status**: Core implementation complete using `github.com/ucan-wg/go-ucan v1.1.0`. Deprecated JWT-based files deleted. Remaining work is database integration and MPC signing. -### Current State (DEPRECATED - Must Replace) +### Completed Implementation -The following files use the **old JWT-based format** and must be rewritten: +The following files implement UCAN v1.0.0-rc.1 using the official go-ucan library: -| File | Status | Issue | -|------|--------|-------| -| `jwt.go` | DEPRECATED | Uses `github.com/golang-jwt/jwt/v5`, old `can`+`with` format | -| `capability.go` | DEPRECATED | Old Attenuation/Resource/Capability model | -| `verifier.go` | DEPRECATED | JWT parsing, old proof chain format | -| `source.go` | DEPRECATED | JWT token creation with MPC | -| `vault.go` | PARTIAL | VaultCapability needs Policy migration | +| File | Status | Description | +|------|--------|-------------| +| `ucan.go` | ✅ Complete | Type re-exports, Sonr commands, pre-parsed constants | +| `policy.go` | ✅ Complete | PolicyBuilder fluent API, Sonr-specific policy helpers | +| `delegation.go` | ✅ Complete | DelegationBuilder fluent API, Sonr delegation helpers | +| `invocation.go` | ✅ Complete | InvocationBuilder fluent API, Sonr invocation helpers | +| `types.go` | ✅ Complete | ValidationError, Capability, ExecutionResult, Sonr types | -### Reference Implementation (Already Compliant) +### Dependencies Added -These files are already aligned with v1.0.0-rc.1: +- `github.com/ucan-wg/go-ucan v1.1.0` - Official UCAN library +- `github.com/ipld/go-ipld-prime v0.21.0` - IPLD encoding +- `github.com/MetaMask/go-did-it v1.0.0-pre1` - DID handling (indirect) +- `github.com/ipfs/go-cid v0.5.0` - Content addressing (indirect) -- `src/ucan.ts` - TypeScript types with envelope format -- `internal/codec/ucan-schemas.json` - JSON Schema definitions +### Deleted (Deprecated JWT-based) + +- ~~`jwt.go`~~ - Removed +- ~~`capability.go`~~ - Removed +- ~~`verifier.go`~~ - Removed +- ~~`source.go`~~ - Removed +- ~~`internal/crypto/mpc/spec/`~~ - Entire directory removed ### 1.1 Core Data Structures -- [ ] Create `internal/crypto/ucan/types.go` - v1.0.0-rc.1 types - - [ ] `DelegationPayload` struct (iss, aud, sub, cmd, pol, nonce, meta, nbf, exp) - - [ ] `InvocationPayload` struct (iss, sub, aud, cmd, args, prf, meta, nonce, exp, iat, cause) - - [ ] `Delegation` type as `[Signature, DelegationSigPayload]` tuple - - [ ] `Invocation` type as `[Signature, InvocationSigPayload]` tuple - - [ ] `Task` struct (sub, cmd, args, nonce) - - [ ] `ReceiptPayload` struct (iss, ran, out, fx, meta, iat) - - [ ] `RevocationPayload` struct +- [x] Create `internal/crypto/ucan/types.go` - v1.0.0-rc.1 types + - [x] Re-export `Delegation` and `Invocation` from go-ucan + - [x] `Task` struct (sub, cmd, args, nonce) + - [x] `ReceiptPayload` struct (iss, ran, out, fx, meta, iat) + - [x] `RevocationPayload` struct + - [x] `ValidationError` with error codes matching TypeScript + - [x] `Capability` struct (sub, cmd, pol) + - [x] `ExecutionResult[T, E]` generic type + - [x] Sonr-specific types: `VaultCapability`, `DIDCapability`, `DWNCapability` -- [ ] Create `internal/crypto/ucan/policy.go` - Policy Language - - [ ] `PolicyStatement` union type - - [ ] `EqualityStatement` - `["==", selector, value]` / `["!=", selector, value]` - - [ ] `InequalityStatement` - `[">", selector, number]` etc. - - [ ] `LikeStatement` - `["like", selector, glob]` - - [ ] `NotStatement` - `["not", statement]` - - [ ] `AndStatement` / `OrStatement` - logical connectives - - [ ] `AllStatement` / `AnyStatement` - quantifiers - - [ ] `Selector` parser (jq-inspired: `.foo`, `.bar[0]`, `.items[-1]`) - - [ ] `GlobPattern` matcher +- [x] Create `internal/crypto/ucan/policy.go` - Policy Language + - [x] `PolicyBuilder` fluent API with all operators + - [x] `Equal`, `NotEqual` - equality statements + - [x] `GreaterThan`, `LessThan`, etc. - inequality statements + - [x] `Like` - glob pattern matching + - [x] `Not`, `And`, `Or` - logical connectives + - [x] `All`, `Any` - quantifiers + - [x] Sonr helpers: `VaultPolicy`, `DIDPolicy`, `ChainPolicy`, `AccountPolicy` -- [ ] Create `internal/crypto/ucan/command.go` - Command types - - [ ] `Command` type with validation (must start with `/`, lowercase, no trailing slash) - - [ ] Standard commands: `/crud/*`, `/msg/*`, `/ucan/revoke`, `/wasm/run` - - [ ] Custom Sonr commands: `/vault/*`, `/did/*`, `/dwn/*` +- [x] Create `internal/crypto/ucan/ucan.go` - Command types + - [x] `Command` type re-exported from go-ucan + - [x] Sonr commands: `/vault/*`, `/did/*`, `/dwn/*`, `/ucan/revoke` + - [x] Pre-parsed command constants: `VaultRead`, `VaultWrite`, `DIDUpdate`, etc. + - [x] `CommandSubsumes()` helper using go-ucan's `Covers()` method ### 1.2 Envelope Format & Encoding -- [ ] Create `internal/crypto/ucan/envelope.go` - Envelope handling - - [ ] `UCANEnvelope[P]` generic type as `[Signature, {h: VarsigHeader} & P]` - - [ ] Encode envelope to CBOR (requires `github.com/fxamacker/cbor/v2`) - - [ ] Decode envelope from CBOR - - [ ] DAG-JSON encoding for interop - - [ ] CID computation (DAG-CBOR codec, SHA-256 multihash, base58btc) +- [x] Envelope handling via go-ucan library + - [x] `ToSealed()` method produces DAG-CBOR bytes + CID + - [x] `ToDagCbor()`, `ToDagJson()` encoding methods + - [x] CID computation handled by go-ucan -- [ ] Create `internal/crypto/ucan/varsig.go` - Varsig v1 headers - - [ ] Varsig header encoding/decoding - - [ ] Algorithm metadata extraction - - [ ] Support Ed25519, P-256, secp256k1 +- [x] Varsig support via go-ucan library + - [x] Ed25519, P-256, secp256k1 via `go-did-it/crypto` ### 1.3 Delegation Operations -- [ ] Create `internal/crypto/ucan/delegation.go` - Delegation creation/validation - - [ ] `NewDelegation(issuer, audience, subject, cmd, policy, exp, meta)` builder - - [ ] Sign delegation with issuer private key - - [ ] Validate delegation signature - - [ ] Validate delegation payload (temporal, structural) - - [ ] Extract `Capability` from delegation (sub + cmd + pol) +- [x] Create `internal/crypto/ucan/delegation.go` - Delegation creation/validation + - [x] `DelegationBuilder` fluent API + - [x] `NewDelegation`, `NewRootDelegation`, `NewPowerlineDelegation` re-exports + - [x] `BuildSealed(privKey)` for signing + - [x] Sonr helpers: `NewVaultDelegation`, `NewDIDDelegation`, `NewDWNDelegation` + - [x] Temporal options: `ExpiresAt`, `ExpiresIn`, `NotBefore`, `NotBeforeIn` ### 1.4 Invocation Operations -- [ ] Create `internal/crypto/ucan/invocation.go` - Invocation creation/validation - - [ ] `NewInvocation(issuer, subject, cmd, args, proofs, exp)` builder - - [ ] Sign invocation with invoker private key - - [ ] Validate invocation signature - - [ ] Validate proof chain (CID references to delegations) - - [ ] Evaluate policies against invocation args +- [x] Create `internal/crypto/ucan/invocation.go` - Invocation creation/validation + - [x] `InvocationBuilder` fluent API + - [x] `NewInvocation` re-export + - [x] `BuildSealed(privKey)` for signing + - [x] Proof chain management: `Proof()`, `Proofs()` + - [x] Sonr helpers: `VaultReadInvocation`, `VaultSignInvocation`, `DIDUpdateInvocation` ### 1.5 Policy Evaluation Engine -- [ ] Create `internal/crypto/ucan/eval.go` - Policy evaluation - - [ ] `EvaluatePolicy(policy Policy, args Arguments) (bool, error)` - - [ ] Selector resolution against IPLD data - - [ ] Equality comparison (deep IPLD equality) - - [ ] Numeric comparisons - - [ ] Glob pattern matching for `like` operator - - [ ] Logical connectives (`and`, `or`, `not`) - - [ ] Quantifiers (`all`, `any`) over collections +> Note: go-ucan provides `ExecutionAllowed()` on invocations which validates proofs and evaluates policies. + +- [x] Policy evaluation via go-ucan's `invocation.ExecutionAllowed(loader)` +- [ ] Create `internal/crypto/ucan/eval.go` - Additional evaluation helpers (if needed) + - [ ] Custom selector resolution for Sonr-specific args + - [ ] Caching layer for repeated evaluations ### 1.6 Proof Chain Validation -- [ ] Create `internal/crypto/ucan/chain.go` - Chain validation - - [ ] Resolve CID to Delegation (requires delegation store) - - [ ] Validate chain continuity (child.iss == parent.aud) - - [ ] Validate capability attenuation (child.cmd subsumes parent.cmd) - - [ ] Validate policy attenuation (child.pol more restrictive than parent.pol) - - [ ] Validate temporal bounds (child.exp <= parent.exp) - - [ ] Check revocation status for all chain members +> Note: go-ucan handles chain validation internally via `ExecutionAllowed()`. + +- [x] Chain validation via go-ucan library +- [ ] Create `internal/crypto/ucan/store.go` - Delegation store + - [ ] Implement `delegation.Loader` interface + - [ ] `GetDelegation(cid.Cid) (*delegation.Token, error)` + - [ ] Cache loaded delegations for performance ### 1.7 Revocation +- [x] `RevocationInvocation()` helper in `invocation.go` - [ ] Create `internal/crypto/ucan/revocation.go` - Revocation handling - - [ ] `NewRevocation(revoker, delegation_cid)` builder - - [ ] Validate revoker is in delegation's issuer chain - - [ ] Store revocation in database - - [ ] Query revocation status by CID + - [ ] Revocation store implementation + - [ ] `IsRevoked(cid.Cid) (bool, error)` query + - [ ] Integration with chain validation ### 1.8 Database Integration @@ -141,20 +143,18 @@ These files are already aligned with v1.0.0-rc.1: - [ ] `InsertInvocation`, `GetInvocationByCID` - [ ] `InsertRevocation`, `IsRevoked`, `GetRevocationsByDelegation` -### 1.9 Migration from Old Format +### 1.9 MPC Signing Integration -- [ ] Create migration script for existing UCAN data (if any) -- [ ] Remove deprecated files after migration complete: - - [ ] `jwt.go` - Remove entirely - - [ ] `capability.go` - Replace with policy-based capabilities - - [ ] `verifier.go` - Replace with envelope-based verification - - [ ] `source.go` - Replace with envelope-based token creation +- [ ] Create `internal/crypto/ucan/signer.go` - MPC key integration + - [ ] Implement `crypto.PrivateKeySigningBytes` interface for MPC + - [ ] Sign delegations with MPC key shares + - [ ] Sign invocations with MPC key shares ### 1.10 Testing -- [ ] Unit tests for policy evaluation -- [ ] Unit tests for envelope encoding/decoding -- [ ] Unit tests for chain validation +- [ ] Unit tests for builders (DelegationBuilder, InvocationBuilder) +- [ ] Unit tests for policy helpers +- [ ] Unit tests for Sonr-specific invocations - [ ] Interoperability tests against TypeScript implementation - [ ] Test vectors from UCAN spec @@ -478,23 +478,26 @@ These files are already aligned with v1.0.0-rc.1: ## Priority Order -1. **CRITICAL (Spec Compliance)** - - UCAN v1.0.0-rc.1 Migration (Section 1) - - Core data structures (1.1) - - Envelope format (1.2) - - Delegation operations (1.3) - - Policy evaluation (1.5) +1. **CRITICAL (Spec Compliance)** - ✅ Core Complete + - ~~UCAN v1.0.0-rc.1 Migration (Section 1)~~ ✅ Core types, builders, policies done + - ~~Core data structures (1.1)~~ ✅ Using go-ucan v1.1.0 + - ~~Envelope format (1.2)~~ ✅ Handled by go-ucan + - ~~Delegation operations (1.3)~~ ✅ DelegationBuilder complete + - ~~Invocation operations (1.4)~~ ✅ InvocationBuilder complete + - Database integration (1.8) - Next priority + - MPC signing integration (1.9) - Next priority 2. **High Priority (Core Functionality)** - Database Serialization (3.1, 3.2) - Credential Creation (6.2, 4.7) - Key Share Actions (4.1) - Account Actions (4.6) + - UCAN Database Integration (1.8) 3. **Medium Priority (Authorization)** - - Invocation operations (1.4) - - Proof chain validation (1.6) - - Revocation (1.7) + - Delegation store (1.6) + - Revocation store (1.7) + - MPC Signing (1.9) - Encryption Strategy (2.1, 2.2) 4. **Lower Priority (Enhancement)** @@ -506,13 +509,39 @@ These files are already aligned with v1.0.0-rc.1: --- +## Completed Items + +### UCAN v1.0.0-rc.1 Core (January 2025) + +The following was completed using `github.com/ucan-wg/go-ucan v1.1.0`: + +- ✅ Type re-exports from go-ucan (Delegation, Invocation, Command, Policy) +- ✅ Sonr command constants (/vault/*, /did/*, /dwn/*) +- ✅ DelegationBuilder fluent API with Sonr-specific helpers +- ✅ InvocationBuilder fluent API with Sonr-specific helpers +- ✅ PolicyBuilder fluent API with all operators +- ✅ Sonr policy helpers (VaultPolicy, DIDPolicy, ChainPolicy) +- ✅ ValidationError types matching TypeScript definitions +- ✅ Capability, ExecutionResult, and related types + +### Deleted (Deprecated JWT-based) + +- ✅ Deleted `jwt.go` - Old JWT token handling +- ✅ Deleted `capability.go` - Old Attenuation/Resource/Capability model +- ✅ Deleted `verifier.go` - Old JWT verification +- ✅ Deleted `source.go` - Old JWT token creation +- ✅ Deleted `internal/crypto/mpc/spec/` - Old MPC JWT integration +- ✅ Removed `github.com/golang-jwt/jwt/v5` dependency + +--- + ## Deprecated Items (Removed) The following items from the previous TODO have been removed as they reference the **deprecated JWT-based UCAN format**: -- ~~Section 4.1 "Token Validation" - JWT parsing~~ -> Replaced by envelope validation (1.2, 1.3) -- ~~Section 4.2 "Capability Verification" - `can`/`with` format~~ -> Replaced by policy evaluation (1.5) -- ~~Section 4.3 "Proof Chain Validation" - JWT proof strings~~ -> Replaced by CID-based chain (1.6) +- ~~Section 4.1 "Token Validation" - JWT parsing~~ -> Replaced by go-ucan validation +- ~~Section 4.2 "Capability Verification" - `can`/`with` format~~ -> Replaced by policy evaluation +- ~~Section 4.3 "Proof Chain Validation" - JWT proof strings~~ -> Replaced by CID-based chain - ~~Section 3.2 "UCAN Token Actions" - Old format~~ -> Replaced by v1.0.0-rc.1 actions (4.2) - ~~Section 3.3 "Delegation Actions" - Old delegation model~~ -> Merged into Section 1 and 4.2 diff --git a/internal/keybase/actions.go b/internal/keybase/actions.go index 44c9534..54b6dd9 100644 --- a/internal/keybase/actions.go +++ b/internal/keybase/actions.go @@ -72,7 +72,7 @@ func (am *ActionManager) ListAccounts(ctx context.Context) ([]AccountResult, err AddressIndex: row.AddressIndex, Label: label, IsDefault: row.IsDefault == 1, - PublicKey: row.PublicKey, + PublicKey: row.SharePublicKey, Curve: row.Curve, CreatedAt: row.CreatedAt, } @@ -481,7 +481,7 @@ func (am *ActionManager) ResolveDID(ctx context.Context, did string) (*DIDDocume AddressIndex: row.AddressIndex, Label: label, IsDefault: row.IsDefault == 1, - PublicKey: row.PublicKey, + PublicKey: row.SharePublicKey, Curve: row.Curve, CreatedAt: row.CreatedAt, } diff --git a/internal/keybase/actions_delegation.go b/internal/keybase/actions_delegation.go index eab2434..73eb631 100644 --- a/internal/keybase/actions_delegation.go +++ b/internal/keybase/actions_delegation.go @@ -2,37 +2,46 @@ package keybase import ( "context" - "encoding/json" "fmt" ) +// ============================================================================= +// DELEGATION ACTIONS (UCAN v1.0.0-rc.1) +// ============================================================================= + +// DelegationResult represents a delegation in API responses. type DelegationResult struct { - ID int64 `json:"id"` - UcanID int64 `json:"ucan_id"` - Delegator string `json:"delegator"` - Delegate string `json:"delegate"` - Resource string `json:"resource"` - Action string `json:"action"` - Caveats json.RawMessage `json:"caveats"` - Depth int64 `json:"depth"` - Status string `json:"status"` - CreatedAt string `json:"created_at"` - ExpiresAt string `json:"expires_at,omitempty"` + ID int64 `json:"id"` + CID string `json:"cid"` + Issuer string `json:"iss"` + Audience string `json:"aud"` + Subject string `json:"sub,omitempty"` + Command string `json:"cmd"` + Policy string `json:"pol,omitempty"` + NotBefore string `json:"nbf,omitempty"` + Expiration string `json:"exp,omitempty"` + IsRoot bool `json:"is_root"` + IsPowerline bool `json:"is_powerline"` + CreatedAt string `json:"created_at"` } -type NewDelegationInput struct { - UcanID int64 `json:"ucan_id"` - Delegator string `json:"delegator"` - Delegate string `json:"delegate"` - Resource string `json:"resource"` - Action string `json:"action"` - Caveats json.RawMessage `json:"caveats,omitempty"` - ParentID int64 `json:"parent_id,omitempty"` - Depth int64 `json:"depth"` - ExpiresAt string `json:"expires_at,omitempty"` +// StoreDelegationParams contains parameters for storing a delegation. +type StoreDelegationParams struct { + CID string `json:"cid"` + Envelope []byte `json:"envelope"` + Issuer string `json:"iss"` + Audience string `json:"aud"` + Subject string `json:"sub,omitempty"` + Command string `json:"cmd"` + Policy string `json:"pol,omitempty"` + NotBefore string `json:"nbf,omitempty"` + Expiration string `json:"exp,omitempty"` + IsRoot bool `json:"is_root"` + IsPowerline bool `json:"is_powerline"` } -func (am *ActionManager) CreateDelegation(ctx context.Context, params NewDelegationInput) (*DelegationResult, error) { +// StoreDelegation stores a new UCAN delegation envelope. +func (am *ActionManager) StoreDelegation(ctx context.Context, params StoreDelegationParams) (*DelegationResult, error) { am.kb.mu.Lock() defer am.kb.mu.Unlock() @@ -40,75 +49,78 @@ func (am *ActionManager) CreateDelegation(ctx context.Context, params NewDelegat return nil, fmt.Errorf("DID not initialized") } - var parentID *int64 - if params.ParentID != 0 { - parentID = ¶ms.ParentID + var sub, pol, nbf, exp *string + if params.Subject != "" { + sub = ¶ms.Subject + } + if params.Policy != "" { + pol = ¶ms.Policy + } + if params.NotBefore != "" { + nbf = ¶ms.NotBefore + } + if params.Expiration != "" { + exp = ¶ms.Expiration } - var expiresAt *string - if params.ExpiresAt != "" { - expiresAt = ¶ms.ExpiresAt + isRoot := int64(0) + if params.IsRoot { + isRoot = 1 } - - caveats := params.Caveats - if caveats == nil { - caveats = json.RawMessage(`{}`) + isPowerline := int64(0) + if params.IsPowerline { + isPowerline = 1 } d, err := am.kb.queries.CreateDelegation(ctx, CreateDelegationParams{ - DidID: am.kb.didID, - UcanID: params.UcanID, - Delegator: params.Delegator, - Delegate: params.Delegate, - Resource: params.Resource, - Action: params.Action, - Caveats: caveats, - ParentID: parentID, - Depth: params.Depth, - ExpiresAt: expiresAt, + DidID: am.kb.didID, + Cid: params.CID, + Envelope: params.Envelope, + Iss: params.Issuer, + Aud: params.Audience, + Sub: sub, + Cmd: params.Command, + Pol: pol, + Nbf: nbf, + Exp: exp, + IsRoot: isRoot, + IsPowerline: isPowerline, }) if err != nil { return nil, fmt.Errorf("create delegation: %w", err) } - return delegationToResult(&d), nil + return delegationToResult(d), nil } -func (am *ActionManager) ListDelegationsByDelegator(ctx context.Context, delegator string) ([]DelegationResult, error) { +// GetDelegationByCID retrieves a delegation by its CID. +func (am *ActionManager) GetDelegationByCID(ctx context.Context, cid string) (*DelegationResult, error) { am.kb.mu.RLock() defer am.kb.mu.RUnlock() - delegations, err := am.kb.queries.ListDelegationsByDelegator(ctx, delegator) + d, err := am.kb.queries.GetDelegationByCID(ctx, cid) if err != nil { - return nil, fmt.Errorf("list delegations by delegator: %w", err) + return nil, fmt.Errorf("get delegation: %w", err) } - results := make([]DelegationResult, len(delegations)) - for i, d := range delegations { - results[i] = *delegationToResult(&d) - } - - return results, nil + return delegationToResult(d), nil } -func (am *ActionManager) ListDelegationsByDelegate(ctx context.Context, delegate string) ([]DelegationResult, error) { +// GetDelegationEnvelope retrieves the raw CBOR envelope for a delegation. +func (am *ActionManager) GetDelegationEnvelope(ctx context.Context, cid string) ([]byte, error) { am.kb.mu.RLock() defer am.kb.mu.RUnlock() - delegations, err := am.kb.queries.ListDelegationsByDelegate(ctx, delegate) + envelope, err := am.kb.queries.GetDelegationEnvelopeByCID(ctx, cid) if err != nil { - return nil, fmt.Errorf("list delegations by delegate: %w", err) + return nil, fmt.Errorf("get delegation envelope: %w", err) } - results := make([]DelegationResult, len(delegations)) - for i, d := range delegations { - results[i] = *delegationToResult(&d) - } - - return results, nil + return envelope, nil } -func (am *ActionManager) ListDelegationsForResource(ctx context.Context, resource string) ([]DelegationResult, error) { +// ListDelegations returns all active delegations for the current DID. +func (am *ActionManager) ListDelegations(ctx context.Context) ([]DelegationResult, error) { am.kb.mu.RLock() defer am.kb.mu.RUnlock() @@ -116,66 +128,191 @@ func (am *ActionManager) ListDelegationsForResource(ctx context.Context, resourc return []DelegationResult{}, nil } - delegations, err := am.kb.queries.ListDelegationsForResource(ctx, ListDelegationsForResourceParams{ - DidID: am.kb.didID, - Resource: resource, - }) + delegations, err := am.kb.queries.ListDelegationsByDID(ctx, am.kb.didID) if err != nil { - return nil, fmt.Errorf("list delegations for resource: %w", err) + return nil, fmt.Errorf("list delegations: %w", err) } results := make([]DelegationResult, len(delegations)) for i, d := range delegations { - results[i] = *delegationToResult(&d) + results[i] = *delegationToResult(d) } return results, nil } -func (am *ActionManager) GetDelegationChain(ctx context.Context, delegationID int64) ([]DelegationResult, error) { +// ListDelegationsByIssuer returns delegations issued by a specific DID. +func (am *ActionManager) ListDelegationsByIssuer(ctx context.Context, issuer string) ([]DelegationResult, error) { am.kb.mu.RLock() defer am.kb.mu.RUnlock() - delegations, err := am.kb.queries.GetDelegationChain(ctx, GetDelegationChainParams{ - ID: delegationID, - ParentID: &delegationID, - }) + delegations, err := am.kb.queries.ListDelegationsByIssuer(ctx, issuer) if err != nil { - return nil, fmt.Errorf("get delegation chain: %w", err) + return nil, fmt.Errorf("list delegations by issuer: %w", err) } results := make([]DelegationResult, len(delegations)) for i, d := range delegations { - results[i] = *delegationToResult(&d) + results[i] = *delegationToResult(d) } return results, nil } -func (am *ActionManager) RevokeDelegation(ctx context.Context, delegationID int64) error { +// ListDelegationsByAudience returns delegations granted to a specific DID. +func (am *ActionManager) ListDelegationsByAudience(ctx context.Context, audience string) ([]DelegationResult, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + delegations, err := am.kb.queries.ListDelegationsByAudience(ctx, audience) + if err != nil { + return nil, fmt.Errorf("list delegations by audience: %w", err) + } + + results := make([]DelegationResult, len(delegations)) + for i, d := range delegations { + results[i] = *delegationToResult(d) + } + + return results, nil +} + +// ListDelegationsForCommand returns delegations that grant a specific command. +func (am *ActionManager) ListDelegationsForCommand(ctx context.Context, cmd string) ([]DelegationResult, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + if am.kb.didID == 0 { + return []DelegationResult{}, nil + } + + delegations, err := am.kb.queries.ListDelegationsForCommand(ctx, ListDelegationsForCommandParams{ + DidID: am.kb.didID, + Cmd: cmd, + Cmd_2: cmd, + }) + if err != nil { + return nil, fmt.Errorf("list delegations for command: %w", err) + } + + results := make([]DelegationResult, len(delegations)) + for i, d := range delegations { + results[i] = *delegationToResult(d) + } + + return results, nil +} + +// IsDelegationRevoked checks if a delegation has been revoked. +func (am *ActionManager) IsDelegationRevoked(ctx context.Context, cid string) (bool, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + revoked, err := am.kb.queries.IsDelegationRevoked(ctx, cid) + if err != nil { + return false, fmt.Errorf("check revocation: %w", err) + } + + return revoked == 1, nil +} + +// RevokeDelegationParams contains parameters for revoking a delegation. +type RevokeDelegationParams struct { + DelegationCID string `json:"delegation_cid"` + RevokedBy string `json:"revoked_by"` + InvocationCID string `json:"invocation_cid,omitempty"` + Reason string `json:"reason,omitempty"` +} + +// RevokeDelegation revokes a delegation by creating a revocation record. +func (am *ActionManager) RevokeDelegation(ctx context.Context, params RevokeDelegationParams) error { am.kb.mu.Lock() defer am.kb.mu.Unlock() - return am.kb.queries.RevokeDelegation(ctx, delegationID) + var invocationCID, reason *string + if params.InvocationCID != "" { + invocationCID = ¶ms.InvocationCID + } + if params.Reason != "" { + reason = ¶ms.Reason + } + + err := am.kb.queries.CreateRevocation(ctx, CreateRevocationParams{ + DelegationCid: params.DelegationCID, + RevokedBy: params.RevokedBy, + InvocationCid: invocationCID, + Reason: reason, + }) + if err != nil { + return fmt.Errorf("create revocation: %w", err) + } + + return nil } -func delegationToResult(d *Delegation) *DelegationResult { - expiresAt := "" - if d.ExpiresAt != nil { - expiresAt = *d.ExpiresAt +// DeleteDelegation deletes a delegation from the database. +func (am *ActionManager) DeleteDelegation(ctx context.Context, cid string) error { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + if am.kb.didID == 0 { + return fmt.Errorf("DID not initialized") + } + + err := am.kb.queries.DeleteDelegation(ctx, DeleteDelegationParams{ + Cid: cid, + DidID: am.kb.didID, + }) + if err != nil { + return fmt.Errorf("delete delegation: %w", err) + } + + return nil +} + +// CleanExpiredDelegations removes delegations expired more than 30 days ago. +func (am *ActionManager) CleanExpiredDelegations(ctx context.Context) error { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + if err := am.kb.queries.CleanExpiredDelegations(ctx); err != nil { + return fmt.Errorf("clean expired delegations: %w", err) + } + + return nil +} + +// delegationToResult converts a UcanDelegation to DelegationResult. +func delegationToResult(d UcanDelegation) *DelegationResult { + subject := "" + if d.Sub != nil { + subject = *d.Sub + } + policy := "" + if d.Pol != nil { + policy = *d.Pol + } + notBefore := "" + if d.Nbf != nil { + notBefore = *d.Nbf + } + expiration := "" + if d.Exp != nil { + expiration = *d.Exp } return &DelegationResult{ - ID: d.ID, - UcanID: d.UcanID, - Delegator: d.Delegator, - Delegate: d.Delegate, - Resource: d.Resource, - Action: d.Action, - Caveats: d.Caveats, - Depth: d.Depth, - Status: d.Status, - CreatedAt: d.CreatedAt, - ExpiresAt: expiresAt, + ID: d.ID, + CID: d.Cid, + Issuer: d.Iss, + Audience: d.Aud, + Subject: subject, + Command: d.Cmd, + Policy: policy, + NotBefore: notBefore, + Expiration: expiration, + IsRoot: d.IsRoot == 1, + IsPowerline: d.IsPowerline == 1, + CreatedAt: d.CreatedAt, } } diff --git a/internal/keybase/actions_grant.go b/internal/keybase/actions_grant.go index dbf9949..904c1c1 100644 --- a/internal/keybase/actions_grant.go +++ b/internal/keybase/actions_grant.go @@ -7,11 +7,11 @@ import ( ) type NewGrantInput struct { - ServiceID int64 `json:"service_id"` - UcanID int64 `json:"ucan_id,omitempty"` - Scopes json.RawMessage `json:"scopes"` - Accounts json.RawMessage `json:"accounts"` - ExpiresAt string `json:"expires_at,omitempty"` + ServiceID int64 `json:"service_id"` + DelegationCID string `json:"delegation_cid,omitempty"` + Scopes json.RawMessage `json:"scopes"` + Accounts json.RawMessage `json:"accounts"` + ExpiresAt string `json:"expires_at,omitempty"` } func (am *ActionManager) CreateGrant(ctx context.Context, params NewGrantInput) (*GrantResult, error) { @@ -22,9 +22,9 @@ func (am *ActionManager) CreateGrant(ctx context.Context, params NewGrantInput) return nil, fmt.Errorf("DID not initialized") } - var ucanID *int64 - if params.UcanID != 0 { - ucanID = ¶ms.UcanID + var delegationCID *string + if params.DelegationCID != "" { + delegationCID = ¶ms.DelegationCID } var expiresAt *string @@ -42,12 +42,12 @@ func (am *ActionManager) CreateGrant(ctx context.Context, params NewGrantInput) } g, err := am.kb.queries.CreateGrant(ctx, CreateGrantParams{ - DidID: am.kb.didID, - ServiceID: params.ServiceID, - UcanID: ucanID, - Scopes: scopes, - Accounts: accounts, - ExpiresAt: expiresAt, + DidID: am.kb.didID, + ServiceID: params.ServiceID, + DelegationCid: delegationCID, + Scopes: scopes, + Accounts: accounts, + ExpiresAt: expiresAt, }) if err != nil { return nil, fmt.Errorf("create grant: %w", err) diff --git a/internal/keybase/actions_ucan.go b/internal/keybase/actions_ucan.go deleted file mode 100644 index eb367e9..0000000 --- a/internal/keybase/actions_ucan.go +++ /dev/null @@ -1,205 +0,0 @@ -package keybase - -import ( - "context" - "encoding/json" - "fmt" -) - -type UCANResult struct { - ID int64 `json:"id"` - CID string `json:"cid"` - Issuer string `json:"issuer"` - Audience string `json:"audience"` - Subject string `json:"subject,omitempty"` - Capabilities json.RawMessage `json:"capabilities"` - NotBefore string `json:"not_before,omitempty"` - ExpiresAt string `json:"expires_at"` - IsRevoked bool `json:"is_revoked"` - CreatedAt string `json:"created_at"` -} - -type NewUCANInput struct { - CID string `json:"cid"` - Issuer string `json:"issuer"` - Audience string `json:"audience"` - Subject string `json:"subject,omitempty"` - Capabilities json.RawMessage `json:"capabilities"` - ProofChain json.RawMessage `json:"proof_chain,omitempty"` - NotBefore string `json:"not_before,omitempty"` - ExpiresAt string `json:"expires_at"` - Nonce string `json:"nonce,omitempty"` - Facts json.RawMessage `json:"facts,omitempty"` - Signature string `json:"signature"` - RawToken string `json:"raw_token"` -} - -func (am *ActionManager) CreateUCAN(ctx context.Context, params NewUCANInput) (*UCANResult, error) { - am.kb.mu.Lock() - defer am.kb.mu.Unlock() - - if am.kb.didID == 0 { - return nil, fmt.Errorf("DID not initialized") - } - - var subject, notBefore, nonce *string - if params.Subject != "" { - subject = ¶ms.Subject - } - if params.NotBefore != "" { - notBefore = ¶ms.NotBefore - } - if params.Nonce != "" { - nonce = ¶ms.Nonce - } - - proofChain := params.ProofChain - if proofChain == nil { - proofChain = json.RawMessage(`[]`) - } - facts := params.Facts - if facts == nil { - facts = json.RawMessage(`{}`) - } - - ucan, err := am.kb.queries.CreateUCAN(ctx, CreateUCANParams{ - DidID: am.kb.didID, - Cid: params.CID, - Issuer: params.Issuer, - Audience: params.Audience, - Subject: subject, - Capabilities: params.Capabilities, - ProofChain: proofChain, - NotBefore: notBefore, - ExpiresAt: params.ExpiresAt, - Nonce: nonce, - Facts: facts, - Signature: params.Signature, - RawToken: params.RawToken, - }) - if err != nil { - return nil, fmt.Errorf("create ucan: %w", err) - } - - return ucanToResult(&ucan), nil -} - -func (am *ActionManager) ListUCANs(ctx context.Context) ([]UCANResult, error) { - am.kb.mu.RLock() - defer am.kb.mu.RUnlock() - - if am.kb.didID == 0 { - return []UCANResult{}, nil - } - - ucans, err := am.kb.queries.ListUCANsByDID(ctx, am.kb.didID) - if err != nil { - return nil, fmt.Errorf("list ucans: %w", err) - } - - results := make([]UCANResult, len(ucans)) - for i, u := range ucans { - results[i] = *ucanToResult(&u) - } - - return results, nil -} - -func (am *ActionManager) GetUCANByCID(ctx context.Context, cid string) (*UCANResult, error) { - am.kb.mu.RLock() - defer am.kb.mu.RUnlock() - - ucan, err := am.kb.queries.GetUCANByCID(ctx, cid) - if err != nil { - return nil, fmt.Errorf("get ucan: %w", err) - } - - return ucanToResult(&ucan), nil -} - -func (am *ActionManager) ListUCANsByAudience(ctx context.Context, audience string) ([]UCANResult, error) { - am.kb.mu.RLock() - defer am.kb.mu.RUnlock() - - ucans, err := am.kb.queries.ListUCANsByAudience(ctx, audience) - if err != nil { - return nil, fmt.Errorf("list ucans by audience: %w", err) - } - - results := make([]UCANResult, len(ucans)) - for i, u := range ucans { - results[i] = *ucanToResult(&u) - } - - return results, nil -} - -func (am *ActionManager) RevokeUCAN(ctx context.Context, cid string) error { - am.kb.mu.Lock() - defer am.kb.mu.Unlock() - - return am.kb.queries.RevokeUCAN(ctx, cid) -} - -func (am *ActionManager) IsUCANRevoked(ctx context.Context, cid string) (bool, error) { - am.kb.mu.RLock() - defer am.kb.mu.RUnlock() - - revoked, err := am.kb.queries.IsUCANRevoked(ctx, cid) - if err != nil { - return false, fmt.Errorf("check ucan revocation: %w", err) - } - - return revoked == 1, nil -} - -func (am *ActionManager) CreateRevocation(ctx context.Context, ucanCID string, revokedBy string, reason string) error { - am.kb.mu.Lock() - defer am.kb.mu.Unlock() - - var reasonPtr *string - if reason != "" { - reasonPtr = &reason - } - - if err := am.kb.queries.RevokeUCAN(ctx, ucanCID); err != nil { - return fmt.Errorf("revoke ucan token: %w", err) - } - - return am.kb.queries.CreateRevocation(ctx, CreateRevocationParams{ - UcanCid: ucanCID, - RevokedBy: revokedBy, - Reason: reasonPtr, - }) -} - -func (am *ActionManager) CleanExpiredUCANs(ctx context.Context) error { - am.kb.mu.Lock() - defer am.kb.mu.Unlock() - - return am.kb.queries.CleanExpiredUCANs(ctx) -} - -func ucanToResult(u *UcanToken) *UCANResult { - subject := "" - if u.Subject != nil { - subject = *u.Subject - } - notBefore := "" - if u.NotBefore != nil { - notBefore = *u.NotBefore - } - - return &UCANResult{ - ID: u.ID, - CID: u.Cid, - Issuer: u.Issuer, - Audience: u.Audience, - Subject: subject, - Capabilities: u.Capabilities, - NotBefore: notBefore, - ExpiresAt: u.ExpiresAt, - IsRevoked: u.IsRevoked == 1, - CreatedAt: u.CreatedAt, - } -} diff --git a/internal/keybase/db.go b/internal/keybase/db.go index 111c37b..cf16c1f 100644 --- a/internal/keybase/db.go +++ b/internal/keybase/db.go @@ -10,10 +10,10 @@ import ( ) type DBTX interface { - ExecContext(context.Context, string, ...any) (sql.Result, error) + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) PrepareContext(context.Context, string) (*sql.Stmt, error) - QueryContext(context.Context, string, ...any) (*sql.Rows, error) - QueryRowContext(context.Context, string, ...any) *sql.Row + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row } func New(db DBTX) *Queries { diff --git a/internal/keybase/models.go b/internal/keybase/models.go index 2f44611..65b3e81 100644 --- a/internal/keybase/models.go +++ b/internal/keybase/models.go @@ -40,22 +40,6 @@ type Credential struct { LastUsed string `json:"last_used"` } -type Delegation struct { - ID int64 `json:"id"` - DidID int64 `json:"did_id"` - UcanID int64 `json:"ucan_id"` - Delegator string `json:"delegator"` - Delegate string `json:"delegate"` - Resource string `json:"resource"` - Action string `json:"action"` - Caveats json.RawMessage `json:"caveats"` - ParentID *int64 `json:"parent_id"` - Depth int64 `json:"depth"` - Status string `json:"status"` - CreatedAt string `json:"created_at"` - ExpiresAt *string `json:"expires_at"` -} - type DidDocument struct { ID int64 `json:"id"` Did string `json:"did"` @@ -68,16 +52,16 @@ type DidDocument struct { } type Grant struct { - ID int64 `json:"id"` - DidID int64 `json:"did_id"` - ServiceID int64 `json:"service_id"` - UcanID *int64 `json:"ucan_id"` - Scopes json.RawMessage `json:"scopes"` - Accounts json.RawMessage `json:"accounts"` - Status string `json:"status"` - GrantedAt string `json:"granted_at"` - LastUsed *string `json:"last_used"` - ExpiresAt *string `json:"expires_at"` + ID int64 `json:"id"` + DidID int64 `json:"did_id"` + ServiceID int64 `json:"service_id"` + DelegationCid *string `json:"delegation_cid"` + Scopes json.RawMessage `json:"scopes"` + Accounts json.RawMessage `json:"accounts"` + Status string `json:"status"` + GrantedAt string `json:"granted_at"` + LastUsed *string `json:"last_used"` + ExpiresAt *string `json:"expires_at"` } type KeyShare struct { @@ -131,31 +115,47 @@ type SyncCheckpoint struct { LastSynced string `json:"last_synced"` } -type UcanRevocation struct { - ID int64 `json:"id"` - UcanCid string `json:"ucan_cid"` - RevokedBy string `json:"revoked_by"` - Reason *string `json:"reason"` - RevokedAt string `json:"revoked_at"` +type UcanDelegation struct { + ID int64 `json:"id"` + DidID int64 `json:"did_id"` + Cid string `json:"cid"` + Envelope []byte `json:"envelope"` + Iss string `json:"iss"` + Aud string `json:"aud"` + Sub *string `json:"sub"` + Cmd string `json:"cmd"` + Pol *string `json:"pol"` + Nbf *string `json:"nbf"` + Exp *string `json:"exp"` + IsRoot int64 `json:"is_root"` + IsPowerline int64 `json:"is_powerline"` + CreatedAt string `json:"created_at"` } -type UcanToken struct { - ID int64 `json:"id"` - DidID int64 `json:"did_id"` - Cid string `json:"cid"` - Issuer string `json:"issuer"` - Audience string `json:"audience"` - Subject *string `json:"subject"` - Capabilities json.RawMessage `json:"capabilities"` - ProofChain json.RawMessage `json:"proof_chain"` - NotBefore *string `json:"not_before"` - ExpiresAt string `json:"expires_at"` - Nonce *string `json:"nonce"` - Facts json.RawMessage `json:"facts"` - Signature string `json:"signature"` - RawToken string `json:"raw_token"` - IsRevoked int64 `json:"is_revoked"` - CreatedAt string `json:"created_at"` +type UcanInvocation struct { + ID int64 `json:"id"` + DidID int64 `json:"did_id"` + Cid string `json:"cid"` + Envelope []byte `json:"envelope"` + Iss string `json:"iss"` + Sub string `json:"sub"` + Aud *string `json:"aud"` + Cmd string `json:"cmd"` + Prf string `json:"prf"` + Exp *string `json:"exp"` + Iat *string `json:"iat"` + ExecutedAt *string `json:"executed_at"` + ResultCid *string `json:"result_cid"` + CreatedAt string `json:"created_at"` +} + +type UcanRevocation struct { + ID int64 `json:"id"` + DelegationCid string `json:"delegation_cid"` + RevokedBy string `json:"revoked_by"` + InvocationCid *string `json:"invocation_cid"` + Reason *string `json:"reason"` + RevokedAt string `json:"revoked_at"` } type VerificationMethod struct { diff --git a/internal/keybase/querier.go b/internal/keybase/querier.go index c04bbb9..43e960b 100644 --- a/internal/keybase/querier.go +++ b/internal/keybase/querier.go @@ -10,22 +10,27 @@ import ( type Querier interface { ArchiveKeyShare(ctx context.Context, id int64) error - CleanExpiredUCANs(ctx context.Context) error + CleanExpiredDelegations(ctx context.Context) error + CleanOldInvocations(ctx context.Context) error CountActiveGrants(ctx context.Context, didID int64) (int64, error) CountCredentialsByDID(ctx context.Context, didID int64) (int64, error) CreateAccount(ctx context.Context, arg CreateAccountParams) (Account, error) CreateCredential(ctx context.Context, arg CreateCredentialParams) (Credential, error) CreateDID(ctx context.Context, arg CreateDIDParams) (DidDocument, error) - CreateDelegation(ctx context.Context, arg CreateDelegationParams) (Delegation, error) + CreateDelegation(ctx context.Context, arg CreateDelegationParams) (UcanDelegation, error) CreateGrant(ctx context.Context, arg CreateGrantParams) (Grant, error) + CreateInvocation(ctx context.Context, arg CreateInvocationParams) (UcanInvocation, error) CreateKeyShare(ctx context.Context, arg CreateKeyShareParams) (KeyShare, error) + // ============================================================================= + // UCAN REVOCATION QUERIES + // ============================================================================= CreateRevocation(ctx context.Context, arg CreateRevocationParams) error CreateService(ctx context.Context, arg CreateServiceParams) (Service, error) CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error) - CreateUCAN(ctx context.Context, arg CreateUCANParams) (UcanToken, error) CreateVerificationMethod(ctx context.Context, arg CreateVerificationMethodParams) (VerificationMethod, error) DeleteAccount(ctx context.Context, arg DeleteAccountParams) error DeleteCredential(ctx context.Context, arg DeleteCredentialParams) error + DeleteDelegation(ctx context.Context, arg DeleteDelegationParams) error DeleteExpiredSessions(ctx context.Context) error DeleteKeyShare(ctx context.Context, arg DeleteKeyShareParams) error DeleteSession(ctx context.Context, id int64) error @@ -39,10 +44,20 @@ type Querier interface { GetDIDByDID(ctx context.Context, did string) (DidDocument, error) GetDIDByID(ctx context.Context, id int64) (DidDocument, error) GetDefaultAccount(ctx context.Context, arg GetDefaultAccountParams) (Account, error) - GetDelegationChain(ctx context.Context, arg GetDelegationChainParams) ([]Delegation, error) + // ============================================================================= + // UCAN DELEGATION QUERIES (v1.0.0-rc.1) + // ============================================================================= + GetDelegationByCID(ctx context.Context, cid string) (UcanDelegation, error) + GetDelegationEnvelopeByCID(ctx context.Context, cid string) ([]byte, error) GetGrantByService(ctx context.Context, arg GetGrantByServiceParams) (Grant, error) + // ============================================================================= + // UCAN INVOCATION QUERIES (v1.0.0-rc.1) + // ============================================================================= + GetInvocationByCID(ctx context.Context, cid string) (UcanInvocation, error) + GetInvocationEnvelopeByCID(ctx context.Context, cid string) ([]byte, error) GetKeyShareByID(ctx context.Context, shareID string) (KeyShare, error) GetKeyShareByKeyID(ctx context.Context, arg GetKeyShareByKeyIDParams) (KeyShare, error) + GetRevocation(ctx context.Context, delegationCid string) (UcanRevocation, error) GetServiceByID(ctx context.Context, id int64) (Service, error) // ============================================================================= // SERVICE QUERIES @@ -53,9 +68,8 @@ type Querier interface { // SYNC QUERIES // ============================================================================= GetSyncCheckpoint(ctx context.Context, arg GetSyncCheckpointParams) (SyncCheckpoint, error) - GetUCANByCID(ctx context.Context, cid string) (UcanToken, error) GetVerificationMethod(ctx context.Context, arg GetVerificationMethodParams) (VerificationMethod, error) - IsUCANRevoked(ctx context.Context, ucanCid string) (int64, error) + IsDelegationRevoked(ctx context.Context, delegationCid string) (int64, error) ListAccountsByChain(ctx context.Context, arg ListAccountsByChainParams) ([]Account, error) // ============================================================================= // ACCOUNT QUERIES @@ -66,41 +80,41 @@ type Querier interface { // CREDENTIAL QUERIES // ============================================================================= ListCredentialsByDID(ctx context.Context, didID int64) ([]Credential, error) - ListDelegationsByDelegate(ctx context.Context, delegate string) ([]Delegation, error) - // ============================================================================= - // DELEGATION QUERIES - // ============================================================================= - ListDelegationsByDelegator(ctx context.Context, delegator string) ([]Delegation, error) - ListDelegationsForResource(ctx context.Context, arg ListDelegationsForResourceParams) ([]Delegation, error) + ListDelegationsByAudience(ctx context.Context, aud string) ([]UcanDelegation, error) + ListDelegationsByDID(ctx context.Context, didID int64) ([]UcanDelegation, error) + ListDelegationsByIssuer(ctx context.Context, iss string) ([]UcanDelegation, error) + ListDelegationsBySubject(ctx context.Context, sub *string) ([]UcanDelegation, error) + ListDelegationsForCommand(ctx context.Context, arg ListDelegationsForCommandParams) ([]UcanDelegation, error) // ============================================================================= // GRANT QUERIES // ============================================================================= ListGrantsByDID(ctx context.Context, didID int64) ([]ListGrantsByDIDRow, error) + ListInvocationsByDID(ctx context.Context, arg ListInvocationsByDIDParams) ([]UcanInvocation, error) + ListInvocationsByIssuer(ctx context.Context, arg ListInvocationsByIssuerParams) ([]UcanInvocation, error) + ListInvocationsBySubject(ctx context.Context, arg ListInvocationsBySubjectParams) ([]UcanInvocation, error) + ListInvocationsForCommand(ctx context.Context, arg ListInvocationsForCommandParams) ([]UcanInvocation, error) // ============================================================================= // KEY SHARE QUERIES // ============================================================================= ListKeySharesByDID(ctx context.Context, didID int64) ([]KeyShare, error) + ListPendingInvocations(ctx context.Context, didID int64) ([]UcanInvocation, error) + ListPowerlineDelegations(ctx context.Context, didID int64) ([]UcanDelegation, error) + ListRevocationsByRevoker(ctx context.Context, revokedBy string) ([]UcanRevocation, error) + ListRootDelegations(ctx context.Context, didID int64) ([]UcanDelegation, error) // ============================================================================= // SESSION QUERIES // ============================================================================= ListSessionsByDID(ctx context.Context, didID int64) ([]ListSessionsByDIDRow, error) ListSyncCheckpoints(ctx context.Context, didID int64) ([]SyncCheckpoint, error) - ListUCANsByAudience(ctx context.Context, audience string) ([]UcanToken, error) - // ============================================================================= - // UCAN TOKEN QUERIES - // ============================================================================= - ListUCANsByDID(ctx context.Context, didID int64) ([]UcanToken, error) // ============================================================================= // VERIFICATION METHOD QUERIES // ============================================================================= ListVerificationMethods(ctx context.Context, didID int64) ([]VerificationMethod, error) ListVerifiedServices(ctx context.Context) ([]Service, error) + MarkInvocationExecuted(ctx context.Context, arg MarkInvocationExecutedParams) error ReactivateGrant(ctx context.Context, id int64) error RenameCredential(ctx context.Context, arg RenameCredentialParams) error - RevokeDelegation(ctx context.Context, id int64) error - RevokeDelegationChain(ctx context.Context, arg RevokeDelegationChainParams) error RevokeGrant(ctx context.Context, id int64) error - RevokeUCAN(ctx context.Context, cid string) error RotateKeyShare(ctx context.Context, id int64) error SetCurrentSession(ctx context.Context, arg SetCurrentSessionParams) error SetDefaultAccount(ctx context.Context, arg SetDefaultAccountParams) error diff --git a/internal/keybase/query.sql.go b/internal/keybase/query.sql.go index 09086ce..289f411 100644 --- a/internal/keybase/query.sql.go +++ b/internal/keybase/query.sql.go @@ -19,12 +19,21 @@ func (q *Queries) ArchiveKeyShare(ctx context.Context, id int64) error { return err } -const cleanExpiredUCANs = `-- name: CleanExpiredUCANs :exec -DELETE FROM ucan_tokens WHERE expires_at < datetime('now', '-30 days') +const cleanExpiredDelegations = `-- name: CleanExpiredDelegations :exec +DELETE FROM ucan_delegations WHERE exp < datetime('now', '-30 days') ` -func (q *Queries) CleanExpiredUCANs(ctx context.Context) error { - _, err := q.db.ExecContext(ctx, cleanExpiredUCANs) +func (q *Queries) CleanExpiredDelegations(ctx context.Context) error { + _, err := q.db.ExecContext(ctx, cleanExpiredDelegations) + return err +} + +const cleanOldInvocations = `-- name: CleanOldInvocations :exec +DELETE FROM ucan_invocations WHERE created_at < datetime('now', '-90 days') +` + +func (q *Queries) CleanOldInvocations(ctx context.Context) error { + _, err := q.db.ExecContext(ctx, cleanOldInvocations) return err } @@ -188,78 +197,83 @@ func (q *Queries) CreateDID(ctx context.Context, arg CreateDIDParams) (DidDocume } const createDelegation = `-- name: CreateDelegation :one -INSERT INTO delegations ( - did_id, ucan_id, delegator, delegate, resource, action, caveats, parent_id, depth, expires_at +INSERT INTO ucan_delegations ( + did_id, cid, envelope, iss, aud, sub, cmd, pol, nbf, exp, is_root, is_powerline ) -VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) -RETURNING id, did_id, ucan_id, delegator, delegate, resource, "action", caveats, parent_id, depth, status, created_at, expires_at +VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +RETURNING id, did_id, cid, envelope, iss, aud, sub, cmd, pol, nbf, exp, is_root, is_powerline, created_at ` type CreateDelegationParams struct { - DidID int64 `json:"did_id"` - UcanID int64 `json:"ucan_id"` - Delegator string `json:"delegator"` - Delegate string `json:"delegate"` - Resource string `json:"resource"` - Action string `json:"action"` - Caveats json.RawMessage `json:"caveats"` - ParentID *int64 `json:"parent_id"` - Depth int64 `json:"depth"` - ExpiresAt *string `json:"expires_at"` + DidID int64 `json:"did_id"` + Cid string `json:"cid"` + Envelope []byte `json:"envelope"` + Iss string `json:"iss"` + Aud string `json:"aud"` + Sub *string `json:"sub"` + Cmd string `json:"cmd"` + Pol *string `json:"pol"` + Nbf *string `json:"nbf"` + Exp *string `json:"exp"` + IsRoot int64 `json:"is_root"` + IsPowerline int64 `json:"is_powerline"` } -func (q *Queries) CreateDelegation(ctx context.Context, arg CreateDelegationParams) (Delegation, error) { +func (q *Queries) CreateDelegation(ctx context.Context, arg CreateDelegationParams) (UcanDelegation, error) { row := q.db.QueryRowContext(ctx, createDelegation, arg.DidID, - arg.UcanID, - arg.Delegator, - arg.Delegate, - arg.Resource, - arg.Action, - arg.Caveats, - arg.ParentID, - arg.Depth, - arg.ExpiresAt, + arg.Cid, + arg.Envelope, + arg.Iss, + arg.Aud, + arg.Sub, + arg.Cmd, + arg.Pol, + arg.Nbf, + arg.Exp, + arg.IsRoot, + arg.IsPowerline, ) - var i Delegation + var i UcanDelegation err := row.Scan( &i.ID, &i.DidID, - &i.UcanID, - &i.Delegator, - &i.Delegate, - &i.Resource, - &i.Action, - &i.Caveats, - &i.ParentID, - &i.Depth, - &i.Status, + &i.Cid, + &i.Envelope, + &i.Iss, + &i.Aud, + &i.Sub, + &i.Cmd, + &i.Pol, + &i.Nbf, + &i.Exp, + &i.IsRoot, + &i.IsPowerline, &i.CreatedAt, - &i.ExpiresAt, ) return i, err } const createGrant = `-- name: CreateGrant :one -INSERT INTO grants (did_id, service_id, ucan_id, scopes, accounts, expires_at) +INSERT INTO grants (did_id, service_id, delegation_cid, scopes, accounts, expires_at) VALUES (?, ?, ?, ?, ?, ?) -RETURNING id, did_id, service_id, ucan_id, scopes, accounts, status, granted_at, last_used, expires_at +RETURNING id, did_id, service_id, delegation_cid, scopes, accounts, status, granted_at, last_used, expires_at ` type CreateGrantParams struct { - DidID int64 `json:"did_id"` - ServiceID int64 `json:"service_id"` - UcanID *int64 `json:"ucan_id"` - Scopes json.RawMessage `json:"scopes"` - Accounts json.RawMessage `json:"accounts"` - ExpiresAt *string `json:"expires_at"` + DidID int64 `json:"did_id"` + ServiceID int64 `json:"service_id"` + DelegationCid *string `json:"delegation_cid"` + Scopes json.RawMessage `json:"scopes"` + Accounts json.RawMessage `json:"accounts"` + ExpiresAt *string `json:"expires_at"` } func (q *Queries) CreateGrant(ctx context.Context, arg CreateGrantParams) (Grant, error) { row := q.db.QueryRowContext(ctx, createGrant, arg.DidID, arg.ServiceID, - arg.UcanID, + arg.DelegationCid, arg.Scopes, arg.Accounts, arg.ExpiresAt, @@ -269,7 +283,7 @@ func (q *Queries) CreateGrant(ctx context.Context, arg CreateGrantParams) (Grant &i.ID, &i.DidID, &i.ServiceID, - &i.UcanID, + &i.DelegationCid, &i.Scopes, &i.Accounts, &i.Status, @@ -280,6 +294,60 @@ func (q *Queries) CreateGrant(ctx context.Context, arg CreateGrantParams) (Grant return i, err } +const createInvocation = `-- name: CreateInvocation :one +INSERT INTO ucan_invocations ( + did_id, cid, envelope, iss, sub, aud, cmd, prf, exp, iat +) +VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +RETURNING id, did_id, cid, envelope, iss, sub, aud, cmd, prf, exp, iat, executed_at, result_cid, created_at +` + +type CreateInvocationParams struct { + DidID int64 `json:"did_id"` + Cid string `json:"cid"` + Envelope []byte `json:"envelope"` + Iss string `json:"iss"` + Sub string `json:"sub"` + Aud *string `json:"aud"` + Cmd string `json:"cmd"` + Prf string `json:"prf"` + Exp *string `json:"exp"` + Iat *string `json:"iat"` +} + +func (q *Queries) CreateInvocation(ctx context.Context, arg CreateInvocationParams) (UcanInvocation, error) { + row := q.db.QueryRowContext(ctx, createInvocation, + arg.DidID, + arg.Cid, + arg.Envelope, + arg.Iss, + arg.Sub, + arg.Aud, + arg.Cmd, + arg.Prf, + arg.Exp, + arg.Iat, + ) + var i UcanInvocation + err := row.Scan( + &i.ID, + &i.DidID, + &i.Cid, + &i.Envelope, + &i.Iss, + &i.Sub, + &i.Aud, + &i.Cmd, + &i.Prf, + &i.Exp, + &i.Iat, + &i.ExecutedAt, + &i.ResultCid, + &i.CreatedAt, + ) + return i, err +} + const createKeyShare = `-- name: CreateKeyShare :one INSERT INTO key_shares ( did_id, share_id, key_id, party_index, threshold, total_parties, @@ -339,18 +407,28 @@ func (q *Queries) CreateKeyShare(ctx context.Context, arg CreateKeyShareParams) } const createRevocation = `-- name: CreateRevocation :exec -INSERT INTO ucan_revocations (ucan_cid, revoked_by, reason) -VALUES (?, ?, ?) + +INSERT INTO ucan_revocations (delegation_cid, revoked_by, invocation_cid, reason) +VALUES (?, ?, ?, ?) ` type CreateRevocationParams struct { - UcanCid string `json:"ucan_cid"` - RevokedBy string `json:"revoked_by"` - Reason *string `json:"reason"` + DelegationCid string `json:"delegation_cid"` + RevokedBy string `json:"revoked_by"` + InvocationCid *string `json:"invocation_cid"` + Reason *string `json:"reason"` } +// ============================================================================= +// UCAN REVOCATION QUERIES +// ============================================================================= func (q *Queries) CreateRevocation(ctx context.Context, arg CreateRevocationParams) error { - _, err := q.db.ExecContext(ctx, createRevocation, arg.UcanCid, arg.RevokedBy, arg.Reason) + _, err := q.db.ExecContext(ctx, createRevocation, + arg.DelegationCid, + arg.RevokedBy, + arg.InvocationCid, + arg.Reason, + ) return err } @@ -434,69 +512,6 @@ func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (S return i, err } -const createUCAN = `-- name: CreateUCAN :one -INSERT INTO ucan_tokens ( - did_id, cid, issuer, audience, subject, capabilities, - proof_chain, not_before, expires_at, nonce, facts, signature, raw_token -) -VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) -RETURNING id, did_id, cid, issuer, audience, subject, capabilities, proof_chain, not_before, expires_at, nonce, facts, signature, raw_token, is_revoked, created_at -` - -type CreateUCANParams struct { - DidID int64 `json:"did_id"` - Cid string `json:"cid"` - Issuer string `json:"issuer"` - Audience string `json:"audience"` - Subject *string `json:"subject"` - Capabilities json.RawMessage `json:"capabilities"` - ProofChain json.RawMessage `json:"proof_chain"` - NotBefore *string `json:"not_before"` - ExpiresAt string `json:"expires_at"` - Nonce *string `json:"nonce"` - Facts json.RawMessage `json:"facts"` - Signature string `json:"signature"` - RawToken string `json:"raw_token"` -} - -func (q *Queries) CreateUCAN(ctx context.Context, arg CreateUCANParams) (UcanToken, error) { - row := q.db.QueryRowContext(ctx, createUCAN, - arg.DidID, - arg.Cid, - arg.Issuer, - arg.Audience, - arg.Subject, - arg.Capabilities, - arg.ProofChain, - arg.NotBefore, - arg.ExpiresAt, - arg.Nonce, - arg.Facts, - arg.Signature, - arg.RawToken, - ) - var i UcanToken - err := row.Scan( - &i.ID, - &i.DidID, - &i.Cid, - &i.Issuer, - &i.Audience, - &i.Subject, - &i.Capabilities, - &i.ProofChain, - &i.NotBefore, - &i.ExpiresAt, - &i.Nonce, - &i.Facts, - &i.Signature, - &i.RawToken, - &i.IsRevoked, - &i.CreatedAt, - ) - return i, err -} - const createVerificationMethod = `-- name: CreateVerificationMethod :one INSERT INTO verification_methods (did_id, method_id, method_type, controller, public_key, purpose) VALUES (?, ?, ?, ?, ?, ?) @@ -563,6 +578,20 @@ func (q *Queries) DeleteCredential(ctx context.Context, arg DeleteCredentialPara return err } +const deleteDelegation = `-- name: DeleteDelegation :exec +DELETE FROM ucan_delegations WHERE cid = ? AND did_id = ? +` + +type DeleteDelegationParams struct { + Cid string `json:"cid"` + DidID int64 `json:"did_id"` +} + +func (q *Queries) DeleteDelegation(ctx context.Context, arg DeleteDelegationParams) error { + _, err := q.db.ExecContext(ctx, deleteDelegation, arg.Cid, arg.DidID) + return err +} + const deleteExpiredSessions = `-- name: DeleteExpiredSessions :exec DELETE FROM sessions WHERE expires_at < datetime('now') ` @@ -747,54 +776,49 @@ func (q *Queries) GetDefaultAccount(ctx context.Context, arg GetDefaultAccountPa return i, err } -const getDelegationChain = `-- name: GetDelegationChain :many -SELECT id, did_id, ucan_id, delegator, delegate, resource, "action", caveats, parent_id, depth, status, created_at, expires_at FROM delegations WHERE id = ? OR parent_id = ? ORDER BY depth DESC +const getDelegationByCID = `-- name: GetDelegationByCID :one + +SELECT id, did_id, cid, envelope, iss, aud, sub, cmd, pol, nbf, exp, is_root, is_powerline, created_at FROM ucan_delegations WHERE cid = ? LIMIT 1 ` -type GetDelegationChainParams struct { - ID int64 `json:"id"` - ParentID *int64 `json:"parent_id"` +// ============================================================================= +// UCAN DELEGATION QUERIES (v1.0.0-rc.1) +// ============================================================================= +func (q *Queries) GetDelegationByCID(ctx context.Context, cid string) (UcanDelegation, error) { + row := q.db.QueryRowContext(ctx, getDelegationByCID, cid) + var i UcanDelegation + err := row.Scan( + &i.ID, + &i.DidID, + &i.Cid, + &i.Envelope, + &i.Iss, + &i.Aud, + &i.Sub, + &i.Cmd, + &i.Pol, + &i.Nbf, + &i.Exp, + &i.IsRoot, + &i.IsPowerline, + &i.CreatedAt, + ) + return i, err } -func (q *Queries) GetDelegationChain(ctx context.Context, arg GetDelegationChainParams) ([]Delegation, error) { - rows, err := q.db.QueryContext(ctx, getDelegationChain, arg.ID, arg.ParentID) - if err != nil { - return nil, err - } - defer rows.Close() - items := []Delegation{} - for rows.Next() { - var i Delegation - if err := rows.Scan( - &i.ID, - &i.DidID, - &i.UcanID, - &i.Delegator, - &i.Delegate, - &i.Resource, - &i.Action, - &i.Caveats, - &i.ParentID, - &i.Depth, - &i.Status, - &i.CreatedAt, - &i.ExpiresAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil +const getDelegationEnvelopeByCID = `-- name: GetDelegationEnvelopeByCID :one +SELECT envelope FROM ucan_delegations WHERE cid = ? LIMIT 1 +` + +func (q *Queries) GetDelegationEnvelopeByCID(ctx context.Context, cid string) ([]byte, error) { + row := q.db.QueryRowContext(ctx, getDelegationEnvelopeByCID, cid) + var envelope []byte + err := row.Scan(&envelope) + return envelope, err } const getGrantByService = `-- name: GetGrantByService :one -SELECT id, did_id, service_id, ucan_id, scopes, accounts, status, granted_at, last_used, expires_at FROM grants WHERE did_id = ? AND service_id = ? LIMIT 1 +SELECT id, did_id, service_id, delegation_cid, scopes, accounts, status, granted_at, last_used, expires_at FROM grants WHERE did_id = ? AND service_id = ? LIMIT 1 ` type GetGrantByServiceParams struct { @@ -809,7 +833,7 @@ func (q *Queries) GetGrantByService(ctx context.Context, arg GetGrantByServicePa &i.ID, &i.DidID, &i.ServiceID, - &i.UcanID, + &i.DelegationCid, &i.Scopes, &i.Accounts, &i.Status, @@ -820,6 +844,47 @@ func (q *Queries) GetGrantByService(ctx context.Context, arg GetGrantByServicePa return i, err } +const getInvocationByCID = `-- name: GetInvocationByCID :one + +SELECT id, did_id, cid, envelope, iss, sub, aud, cmd, prf, exp, iat, executed_at, result_cid, created_at FROM ucan_invocations WHERE cid = ? LIMIT 1 +` + +// ============================================================================= +// UCAN INVOCATION QUERIES (v1.0.0-rc.1) +// ============================================================================= +func (q *Queries) GetInvocationByCID(ctx context.Context, cid string) (UcanInvocation, error) { + row := q.db.QueryRowContext(ctx, getInvocationByCID, cid) + var i UcanInvocation + err := row.Scan( + &i.ID, + &i.DidID, + &i.Cid, + &i.Envelope, + &i.Iss, + &i.Sub, + &i.Aud, + &i.Cmd, + &i.Prf, + &i.Exp, + &i.Iat, + &i.ExecutedAt, + &i.ResultCid, + &i.CreatedAt, + ) + return i, err +} + +const getInvocationEnvelopeByCID = `-- name: GetInvocationEnvelopeByCID :one +SELECT envelope FROM ucan_invocations WHERE cid = ? LIMIT 1 +` + +func (q *Queries) GetInvocationEnvelopeByCID(ctx context.Context, cid string) ([]byte, error) { + row := q.db.QueryRowContext(ctx, getInvocationEnvelopeByCID, cid) + var envelope []byte + err := row.Scan(&envelope) + return envelope, err +} + const getKeyShareByID = `-- name: GetKeyShareByID :one SELECT id, did_id, share_id, key_id, party_index, threshold, total_parties, curve, share_data, public_key, chain_code, derivation_path, status, created_at, rotated_at FROM key_shares WHERE share_id = ? LIMIT 1 ` @@ -879,6 +944,24 @@ func (q *Queries) GetKeyShareByKeyID(ctx context.Context, arg GetKeyShareByKeyID return i, err } +const getRevocation = `-- name: GetRevocation :one +SELECT id, delegation_cid, revoked_by, invocation_cid, reason, revoked_at FROM ucan_revocations WHERE delegation_cid = ? LIMIT 1 +` + +func (q *Queries) GetRevocation(ctx context.Context, delegationCid string) (UcanRevocation, error) { + row := q.db.QueryRowContext(ctx, getRevocation, delegationCid) + var i UcanRevocation + err := row.Scan( + &i.ID, + &i.DelegationCid, + &i.RevokedBy, + &i.InvocationCid, + &i.Reason, + &i.RevokedAt, + ) + return i, err +} + const getServiceByID = `-- name: GetServiceByID :one SELECT id, origin, name, description, logo_url, did, is_verified, metadata, created_at FROM services WHERE id = ? LIMIT 1 ` @@ -973,34 +1056,6 @@ func (q *Queries) GetSyncCheckpoint(ctx context.Context, arg GetSyncCheckpointPa return i, err } -const getUCANByCID = `-- name: GetUCANByCID :one -SELECT id, did_id, cid, issuer, audience, subject, capabilities, proof_chain, not_before, expires_at, nonce, facts, signature, raw_token, is_revoked, created_at FROM ucan_tokens WHERE cid = ? LIMIT 1 -` - -func (q *Queries) GetUCANByCID(ctx context.Context, cid string) (UcanToken, error) { - row := q.db.QueryRowContext(ctx, getUCANByCID, cid) - var i UcanToken - err := row.Scan( - &i.ID, - &i.DidID, - &i.Cid, - &i.Issuer, - &i.Audience, - &i.Subject, - &i.Capabilities, - &i.ProofChain, - &i.NotBefore, - &i.ExpiresAt, - &i.Nonce, - &i.Facts, - &i.Signature, - &i.RawToken, - &i.IsRevoked, - &i.CreatedAt, - ) - return i, err -} - const getVerificationMethod = `-- name: GetVerificationMethod :one SELECT id, did_id, method_id, method_type, controller, public_key, purpose, created_at FROM verification_methods WHERE did_id = ? AND method_id = ? LIMIT 1 ` @@ -1026,12 +1081,12 @@ func (q *Queries) GetVerificationMethod(ctx context.Context, arg GetVerification return i, err } -const isUCANRevoked = `-- name: IsUCANRevoked :one -SELECT EXISTS(SELECT 1 FROM ucan_revocations WHERE ucan_cid = ?) as revoked +const isDelegationRevoked = `-- name: IsDelegationRevoked :one +SELECT EXISTS(SELECT 1 FROM ucan_revocations WHERE delegation_cid = ?) as revoked ` -func (q *Queries) IsUCANRevoked(ctx context.Context, ucanCid string) (int64, error) { - row := q.db.QueryRowContext(ctx, isUCANRevoked, ucanCid) +func (q *Queries) IsDelegationRevoked(ctx context.Context, delegationCid string) (int64, error) { + row := q.db.QueryRowContext(ctx, isDelegationRevoked, delegationCid) var revoked int64 err := row.Scan(&revoked) return revoked, err @@ -1083,7 +1138,7 @@ func (q *Queries) ListAccountsByChain(ctx context.Context, arg ListAccountsByCha const listAccountsByDID = `-- name: ListAccountsByDID :many -SELECT a.id, a.did_id, a.key_share_id, a.address, a.chain_id, a.coin_type, a.account_index, a.address_index, a.label, a.is_default, a.created_at, k.public_key, k.curve +SELECT a.id, a.did_id, a.key_share_id, a.address, a.chain_id, a.coin_type, a.account_index, a.address_index, a.label, a.is_default, a.created_at, k.public_key as share_public_key, k.curve FROM accounts a JOIN key_shares k ON a.key_share_id = k.id WHERE a.did_id = ? @@ -1091,19 +1146,19 @@ ORDER BY a.is_default DESC, a.created_at ` type ListAccountsByDIDRow struct { - ID int64 `json:"id"` - DidID int64 `json:"did_id"` - KeyShareID int64 `json:"key_share_id"` - Address string `json:"address"` - ChainID string `json:"chain_id"` - CoinType int64 `json:"coin_type"` - AccountIndex int64 `json:"account_index"` - AddressIndex int64 `json:"address_index"` - Label *string `json:"label"` - IsDefault int64 `json:"is_default"` - CreatedAt string `json:"created_at"` - PublicKey string `json:"public_key"` - Curve string `json:"curve"` + ID int64 `json:"id"` + DidID int64 `json:"did_id"` + KeyShareID int64 `json:"key_share_id"` + Address string `json:"address"` + ChainID string `json:"chain_id"` + CoinType int64 `json:"coin_type"` + AccountIndex int64 `json:"account_index"` + AddressIndex int64 `json:"address_index"` + Label *string `json:"label"` + IsDefault int64 `json:"is_default"` + CreatedAt string `json:"created_at"` + SharePublicKey string `json:"share_public_key"` + Curve string `json:"curve"` } // ============================================================================= @@ -1130,7 +1185,7 @@ func (q *Queries) ListAccountsByDID(ctx context.Context, didID int64) ([]ListAcc &i.Label, &i.IsDefault, &i.CreatedAt, - &i.PublicKey, + &i.SharePublicKey, &i.Curve, ); err != nil { return nil, err @@ -1229,35 +1284,36 @@ func (q *Queries) ListCredentialsByDID(ctx context.Context, didID int64) ([]Cred return items, nil } -const listDelegationsByDelegate = `-- name: ListDelegationsByDelegate :many -SELECT id, did_id, ucan_id, delegator, delegate, resource, "action", caveats, parent_id, depth, status, created_at, expires_at FROM delegations -WHERE delegate = ? AND status = 'active' AND (expires_at IS NULL OR expires_at > datetime('now')) +const listDelegationsByAudience = `-- name: ListDelegationsByAudience :many +SELECT id, did_id, cid, envelope, iss, aud, sub, cmd, pol, nbf, exp, is_root, is_powerline, created_at FROM ucan_delegations +WHERE aud = ? AND (exp IS NULL OR exp > datetime('now')) ORDER BY created_at DESC ` -func (q *Queries) ListDelegationsByDelegate(ctx context.Context, delegate string) ([]Delegation, error) { - rows, err := q.db.QueryContext(ctx, listDelegationsByDelegate, delegate) +func (q *Queries) ListDelegationsByAudience(ctx context.Context, aud string) ([]UcanDelegation, error) { + rows, err := q.db.QueryContext(ctx, listDelegationsByAudience, aud) if err != nil { return nil, err } defer rows.Close() - items := []Delegation{} + items := []UcanDelegation{} for rows.Next() { - var i Delegation + var i UcanDelegation if err := rows.Scan( &i.ID, &i.DidID, - &i.UcanID, - &i.Delegator, - &i.Delegate, - &i.Resource, - &i.Action, - &i.Caveats, - &i.ParentID, - &i.Depth, - &i.Status, + &i.Cid, + &i.Envelope, + &i.Iss, + &i.Aud, + &i.Sub, + &i.Cmd, + &i.Pol, + &i.Nbf, + &i.Exp, + &i.IsRoot, + &i.IsPowerline, &i.CreatedAt, - &i.ExpiresAt, ); err != nil { return nil, err } @@ -1272,39 +1328,36 @@ func (q *Queries) ListDelegationsByDelegate(ctx context.Context, delegate string return items, nil } -const listDelegationsByDelegator = `-- name: ListDelegationsByDelegator :many - -SELECT id, did_id, ucan_id, delegator, delegate, resource, "action", caveats, parent_id, depth, status, created_at, expires_at FROM delegations -WHERE delegator = ? AND status = 'active' +const listDelegationsByDID = `-- name: ListDelegationsByDID :many +SELECT id, did_id, cid, envelope, iss, aud, sub, cmd, pol, nbf, exp, is_root, is_powerline, created_at FROM ucan_delegations +WHERE did_id = ? AND (exp IS NULL OR exp > datetime('now')) ORDER BY created_at DESC ` -// ============================================================================= -// DELEGATION QUERIES -// ============================================================================= -func (q *Queries) ListDelegationsByDelegator(ctx context.Context, delegator string) ([]Delegation, error) { - rows, err := q.db.QueryContext(ctx, listDelegationsByDelegator, delegator) +func (q *Queries) ListDelegationsByDID(ctx context.Context, didID int64) ([]UcanDelegation, error) { + rows, err := q.db.QueryContext(ctx, listDelegationsByDID, didID) if err != nil { return nil, err } defer rows.Close() - items := []Delegation{} + items := []UcanDelegation{} for rows.Next() { - var i Delegation + var i UcanDelegation if err := rows.Scan( &i.ID, &i.DidID, - &i.UcanID, - &i.Delegator, - &i.Delegate, - &i.Resource, - &i.Action, - &i.Caveats, - &i.ParentID, - &i.Depth, - &i.Status, + &i.Cid, + &i.Envelope, + &i.Iss, + &i.Aud, + &i.Sub, + &i.Cmd, + &i.Pol, + &i.Nbf, + &i.Exp, + &i.IsRoot, + &i.IsPowerline, &i.CreatedAt, - &i.ExpiresAt, ); err != nil { return nil, err } @@ -1319,40 +1372,132 @@ func (q *Queries) ListDelegationsByDelegator(ctx context.Context, delegator stri return items, nil } -const listDelegationsForResource = `-- name: ListDelegationsForResource :many -SELECT id, did_id, ucan_id, delegator, delegate, resource, "action", caveats, parent_id, depth, status, created_at, expires_at FROM delegations -WHERE did_id = ? AND resource = ? AND status = 'active' -ORDER BY depth, created_at +const listDelegationsByIssuer = `-- name: ListDelegationsByIssuer :many +SELECT id, did_id, cid, envelope, iss, aud, sub, cmd, pol, nbf, exp, is_root, is_powerline, created_at FROM ucan_delegations +WHERE iss = ? AND (exp IS NULL OR exp > datetime('now')) +ORDER BY created_at DESC ` -type ListDelegationsForResourceParams struct { - DidID int64 `json:"did_id"` - Resource string `json:"resource"` -} - -func (q *Queries) ListDelegationsForResource(ctx context.Context, arg ListDelegationsForResourceParams) ([]Delegation, error) { - rows, err := q.db.QueryContext(ctx, listDelegationsForResource, arg.DidID, arg.Resource) +func (q *Queries) ListDelegationsByIssuer(ctx context.Context, iss string) ([]UcanDelegation, error) { + rows, err := q.db.QueryContext(ctx, listDelegationsByIssuer, iss) if err != nil { return nil, err } defer rows.Close() - items := []Delegation{} + items := []UcanDelegation{} for rows.Next() { - var i Delegation + var i UcanDelegation if err := rows.Scan( &i.ID, &i.DidID, - &i.UcanID, - &i.Delegator, - &i.Delegate, - &i.Resource, - &i.Action, - &i.Caveats, - &i.ParentID, - &i.Depth, - &i.Status, + &i.Cid, + &i.Envelope, + &i.Iss, + &i.Aud, + &i.Sub, + &i.Cmd, + &i.Pol, + &i.Nbf, + &i.Exp, + &i.IsRoot, + &i.IsPowerline, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listDelegationsBySubject = `-- name: ListDelegationsBySubject :many +SELECT id, did_id, cid, envelope, iss, aud, sub, cmd, pol, nbf, exp, is_root, is_powerline, created_at FROM ucan_delegations +WHERE sub = ? AND (exp IS NULL OR exp > datetime('now')) +ORDER BY created_at DESC +` + +func (q *Queries) ListDelegationsBySubject(ctx context.Context, sub *string) ([]UcanDelegation, error) { + rows, err := q.db.QueryContext(ctx, listDelegationsBySubject, sub) + if err != nil { + return nil, err + } + defer rows.Close() + items := []UcanDelegation{} + for rows.Next() { + var i UcanDelegation + if err := rows.Scan( + &i.ID, + &i.DidID, + &i.Cid, + &i.Envelope, + &i.Iss, + &i.Aud, + &i.Sub, + &i.Cmd, + &i.Pol, + &i.Nbf, + &i.Exp, + &i.IsRoot, + &i.IsPowerline, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listDelegationsForCommand = `-- name: ListDelegationsForCommand :many +SELECT id, did_id, cid, envelope, iss, aud, sub, cmd, pol, nbf, exp, is_root, is_powerline, created_at FROM ucan_delegations +WHERE did_id = ? + AND (cmd = ? OR cmd = '/' OR ? LIKE cmd || '/%') + AND (exp IS NULL OR exp > datetime('now')) +ORDER BY created_at DESC +` + +type ListDelegationsForCommandParams struct { + DidID int64 `json:"did_id"` + Cmd string `json:"cmd"` + Cmd_2 string `json:"cmd_2"` +} + +func (q *Queries) ListDelegationsForCommand(ctx context.Context, arg ListDelegationsForCommandParams) ([]UcanDelegation, error) { + rows, err := q.db.QueryContext(ctx, listDelegationsForCommand, arg.DidID, arg.Cmd, arg.Cmd_2) + if err != nil { + return nil, err + } + defer rows.Close() + items := []UcanDelegation{} + for rows.Next() { + var i UcanDelegation + if err := rows.Scan( + &i.ID, + &i.DidID, + &i.Cid, + &i.Envelope, + &i.Iss, + &i.Aud, + &i.Sub, + &i.Cmd, + &i.Pol, + &i.Nbf, + &i.Exp, + &i.IsRoot, + &i.IsPowerline, &i.CreatedAt, - &i.ExpiresAt, ); err != nil { return nil, err } @@ -1369,7 +1514,7 @@ func (q *Queries) ListDelegationsForResource(ctx context.Context, arg ListDelega const listGrantsByDID = `-- name: ListGrantsByDID :many -SELECT g.id, g.did_id, g.service_id, g.ucan_id, g.scopes, g.accounts, g.status, g.granted_at, g.last_used, g.expires_at, s.name as service_name, s.origin as service_origin, s.logo_url as service_logo +SELECT g.id, g.did_id, g.service_id, g.delegation_cid, g.scopes, g.accounts, g.status, g.granted_at, g.last_used, g.expires_at, s.name as service_name, s.origin as service_origin, s.logo_url as service_logo FROM grants g JOIN services s ON g.service_id = s.id WHERE g.did_id = ? AND g.status = 'active' @@ -1380,7 +1525,7 @@ type ListGrantsByDIDRow struct { ID int64 `json:"id"` DidID int64 `json:"did_id"` ServiceID int64 `json:"service_id"` - UcanID *int64 `json:"ucan_id"` + DelegationCid *string `json:"delegation_cid"` Scopes json.RawMessage `json:"scopes"` Accounts json.RawMessage `json:"accounts"` Status string `json:"status"` @@ -1408,7 +1553,7 @@ func (q *Queries) ListGrantsByDID(ctx context.Context, didID int64) ([]ListGrant &i.ID, &i.DidID, &i.ServiceID, - &i.UcanID, + &i.DelegationCid, &i.Scopes, &i.Accounts, &i.Status, @@ -1432,6 +1577,207 @@ func (q *Queries) ListGrantsByDID(ctx context.Context, didID int64) ([]ListGrant return items, nil } +const listInvocationsByDID = `-- name: ListInvocationsByDID :many +SELECT id, did_id, cid, envelope, iss, sub, aud, cmd, prf, exp, iat, executed_at, result_cid, created_at FROM ucan_invocations +WHERE did_id = ? +ORDER BY created_at DESC +LIMIT ? +` + +type ListInvocationsByDIDParams struct { + DidID int64 `json:"did_id"` + Limit int64 `json:"limit"` +} + +func (q *Queries) ListInvocationsByDID(ctx context.Context, arg ListInvocationsByDIDParams) ([]UcanInvocation, error) { + rows, err := q.db.QueryContext(ctx, listInvocationsByDID, arg.DidID, arg.Limit) + if err != nil { + return nil, err + } + defer rows.Close() + items := []UcanInvocation{} + for rows.Next() { + var i UcanInvocation + if err := rows.Scan( + &i.ID, + &i.DidID, + &i.Cid, + &i.Envelope, + &i.Iss, + &i.Sub, + &i.Aud, + &i.Cmd, + &i.Prf, + &i.Exp, + &i.Iat, + &i.ExecutedAt, + &i.ResultCid, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listInvocationsByIssuer = `-- name: ListInvocationsByIssuer :many +SELECT id, did_id, cid, envelope, iss, sub, aud, cmd, prf, exp, iat, executed_at, result_cid, created_at FROM ucan_invocations +WHERE iss = ? +ORDER BY created_at DESC +LIMIT ? +` + +type ListInvocationsByIssuerParams struct { + Iss string `json:"iss"` + Limit int64 `json:"limit"` +} + +func (q *Queries) ListInvocationsByIssuer(ctx context.Context, arg ListInvocationsByIssuerParams) ([]UcanInvocation, error) { + rows, err := q.db.QueryContext(ctx, listInvocationsByIssuer, arg.Iss, arg.Limit) + if err != nil { + return nil, err + } + defer rows.Close() + items := []UcanInvocation{} + for rows.Next() { + var i UcanInvocation + if err := rows.Scan( + &i.ID, + &i.DidID, + &i.Cid, + &i.Envelope, + &i.Iss, + &i.Sub, + &i.Aud, + &i.Cmd, + &i.Prf, + &i.Exp, + &i.Iat, + &i.ExecutedAt, + &i.ResultCid, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listInvocationsBySubject = `-- name: ListInvocationsBySubject :many +SELECT id, did_id, cid, envelope, iss, sub, aud, cmd, prf, exp, iat, executed_at, result_cid, created_at FROM ucan_invocations +WHERE sub = ? +ORDER BY created_at DESC +LIMIT ? +` + +type ListInvocationsBySubjectParams struct { + Sub string `json:"sub"` + Limit int64 `json:"limit"` +} + +func (q *Queries) ListInvocationsBySubject(ctx context.Context, arg ListInvocationsBySubjectParams) ([]UcanInvocation, error) { + rows, err := q.db.QueryContext(ctx, listInvocationsBySubject, arg.Sub, arg.Limit) + if err != nil { + return nil, err + } + defer rows.Close() + items := []UcanInvocation{} + for rows.Next() { + var i UcanInvocation + if err := rows.Scan( + &i.ID, + &i.DidID, + &i.Cid, + &i.Envelope, + &i.Iss, + &i.Sub, + &i.Aud, + &i.Cmd, + &i.Prf, + &i.Exp, + &i.Iat, + &i.ExecutedAt, + &i.ResultCid, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listInvocationsForCommand = `-- name: ListInvocationsForCommand :many +SELECT id, did_id, cid, envelope, iss, sub, aud, cmd, prf, exp, iat, executed_at, result_cid, created_at FROM ucan_invocations +WHERE did_id = ? AND cmd = ? +ORDER BY created_at DESC +LIMIT ? +` + +type ListInvocationsForCommandParams struct { + DidID int64 `json:"did_id"` + Cmd string `json:"cmd"` + Limit int64 `json:"limit"` +} + +func (q *Queries) ListInvocationsForCommand(ctx context.Context, arg ListInvocationsForCommandParams) ([]UcanInvocation, error) { + rows, err := q.db.QueryContext(ctx, listInvocationsForCommand, arg.DidID, arg.Cmd, arg.Limit) + if err != nil { + return nil, err + } + defer rows.Close() + items := []UcanInvocation{} + for rows.Next() { + var i UcanInvocation + if err := rows.Scan( + &i.ID, + &i.DidID, + &i.Cid, + &i.Envelope, + &i.Iss, + &i.Sub, + &i.Aud, + &i.Cmd, + &i.Prf, + &i.Exp, + &i.Iat, + &i.ExecutedAt, + &i.ResultCid, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const listKeySharesByDID = `-- name: ListKeySharesByDID :many SELECT id, did_id, share_id, key_id, party_index, threshold, total_parties, curve, share_data, public_key, chain_code, derivation_path, status, created_at, rotated_at FROM key_shares WHERE did_id = ? AND status = 'active' ORDER BY created_at @@ -1479,6 +1825,174 @@ func (q *Queries) ListKeySharesByDID(ctx context.Context, didID int64) ([]KeySha return items, nil } +const listPendingInvocations = `-- name: ListPendingInvocations :many +SELECT id, did_id, cid, envelope, iss, sub, aud, cmd, prf, exp, iat, executed_at, result_cid, created_at FROM ucan_invocations +WHERE did_id = ? AND executed_at IS NULL AND (exp IS NULL OR exp > datetime('now')) +ORDER BY created_at ASC +` + +func (q *Queries) ListPendingInvocations(ctx context.Context, didID int64) ([]UcanInvocation, error) { + rows, err := q.db.QueryContext(ctx, listPendingInvocations, didID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []UcanInvocation{} + for rows.Next() { + var i UcanInvocation + if err := rows.Scan( + &i.ID, + &i.DidID, + &i.Cid, + &i.Envelope, + &i.Iss, + &i.Sub, + &i.Aud, + &i.Cmd, + &i.Prf, + &i.Exp, + &i.Iat, + &i.ExecutedAt, + &i.ResultCid, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listPowerlineDelegations = `-- name: ListPowerlineDelegations :many +SELECT id, did_id, cid, envelope, iss, aud, sub, cmd, pol, nbf, exp, is_root, is_powerline, created_at FROM ucan_delegations +WHERE did_id = ? AND is_powerline = 1 AND (exp IS NULL OR exp > datetime('now')) +ORDER BY created_at DESC +` + +func (q *Queries) ListPowerlineDelegations(ctx context.Context, didID int64) ([]UcanDelegation, error) { + rows, err := q.db.QueryContext(ctx, listPowerlineDelegations, didID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []UcanDelegation{} + for rows.Next() { + var i UcanDelegation + if err := rows.Scan( + &i.ID, + &i.DidID, + &i.Cid, + &i.Envelope, + &i.Iss, + &i.Aud, + &i.Sub, + &i.Cmd, + &i.Pol, + &i.Nbf, + &i.Exp, + &i.IsRoot, + &i.IsPowerline, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listRevocationsByRevoker = `-- name: ListRevocationsByRevoker :many +SELECT id, delegation_cid, revoked_by, invocation_cid, reason, revoked_at FROM ucan_revocations +WHERE revoked_by = ? +ORDER BY revoked_at DESC +` + +func (q *Queries) ListRevocationsByRevoker(ctx context.Context, revokedBy string) ([]UcanRevocation, error) { + rows, err := q.db.QueryContext(ctx, listRevocationsByRevoker, revokedBy) + if err != nil { + return nil, err + } + defer rows.Close() + items := []UcanRevocation{} + for rows.Next() { + var i UcanRevocation + if err := rows.Scan( + &i.ID, + &i.DelegationCid, + &i.RevokedBy, + &i.InvocationCid, + &i.Reason, + &i.RevokedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listRootDelegations = `-- name: ListRootDelegations :many +SELECT id, did_id, cid, envelope, iss, aud, sub, cmd, pol, nbf, exp, is_root, is_powerline, created_at FROM ucan_delegations +WHERE did_id = ? AND is_root = 1 AND (exp IS NULL OR exp > datetime('now')) +ORDER BY created_at DESC +` + +func (q *Queries) ListRootDelegations(ctx context.Context, didID int64) ([]UcanDelegation, error) { + rows, err := q.db.QueryContext(ctx, listRootDelegations, didID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []UcanDelegation{} + for rows.Next() { + var i UcanDelegation + if err := rows.Scan( + &i.ID, + &i.DidID, + &i.Cid, + &i.Envelope, + &i.Iss, + &i.Aud, + &i.Sub, + &i.Cmd, + &i.Pol, + &i.Nbf, + &i.Exp, + &i.IsRoot, + &i.IsPowerline, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const listSessionsByDID = `-- name: ListSessionsByDID :many SELECT s.id, s.did_id, s.credential_id, s.session_id, s.device_info, s.is_current, s.last_activity, s.expires_at, s.created_at, c.device_name, c.authenticator @@ -1574,102 +2088,6 @@ func (q *Queries) ListSyncCheckpoints(ctx context.Context, didID int64) ([]SyncC return items, nil } -const listUCANsByAudience = `-- name: ListUCANsByAudience :many -SELECT id, did_id, cid, issuer, audience, subject, capabilities, proof_chain, not_before, expires_at, nonce, facts, signature, raw_token, is_revoked, created_at FROM ucan_tokens -WHERE audience = ? AND is_revoked = 0 AND expires_at > datetime('now') -ORDER BY created_at DESC -` - -func (q *Queries) ListUCANsByAudience(ctx context.Context, audience string) ([]UcanToken, error) { - rows, err := q.db.QueryContext(ctx, listUCANsByAudience, audience) - if err != nil { - return nil, err - } - defer rows.Close() - items := []UcanToken{} - for rows.Next() { - var i UcanToken - if err := rows.Scan( - &i.ID, - &i.DidID, - &i.Cid, - &i.Issuer, - &i.Audience, - &i.Subject, - &i.Capabilities, - &i.ProofChain, - &i.NotBefore, - &i.ExpiresAt, - &i.Nonce, - &i.Facts, - &i.Signature, - &i.RawToken, - &i.IsRevoked, - &i.CreatedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const listUCANsByDID = `-- name: ListUCANsByDID :many - -SELECT id, did_id, cid, issuer, audience, subject, capabilities, proof_chain, not_before, expires_at, nonce, facts, signature, raw_token, is_revoked, created_at FROM ucan_tokens -WHERE did_id = ? AND is_revoked = 0 AND expires_at > datetime('now') -ORDER BY created_at DESC -` - -// ============================================================================= -// UCAN TOKEN QUERIES -// ============================================================================= -func (q *Queries) ListUCANsByDID(ctx context.Context, didID int64) ([]UcanToken, error) { - rows, err := q.db.QueryContext(ctx, listUCANsByDID, didID) - if err != nil { - return nil, err - } - defer rows.Close() - items := []UcanToken{} - for rows.Next() { - var i UcanToken - if err := rows.Scan( - &i.ID, - &i.DidID, - &i.Cid, - &i.Issuer, - &i.Audience, - &i.Subject, - &i.Capabilities, - &i.ProofChain, - &i.NotBefore, - &i.ExpiresAt, - &i.Nonce, - &i.Facts, - &i.Signature, - &i.RawToken, - &i.IsRevoked, - &i.CreatedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - const listVerificationMethods = `-- name: ListVerificationMethods :many SELECT id, did_id, method_id, method_type, controller, public_key, purpose, created_at FROM verification_methods WHERE did_id = ? ORDER BY created_at @@ -1747,6 +2165,22 @@ func (q *Queries) ListVerifiedServices(ctx context.Context) ([]Service, error) { return items, nil } +const markInvocationExecuted = `-- name: MarkInvocationExecuted :exec +UPDATE ucan_invocations +SET executed_at = datetime('now'), result_cid = ? +WHERE cid = ? +` + +type MarkInvocationExecutedParams struct { + ResultCid *string `json:"result_cid"` + Cid string `json:"cid"` +} + +func (q *Queries) MarkInvocationExecuted(ctx context.Context, arg MarkInvocationExecutedParams) error { + _, err := q.db.ExecContext(ctx, markInvocationExecuted, arg.ResultCid, arg.Cid) + return err +} + const reactivateGrant = `-- name: ReactivateGrant :exec UPDATE grants SET status = 'active' WHERE id = ? AND status = 'suspended' ` @@ -1770,29 +2204,6 @@ func (q *Queries) RenameCredential(ctx context.Context, arg RenameCredentialPara return err } -const revokeDelegation = `-- name: RevokeDelegation :exec -UPDATE delegations SET status = 'revoked' WHERE id = ? -` - -func (q *Queries) RevokeDelegation(ctx context.Context, id int64) error { - _, err := q.db.ExecContext(ctx, revokeDelegation, id) - return err -} - -const revokeDelegationChain = `-- name: RevokeDelegationChain :exec -UPDATE delegations SET status = 'revoked' WHERE id = ? OR parent_id = ? -` - -type RevokeDelegationChainParams struct { - ID int64 `json:"id"` - ParentID *int64 `json:"parent_id"` -} - -func (q *Queries) RevokeDelegationChain(ctx context.Context, arg RevokeDelegationChainParams) error { - _, err := q.db.ExecContext(ctx, revokeDelegationChain, arg.ID, arg.ParentID) - return err -} - const revokeGrant = `-- name: RevokeGrant :exec UPDATE grants SET status = 'revoked' WHERE id = ? ` @@ -1802,15 +2213,6 @@ func (q *Queries) RevokeGrant(ctx context.Context, id int64) error { return err } -const revokeUCAN = `-- name: RevokeUCAN :exec -UPDATE ucan_tokens SET is_revoked = 1 WHERE cid = ? -` - -func (q *Queries) RevokeUCAN(ctx context.Context, cid string) error { - _, err := q.db.ExecContext(ctx, revokeUCAN, cid) - return err -} - const rotateKeyShare = `-- name: RotateKeyShare :exec UPDATE key_shares SET status = 'rotating', rotated_at = datetime('now') diff --git a/internal/migrations/query.sql b/internal/migrations/query.sql index 43caf05..26abf31 100644 --- a/internal/migrations/query.sql +++ b/internal/migrations/query.sql @@ -108,7 +108,7 @@ DELETE FROM key_shares WHERE id = ? AND did_id = ?; -- ============================================================================= -- name: ListAccountsByDID :many -SELECT a.*, k.public_key, k.curve +SELECT a.*, k.public_key as share_public_key, k.curve FROM accounts a JOIN key_shares k ON a.key_share_id = k.id WHERE a.did_id = ? @@ -140,42 +140,137 @@ UPDATE accounts SET label = ? WHERE id = ?; DELETE FROM accounts WHERE id = ? AND did_id = ?; -- ============================================================================= --- UCAN TOKEN QUERIES +-- UCAN DELEGATION QUERIES (v1.0.0-rc.1) -- ============================================================================= --- name: ListUCANsByDID :many -SELECT * FROM ucan_tokens -WHERE did_id = ? AND is_revoked = 0 AND expires_at > datetime('now') -ORDER BY created_at DESC; +-- name: GetDelegationByCID :one +SELECT * FROM ucan_delegations WHERE cid = ? LIMIT 1; --- name: ListUCANsByAudience :many -SELECT * FROM ucan_tokens -WHERE audience = ? AND is_revoked = 0 AND expires_at > datetime('now') -ORDER BY created_at DESC; +-- name: GetDelegationEnvelopeByCID :one +SELECT envelope FROM ucan_delegations WHERE cid = ? LIMIT 1; --- name: GetUCANByCID :one -SELECT * FROM ucan_tokens WHERE cid = ? LIMIT 1; - --- name: CreateUCAN :one -INSERT INTO ucan_tokens ( - did_id, cid, issuer, audience, subject, capabilities, - proof_chain, not_before, expires_at, nonce, facts, signature, raw_token +-- name: CreateDelegation :one +INSERT INTO ucan_delegations ( + did_id, cid, envelope, iss, aud, sub, cmd, pol, nbf, exp, is_root, is_powerline ) -VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *; --- name: RevokeUCAN :exec -UPDATE ucan_tokens SET is_revoked = 1 WHERE cid = ?; +-- name: ListDelegationsByDID :many +SELECT * FROM ucan_delegations +WHERE did_id = ? AND (exp IS NULL OR exp > datetime('now')) +ORDER BY created_at DESC; --- name: IsUCANRevoked :one -SELECT EXISTS(SELECT 1 FROM ucan_revocations WHERE ucan_cid = ?) as revoked; +-- name: ListDelegationsByIssuer :many +SELECT * FROM ucan_delegations +WHERE iss = ? AND (exp IS NULL OR exp > datetime('now')) +ORDER BY created_at DESC; + +-- name: ListDelegationsByAudience :many +SELECT * FROM ucan_delegations +WHERE aud = ? AND (exp IS NULL OR exp > datetime('now')) +ORDER BY created_at DESC; + +-- name: ListDelegationsBySubject :many +SELECT * FROM ucan_delegations +WHERE sub = ? AND (exp IS NULL OR exp > datetime('now')) +ORDER BY created_at DESC; + +-- name: ListDelegationsForCommand :many +SELECT * FROM ucan_delegations +WHERE did_id = ? + AND (cmd = ? OR cmd = '/' OR ? LIKE cmd || '/%') + AND (exp IS NULL OR exp > datetime('now')) +ORDER BY created_at DESC; + +-- name: ListRootDelegations :many +SELECT * FROM ucan_delegations +WHERE did_id = ? AND is_root = 1 AND (exp IS NULL OR exp > datetime('now')) +ORDER BY created_at DESC; + +-- name: ListPowerlineDelegations :many +SELECT * FROM ucan_delegations +WHERE did_id = ? AND is_powerline = 1 AND (exp IS NULL OR exp > datetime('now')) +ORDER BY created_at DESC; + +-- name: DeleteDelegation :exec +DELETE FROM ucan_delegations WHERE cid = ? AND did_id = ?; + +-- name: CleanExpiredDelegations :exec +DELETE FROM ucan_delegations WHERE exp < datetime('now', '-30 days'); + +-- ============================================================================= +-- UCAN INVOCATION QUERIES (v1.0.0-rc.1) +-- ============================================================================= + +-- name: GetInvocationByCID :one +SELECT * FROM ucan_invocations WHERE cid = ? LIMIT 1; + +-- name: GetInvocationEnvelopeByCID :one +SELECT envelope FROM ucan_invocations WHERE cid = ? LIMIT 1; + +-- name: CreateInvocation :one +INSERT INTO ucan_invocations ( + did_id, cid, envelope, iss, sub, aud, cmd, prf, exp, iat +) +VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +RETURNING *; + +-- name: ListInvocationsByDID :many +SELECT * FROM ucan_invocations +WHERE did_id = ? +ORDER BY created_at DESC +LIMIT ?; + +-- name: ListInvocationsByIssuer :many +SELECT * FROM ucan_invocations +WHERE iss = ? +ORDER BY created_at DESC +LIMIT ?; + +-- name: ListInvocationsBySubject :many +SELECT * FROM ucan_invocations +WHERE sub = ? +ORDER BY created_at DESC +LIMIT ?; + +-- name: ListInvocationsForCommand :many +SELECT * FROM ucan_invocations +WHERE did_id = ? AND cmd = ? +ORDER BY created_at DESC +LIMIT ?; + +-- name: MarkInvocationExecuted :exec +UPDATE ucan_invocations +SET executed_at = datetime('now'), result_cid = ? +WHERE cid = ?; + +-- name: ListPendingInvocations :many +SELECT * FROM ucan_invocations +WHERE did_id = ? AND executed_at IS NULL AND (exp IS NULL OR exp > datetime('now')) +ORDER BY created_at ASC; + +-- name: CleanOldInvocations :exec +DELETE FROM ucan_invocations WHERE created_at < datetime('now', '-90 days'); + +-- ============================================================================= +-- UCAN REVOCATION QUERIES +-- ============================================================================= -- name: CreateRevocation :exec -INSERT INTO ucan_revocations (ucan_cid, revoked_by, reason) -VALUES (?, ?, ?); +INSERT INTO ucan_revocations (delegation_cid, revoked_by, invocation_cid, reason) +VALUES (?, ?, ?, ?); --- name: CleanExpiredUCANs :exec -DELETE FROM ucan_tokens WHERE expires_at < datetime('now', '-30 days'); +-- name: IsDelegationRevoked :one +SELECT EXISTS(SELECT 1 FROM ucan_revocations WHERE delegation_cid = ?) as revoked; + +-- name: GetRevocation :one +SELECT * FROM ucan_revocations WHERE delegation_cid = ? LIMIT 1; + +-- name: ListRevocationsByRevoker :many +SELECT * FROM ucan_revocations +WHERE revoked_by = ? +ORDER BY revoked_at DESC; -- ============================================================================= -- SESSION QUERIES @@ -251,7 +346,7 @@ ORDER BY g.last_used DESC NULLS LAST; SELECT * FROM grants WHERE did_id = ? AND service_id = ? LIMIT 1; -- name: CreateGrant :one -INSERT INTO grants (did_id, service_id, ucan_id, scopes, accounts, expires_at) +INSERT INTO grants (did_id, service_id, delegation_cid, scopes, accounts, expires_at) VALUES (?, ?, ?, ?, ?, ?) RETURNING *; @@ -273,41 +368,6 @@ UPDATE grants SET status = 'active' WHERE id = ? AND status = 'suspended'; -- name: CountActiveGrants :one SELECT COUNT(*) FROM grants WHERE did_id = ? AND status = 'active'; --- ============================================================================= --- DELEGATION QUERIES --- ============================================================================= - --- name: ListDelegationsByDelegator :many -SELECT * FROM delegations -WHERE delegator = ? AND status = 'active' -ORDER BY created_at DESC; - --- name: ListDelegationsByDelegate :many -SELECT * FROM delegations -WHERE delegate = ? AND status = 'active' AND (expires_at IS NULL OR expires_at > datetime('now')) -ORDER BY created_at DESC; - --- name: ListDelegationsForResource :many -SELECT * FROM delegations -WHERE did_id = ? AND resource = ? AND status = 'active' -ORDER BY depth, created_at; - --- name: GetDelegationChain :many -SELECT * FROM delegations WHERE id = ? OR parent_id = ? ORDER BY depth DESC; - --- name: CreateDelegation :one -INSERT INTO delegations ( - did_id, ucan_id, delegator, delegate, resource, action, caveats, parent_id, depth, expires_at -) -VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) -RETURNING *; - --- name: RevokeDelegation :exec -UPDATE delegations SET status = 'revoked' WHERE id = ?; - --- name: RevokeDelegationChain :exec -UPDATE delegations SET status = 'revoked' WHERE id = ? OR parent_id = ?; - -- ============================================================================= -- SYNC QUERIES -- ============================================================================= diff --git a/internal/migrations/schema.sql b/internal/migrations/schema.sql index cb78c4b..317e255 100644 --- a/internal/migrations/schema.sql +++ b/internal/migrations/schema.sql @@ -1,6 +1,7 @@ -- ============================================================================= -- NEBULA KEY ENCLAVE SCHEMA -- Encrypted SQLite database for sensitive wallet data +-- UCAN v1.0.0-rc.1 compliant -- ============================================================================= PRAGMA foreign_keys = ON; @@ -112,44 +113,97 @@ CREATE INDEX idx_accounts_address ON accounts(address); CREATE INDEX idx_accounts_chain_id ON accounts(chain_id); -- ============================================================================= --- UCAN AUTHORIZATION +-- UCAN AUTHORIZATION (v1.0.0-rc.1) -- ============================================================================= --- UCAN Tokens: Capability authorization tokens -CREATE TABLE IF NOT EXISTS ucan_tokens ( +-- UCAN Delegations: v1.0.0-rc.1 delegation envelopes +-- Stores sealed DAG-CBOR envelopes with extracted fields for indexing +CREATE TABLE IF NOT EXISTS ucan_delegations ( id INTEGER PRIMARY KEY, did_id INTEGER NOT NULL REFERENCES did_documents(id) ON DELETE CASCADE, - cid TEXT NOT NULL UNIQUE, -- Content ID of UCAN (for dedup) - issuer TEXT NOT NULL, -- iss: DID of issuer - audience TEXT NOT NULL, -- aud: DID of recipient - subject TEXT, -- sub: DID token is about (optional) - capabilities TEXT NOT NULL, -- JSON array of capabilities - proof_chain TEXT DEFAULT '[]', -- JSON array of parent UCAN CIDs - not_before TEXT, -- nbf: validity start - expires_at TEXT NOT NULL, -- exp: expiration time - nonce TEXT, -- Replay protection - facts TEXT DEFAULT '{}', -- Additional facts (JSON) - signature TEXT NOT NULL, -- Base64 encoded signature - raw_token TEXT NOT NULL, -- Full encoded UCAN token - is_revoked INTEGER NOT NULL DEFAULT 0, + + -- Content Identifier (immutable, unique) + cid TEXT NOT NULL UNIQUE, + + -- Sealed envelope (DAG-CBOR encoded) + envelope BLOB NOT NULL, + + -- Extracted fields for indexing/queries + iss TEXT NOT NULL, -- Issuer DID + aud TEXT NOT NULL, -- Audience DID + sub TEXT, -- Subject DID (null = powerline) + cmd TEXT NOT NULL, -- Command (e.g., "/vault/read") + + -- Policy stored as JSON for inspection + pol TEXT DEFAULT '[]', -- Policy JSON + + -- Temporal fields + nbf TEXT, -- Not before (ISO8601) + exp TEXT, -- Expiration (ISO8601, null = never) + + -- Metadata + is_root INTEGER NOT NULL DEFAULT 0, -- iss == sub + is_powerline INTEGER NOT NULL DEFAULT 0, -- sub IS NULL created_at TEXT NOT NULL DEFAULT (datetime('now')) ); -CREATE INDEX idx_ucan_tokens_did_id ON ucan_tokens(did_id); -CREATE INDEX idx_ucan_tokens_issuer ON ucan_tokens(issuer); -CREATE INDEX idx_ucan_tokens_audience ON ucan_tokens(audience); -CREATE INDEX idx_ucan_tokens_expires_at ON ucan_tokens(expires_at); +CREATE INDEX idx_ucan_delegations_cid ON ucan_delegations(cid); +CREATE INDEX idx_ucan_delegations_did_id ON ucan_delegations(did_id); +CREATE INDEX idx_ucan_delegations_iss ON ucan_delegations(iss); +CREATE INDEX idx_ucan_delegations_aud ON ucan_delegations(aud); +CREATE INDEX idx_ucan_delegations_sub ON ucan_delegations(sub); +CREATE INDEX idx_ucan_delegations_cmd ON ucan_delegations(cmd); +CREATE INDEX idx_ucan_delegations_exp ON ucan_delegations(exp); --- UCAN Revocations: Revoked UCAN tokens +-- UCAN Invocations: v1.0.0-rc.1 invocation envelopes (audit log) +CREATE TABLE IF NOT EXISTS ucan_invocations ( + id INTEGER PRIMARY KEY, + did_id INTEGER NOT NULL REFERENCES did_documents(id) ON DELETE CASCADE, + + -- Content Identifier + cid TEXT NOT NULL UNIQUE, + + -- Sealed envelope (DAG-CBOR encoded) + envelope BLOB NOT NULL, + + -- Extracted fields for indexing + iss TEXT NOT NULL, -- Invoker DID + sub TEXT NOT NULL, -- Subject DID + aud TEXT, -- Executor DID (if different from sub) + cmd TEXT NOT NULL, -- Command invoked + + -- Proof chain (JSON array of delegation CIDs) + prf TEXT NOT NULL DEFAULT '[]', + + -- Temporal + exp TEXT, -- Expiration + iat TEXT, -- Issued at + + -- Execution tracking + executed_at TEXT, -- When actually executed + result_cid TEXT, -- CID of receipt (if executed) + + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE INDEX idx_ucan_invocations_cid ON ucan_invocations(cid); +CREATE INDEX idx_ucan_invocations_did_id ON ucan_invocations(did_id); +CREATE INDEX idx_ucan_invocations_iss ON ucan_invocations(iss); +CREATE INDEX idx_ucan_invocations_sub ON ucan_invocations(sub); +CREATE INDEX idx_ucan_invocations_cmd ON ucan_invocations(cmd); + +-- UCAN Revocations: Track revoked delegations CREATE TABLE IF NOT EXISTS ucan_revocations ( id INTEGER PRIMARY KEY, - ucan_cid TEXT NOT NULL UNIQUE, -- CID of revoked UCAN - revoked_by TEXT NOT NULL, -- DID that revoked + delegation_cid TEXT NOT NULL UNIQUE, -- CID of revoked delegation + revoked_by TEXT NOT NULL, -- Revoker DID + invocation_cid TEXT, -- CID of revocation invocation reason TEXT, revoked_at TEXT NOT NULL DEFAULT (datetime('now')) ); -CREATE INDEX idx_ucan_revocations_cid ON ucan_revocations(ucan_cid); +CREATE INDEX idx_ucan_revocations_delegation_cid ON ucan_revocations(delegation_cid); +CREATE INDEX idx_ucan_revocations_revoked_by ON ucan_revocations(revoked_by); -- ============================================================================= -- DEVICE SESSIONS @@ -191,12 +245,12 @@ CREATE TABLE IF NOT EXISTS services ( CREATE INDEX idx_services_origin ON services(origin); --- Grants: User grants to services +-- Grants: User grants to services (backed by UCAN delegations) CREATE TABLE IF NOT EXISTS grants ( id INTEGER PRIMARY KEY, did_id INTEGER NOT NULL REFERENCES did_documents(id) ON DELETE CASCADE, service_id INTEGER NOT NULL REFERENCES services(id) ON DELETE CASCADE, - ucan_id INTEGER REFERENCES ucan_tokens(id) ON DELETE SET NULL, + delegation_cid TEXT REFERENCES ucan_delegations(cid) ON DELETE SET NULL, -- v1.0.0-rc.1 delegation scopes TEXT NOT NULL DEFAULT '[]', -- JSON array of granted scopes accounts TEXT NOT NULL DEFAULT '[]', -- JSON array of account IDs exposed status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'suspended', 'revoked')), @@ -209,32 +263,7 @@ CREATE TABLE IF NOT EXISTS grants ( CREATE INDEX idx_grants_did_id ON grants(did_id); CREATE INDEX idx_grants_service_id ON grants(service_id); CREATE INDEX idx_grants_status ON grants(status); - --- ============================================================================= --- CAPABILITY DELEGATIONS --- ============================================================================= - --- Delegations: Capability delegation chains -CREATE TABLE IF NOT EXISTS delegations ( - id INTEGER PRIMARY KEY, - did_id INTEGER NOT NULL REFERENCES did_documents(id) ON DELETE CASCADE, - ucan_id INTEGER NOT NULL REFERENCES ucan_tokens(id) ON DELETE CASCADE, - delegator TEXT NOT NULL, -- DID that delegated - delegate TEXT NOT NULL, -- DID that received delegation - resource TEXT NOT NULL, -- Resource URI (e.g., "sonr://vault/*") - action TEXT NOT NULL, -- Action (e.g., "sign", "read", "write") - caveats TEXT DEFAULT '{}', -- JSON: restrictions/conditions - parent_id INTEGER REFERENCES delegations(id), -- Parent delegation (for chains) - depth INTEGER NOT NULL DEFAULT 0, -- Delegation depth (0 = root) - status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'revoked', 'expired')), - created_at TEXT NOT NULL DEFAULT (datetime('now')), - expires_at TEXT -); - -CREATE INDEX idx_delegations_did_id ON delegations(did_id); -CREATE INDEX idx_delegations_delegator ON delegations(delegator); -CREATE INDEX idx_delegations_delegate ON delegations(delegate); -CREATE INDEX idx_delegations_resource ON delegations(resource); +CREATE INDEX idx_grants_delegation_cid ON grants(delegation_cid); -- ============================================================================= -- SYNC STATE -- 2.43.0 From 761bb107be5b71095a3081999b263c53947ba561 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Thu, 8 Jan 2026 16:37:39 -0500 Subject: [PATCH 12/35] docs(UCAN_SCHEMA_PROPOSAL): add UCAN v1.0.0-rc.1 schema proposal documentation --- docs/UCAN_SCHEMA_PROPOSAL.md | 311 +++++++++++++++++++++++++++++++++++ 1 file changed, 311 insertions(+) create mode 100644 docs/UCAN_SCHEMA_PROPOSAL.md diff --git a/docs/UCAN_SCHEMA_PROPOSAL.md b/docs/UCAN_SCHEMA_PROPOSAL.md new file mode 100644 index 0000000..b7405ca --- /dev/null +++ b/docs/UCAN_SCHEMA_PROPOSAL.md @@ -0,0 +1,311 @@ +# UCAN v1.0.0-rc.1 Schema Proposal + +## Overview + +This document proposes schema changes to migrate from JWT-based UCAN to v1.0.0-rc.1 envelope format. + +## Design Principles + +1. **Single Database** - All data in one SQLite for WASM portability +2. **CID-based Lookup** - Primary key is content identifier (immutable) +3. **Binary Storage** - DAG-CBOR envelopes stored as BLOBs +4. **Indexed Fields** - Extract key fields for efficient queries +5. **DID Ownership** - Foreign key to did_documents for access control + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ WASM Plugin (Enclave) │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ WebAuthn │ │ DID │ │ UCAN │ │ +│ │ (AuthN) │ │ (Identity) │ │ (AuthZ) │ │ +│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ +│ │ │ │ │ +│ ▼ ▼ ▼ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ SQLite Database │ │ +│ │ ┌────────────┐ ┌────────────┐ ┌─────────────────────┐ │ │ +│ │ │credentials │ │did_documents│ │ ucan_delegations │ │ │ +│ │ │ │ │ │ │ ucan_invocations │ │ │ +│ │ │ │ │ │ │ ucan_revocations │ │ │ +│ │ └────────────┘ └────────────┘ └─────────────────────┘ │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## Schema Changes + +### 1. Replace `ucan_tokens` with `ucan_delegations` + +```sql +-- DROP TABLE IF EXISTS ucan_tokens; -- Migration step + +-- UCAN Delegations: v1.0.0-rc.1 delegation envelopes +CREATE TABLE IF NOT EXISTS ucan_delegations ( + id INTEGER PRIMARY KEY, + did_id INTEGER NOT NULL REFERENCES did_documents(id) ON DELETE CASCADE, + + -- Content Identifier (immutable, unique) + cid TEXT NOT NULL UNIQUE, + + -- Sealed envelope (DAG-CBOR encoded) + envelope BLOB NOT NULL, + + -- Extracted fields for indexing/queries + iss TEXT NOT NULL, -- Issuer DID + aud TEXT NOT NULL, -- Audience DID + sub TEXT, -- Subject DID (null = powerline) + cmd TEXT NOT NULL, -- Command (e.g., "/vault/read") + + -- Policy stored as JSON for inspection (actual evaluation uses envelope) + pol TEXT DEFAULT '[]', -- Policy JSON + + -- Temporal fields + nbf TEXT, -- Not before (ISO8601) + exp TEXT, -- Expiration (ISO8601, null = never) + + -- Metadata + is_root INTEGER NOT NULL DEFAULT 0, -- iss == sub + is_powerline INTEGER NOT NULL DEFAULT 0, -- sub IS NULL + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE INDEX idx_ucan_delegations_cid ON ucan_delegations(cid); +CREATE INDEX idx_ucan_delegations_did_id ON ucan_delegations(did_id); +CREATE INDEX idx_ucan_delegations_iss ON ucan_delegations(iss); +CREATE INDEX idx_ucan_delegations_aud ON ucan_delegations(aud); +CREATE INDEX idx_ucan_delegations_sub ON ucan_delegations(sub); +CREATE INDEX idx_ucan_delegations_cmd ON ucan_delegations(cmd); +CREATE INDEX idx_ucan_delegations_exp ON ucan_delegations(exp); +``` + +### 2. Add `ucan_invocations` Table + +```sql +-- UCAN Invocations: v1.0.0-rc.1 invocation envelopes (audit log) +CREATE TABLE IF NOT EXISTS ucan_invocations ( + id INTEGER PRIMARY KEY, + did_id INTEGER NOT NULL REFERENCES did_documents(id) ON DELETE CASCADE, + + -- Content Identifier + cid TEXT NOT NULL UNIQUE, + + -- Sealed envelope (DAG-CBOR encoded) + envelope BLOB NOT NULL, + + -- Extracted fields for indexing + iss TEXT NOT NULL, -- Invoker DID + sub TEXT NOT NULL, -- Subject DID + aud TEXT, -- Executor DID (if different from sub) + cmd TEXT NOT NULL, -- Command invoked + + -- Proof chain (JSON array of delegation CIDs) + prf TEXT NOT NULL DEFAULT '[]', + + -- Temporal + exp TEXT, -- Expiration + iat TEXT, -- Issued at + + -- Execution tracking + executed_at TEXT, -- When actually executed + result_cid TEXT, -- CID of receipt (if executed) + + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE INDEX idx_ucan_invocations_cid ON ucan_invocations(cid); +CREATE INDEX idx_ucan_invocations_iss ON ucan_invocations(iss); +CREATE INDEX idx_ucan_invocations_sub ON ucan_invocations(sub); +CREATE INDEX idx_ucan_invocations_cmd ON ucan_invocations(cmd); +``` + +### 3. Update `ucan_revocations` (Minor Changes) + +```sql +-- UCAN Revocations: Track revoked delegations +-- (Mostly unchanged, but add invocation reference) +CREATE TABLE IF NOT EXISTS ucan_revocations ( + id INTEGER PRIMARY KEY, + delegation_cid TEXT NOT NULL UNIQUE, -- CID of revoked delegation + revoked_by TEXT NOT NULL, -- Revoker DID + invocation_cid TEXT, -- CID of revocation invocation + reason TEXT, + revoked_at TEXT NOT NULL DEFAULT (datetime('now')), + + FOREIGN KEY (delegation_cid) REFERENCES ucan_delegations(cid) ON DELETE CASCADE +); + +CREATE INDEX idx_ucan_revocations_delegation_cid ON ucan_revocations(delegation_cid); +CREATE INDEX idx_ucan_revocations_revoked_by ON ucan_revocations(revoked_by); +``` + +### 4. Remove `delegations` Table + +The old `delegations` table extracted fields from `ucan_tokens`. In v1.0.0-rc.1, the delegation IS the token. The `ucan_delegations` table replaces both. + +```sql +-- DROP TABLE IF EXISTS delegations; -- Migration step +``` + +## Query Examples + +### Get Delegation by CID (for go-ucan Loader) + +```sql +-- name: GetDelegationByCID :one +SELECT envelope FROM ucan_delegations WHERE cid = ? LIMIT 1; +``` + +### List Delegations Granted TO a DID (audience) + +```sql +-- name: ListDelegationsToAudience :many +SELECT * FROM ucan_delegations +WHERE aud = ? AND (exp IS NULL OR exp > datetime('now')) +ORDER BY created_at DESC; +``` + +### List Delegations Granted BY a DID (issuer) + +```sql +-- name: ListDelegationsByIssuer :many +SELECT * FROM ucan_delegations +WHERE iss = ? AND (exp IS NULL OR exp > datetime('now')) +ORDER BY created_at DESC; +``` + +### Find Delegations for a Command + +```sql +-- name: ListDelegationsForCommand :many +SELECT * FROM ucan_delegations +WHERE did_id = ? + AND (cmd = ? OR cmd = '/' OR ? LIKE cmd || '/%') + AND (exp IS NULL OR exp > datetime('now')) +ORDER BY created_at DESC; +``` + +### Check if Delegation is Revoked + +```sql +-- name: IsDelegationRevoked :one +SELECT EXISTS(SELECT 1 FROM ucan_revocations WHERE delegation_cid = ?) as revoked; +``` + +## Go Integration + +### Delegation Loader Implementation + +```go +// internal/keybase/ucan_loader.go + +package keybase + +import ( + "context" + "fmt" + + "github.com/ipfs/go-cid" + "github.com/ucan-wg/go-ucan/token/delegation" +) + +// DelegationLoader implements delegation.Loader for go-ucan +type DelegationLoader struct { + queries *Queries +} + +func NewDelegationLoader(queries *Queries) *DelegationLoader { + return &DelegationLoader{queries: queries} +} + +// GetDelegation implements delegation.Loader +func (l *DelegationLoader) GetDelegation(c cid.Cid) (*delegation.Token, error) { + ctx := context.Background() + + envelope, err := l.queries.GetDelegationEnvelopeByCID(ctx, c.String()) + if err != nil { + return nil, fmt.Errorf("delegation not found: %s", c) + } + + // Decode DAG-CBOR envelope to delegation token + return delegation.FromSealed(envelope) +} +``` + +### Action Manager Integration + +```go +// internal/keybase/actions_delegation_v2.go + +type DelegationV2Result struct { + CID string `json:"cid"` + Issuer string `json:"iss"` + Audience string `json:"aud"` + Subject string `json:"sub,omitempty"` + Command string `json:"cmd"` + Policy string `json:"pol"` + ExpiresAt string `json:"exp,omitempty"` + IsRoot bool `json:"is_root"` + CreatedAt string `json:"created_at"` +} + +func (am *ActionManager) StoreDelegation(ctx context.Context, sealed []byte, c cid.Cid) (*DelegationV2Result, error) { + // Decode to extract indexed fields + token, err := delegation.FromSealed(sealed) + if err != nil { + return nil, err + } + + // Store in database + result, err := am.kb.queries.CreateDelegationV2(ctx, CreateDelegationV2Params{ + DidID: am.kb.didID, + Cid: c.String(), + Envelope: sealed, + Iss: token.Issuer().String(), + Aud: token.Audience().String(), + Sub: didToNullable(token.Subject()), + Cmd: token.Command().String(), + Pol: policyToJSON(token.Policy()), + Exp: timeToNullable(token.Expiration()), + Nbf: timeToNullable(token.NotBefore()), + IsRoot: boolToInt(token.IsRoot()), + IsPowerline: boolToInt(token.IsPowerline()), + }) + + return delegationV2ToResult(result), nil +} +``` + +## Migration Path + +1. **Create new tables** alongside old ones +2. **Migrate existing data** (if any JWT tokens exist) + - Parse old `raw_token` + - Re-encode as v1.0.0-rc.1 envelope (requires re-signing) + - Or: Mark old tokens as legacy, start fresh with v1.0.0-rc.1 +3. **Update ActionManager** to use new tables +4. **Drop old tables** after migration verified + +## Benefits + +| Aspect | Old (JWT) | New (v1.0.0-rc.1) | +|--------|-----------|-------------------| +| Storage | JSON text | Binary CBOR (smaller) | +| Verification | Parse JWT, verify sig | go-ucan handles it | +| Proof Chain | JSON array in token | Separate CID references | +| Policy | `capabilities` JSON | Structured `pol` field | +| Interop | Non-standard | Spec-compliant | + +## Conclusion + +Integrating UCAN v1.0.0-rc.1 into the keybase schema: + +1. **Maintains single-database portability** for WASM plugin +2. **Leverages existing infrastructure** (SQLC, ActionManager) +3. **Enables foreign key relationships** with DID documents +4. **Provides efficient queries** via indexed fields +5. **Supports go-ucan integration** via `delegation.Loader` -- 2.43.0 From ea07a510449c03c1bcddb8176acbdedefc2237cd Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Thu, 8 Jan 2026 16:42:15 -0500 Subject: [PATCH 13/35] refactor(main): rename UCAN to Delegation in main.go --- main.go | 81 ++++++++++++++++++++++++++++++++------------------------- 1 file changed, 45 insertions(+), 36 deletions(-) diff --git a/main.go b/main.go index d60fcb7..08b1042 100644 --- a/main.go +++ b/main.go @@ -362,7 +362,7 @@ func validateUCAN(token string, params *types.FilterParams) error { if err == nil { if cid, ok := claims["cid"].(string); ok { ctx := context.Background() - revoked, err := am.IsUCANRevoked(ctx, cid) + revoked, err := am.IsDelegationRevoked(ctx, cid) if err == nil && revoked { return errors.New("token has been revoked") } @@ -675,40 +675,44 @@ func executeUCANAction(params *types.FilterParams) (json.RawMessage, error) { switch params.Action { case "list": - ucans, err := am.ListUCANs(ctx) + delegations, err := am.ListDelegations(ctx) if err != nil { - return nil, fmt.Errorf("list ucans: %w", err) + return nil, fmt.Errorf("list delegations: %w", err) } - return json.Marshal(ucans) + return json.Marshal(delegations) case "get": if params.Subject == "" { return nil, errors.New("subject (cid) required for get action") } - ucan, err := am.GetUCANByCID(ctx, params.Subject) + delegation, err := am.GetDelegationByCID(ctx, params.Subject) if err != nil { - return nil, fmt.Errorf("get ucan: %w", err) + return nil, fmt.Errorf("get delegation: %w", err) } - return json.Marshal(ucan) + return json.Marshal(delegation) case "revoke": if params.Subject == "" { return nil, errors.New("subject (cid) required for revoke action") } - if err := am.RevokeUCAN(ctx, params.Subject); err != nil { - return nil, fmt.Errorf("revoke ucan: %w", err) + if err := am.RevokeDelegation(ctx, keybase.RevokeDelegationParams{ + DelegationCID: params.Subject, + RevokedBy: state.GetDID(), + Reason: "user revoked", + }); err != nil { + return nil, fmt.Errorf("revoke delegation: %w", err) } return json.Marshal(map[string]bool{"revoked": true}) case "verify": if params.Subject == "" { return nil, errors.New("subject (cid) required for verify action") } - revoked, err := am.IsUCANRevoked(ctx, params.Subject) + revoked, err := am.IsDelegationRevoked(ctx, params.Subject) if err != nil { - return nil, fmt.Errorf("check ucan: %w", err) + return nil, fmt.Errorf("check delegation: %w", err) } return json.Marshal(map[string]bool{"valid": !revoked, "revoked": revoked}) case "cleanup": - if err := am.CleanExpiredUCANs(ctx); err != nil { - return nil, fmt.Errorf("cleanup ucans: %w", err) + if err := am.CleanExpiredDelegations(ctx); err != nil { + return nil, fmt.Errorf("cleanup delegations: %w", err) } return json.Marshal(map[string]bool{"cleaned": true}) default: @@ -727,56 +731,61 @@ func executeDelegationAction(params *types.FilterParams) (json.RawMessage, error switch params.Action { case "list": if params.Subject == "" { - return nil, errors.New("subject (delegator or delegate DID) required for list action") + return nil, errors.New("subject (issuer DID) required for list action") } - delegations, err := am.ListDelegationsByDelegator(ctx, params.Subject) + delegations, err := am.ListDelegationsByIssuer(ctx, params.Subject) if err != nil { return nil, fmt.Errorf("list delegations: %w", err) } return json.Marshal(delegations) case "list_received": if params.Subject == "" { - return nil, errors.New("subject (delegate DID) required for list_received action") + return nil, errors.New("subject (audience DID) required for list_received action") } - delegations, err := am.ListDelegationsByDelegate(ctx, params.Subject) + delegations, err := am.ListDelegationsByAudience(ctx, params.Subject) if err != nil { return nil, fmt.Errorf("list received delegations: %w", err) } return json.Marshal(delegations) - case "list_resource": + case "list_command": if params.Subject == "" { - return nil, errors.New("subject (resource) required for list_resource action") + return nil, errors.New("subject (command) required for list_command action") } - delegations, err := am.ListDelegationsForResource(ctx, params.Subject) + delegations, err := am.ListDelegationsForCommand(ctx, params.Subject) if err != nil { - return nil, fmt.Errorf("list delegations for resource: %w", err) + return nil, fmt.Errorf("list delegations for command: %w", err) } return json.Marshal(delegations) - case "chain": + case "get": if params.Subject == "" { - return nil, errors.New("subject (delegation_id) required for chain action") + return nil, errors.New("subject (cid) required for get action") } - var delegationID int64 - if _, err := fmt.Sscanf(params.Subject, "%d", &delegationID); err != nil { - return nil, fmt.Errorf("invalid delegation_id: %w", err) - } - chain, err := am.GetDelegationChain(ctx, delegationID) + delegation, err := am.GetDelegationByCID(ctx, params.Subject) if err != nil { - return nil, fmt.Errorf("get delegation chain: %w", err) + return nil, fmt.Errorf("get delegation: %w", err) } - return json.Marshal(chain) + return json.Marshal(delegation) case "revoke": if params.Subject == "" { - return nil, errors.New("subject (delegation_id) required for revoke action") + return nil, errors.New("subject (cid) required for revoke action") } - var delegationID int64 - if _, err := fmt.Sscanf(params.Subject, "%d", &delegationID); err != nil { - return nil, fmt.Errorf("invalid delegation_id: %w", err) - } - if err := am.RevokeDelegation(ctx, delegationID); err != nil { + if err := am.RevokeDelegation(ctx, keybase.RevokeDelegationParams{ + DelegationCID: params.Subject, + RevokedBy: state.GetDID(), + Reason: "user revoked", + }); err != nil { return nil, fmt.Errorf("revoke delegation: %w", err) } return json.Marshal(map[string]bool{"revoked": true}) + case "verify": + if params.Subject == "" { + return nil, errors.New("subject (cid) required for verify action") + } + revoked, err := am.IsDelegationRevoked(ctx, params.Subject) + if err != nil { + return nil, fmt.Errorf("check delegation: %w", err) + } + return json.Marshal(map[string]bool{"valid": !revoked, "revoked": revoked}) default: return nil, fmt.Errorf("unknown action for delegations: %s", params.Action) } -- 2.43.0 From 69b0eca088307ed060b0eaee0343efd1d8f89633 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Thu, 8 Jan 2026 16:42:16 -0500 Subject: [PATCH 14/35] feat(keybase): add invocation actions for UCAN v1.0.0-rc.1 --- internal/keybase/actions_invocation.go | 257 +++++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 internal/keybase/actions_invocation.go diff --git a/internal/keybase/actions_invocation.go b/internal/keybase/actions_invocation.go new file mode 100644 index 0000000..8d096fa --- /dev/null +++ b/internal/keybase/actions_invocation.go @@ -0,0 +1,257 @@ +package keybase + +import ( + "context" + "fmt" +) + +// ============================================================================= +// INVOCATION ACTIONS (UCAN v1.0.0-rc.1) +// ============================================================================= + +// InvocationResult represents an invocation in API responses. +type InvocationResult struct { + ID int64 `json:"id"` + CID string `json:"cid"` + Issuer string `json:"iss"` + Subject string `json:"sub"` + Audience string `json:"aud,omitempty"` + Command string `json:"cmd"` + Proofs string `json:"prf"` + Expiration string `json:"exp,omitempty"` + IssuedAt string `json:"iat,omitempty"` + ExecutedAt string `json:"executed_at,omitempty"` + ResultCID string `json:"result_cid,omitempty"` + CreatedAt string `json:"created_at"` +} + +// StoreInvocationParams contains parameters for storing an invocation. +type StoreInvocationParams struct { + CID string `json:"cid"` + Envelope []byte `json:"envelope"` + Issuer string `json:"iss"` + Subject string `json:"sub"` + Audience string `json:"aud,omitempty"` + Command string `json:"cmd"` + Proofs string `json:"prf"` + Expiration string `json:"exp,omitempty"` + IssuedAt string `json:"iat,omitempty"` +} + +// StoreInvocation stores a new UCAN invocation envelope. +func (am *ActionManager) StoreInvocation(ctx context.Context, params StoreInvocationParams) (*InvocationResult, error) { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + if am.kb.didID == 0 { + return nil, fmt.Errorf("DID not initialized") + } + + var aud, exp, iat *string + if params.Audience != "" { + aud = ¶ms.Audience + } + if params.Expiration != "" { + exp = ¶ms.Expiration + } + if params.IssuedAt != "" { + iat = ¶ms.IssuedAt + } + + inv, err := am.kb.queries.CreateInvocation(ctx, CreateInvocationParams{ + DidID: am.kb.didID, + Cid: params.CID, + Envelope: params.Envelope, + Iss: params.Issuer, + Sub: params.Subject, + Aud: aud, + Cmd: params.Command, + Prf: params.Proofs, + Exp: exp, + Iat: iat, + }) + if err != nil { + return nil, fmt.Errorf("create invocation: %w", err) + } + + return invocationToResult(inv), nil +} + +// GetInvocationByCID retrieves an invocation by its CID. +func (am *ActionManager) GetInvocationByCID(ctx context.Context, cid string) (*InvocationResult, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + inv, err := am.kb.queries.GetInvocationByCID(ctx, cid) + if err != nil { + return nil, fmt.Errorf("get invocation: %w", err) + } + + return invocationToResult(inv), nil +} + +// GetInvocationEnvelope retrieves the raw CBOR envelope for an invocation. +func (am *ActionManager) GetInvocationEnvelope(ctx context.Context, cid string) ([]byte, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + envelope, err := am.kb.queries.GetInvocationEnvelopeByCID(ctx, cid) + if err != nil { + return nil, fmt.Errorf("get invocation envelope: %w", err) + } + + return envelope, nil +} + +// ListInvocations returns recent invocations for the current DID. +func (am *ActionManager) ListInvocations(ctx context.Context, limit int64) ([]InvocationResult, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + if am.kb.didID == 0 { + return []InvocationResult{}, nil + } + + if limit <= 0 { + limit = 50 + } + + invocations, err := am.kb.queries.ListInvocationsByDID(ctx, ListInvocationsByDIDParams{ + DidID: am.kb.didID, + Limit: limit, + }) + if err != nil { + return nil, fmt.Errorf("list invocations: %w", err) + } + + results := make([]InvocationResult, len(invocations)) + for i, inv := range invocations { + results[i] = *invocationToResult(inv) + } + + return results, nil +} + +// ListInvocationsByCommand returns invocations for a specific command. +func (am *ActionManager) ListInvocationsByCommand(ctx context.Context, cmd string, limit int64) ([]InvocationResult, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + if am.kb.didID == 0 { + return []InvocationResult{}, nil + } + + if limit <= 0 { + limit = 50 + } + + invocations, err := am.kb.queries.ListInvocationsForCommand(ctx, ListInvocationsForCommandParams{ + DidID: am.kb.didID, + Cmd: cmd, + Limit: limit, + }) + if err != nil { + return nil, fmt.Errorf("list invocations for command: %w", err) + } + + results := make([]InvocationResult, len(invocations)) + for i, inv := range invocations { + results[i] = *invocationToResult(inv) + } + + return results, nil +} + +// ListPendingInvocations returns invocations that haven't been executed yet. +func (am *ActionManager) ListPendingInvocations(ctx context.Context) ([]InvocationResult, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + if am.kb.didID == 0 { + return []InvocationResult{}, nil + } + + invocations, err := am.kb.queries.ListPendingInvocations(ctx, am.kb.didID) + if err != nil { + return nil, fmt.Errorf("list pending invocations: %w", err) + } + + results := make([]InvocationResult, len(invocations)) + for i, inv := range invocations { + results[i] = *invocationToResult(inv) + } + + return results, nil +} + +// MarkInvocationExecuted marks an invocation as executed with an optional result CID. +func (am *ActionManager) MarkInvocationExecuted(ctx context.Context, cid string, resultCID string) error { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + var result *string + if resultCID != "" { + result = &resultCID + } + + err := am.kb.queries.MarkInvocationExecuted(ctx, MarkInvocationExecutedParams{ + ResultCid: result, + Cid: cid, + }) + if err != nil { + return fmt.Errorf("mark invocation executed: %w", err) + } + + return nil +} + +// CleanOldInvocations removes invocations older than 90 days. +func (am *ActionManager) CleanOldInvocations(ctx context.Context) error { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + if err := am.kb.queries.CleanOldInvocations(ctx); err != nil { + return fmt.Errorf("clean old invocations: %w", err) + } + + return nil +} + +// invocationToResult converts a UcanInvocation to InvocationResult. +func invocationToResult(inv UcanInvocation) *InvocationResult { + audience := "" + if inv.Aud != nil { + audience = *inv.Aud + } + expiration := "" + if inv.Exp != nil { + expiration = *inv.Exp + } + issuedAt := "" + if inv.Iat != nil { + issuedAt = *inv.Iat + } + executedAt := "" + if inv.ExecutedAt != nil { + executedAt = *inv.ExecutedAt + } + resultCID := "" + if inv.ResultCid != nil { + resultCID = *inv.ResultCid + } + + return &InvocationResult{ + ID: inv.ID, + CID: inv.Cid, + Issuer: inv.Iss, + Subject: inv.Sub, + Audience: audience, + Command: inv.Cmd, + Proofs: inv.Prf, + Expiration: expiration, + IssuedAt: issuedAt, + ExecutedAt: executedAt, + ResultCID: resultCID, + CreatedAt: inv.CreatedAt, + } +} -- 2.43.0 From 9bec58e29311695bc792ce144f487fddb2e56f8a Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Thu, 8 Jan 2026 19:34:27 -0500 Subject: [PATCH 15/35] docs(TODO): update UCAN v1.0.0-rc.1 migration status and completed items --- TODO.md | 139 +++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 103 insertions(+), 36 deletions(-) diff --git a/TODO.md b/TODO.md index 512c4b6..1614d12 100644 --- a/TODO.md +++ b/TODO.md @@ -6,12 +6,13 @@ Remaining tasks from [MIGRATION.md](./MIGRATION.md) for the Nebula Key Enclave. | Category | Status | Notes | |----------|--------|-------| -| Schema (10 tables) | Complete | `internal/migrations/schema.sql` | -| SQLC Queries | Complete | `internal/migrations/query.sql` | +| Schema (10 tables) | Complete | `internal/migrations/schema.sql` - Updated for v1.0.0-rc.1 | +| SQLC Queries | Complete | `internal/migrations/query.sql` - CID-based queries added | | Generated Code | Complete | `internal/keybase/*.go` | | Basic Plugin Functions | Complete | `generate`, `load`, `exec`, `query`, `ping` | | Encryption | Not Started | WebAuthn PRF key derivation needed | -| **UCAN v1.0.0-rc.1** | **In Progress** | Core types, builders, and policies complete. Uses `go-ucan v1.1.0` | +| **UCAN v1.0.0-rc.1** | **Complete** | Core types, builders, policies, DB actions all complete | +| UCAN DB Actions | Complete | `actions_delegation.go`, `actions_invocation.go` | | MPC Key Shares | Not Started | Key share management missing | | Database Serialization | Incomplete | Export dumps comments only | @@ -117,31 +118,51 @@ The following files implement UCAN v1.0.0-rc.1 using the official go-ucan librar > Note: go-ucan handles chain validation internally via `ExecutionAllowed()`. - [x] Chain validation via go-ucan library -- [ ] Create `internal/crypto/ucan/store.go` - Delegation store - - [ ] Implement `delegation.Loader` interface +- [x] Delegation storage in SQLite via `actions_delegation.go` + - [x] `GetDelegationByCID`, `GetDelegationEnvelope` methods + - [x] `ListDelegations*` methods for chain traversal +- [ ] Create `internal/crypto/ucan/store.go` - Delegation loader for go-ucan + - [ ] Implement `delegation.Loader` interface wrapping keybase actions - [ ] `GetDelegation(cid.Cid) (*delegation.Token, error)` - [ ] Cache loaded delegations for performance ### 1.7 Revocation - [x] `RevocationInvocation()` helper in `invocation.go` -- [ ] Create `internal/crypto/ucan/revocation.go` - Revocation handling - - [ ] Revocation store implementation - - [ ] `IsRevoked(cid.Cid) (bool, error)` query - - [ ] Integration with chain validation +- [x] Revocation storage via `actions_delegation.go` + - [x] `RevokeDelegation(ctx, params)` - Create revocation record + - [x] `IsDelegationRevoked(ctx, cid) (bool, error)` - Query revocation status +- [ ] Create `internal/crypto/ucan/revocation.go` - Revocation checker for go-ucan + - [ ] Implement revocation checking interface + - [ ] Integration with chain validation via `ExecutionAllowed()` ### 1.8 Database Integration -- [ ] Update `internal/migrations/schema.sql` for v1.0.0-rc.1 - - [ ] `ucan_delegations` table (cid, envelope_cbor, iss, aud, sub, cmd, exp, created_at) - - [ ] `ucan_invocations` table (cid, envelope_cbor, iss, sub, cmd, exp, created_at) - - [ ] `ucan_revocations` table (cid, delegation_cid, revoker, created_at) - - [ ] Indexes on iss, aud, sub, cmd for efficient queries +- [x] Update `internal/migrations/schema.sql` for v1.0.0-rc.1 + - [x] `ucan_delegations` table (cid, envelope BLOB, iss, aud, sub, cmd, pol, nbf, exp, is_root, is_powerline) + - [x] `ucan_invocations` table (cid, envelope BLOB, iss, sub, aud, cmd, prf, exp, iat, executed_at, result_cid) + - [x] `ucan_revocations` table (delegation_cid, revoked_by, invocation_cid, reason) + - [x] Indexes on iss, aud, sub, cmd for efficient queries -- [ ] Update `internal/migrations/query.sql` for v1.0.0-rc.1 - - [ ] `InsertDelegation`, `GetDelegationByCID`, `ListDelegationsByAudience` - - [ ] `InsertInvocation`, `GetInvocationByCID` - - [ ] `InsertRevocation`, `IsRevoked`, `GetRevocationsByDelegation` +- [x] Update `internal/migrations/query.sql` for v1.0.0-rc.1 + - [x] `CreateDelegation`, `GetDelegationByCID`, `GetDelegationEnvelopeByCID` + - [x] `ListDelegationsByDID`, `ListDelegationsByIssuer`, `ListDelegationsByAudience`, `ListDelegationsBySubject` + - [x] `ListDelegationsForCommand`, `ListRootDelegations`, `ListPowerlineDelegations` + - [x] `CreateInvocation`, `GetInvocationByCID`, `GetInvocationEnvelopeByCID` + - [x] `ListInvocationsByDID`, `ListInvocationsByIssuer`, `ListInvocationsForCommand` + - [x] `MarkInvocationExecuted`, `ListPendingInvocations` + - [x] `CreateRevocation`, `IsDelegationRevoked`, `GetRevocation`, `ListRevocationsByRevoker` + +- [x] Create `internal/keybase/actions_delegation.go` - Delegation action handlers + - [x] `StoreDelegation`, `GetDelegationByCID`, `GetDelegationEnvelope` + - [x] `ListDelegations`, `ListDelegationsByIssuer`, `ListDelegationsByAudience` + - [x] `ListDelegationsForCommand`, `IsDelegationRevoked`, `RevokeDelegation` + - [x] `DeleteDelegation`, `CleanExpiredDelegations` + +- [x] Create `internal/keybase/actions_invocation.go` - Invocation action handlers + - [x] `StoreInvocation`, `GetInvocationByCID`, `GetInvocationEnvelope` + - [x] `ListInvocations`, `ListInvocationsByCommand`, `ListPendingInvocations` + - [x] `MarkInvocationExecuted`, `CleanOldInvocations` ### 1.9 MPC Signing Integration @@ -221,15 +242,26 @@ The following files implement UCAN v1.0.0-rc.1 using the official go-ucan librar ### 4.2 UCAN Token Actions (v1.0.0-rc.1) -- [ ] `CreateDelegation(ctx, params) (*DelegationResult, error)` -- [ ] `ListDelegations(ctx) ([]DelegationResult, error)` -- [ ] `GetDelegationByCID(ctx, cid) (*DelegationResult, error)` -- [ ] `ListDelegationsByAudience(ctx, audience) ([]DelegationResult, error)` -- [ ] `CreateInvocation(ctx, params) (*InvocationResult, error)` -- [ ] `ValidateInvocation(ctx, invocation) (*ValidationResult, error)` -- [ ] `RevokeUCAN(ctx, cid) error` -- [ ] `IsUCANRevoked(ctx, cid) (bool, error)` -- [ ] `CleanExpiredUCANs(ctx) error` +- [x] `StoreDelegation(ctx, params) (*DelegationResult, error)` +- [x] `ListDelegations(ctx) ([]DelegationResult, error)` +- [x] `GetDelegationByCID(ctx, cid) (*DelegationResult, error)` +- [x] `GetDelegationEnvelope(ctx, cid) ([]byte, error)` +- [x] `ListDelegationsByIssuer(ctx, issuer) ([]DelegationResult, error)` +- [x] `ListDelegationsByAudience(ctx, audience) ([]DelegationResult, error)` +- [x] `ListDelegationsForCommand(ctx, cmd) ([]DelegationResult, error)` +- [x] `StoreInvocation(ctx, params) (*InvocationResult, error)` +- [x] `GetInvocationByCID(ctx, cid) (*InvocationResult, error)` +- [x] `GetInvocationEnvelope(ctx, cid) ([]byte, error)` +- [x] `ListInvocations(ctx, limit) ([]InvocationResult, error)` +- [x] `ListInvocationsByCommand(ctx, cmd, limit) ([]InvocationResult, error)` +- [x] `ListPendingInvocations(ctx) ([]InvocationResult, error)` +- [x] `MarkInvocationExecuted(ctx, cid, resultCID) error` +- [x] `RevokeDelegation(ctx, params) error` +- [x] `IsDelegationRevoked(ctx, cid) (bool, error)` +- [x] `DeleteDelegation(ctx, cid) error` +- [x] `CleanExpiredDelegations(ctx) error` +- [x] `CleanOldInvocations(ctx) error` +- [ ] `ValidateInvocation(ctx, invocation) (*ValidationResult, error)` - Requires delegation.Loader ### 4.3 Verification Method Actions @@ -323,10 +355,11 @@ The following files implement UCAN v1.0.0-rc.1 using the official go-ucan librar ### 6.1 Extend `exec` Resource Handlers - [ ] Add `key_shares` resource handler -- [ ] Add `delegations` resource handler (v1.0.0-rc.1) +- [x] Add `ucans` resource handler (v1.0.0-rc.1 delegations) +- [x] Add `delegations` resource handler (v1.0.0-rc.1) - [ ] Add `invocations` resource handler (v1.0.0-rc.1) -- [ ] Add `verification_methods` resource handler -- [ ] Add `services` resource handler +- [x] Add `verification_methods` resource handler +- [x] Add `services` resource handler - [ ] Add `sync_checkpoints` resource handler ### 6.2 Extend `generate` Function @@ -478,13 +511,13 @@ The following files implement UCAN v1.0.0-rc.1 using the official go-ucan librar ## Priority Order -1. **CRITICAL (Spec Compliance)** - ✅ Core Complete - - ~~UCAN v1.0.0-rc.1 Migration (Section 1)~~ ✅ Core types, builders, policies done +1. **CRITICAL (Spec Compliance)** - ✅ Complete + - ~~UCAN v1.0.0-rc.1 Migration (Section 1)~~ ✅ All core items complete - ~~Core data structures (1.1)~~ ✅ Using go-ucan v1.1.0 - ~~Envelope format (1.2)~~ ✅ Handled by go-ucan - ~~Delegation operations (1.3)~~ ✅ DelegationBuilder complete - ~~Invocation operations (1.4)~~ ✅ InvocationBuilder complete - - Database integration (1.8) - Next priority + - ~~Database integration (1.8)~~ ✅ Schema, queries, and actions complete - MPC signing integration (1.9) - Next priority 2. **High Priority (Core Functionality)** @@ -492,11 +525,10 @@ The following files implement UCAN v1.0.0-rc.1 using the official go-ucan librar - Credential Creation (6.2, 4.7) - Key Share Actions (4.1) - Account Actions (4.6) - - UCAN Database Integration (1.8) + - Delegation Loader for go-ucan (1.6) 3. **Medium Priority (Authorization)** - - Delegation store (1.6) - - Revocation store (1.7) + - Revocation checker for go-ucan (1.7) - MPC Signing (1.9) - Encryption Strategy (2.1, 2.2) @@ -511,6 +543,41 @@ The following files implement UCAN v1.0.0-rc.1 using the official go-ucan librar ## Completed Items +### UCAN v1.0.0-rc.1 Database Integration (January 2025) + +Schema and action handlers for storing/querying UCAN delegations and invocations: + +- ✅ `internal/migrations/schema.sql` - v1.0.0-rc.1 tables + - `ucan_delegations` - CID-indexed delegation storage with envelope BLOB + - `ucan_invocations` - CID-indexed invocation storage with execution tracking + - `ucan_revocations` - Revocation records with reason and invocation CID + - Updated `grants` table to use `delegation_cid` instead of `ucan_id` + +- ✅ `internal/migrations/query.sql` - CID-based queries + - Delegation CRUD: Create, Get by CID, List by DID/Issuer/Audience/Subject/Command + - Invocation CRUD: Create, Get by CID, List by DID/Issuer/Command, Mark executed + - Revocation: Create, Check revoked, Get revocation, List by revoker + +- ✅ `internal/keybase/actions_delegation.go` - Delegation action handlers + - StoreDelegation, GetDelegationByCID, GetDelegationEnvelope + - ListDelegations, ListDelegationsByIssuer, ListDelegationsByAudience + - ListDelegationsForCommand, IsDelegationRevoked, RevokeDelegation + - DeleteDelegation, CleanExpiredDelegations + +- ✅ `internal/keybase/actions_invocation.go` - Invocation action handlers + - StoreInvocation, GetInvocationByCID, GetInvocationEnvelope + - ListInvocations, ListInvocationsByCommand, ListPendingInvocations + - MarkInvocationExecuted, CleanOldInvocations + +- ✅ `main.go` - Updated exec handlers for v1.0.0-rc.1 + - `executeUCANAction` uses delegation methods (list, get, revoke, verify, cleanup) + - `executeDelegationAction` uses CID-based methods (list by issuer/audience/command) + - `validateUCAN` uses `IsDelegationRevoked` instead of old `IsUCANRevoked` + +- ✅ Deleted old action files + - `internal/keybase/actions_ucan.go` - Old JWT-based UCAN actions + - `internal/keybase/actions_delegation.go` - Old ID-based delegation actions + ### UCAN v1.0.0-rc.1 Core (January 2025) The following was completed using `github.com/ucan-wg/go-ucan v1.1.0`: -- 2.43.0 From e5230a6360e017c0bb3ac1b5be91662d9fbcc28c Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Thu, 8 Jan 2026 20:16:49 -0500 Subject: [PATCH 16/35] docs(migration): update SQLite driver reference in migration guide --- MIGRATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MIGRATION.md b/MIGRATION.md index 23dce5e..b60a63b 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -61,7 +61,7 @@ This document outlines the SQL schema design for the Nebula wallet's **encrypted ### SQLite WASM + Encryption 1. **Encryption**: Database encrypted with key derived from WebAuthn PRF extension -2. **Pure Go Driver**: `modernc.org/sqlite` (WASM compatible, no CGO) +2. **Pure Go Driver**: `github.com/ncruces/go-sqlite3` (WASM compatible, no CGO) 3. **Minimal Schema**: Only security-critical data in enclave 4. **INTEGER PRIMARY KEY**: Auto-increment without AUTOINCREMENT overhead 5. **TEXT for binary**: Base64 encoded (BLOB performance poor in WASM) -- 2.43.0 From 842fdc1923f8537b6ed0e5b70861afbb0c843032 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Thu, 8 Jan 2026 20:16:50 -0500 Subject: [PATCH 17/35] refactor(keybase): switch to native SQLite serialization and deserialization --- TODO.md | 49 ++++++------ go.mod | 1 + go.sum | 2 + internal/keybase/conn.go | 156 +++++++++++-------------------------- internal/types/generate.go | 31 +++++++- main.go | 107 ++++++++++++++++++++++--- 6 files changed, 202 insertions(+), 144 deletions(-) diff --git a/TODO.md b/TODO.md index 1614d12..fc94c26 100644 --- a/TODO.md +++ b/TODO.md @@ -232,13 +232,13 @@ The following files implement UCAN v1.0.0-rc.1 using the official go-ucan librar ### 4.1 Key Share Actions -- [ ] `CreateKeyShare(ctx, params) (*KeyShareResult, error)` -- [ ] `ListKeyShares(ctx) ([]KeyShareResult, error)` -- [ ] `GetKeyShareByID(ctx, shareID) (*KeyShareResult, error)` -- [ ] `GetKeyShareByKeyID(ctx, keyID) (*KeyShareResult, error)` -- [ ] `RotateKeyShare(ctx, shareID) error` -- [ ] `ArchiveKeyShare(ctx, shareID) error` -- [ ] `DeleteKeyShare(ctx, shareID) error` +- [x] `CreateKeyShare(ctx, params) (*KeyShareResult, error)` +- [x] `ListKeyShares(ctx) ([]KeyShareResult, error)` +- [x] `GetKeyShareByID(ctx, shareID) (*KeyShareResult, error)` +- [x] `GetKeyShareByKeyID(ctx, keyID) (*KeyShareResult, error)` +- [x] `RotateKeyShare(ctx, shareID) error` +- [x] `ArchiveKeyShare(ctx, shareID) error` +- [x] `DeleteKeyShare(ctx, shareID) error` ### 4.2 UCAN Token Actions (v1.0.0-rc.1) @@ -290,12 +290,12 @@ The following files implement UCAN v1.0.0-rc.1 using the official go-ucan librar ### 4.6 Account Actions (Extend Existing) -- [ ] `CreateAccount(ctx, params) (*AccountResult, error)` -- [ ] `ListAccountsByChain(ctx, chainID) ([]AccountResult, error)` -- [ ] `GetDefaultAccount(ctx, chainID) (*AccountResult, error)` -- [ ] `SetDefaultAccount(ctx, accountID, chainID) error` -- [ ] `UpdateAccountLabel(ctx, accountID, label) error` -- [ ] `DeleteAccount(ctx, accountID) error` +- [x] `CreateAccount(ctx, params) (*AccountResult, error)` +- [x] `ListAccountsByChain(ctx, chainID) ([]AccountResult, error)` +- [x] `GetDefaultAccount(ctx, chainID) (*AccountResult, error)` +- [x] `SetDefaultAccount(ctx, accountID, chainID) error` +- [x] `UpdateAccountLabel(ctx, accountID, label) error` +- [x] `DeleteAccount(ctx, accountID) error` ### 4.7 Credential Actions (Extend Existing) @@ -327,23 +327,24 @@ The following files implement UCAN v1.0.0-rc.1 using the official go-ucan librar ### 5.1 Key Share Storage -- [ ] Parse key share data from MPC protocol -- [ ] Encrypt share data before storage -- [ ] Store public key and chain code -- [ ] Track party index and threshold +- [x] Parse key share data from MPC protocol - `KeyShareInput` in generate +- [x] Store public key and chain code - `CreateKeyShare` action +- [x] Track party index and threshold - stored in `key_shares` table +- [ ] Encrypt share data before storage - PRF key derivation needed ### 5.2 Account Derivation +- [x] Basic address derivation from public key - `deriveCosmosAddress()` +- [x] Create initial account during generate - `createInitialAccount()` - [ ] Implement BIP44 derivation path parsing -- [ ] Derive addresses from public keys - [ ] Support multiple chains (Cosmos 118, Ethereum 60) -- [ ] Generate proper address encoding per chain +- [ ] Generate proper bech32 address encoding per chain ### 5.3 Key Rotation -- [ ] Implement key rotation workflow -- [ ] Archive old shares -- [ ] Update status transitions +- [x] Implement key rotation workflow - `RotateKeyShare` action +- [x] Archive old shares - `ArchiveKeyShare` action +- [x] Status transitions - managed in database - [ ] Handle rotation failures gracefully --- @@ -364,11 +365,13 @@ The following files implement UCAN v1.0.0-rc.1 using the official go-ucan librar ### 6.2 Extend `generate` Function +- [x] Accept optional MPC keyshare data in input +- [x] Create initial keyshare if provided +- [x] Create initial account from keyshare - [ ] Parse WebAuthn credential properly (CBOR/COSE format) - [ ] Extract public key from credential - [ ] Create initial verification method - [ ] Create initial credential record -- [ ] Generate initial account (if key share provided) ### 6.3 Signing Function diff --git a/go.mod b/go.mod index 272588a..f849419 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( 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/google/uuid v1.6.0 // indirect github.com/gtank/merlin v0.1.1 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 // indirect diff --git a/go.sum b/go.sum index fd744c6..e83332e 100644 --- a/go.sum +++ b/go.sum @@ -27,6 +27,8 @@ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z 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/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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= diff --git a/internal/keybase/conn.go b/internal/keybase/conn.go index fdccd93..43ed1c2 100644 --- a/internal/keybase/conn.go +++ b/internal/keybase/conn.go @@ -1,4 +1,3 @@ -// Package keybase contains the SQLite database for cryptographic keys. package keybase import ( @@ -6,18 +5,22 @@ import ( "database/sql" "encoding/json" "fmt" - "strings" "sync" "enclave/internal/migrations" - _ "github.com/ncruces/go-sqlite3/driver" + "github.com/ncruces/go-sqlite3" + "github.com/ncruces/go-sqlite3/driver" _ "github.com/ncruces/go-sqlite3/embed" + "github.com/ncruces/go-sqlite3/ext/hash" + "github.com/ncruces/go-sqlite3/ext/serdes" + "github.com/ncruces/go-sqlite3/ext/uuid" ) // Keybase encapsulates the encrypted key storage database. type Keybase struct { db *sql.DB + conn *sqlite3.Conn // raw connection for serdes queries *Queries did string didID int64 @@ -38,19 +41,32 @@ func Open() (*Keybase, error) { return instance, nil } - conn, err := sql.Open("sqlite3", ":memory:") + var rawConn *sqlite3.Conn + initCallback := func(conn *sqlite3.Conn) error { + rawConn = conn + if err := hash.Register(conn); err != nil { + return fmt.Errorf("register hash extension: %w", err) + } + if err := uuid.Register(conn); err != nil { + return fmt.Errorf("register uuid extension: %w", err) + } + return nil + } + + db, err := driver.Open(":memory:", initCallback) if err != nil { return nil, fmt.Errorf("keybase: open database: %w", err) } - if _, err := conn.Exec(migrations.SchemaSQL); err != nil { - conn.Close() + if _, err := db.Exec(migrations.SchemaSQL); err != nil { + db.Close() return nil, fmt.Errorf("keybase: init schema: %w", err) } instance = &Keybase{ - db: conn, - queries: New(conn), + db: db, + conn: rawConn, + queries: New(db), } return instance, nil @@ -169,10 +185,21 @@ func (k *Keybase) Initialize(ctx context.Context, credentialBytes []byte) (strin // Load restores the database state from serialized bytes and sets the current DID. func (k *Keybase) Load(ctx context.Context, data []byte) (string, error) { - if len(data) < 10 { + if len(data) < 100 { return "", fmt.Errorf("keybase: invalid database format") } + k.mu.Lock() + defer k.mu.Unlock() + + if k.conn == nil { + return "", fmt.Errorf("keybase: database not initialized") + } + + if err := serdes.Deserialize(k.conn, "main", data); err != nil { + return "", fmt.Errorf("keybase: deserialize database: %w", err) + } + docs, err := k.queries.ListAllDIDs(ctx) if err != nil { return "", fmt.Errorf("keybase: list DIDs: %w", err) @@ -182,127 +209,34 @@ func (k *Keybase) Load(ctx context.Context, data []byte) (string, error) { return "", fmt.Errorf("keybase: no DID found in database") } - k.mu.Lock() k.did = docs[0].Did k.didID = docs[0].ID - k.mu.Unlock() return k.did, nil } -// Serialize exports the database state as bytes. +// Serialize exports the database state as bytes using native SQLite serialization. func (k *Keybase) Serialize() ([]byte, error) { k.mu.RLock() defer k.mu.RUnlock() - if k.db == nil { + if k.conn == nil { return nil, fmt.Errorf("keybase: database not initialized") } - return k.exportDump() -} - -func (k *Keybase) exportDump() ([]byte, error) { - var dump strings.Builder - dump.WriteString(migrations.SchemaSQL + "\n") - - tables := []string{ - "did_documents", "verification_methods", "credentials", - "key_shares", "accounts", "ucan_tokens", "ucan_revocations", - "sessions", "services", "grants", "delegations", "sync_checkpoints", - } - - for _, table := range tables { - if err := k.exportTable(&dump, table); err != nil { - continue - } - } - - return []byte(dump.String()), nil -} - -func (k *Keybase) exportTable(dump *strings.Builder, table string) error { - rows, err := k.db.Query(fmt.Sprintf("SELECT * FROM %s", table)) - if err != nil { - return err - } - defer rows.Close() - - cols, err := rows.Columns() - if err != nil { - return err - } - - for rows.Next() { - values := make([]any, len(cols)) - valuePtrs := make([]any, len(cols)) - for i := range values { - valuePtrs[i] = &values[i] - } - - if err := rows.Scan(valuePtrs...); err != nil { - continue - } - - dump.WriteString(fmt.Sprintf("INSERT INTO %s (", table)) - dump.WriteString(strings.Join(cols, ", ")) - dump.WriteString(") VALUES (") - - for i, val := range values { - if i > 0 { - dump.WriteString(", ") - } - dump.WriteString(formatSQLValue(val)) - } - dump.WriteString(");\n") - } - - return rows.Err() -} - -func formatSQLValue(val any) string { - if val == nil { - return "NULL" - } - - switch v := val.(type) { - case int64: - return fmt.Sprintf("%d", v) - case float64: - return fmt.Sprintf("%f", v) - case bool: - if v { - return "1" - } - return "0" - case []byte: - return fmt.Sprintf("'%s'", escapeSQLString(string(v))) - case string: - return fmt.Sprintf("'%s'", escapeSQLString(v)) - default: - return fmt.Sprintf("'%s'", escapeSQLString(fmt.Sprintf("%v", v))) - } -} - -func escapeSQLString(s string) string { - return strings.ReplaceAll(s, "'", "''") + return serdes.Serialize(k.conn, "main") } func (k *Keybase) RestoreFromDump(data []byte) error { k.mu.Lock() defer k.mu.Unlock() - statements := strings.Split(string(data), ";\n") - for _, stmt := range statements { - stmt = strings.TrimSpace(stmt) - if stmt == "" || strings.HasPrefix(stmt, "--") { - continue - } - if strings.HasPrefix(stmt, "INSERT INTO") { - if _, err := k.db.Exec(stmt); err != nil { - return fmt.Errorf("keybase: failed to execute statement: %w", err) - } - } + if k.conn == nil { + return fmt.Errorf("keybase: database not initialized") + } + + if err := serdes.Deserialize(k.conn, "main", data); err != nil { + return fmt.Errorf("keybase: deserialize database: %w", err) } docs, err := k.queries.ListAllDIDs(context.Background()) diff --git a/internal/types/generate.go b/internal/types/generate.go index 9cd550c..2803f36 100644 --- a/internal/types/generate.go +++ b/internal/types/generate.go @@ -2,11 +2,40 @@ package types // GenerateInput represents the input for the generate function type GenerateInput struct { - Credential string `json:"credential"` // Base64-encoded PublicKeyCredential + Credential string `json:"credential"` // Base64-encoded WebAuthn credential + + // MPC keyshare data (optional - if provided, creates initial keyshare and account) + KeyShare *KeyShareInput `json:"key_share,omitempty"` +} + +// KeyShareInput represents MPC keyshare data for initialization +type KeyShareInput struct { + KeyID string `json:"key_id"` + PartyIndex int64 `json:"party_index"` + Threshold int64 `json:"threshold"` + TotalParties int64 `json:"total_parties"` + Curve string `json:"curve"` + ShareData string `json:"share_data"` + PublicKey string `json:"public_key"` + ChainCode string `json:"chain_code,omitempty"` + DerivationPath string `json:"derivation_path,omitempty"` } // GenerateOutput represents the output of the generate function type GenerateOutput struct { DID string `json:"did"` Database []byte `json:"database"` + + // KeyShare info if a keyshare was provided + KeyShareID string `json:"key_share_id,omitempty"` + + // Account info if an account was created + Account *AccountInfo `json:"account,omitempty"` +} + +// AccountInfo represents created account information +type AccountInfo struct { + Address string `json:"address"` + ChainID string `json:"chain_id"` + CoinType int64 `json:"coin_type"` } diff --git a/main.go b/main.go index 08b1042..8cf697e 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "context" "encoding/base64" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -67,14 +68,14 @@ func generate() int32 { return 1 } - did, err := initializeDatabase(credentialBytes) + result, err := initializeDatabase(credentialBytes, input.KeyShare) if err != nil { pdk.SetError(fmt.Errorf("generate: failed to initialize database: %w", err)) return 1 } state.SetInitialized(true) - state.SetDID(did) + state.SetDID(result.DID) dbBytes, err := serializeDatabase() if err != nil { @@ -83,8 +84,10 @@ func generate() int32 { } output := types.GenerateOutput{ - DID: did, - Database: dbBytes, + DID: result.DID, + Database: dbBytes, + KeyShareID: result.KeyShareID, + Account: result.Account, } if err := pdk.OutputJSON(output); err != nil { @@ -92,7 +95,7 @@ func generate() int32 { return 1 } - pdk.Log(pdk.LogInfo, fmt.Sprintf("generate: created DID %s", did)) + pdk.Log(pdk.LogInfo, fmt.Sprintf("generate: created DID %s", result.DID)) return 0 } @@ -238,20 +241,106 @@ func query() int32 { return 0 } -func initializeDatabase(credentialBytes []byte) (string, error) { +type initResult struct { + DID string + KeyShareID string + Account *types.AccountInfo +} + +func initializeDatabase(credentialBytes []byte, keyShareInput *types.KeyShareInput) (*initResult, error) { kb, err := keybase.Open() if err != nil { - return "", fmt.Errorf("open database: %w", err) + return nil, fmt.Errorf("open database: %w", err) } ctx := context.Background() did, err := kb.Initialize(ctx, credentialBytes) if err != nil { - return "", fmt.Errorf("initialize: %w", err) + return nil, fmt.Errorf("initialize: %w", err) + } + + result := &initResult{DID: did} + + if keyShareInput != nil { + keyShareID, account, err := createInitialKeyShare(ctx, keyShareInput) + if err != nil { + pdk.Log(pdk.LogWarn, fmt.Sprintf("initializeDatabase: failed to create keyshare: %s", err)) + } else { + result.KeyShareID = keyShareID + result.Account = account + pdk.Log(pdk.LogInfo, fmt.Sprintf("initializeDatabase: created keyshare %s", keyShareID)) + } } pdk.Log(pdk.LogDebug, "initializeDatabase: created schema and initial records") - return did, nil + return result, nil +} + +func createInitialKeyShare(ctx context.Context, input *types.KeyShareInput) (string, *types.AccountInfo, error) { + am, err := keybase.NewActionManager() + if err != nil { + return "", nil, fmt.Errorf("action manager: %w", err) + } + + ks, err := am.CreateKeyShare(ctx, keybase.NewKeyShareInput{ + KeyID: input.KeyID, + PartyIndex: input.PartyIndex, + Threshold: input.Threshold, + TotalParties: input.TotalParties, + Curve: input.Curve, + ShareData: input.ShareData, + PublicKey: input.PublicKey, + ChainCode: input.ChainCode, + DerivationPath: input.DerivationPath, + }) + if err != nil { + return "", nil, fmt.Errorf("create keyshare: %w", err) + } + + account, err := createInitialAccount(ctx, am, ks.ID, input.PublicKey) + if err != nil { + pdk.Log(pdk.LogWarn, fmt.Sprintf("createInitialKeyShare: failed to create account: %s", err)) + return ks.ShareID, nil, nil + } + + return ks.ShareID, account, nil +} + +func createInitialAccount(ctx context.Context, am *keybase.ActionManager, keyShareID int64, publicKey string) (*types.AccountInfo, error) { + address := deriveCosmosAddress(publicKey) + if address == "" { + return nil, fmt.Errorf("failed to derive address from public key") + } + + acc, err := am.CreateAccount(ctx, keybase.NewAccountInput{ + KeyShareID: keyShareID, + Address: address, + ChainID: "sonr-testnet-1", + CoinType: 118, + AccountIndex: 0, + AddressIndex: 0, + Label: "Default Account", + }) + if err != nil { + return nil, fmt.Errorf("create account: %w", err) + } + + return &types.AccountInfo{ + Address: acc.Address, + ChainID: acc.ChainID, + CoinType: acc.CoinType, + }, nil +} + +func deriveCosmosAddress(publicKeyHex string) string { + if publicKeyHex == "" { + return "" + } + pubBytes, err := hex.DecodeString(publicKeyHex) + if err != nil || len(pubBytes) < 20 { + return "" + } + return fmt.Sprintf("snr1%x", pubBytes[:20]) } func serializeDatabase() ([]byte, error) { -- 2.43.0 From 527dfee7f60cfbf867630f517f07cdd7913afe9e Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Fri, 9 Jan 2026 08:19:15 -0500 Subject: [PATCH 18/35] refactor(enclave): restructure enclave to use go-pdk and support WASM target --- AGENTS.md | 22 +- Makefile | 2 +- internal/state/state.go | 2 + main.go | 1018 --------------------------------------- 4 files changed, 17 insertions(+), 1027 deletions(-) delete mode 100644 main.go diff --git a/AGENTS.md b/AGENTS.md index a5e70e9..cf229bb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -170,16 +170,22 @@ if err != nil { ``` motr-enclave/ -├── main.go # Plugin entry point, exported functions -├── db/ -│ ├── schema.sql # Database schema -│ ├── query.sql # SQLC query definitions -│ └── *.go # Generated SQLC code -├── sqlc.yaml # SQLC configuration -├── Makefile # Build commands -└── go.mod # Go module +├── cmd/ +│ └── enclave/ +│ └── main.go # Plugin entry point (WASM-only, go-pdk imports) +├── internal/ +│ ├── keybase/ # Database access layer +│ ├── crypto/ # Cryptographic operations +│ ├── state/ # Plugin state management (WASM-only) +│ ├── types/ # Input/output type definitions +│ └── migrations/ # Database migrations +├── sqlc.yaml # SQLC configuration +├── Makefile # Build commands +└── go.mod # Go module ``` +Note: Files with `//go:build wasip1` constraint (cmd/enclave/, internal/state/) only compile for WASM target. + ## Dependencies Install with `make deps`: diff --git a/Makefile b/Makefile index 609b830..0be29eb 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ deps: build: @echo "Building WASM plugin..." - @GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o $(BUILD_DIR)/$(BINARY) . + @GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o $(BUILD_DIR)/$(BINARY) ./cmd/enclave @echo "Built $(BUILD_DIR)/$(BINARY)" sdk: diff --git a/internal/state/state.go b/internal/state/state.go index 98b4c94..7dab026 100644 --- a/internal/state/state.go +++ b/internal/state/state.go @@ -1,3 +1,5 @@ +//go:build wasip1 + // Package state contains the state of the enclave. package state diff --git a/main.go b/main.go deleted file mode 100644 index 8cf697e..0000000 --- a/main.go +++ /dev/null @@ -1,1018 +0,0 @@ -package main - -import ( - "context" - "encoding/base64" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "strings" - - "enclave/internal/keybase" - "enclave/internal/state" - "enclave/internal/types" - - "github.com/extism/go-pdk" -) - -func main() { state.Default() } - -//go:wasmexport ping -func ping() int32 { - pdk.Log(pdk.LogInfo, "ping: received request") - - var input types.PingInput - if err := pdk.InputJSON(&input); err != nil { - output := types.PingOutput{ - Success: false, - Message: fmt.Sprintf("failed to parse input: %s", err), - } - pdk.OutputJSON(output) - return 0 - } - - output := types.PingOutput{ - Success: true, - Message: "pong", - Echo: input.Message, - } - - if err := pdk.OutputJSON(output); err != nil { - pdk.Log(pdk.LogError, fmt.Sprintf("ping: failed to output: %s", err)) - return 1 - } - - pdk.Log(pdk.LogInfo, fmt.Sprintf("ping: responded with echo=%s", input.Message)) - return 0 -} - -//go:wasmexport generate -func generate() int32 { - pdk.Log(pdk.LogInfo, "generate: starting database initialization") - - var input types.GenerateInput - if err := pdk.InputJSON(&input); err != nil { - pdk.SetError(fmt.Errorf("generate: failed to parse input: %w", err)) - return 1 - } - - if input.Credential == "" { - pdk.SetError(errors.New("generate: credential is required")) - return 1 - } - - credentialBytes, err := base64.StdEncoding.DecodeString(input.Credential) - if err != nil { - pdk.SetError(fmt.Errorf("generate: invalid base64 credential: %w", err)) - return 1 - } - - result, err := initializeDatabase(credentialBytes, input.KeyShare) - if err != nil { - pdk.SetError(fmt.Errorf("generate: failed to initialize database: %w", err)) - return 1 - } - - state.SetInitialized(true) - state.SetDID(result.DID) - - dbBytes, err := serializeDatabase() - if err != nil { - pdk.SetError(fmt.Errorf("generate: failed to serialize database: %w", err)) - return 1 - } - - output := types.GenerateOutput{ - DID: result.DID, - Database: dbBytes, - KeyShareID: result.KeyShareID, - Account: result.Account, - } - - if err := pdk.OutputJSON(output); err != nil { - pdk.SetError(fmt.Errorf("generate: failed to output result: %w", err)) - return 1 - } - - pdk.Log(pdk.LogInfo, fmt.Sprintf("generate: created DID %s", result.DID)) - return 0 -} - -//go:wasmexport load -func load() int32 { - pdk.Log(pdk.LogInfo, "load: loading database from buffer") - - var input types.LoadInput - if err := pdk.InputJSON(&input); err != nil { - pdk.SetError(fmt.Errorf("load: failed to parse input: %w", err)) - return 1 - } - - if len(input.Database) == 0 { - pdk.SetError(errors.New("load: database buffer is required")) - return 1 - } - - did, err := loadDatabase(input.Database) - if err != nil { - output := types.LoadOutput{ - Success: false, - Error: err.Error(), - } - pdk.OutputJSON(output) - return 1 - } - - state.SetInitialized(true) - state.SetDID(did) - - output := types.LoadOutput{ - Success: true, - DID: did, - } - - if err := pdk.OutputJSON(output); err != nil { - pdk.SetError(fmt.Errorf("load: failed to output result: %w", err)) - return 1 - } - - pdk.Log(pdk.LogInfo, fmt.Sprintf("load: loaded database for DID %s", did)) - return 0 -} - -//go:wasmexport exec -func exec() int32 { - pdk.Log(pdk.LogInfo, "exec: executing action") - - if !state.IsInitialized() { - output := types.ExecOutput{Success: false, Error: "database not initialized, call generate or load first"} - pdk.OutputJSON(output) - return 0 - } - - var input types.ExecInput - if err := pdk.InputJSON(&input); err != nil { - output := types.ExecOutput{Success: false, Error: fmt.Sprintf("failed to parse input: %s", err)} - pdk.OutputJSON(output) - return 0 - } - - if input.Filter == "" { - output := types.ExecOutput{Success: false, Error: "filter is required"} - pdk.OutputJSON(output) - return 0 - } - - params, err := parseFilter(input.Filter) - if err != nil { - output := types.ExecOutput{Success: false, Error: fmt.Sprintf("invalid filter: %s", err)} - pdk.OutputJSON(output) - return 0 - } - - if input.Token != "" { - if err := validateUCAN(input.Token, params); err != nil { - output := types.ExecOutput{ - Success: false, - Error: fmt.Sprintf("authorization failed: %s", err.Error()), - } - pdk.OutputJSON(output) - return 1 - } - } - - result, err := executeAction(params) - if err != nil { - output := types.ExecOutput{ - Success: false, - Error: err.Error(), - } - pdk.OutputJSON(output) - return 1 - } - - output := types.ExecOutput{ - Success: true, - Result: result, - } - - pdk.OutputJSON(output) - pdk.Log(pdk.LogInfo, fmt.Sprintf("exec: completed %s on %s", params.Action, params.Resource)) - return 0 -} - -//go:wasmexport query -func query() int32 { - pdk.Log(pdk.LogInfo, "query: resolving DID document") - - if !state.IsInitialized() { - pdk.SetError(errors.New("database not initialized, call generate or load first")) - return 1 - } - - var input types.QueryInput - if err := pdk.InputJSON(&input); err != nil { - pdk.SetError(fmt.Errorf("query: failed to parse input: %w", err)) - return 1 - } - - if input.DID == "" { - input.DID = state.GetDID() - } - - if !strings.HasPrefix(input.DID, "did:") { - pdk.SetError(errors.New("query: invalid DID format")) - return 1 - } - - output, err := resolveDID(input.DID) - if err != nil { - pdk.SetError(fmt.Errorf("query: failed to resolve DID: %w", err)) - return 1 - } - - if err := pdk.OutputJSON(output); err != nil { - pdk.SetError(fmt.Errorf("query: failed to output result: %w", err)) - return 1 - } - - pdk.Log(pdk.LogInfo, fmt.Sprintf("query: resolved DID %s", input.DID)) - return 0 -} - -type initResult struct { - DID string - KeyShareID string - Account *types.AccountInfo -} - -func initializeDatabase(credentialBytes []byte, keyShareInput *types.KeyShareInput) (*initResult, error) { - kb, err := keybase.Open() - if err != nil { - return nil, fmt.Errorf("open database: %w", err) - } - - ctx := context.Background() - did, err := kb.Initialize(ctx, credentialBytes) - if err != nil { - return nil, fmt.Errorf("initialize: %w", err) - } - - result := &initResult{DID: did} - - if keyShareInput != nil { - keyShareID, account, err := createInitialKeyShare(ctx, keyShareInput) - if err != nil { - pdk.Log(pdk.LogWarn, fmt.Sprintf("initializeDatabase: failed to create keyshare: %s", err)) - } else { - result.KeyShareID = keyShareID - result.Account = account - pdk.Log(pdk.LogInfo, fmt.Sprintf("initializeDatabase: created keyshare %s", keyShareID)) - } - } - - pdk.Log(pdk.LogDebug, "initializeDatabase: created schema and initial records") - return result, nil -} - -func createInitialKeyShare(ctx context.Context, input *types.KeyShareInput) (string, *types.AccountInfo, error) { - am, err := keybase.NewActionManager() - if err != nil { - return "", nil, fmt.Errorf("action manager: %w", err) - } - - ks, err := am.CreateKeyShare(ctx, keybase.NewKeyShareInput{ - KeyID: input.KeyID, - PartyIndex: input.PartyIndex, - Threshold: input.Threshold, - TotalParties: input.TotalParties, - Curve: input.Curve, - ShareData: input.ShareData, - PublicKey: input.PublicKey, - ChainCode: input.ChainCode, - DerivationPath: input.DerivationPath, - }) - if err != nil { - return "", nil, fmt.Errorf("create keyshare: %w", err) - } - - account, err := createInitialAccount(ctx, am, ks.ID, input.PublicKey) - if err != nil { - pdk.Log(pdk.LogWarn, fmt.Sprintf("createInitialKeyShare: failed to create account: %s", err)) - return ks.ShareID, nil, nil - } - - return ks.ShareID, account, nil -} - -func createInitialAccount(ctx context.Context, am *keybase.ActionManager, keyShareID int64, publicKey string) (*types.AccountInfo, error) { - address := deriveCosmosAddress(publicKey) - if address == "" { - return nil, fmt.Errorf("failed to derive address from public key") - } - - acc, err := am.CreateAccount(ctx, keybase.NewAccountInput{ - KeyShareID: keyShareID, - Address: address, - ChainID: "sonr-testnet-1", - CoinType: 118, - AccountIndex: 0, - AddressIndex: 0, - Label: "Default Account", - }) - if err != nil { - return nil, fmt.Errorf("create account: %w", err) - } - - return &types.AccountInfo{ - Address: acc.Address, - ChainID: acc.ChainID, - CoinType: acc.CoinType, - }, nil -} - -func deriveCosmosAddress(publicKeyHex string) string { - if publicKeyHex == "" { - return "" - } - pubBytes, err := hex.DecodeString(publicKeyHex) - if err != nil || len(pubBytes) < 20 { - return "" - } - return fmt.Sprintf("snr1%x", pubBytes[:20]) -} - -func serializeDatabase() ([]byte, error) { - kb := keybase.Get() - if kb == nil { - return nil, errors.New("database not initialized") - } - return kb.Serialize() -} - -func loadDatabase(data []byte) (string, error) { - if len(data) < 10 { - return "", errors.New("invalid database format") - } - - kb, err := keybase.Open() - if err != nil { - return "", fmt.Errorf("open database: %w", err) - } - - ctx := context.Background() - did, err := kb.Load(ctx, data) - if err != nil { - return "", fmt.Errorf("load DID: %w", err) - } - - pdk.Log(pdk.LogDebug, "loadDatabase: database loaded successfully") - return did, nil -} - -func parseFilter(filter string) (*types.FilterParams, error) { - params := &types.FilterParams{} - parts := strings.FieldsSeq(filter) - - for part := range parts { - kv := strings.SplitN(part, ":", 2) - if len(kv) != 2 { - continue - } - - key, value := kv[0], kv[1] - switch key { - case "resource": - params.Resource = value - case "action": - params.Action = value - case "subject": - params.Subject = value - } - } - - if params.Resource == "" { - return nil, errors.New("resource is required") - } - if params.Action == "" { - return nil, errors.New("action is required") - } - - return params, nil -} - -func validateUCAN(token string, params *types.FilterParams) error { - if token == "" { - return errors.New("token is required") - } - - parts := strings.Split(token, ".") - if len(parts) != 3 { - return errors.New("invalid token format: expected JWT with 3 parts") - } - - payload, err := base64.RawURLEncoding.DecodeString(parts[1]) - if err != nil { - return fmt.Errorf("invalid token payload: %w", err) - } - - var claims map[string]any - if err := json.Unmarshal(payload, &claims); err != nil { - return fmt.Errorf("invalid token claims: %w", err) - } - - if exp, ok := claims["exp"].(float64); ok { - if int64(exp) < currentUnixTime() { - return errors.New("token has expired") - } - } - - if nbf, ok := claims["nbf"].(float64); ok { - if int64(nbf) > currentUnixTime() { - return errors.New("token is not yet valid") - } - } - - if aud, ok := claims["aud"].(string); ok { - currentDID := state.GetDID() - if currentDID != "" && aud != currentDID { - pdk.Log(pdk.LogDebug, fmt.Sprintf("validateUCAN: audience mismatch, expected %s got %s", currentDID, aud)) - } - } - - if att, ok := claims["att"].([]any); ok { - if !checkAttenuations(att, params.Resource, params.Action) { - return fmt.Errorf("token does not grant capability for %s:%s", params.Resource, params.Action) - } - } - - am, err := keybase.NewActionManager() - if err == nil { - if cid, ok := claims["cid"].(string); ok { - ctx := context.Background() - revoked, err := am.IsDelegationRevoked(ctx, cid) - if err == nil && revoked { - return errors.New("token has been revoked") - } - } - } - - pdk.Log(pdk.LogDebug, fmt.Sprintf("validateUCAN: validated token for %s:%s", params.Resource, params.Action)) - return nil -} - -func currentUnixTime() int64 { - return 0 -} - -func checkAttenuations(attenuations []any, resource, action string) bool { - for _, att := range attenuations { - attMap, ok := att.(map[string]any) - if !ok { - continue - } - - with, ok := attMap["with"].(string) - if !ok { - continue - } - - if !matchResource(with, resource) { - continue - } - - can := attMap["can"] - if canStr, ok := can.(string); ok { - if canStr == "*" || canStr == action { - return true - } - } else if canSlice, ok := can.([]any); ok { - for _, c := range canSlice { - if cStr, ok := c.(string); ok { - if cStr == "*" || cStr == action { - return true - } - } - } - } - } - return false -} - -func matchResource(pattern, resource string) bool { - if pattern == resource { - return true - } - - if strings.HasSuffix(pattern, "/*") { - prefix := strings.TrimSuffix(pattern, "/*") - return strings.HasPrefix(resource, prefix) - } - - if strings.Contains(pattern, "://") { - parts := strings.SplitN(pattern, "://", 2) - if len(parts) == 2 && parts[1] == resource { - return true - } - } - - return false -} - -func executeAction(params *types.FilterParams) (json.RawMessage, error) { - switch params.Resource { - case "accounts": - return executeAccountAction(params) - case "credentials": - return executeCredentialAction(params) - case "sessions": - return executeSessionAction(params) - case "grants": - return executeGrantAction(params) - case "key_shares": - return executeKeyShareAction(params) - case "ucans": - return executeUCANAction(params) - case "delegations": - return executeDelegationAction(params) - case "verification_methods": - return executeVerificationMethodAction(params) - case "services": - return executeServiceAction(params) - default: - return nil, fmt.Errorf("unknown resource: %s", params.Resource) - } -} - -func executeAccountAction(params *types.FilterParams) (json.RawMessage, error) { - am, err := keybase.NewActionManager() - if err != nil { - return nil, fmt.Errorf("action manager: %w", err) - } - - ctx := context.Background() - - switch params.Action { - case "list": - accounts, err := am.ListAccounts(ctx) - if err != nil { - return nil, fmt.Errorf("list accounts: %w", err) - } - return json.Marshal(accounts) - case "get": - if params.Subject == "" { - return nil, errors.New("subject (address) required for get action") - } - account, err := am.GetAccountByAddress(ctx, params.Subject) - if err != nil { - return nil, fmt.Errorf("get account: %w", err) - } - return json.Marshal(account) - case "balances": - return fetchAccountBalances(params.Subject) - case "sign": - return json.Marshal(map[string]string{"signature": "placeholder"}) - default: - return nil, fmt.Errorf("unknown action for accounts: %s", params.Action) - } -} - -func fetchAccountBalances(address string) (json.RawMessage, error) { - if address == "" { - address = state.GetDID() - } - - apiBase, ok := state.GetConfig("api_endpoint") - if !ok { - apiBase = "https://api.sonr.io" - } - - url := fmt.Sprintf("%s/cosmos/bank/v1beta1/balances/%s", apiBase, address) - pdk.Log(pdk.LogInfo, fmt.Sprintf("fetchAccountBalances: GET %s", url)) - - req := pdk.NewHTTPRequest(pdk.MethodGet, url) - req.SetHeader("Accept", "application/json") - - res := req.Send() - status := res.Status() - - if status < 200 || status >= 300 { - pdk.Log(pdk.LogError, fmt.Sprintf("fetchAccountBalances: HTTP %d", status)) - return json.Marshal(map[string]any{ - "error": "failed to fetch balances", - "status": status, - "address": address, - }) - } - - body := res.Body() - pdk.Log(pdk.LogDebug, fmt.Sprintf("fetchAccountBalances: received %d bytes", len(body))) - - return body, nil -} - -func executeCredentialAction(params *types.FilterParams) (json.RawMessage, error) { - am, err := keybase.NewActionManager() - if err != nil { - return nil, fmt.Errorf("action manager: %w", err) - } - - ctx := context.Background() - - switch params.Action { - case "list": - credentials, err := am.ListCredentials(ctx) - if err != nil { - return nil, fmt.Errorf("list credentials: %w", err) - } - return json.Marshal(credentials) - case "get": - if params.Subject == "" { - return nil, errors.New("subject (credential_id) required for get action") - } - credential, err := am.GetCredentialByID(ctx, params.Subject) - if err != nil { - return nil, fmt.Errorf("get credential: %w", err) - } - return json.Marshal(credential) - default: - return nil, fmt.Errorf("unknown action for credentials: %s", params.Action) - } -} - -func executeSessionAction(params *types.FilterParams) (json.RawMessage, error) { - am, err := keybase.NewActionManager() - if err != nil { - return nil, fmt.Errorf("action manager: %w", err) - } - - ctx := context.Background() - - switch params.Action { - case "list": - sessions, err := am.ListSessions(ctx) - if err != nil { - return nil, fmt.Errorf("list sessions: %w", err) - } - return json.Marshal(sessions) - case "revoke": - if params.Subject == "" { - return nil, errors.New("subject (session_id) required for revoke action") - } - if err := am.RevokeSession(ctx, params.Subject); err != nil { - return nil, fmt.Errorf("revoke session: %w", err) - } - return json.Marshal(map[string]bool{"revoked": true}) - default: - return nil, fmt.Errorf("unknown action for sessions: %s", params.Action) - } -} - -func executeGrantAction(params *types.FilterParams) (json.RawMessage, error) { - am, err := keybase.NewActionManager() - if err != nil { - return nil, fmt.Errorf("action manager: %w", err) - } - - ctx := context.Background() - - switch params.Action { - case "list": - grants, err := am.ListGrants(ctx) - if err != nil { - return nil, fmt.Errorf("list grants: %w", err) - } - return json.Marshal(grants) - case "revoke": - if params.Subject == "" { - return nil, errors.New("subject (grant_id) required for revoke action") - } - var grantID int64 - if _, err := fmt.Sscanf(params.Subject, "%d", &grantID); err != nil { - return nil, fmt.Errorf("invalid grant_id: %w", err) - } - if err := am.RevokeGrant(ctx, grantID); err != nil { - return nil, fmt.Errorf("revoke grant: %w", err) - } - return json.Marshal(map[string]bool{"revoked": true}) - default: - return nil, fmt.Errorf("unknown action for grants: %s", params.Action) - } -} - -func executeKeyShareAction(params *types.FilterParams) (json.RawMessage, error) { - am, err := keybase.NewActionManager() - if err != nil { - return nil, fmt.Errorf("action manager: %w", err) - } - - ctx := context.Background() - - switch params.Action { - case "list": - shares, err := am.ListKeyShares(ctx) - if err != nil { - return nil, fmt.Errorf("list key shares: %w", err) - } - return json.Marshal(shares) - case "get": - if params.Subject == "" { - return nil, errors.New("subject (share_id) required for get action") - } - share, err := am.GetKeyShareByID(ctx, params.Subject) - if err != nil { - return nil, fmt.Errorf("get key share: %w", err) - } - return json.Marshal(share) - case "rotate": - if params.Subject == "" { - return nil, errors.New("subject (share_id) required for rotate action") - } - if err := am.RotateKeyShare(ctx, params.Subject); err != nil { - return nil, fmt.Errorf("rotate key share: %w", err) - } - return json.Marshal(map[string]bool{"rotated": true}) - case "archive": - if params.Subject == "" { - return nil, errors.New("subject (share_id) required for archive action") - } - if err := am.ArchiveKeyShare(ctx, params.Subject); err != nil { - return nil, fmt.Errorf("archive key share: %w", err) - } - return json.Marshal(map[string]bool{"archived": true}) - case "delete": - if params.Subject == "" { - return nil, errors.New("subject (share_id) required for delete action") - } - if err := am.DeleteKeyShare(ctx, params.Subject); err != nil { - return nil, fmt.Errorf("delete key share: %w", err) - } - return json.Marshal(map[string]bool{"deleted": true}) - default: - return nil, fmt.Errorf("unknown action for key_shares: %s", params.Action) - } -} - -func executeUCANAction(params *types.FilterParams) (json.RawMessage, error) { - am, err := keybase.NewActionManager() - if err != nil { - return nil, fmt.Errorf("action manager: %w", err) - } - - ctx := context.Background() - - switch params.Action { - case "list": - delegations, err := am.ListDelegations(ctx) - if err != nil { - return nil, fmt.Errorf("list delegations: %w", err) - } - return json.Marshal(delegations) - case "get": - if params.Subject == "" { - return nil, errors.New("subject (cid) required for get action") - } - delegation, err := am.GetDelegationByCID(ctx, params.Subject) - if err != nil { - return nil, fmt.Errorf("get delegation: %w", err) - } - return json.Marshal(delegation) - case "revoke": - if params.Subject == "" { - return nil, errors.New("subject (cid) required for revoke action") - } - if err := am.RevokeDelegation(ctx, keybase.RevokeDelegationParams{ - DelegationCID: params.Subject, - RevokedBy: state.GetDID(), - Reason: "user revoked", - }); err != nil { - return nil, fmt.Errorf("revoke delegation: %w", err) - } - return json.Marshal(map[string]bool{"revoked": true}) - case "verify": - if params.Subject == "" { - return nil, errors.New("subject (cid) required for verify action") - } - revoked, err := am.IsDelegationRevoked(ctx, params.Subject) - if err != nil { - return nil, fmt.Errorf("check delegation: %w", err) - } - return json.Marshal(map[string]bool{"valid": !revoked, "revoked": revoked}) - case "cleanup": - if err := am.CleanExpiredDelegations(ctx); err != nil { - return nil, fmt.Errorf("cleanup delegations: %w", err) - } - return json.Marshal(map[string]bool{"cleaned": true}) - default: - return nil, fmt.Errorf("unknown action for ucans: %s", params.Action) - } -} - -func executeDelegationAction(params *types.FilterParams) (json.RawMessage, error) { - am, err := keybase.NewActionManager() - if err != nil { - return nil, fmt.Errorf("action manager: %w", err) - } - - ctx := context.Background() - - switch params.Action { - case "list": - if params.Subject == "" { - return nil, errors.New("subject (issuer DID) required for list action") - } - delegations, err := am.ListDelegationsByIssuer(ctx, params.Subject) - if err != nil { - return nil, fmt.Errorf("list delegations: %w", err) - } - return json.Marshal(delegations) - case "list_received": - if params.Subject == "" { - return nil, errors.New("subject (audience DID) required for list_received action") - } - delegations, err := am.ListDelegationsByAudience(ctx, params.Subject) - if err != nil { - return nil, fmt.Errorf("list received delegations: %w", err) - } - return json.Marshal(delegations) - case "list_command": - if params.Subject == "" { - return nil, errors.New("subject (command) required for list_command action") - } - delegations, err := am.ListDelegationsForCommand(ctx, params.Subject) - if err != nil { - return nil, fmt.Errorf("list delegations for command: %w", err) - } - return json.Marshal(delegations) - case "get": - if params.Subject == "" { - return nil, errors.New("subject (cid) required for get action") - } - delegation, err := am.GetDelegationByCID(ctx, params.Subject) - if err != nil { - return nil, fmt.Errorf("get delegation: %w", err) - } - return json.Marshal(delegation) - case "revoke": - if params.Subject == "" { - return nil, errors.New("subject (cid) required for revoke action") - } - if err := am.RevokeDelegation(ctx, keybase.RevokeDelegationParams{ - DelegationCID: params.Subject, - RevokedBy: state.GetDID(), - Reason: "user revoked", - }); err != nil { - return nil, fmt.Errorf("revoke delegation: %w", err) - } - return json.Marshal(map[string]bool{"revoked": true}) - case "verify": - if params.Subject == "" { - return nil, errors.New("subject (cid) required for verify action") - } - revoked, err := am.IsDelegationRevoked(ctx, params.Subject) - if err != nil { - return nil, fmt.Errorf("check delegation: %w", err) - } - return json.Marshal(map[string]bool{"valid": !revoked, "revoked": revoked}) - default: - return nil, fmt.Errorf("unknown action for delegations: %s", params.Action) - } -} - -func resolveDID(did string) (*types.QueryOutput, error) { - am, err := keybase.NewActionManager() - if err != nil { - return nil, fmt.Errorf("action manager: %w", err) - } - - ctx := context.Background() - doc, err := am.ResolveDID(ctx, did) - if err != nil { - return nil, fmt.Errorf("resolve DID: %w", err) - } - - vms := make([]types.VerificationMethod, len(doc.VerificationMethods)) - for i, vm := range doc.VerificationMethods { - vms[i] = types.VerificationMethod{ - ID: vm.ID, - Type: vm.Type, - Controller: vm.Controller, - PublicKey: vm.PublicKey, - Purpose: vm.Purpose, - } - } - - accounts := make([]types.Account, len(doc.Accounts)) - for i, acc := range doc.Accounts { - accounts[i] = types.Account{ - Address: acc.Address, - ChainID: acc.ChainID, - CoinType: int(acc.CoinType), - AccountIndex: int(acc.AccountIndex), - AddressIndex: int(acc.AddressIndex), - Label: acc.Label, - IsDefault: acc.IsDefault, - } - } - - credentials := make([]types.Credential, len(doc.Credentials)) - for i, cred := range doc.Credentials { - credentials[i] = types.Credential{ - CredentialID: cred.CredentialID, - DeviceName: cred.DeviceName, - DeviceType: cred.DeviceType, - Authenticator: cred.Authenticator, - Transports: cred.Transports, - CreatedAt: cred.CreatedAt, - LastUsed: cred.LastUsed, - } - } - - return &types.QueryOutput{ - DID: doc.DID, - Controller: doc.Controller, - VerificationMethods: vms, - Accounts: accounts, - Credentials: credentials, - }, nil -} - -func executeVerificationMethodAction(params *types.FilterParams) (json.RawMessage, error) { - am, err := keybase.NewActionManager() - if err != nil { - return nil, fmt.Errorf("action manager: %w", err) - } - - ctx := context.Background() - - switch params.Action { - case "list": - vms, err := am.ListVerificationMethodsFull(ctx) - if err != nil { - return nil, fmt.Errorf("list verification methods: %w", err) - } - return json.Marshal(vms) - case "get": - if params.Subject == "" { - return nil, errors.New("subject (method_id) required for get action") - } - vm, err := am.GetVerificationMethod(ctx, params.Subject) - if err != nil { - return nil, fmt.Errorf("get verification method: %w", err) - } - return json.Marshal(vm) - case "delete": - if params.Subject == "" { - return nil, errors.New("subject (method_id) required for delete action") - } - if err := am.DeleteVerificationMethod(ctx, params.Subject); err != nil { - return nil, fmt.Errorf("delete verification method: %w", err) - } - return json.Marshal(map[string]bool{"deleted": true}) - default: - return nil, fmt.Errorf("unknown action for verification_methods: %s", params.Action) - } -} - -func executeServiceAction(params *types.FilterParams) (json.RawMessage, error) { - am, err := keybase.NewActionManager() - if err != nil { - return nil, fmt.Errorf("action manager: %w", err) - } - - ctx := context.Background() - - switch params.Action { - case "list": - services, err := am.ListVerifiedServices(ctx) - if err != nil { - return nil, fmt.Errorf("list verified services: %w", err) - } - return json.Marshal(services) - case "get": - if params.Subject == "" { - return nil, errors.New("subject (origin) required for get action") - } - svc, err := am.GetServiceByOrigin(ctx, params.Subject) - if err != nil { - return nil, fmt.Errorf("get service: %w", err) - } - return json.Marshal(svc) - case "get_by_id": - if params.Subject == "" { - return nil, errors.New("subject (service_id) required for get_by_id action") - } - var serviceID int64 - if _, err := fmt.Sscanf(params.Subject, "%d", &serviceID); err != nil { - return nil, fmt.Errorf("invalid service_id: %w", err) - } - svc, err := am.GetServiceByID(ctx, serviceID) - if err != nil { - return nil, fmt.Errorf("get service by ID: %w", err) - } - return json.Marshal(svc) - default: - return nil, fmt.Errorf("unknown action for services: %s", params.Action) - } -} -- 2.43.0 From 76fb6c27cbd2a9d239ed3d6049d70ec02a7d9e5b Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Fri, 9 Jan 2026 08:19:21 -0500 Subject: [PATCH 19/35] init(enclave): Setup enclave package for wasm actions --- cmd/enclave/main.go | 1020 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1020 insertions(+) create mode 100644 cmd/enclave/main.go diff --git a/cmd/enclave/main.go b/cmd/enclave/main.go new file mode 100644 index 0000000..3e75062 --- /dev/null +++ b/cmd/enclave/main.go @@ -0,0 +1,1020 @@ +//go:build wasip1 + +package main + +import ( + "context" + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "strings" + + "enclave/internal/keybase" + "enclave/internal/state" + "enclave/internal/types" + + "github.com/extism/go-pdk" +) + +func main() { state.Default() } + +//go:wasmexport ping +func ping() int32 { + pdk.Log(pdk.LogInfo, "ping: received request") + + var input types.PingInput + if err := pdk.InputJSON(&input); err != nil { + output := types.PingOutput{ + Success: false, + Message: fmt.Sprintf("failed to parse input: %s", err), + } + pdk.OutputJSON(output) + return 0 + } + + output := types.PingOutput{ + Success: true, + Message: "pong", + Echo: input.Message, + } + + if err := pdk.OutputJSON(output); err != nil { + pdk.Log(pdk.LogError, fmt.Sprintf("ping: failed to output: %s", err)) + return 1 + } + + pdk.Log(pdk.LogInfo, fmt.Sprintf("ping: responded with echo=%s", input.Message)) + return 0 +} + +//go:wasmexport generate +func generate() int32 { + pdk.Log(pdk.LogInfo, "generate: starting database initialization") + + var input types.GenerateInput + if err := pdk.InputJSON(&input); err != nil { + pdk.SetError(fmt.Errorf("generate: failed to parse input: %w", err)) + return 1 + } + + if input.Credential == "" { + pdk.SetError(errors.New("generate: credential is required")) + return 1 + } + + credentialBytes, err := base64.StdEncoding.DecodeString(input.Credential) + if err != nil { + pdk.SetError(fmt.Errorf("generate: invalid base64 credential: %w", err)) + return 1 + } + + result, err := initializeDatabase(credentialBytes, input.KeyShare) + if err != nil { + pdk.SetError(fmt.Errorf("generate: failed to initialize database: %w", err)) + return 1 + } + + state.SetInitialized(true) + state.SetDID(result.DID) + + dbBytes, err := serializeDatabase() + if err != nil { + pdk.SetError(fmt.Errorf("generate: failed to serialize database: %w", err)) + return 1 + } + + output := types.GenerateOutput{ + DID: result.DID, + Database: dbBytes, + KeyShareID: result.KeyShareID, + Account: result.Account, + } + + if err := pdk.OutputJSON(output); err != nil { + pdk.SetError(fmt.Errorf("generate: failed to output result: %w", err)) + return 1 + } + + pdk.Log(pdk.LogInfo, fmt.Sprintf("generate: created DID %s", result.DID)) + return 0 +} + +//go:wasmexport load +func load() int32 { + pdk.Log(pdk.LogInfo, "load: loading database from buffer") + + var input types.LoadInput + if err := pdk.InputJSON(&input); err != nil { + pdk.SetError(fmt.Errorf("load: failed to parse input: %w", err)) + return 1 + } + + if len(input.Database) == 0 { + pdk.SetError(errors.New("load: database buffer is required")) + return 1 + } + + did, err := loadDatabase(input.Database) + if err != nil { + output := types.LoadOutput{ + Success: false, + Error: err.Error(), + } + pdk.OutputJSON(output) + return 1 + } + + state.SetInitialized(true) + state.SetDID(did) + + output := types.LoadOutput{ + Success: true, + DID: did, + } + + if err := pdk.OutputJSON(output); err != nil { + pdk.SetError(fmt.Errorf("load: failed to output result: %w", err)) + return 1 + } + + pdk.Log(pdk.LogInfo, fmt.Sprintf("load: loaded database for DID %s", did)) + return 0 +} + +//go:wasmexport exec +func exec() int32 { + pdk.Log(pdk.LogInfo, "exec: executing action") + + if !state.IsInitialized() { + output := types.ExecOutput{Success: false, Error: "database not initialized, call generate or load first"} + pdk.OutputJSON(output) + return 0 + } + + var input types.ExecInput + if err := pdk.InputJSON(&input); err != nil { + output := types.ExecOutput{Success: false, Error: fmt.Sprintf("failed to parse input: %s", err)} + pdk.OutputJSON(output) + return 0 + } + + if input.Filter == "" { + output := types.ExecOutput{Success: false, Error: "filter is required"} + pdk.OutputJSON(output) + return 0 + } + + params, err := parseFilter(input.Filter) + if err != nil { + output := types.ExecOutput{Success: false, Error: fmt.Sprintf("invalid filter: %s", err)} + pdk.OutputJSON(output) + return 0 + } + + if input.Token != "" { + if err := validateUCAN(input.Token, params); err != nil { + output := types.ExecOutput{ + Success: false, + Error: fmt.Sprintf("authorization failed: %s", err.Error()), + } + pdk.OutputJSON(output) + return 1 + } + } + + result, err := executeAction(params) + if err != nil { + output := types.ExecOutput{ + Success: false, + Error: err.Error(), + } + pdk.OutputJSON(output) + return 1 + } + + output := types.ExecOutput{ + Success: true, + Result: result, + } + + pdk.OutputJSON(output) + pdk.Log(pdk.LogInfo, fmt.Sprintf("exec: completed %s on %s", params.Action, params.Resource)) + return 0 +} + +//go:wasmexport query +func query() int32 { + pdk.Log(pdk.LogInfo, "query: resolving DID document") + + if !state.IsInitialized() { + pdk.SetError(errors.New("database not initialized, call generate or load first")) + return 1 + } + + var input types.QueryInput + if err := pdk.InputJSON(&input); err != nil { + pdk.SetError(fmt.Errorf("query: failed to parse input: %w", err)) + return 1 + } + + if input.DID == "" { + input.DID = state.GetDID() + } + + if !strings.HasPrefix(input.DID, "did:") { + pdk.SetError(errors.New("query: invalid DID format")) + return 1 + } + + output, err := resolveDID(input.DID) + if err != nil { + pdk.SetError(fmt.Errorf("query: failed to resolve DID: %w", err)) + return 1 + } + + if err := pdk.OutputJSON(output); err != nil { + pdk.SetError(fmt.Errorf("query: failed to output result: %w", err)) + return 1 + } + + pdk.Log(pdk.LogInfo, fmt.Sprintf("query: resolved DID %s", input.DID)) + return 0 +} + +type initResult struct { + DID string + KeyShareID string + Account *types.AccountInfo +} + +func initializeDatabase(credentialBytes []byte, keyShareInput *types.KeyShareInput) (*initResult, error) { + kb, err := keybase.Open() + if err != nil { + return nil, fmt.Errorf("open database: %w", err) + } + + ctx := context.Background() + did, err := kb.Initialize(ctx, credentialBytes) + if err != nil { + return nil, fmt.Errorf("initialize: %w", err) + } + + result := &initResult{DID: did} + + if keyShareInput != nil { + keyShareID, account, err := createInitialKeyShare(ctx, keyShareInput) + if err != nil { + pdk.Log(pdk.LogWarn, fmt.Sprintf("initializeDatabase: failed to create keyshare: %s", err)) + } else { + result.KeyShareID = keyShareID + result.Account = account + pdk.Log(pdk.LogInfo, fmt.Sprintf("initializeDatabase: created keyshare %s", keyShareID)) + } + } + + pdk.Log(pdk.LogDebug, "initializeDatabase: created schema and initial records") + return result, nil +} + +func createInitialKeyShare(ctx context.Context, input *types.KeyShareInput) (string, *types.AccountInfo, error) { + am, err := keybase.NewActionManager() + if err != nil { + return "", nil, fmt.Errorf("action manager: %w", err) + } + + ks, err := am.CreateKeyShare(ctx, keybase.NewKeyShareInput{ + KeyID: input.KeyID, + PartyIndex: input.PartyIndex, + Threshold: input.Threshold, + TotalParties: input.TotalParties, + Curve: input.Curve, + ShareData: input.ShareData, + PublicKey: input.PublicKey, + ChainCode: input.ChainCode, + DerivationPath: input.DerivationPath, + }) + if err != nil { + return "", nil, fmt.Errorf("create keyshare: %w", err) + } + + account, err := createInitialAccount(ctx, am, ks.ID, input.PublicKey) + if err != nil { + pdk.Log(pdk.LogWarn, fmt.Sprintf("createInitialKeyShare: failed to create account: %s", err)) + return ks.ShareID, nil, nil + } + + return ks.ShareID, account, nil +} + +func createInitialAccount(ctx context.Context, am *keybase.ActionManager, keyShareID int64, publicKey string) (*types.AccountInfo, error) { + address := deriveCosmosAddress(publicKey) + if address == "" { + return nil, fmt.Errorf("failed to derive address from public key") + } + + acc, err := am.CreateAccount(ctx, keybase.NewAccountInput{ + KeyShareID: keyShareID, + Address: address, + ChainID: "sonr-testnet-1", + CoinType: 118, + AccountIndex: 0, + AddressIndex: 0, + Label: "Default Account", + }) + if err != nil { + return nil, fmt.Errorf("create account: %w", err) + } + + return &types.AccountInfo{ + Address: acc.Address, + ChainID: acc.ChainID, + CoinType: acc.CoinType, + }, nil +} + +func deriveCosmosAddress(publicKeyHex string) string { + if publicKeyHex == "" { + return "" + } + pubBytes, err := hex.DecodeString(publicKeyHex) + if err != nil || len(pubBytes) < 20 { + return "" + } + return fmt.Sprintf("snr1%x", pubBytes[:20]) +} + +func serializeDatabase() ([]byte, error) { + kb := keybase.Get() + if kb == nil { + return nil, errors.New("database not initialized") + } + return kb.Serialize() +} + +func loadDatabase(data []byte) (string, error) { + if len(data) < 10 { + return "", errors.New("invalid database format") + } + + kb, err := keybase.Open() + if err != nil { + return "", fmt.Errorf("open database: %w", err) + } + + ctx := context.Background() + did, err := kb.Load(ctx, data) + if err != nil { + return "", fmt.Errorf("load DID: %w", err) + } + + pdk.Log(pdk.LogDebug, "loadDatabase: database loaded successfully") + return did, nil +} + +func parseFilter(filter string) (*types.FilterParams, error) { + params := &types.FilterParams{} + parts := strings.FieldsSeq(filter) + + for part := range parts { + kv := strings.SplitN(part, ":", 2) + if len(kv) != 2 { + continue + } + + key, value := kv[0], kv[1] + switch key { + case "resource": + params.Resource = value + case "action": + params.Action = value + case "subject": + params.Subject = value + } + } + + if params.Resource == "" { + return nil, errors.New("resource is required") + } + if params.Action == "" { + return nil, errors.New("action is required") + } + + return params, nil +} + +func validateUCAN(token string, params *types.FilterParams) error { + if token == "" { + return errors.New("token is required") + } + + parts := strings.Split(token, ".") + if len(parts) != 3 { + return errors.New("invalid token format: expected JWT with 3 parts") + } + + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + return fmt.Errorf("invalid token payload: %w", err) + } + + var claims map[string]any + if err := json.Unmarshal(payload, &claims); err != nil { + return fmt.Errorf("invalid token claims: %w", err) + } + + if exp, ok := claims["exp"].(float64); ok { + if int64(exp) < currentUnixTime() { + return errors.New("token has expired") + } + } + + if nbf, ok := claims["nbf"].(float64); ok { + if int64(nbf) > currentUnixTime() { + return errors.New("token is not yet valid") + } + } + + if aud, ok := claims["aud"].(string); ok { + currentDID := state.GetDID() + if currentDID != "" && aud != currentDID { + pdk.Log(pdk.LogDebug, fmt.Sprintf("validateUCAN: audience mismatch, expected %s got %s", currentDID, aud)) + } + } + + if att, ok := claims["att"].([]any); ok { + if !checkAttenuations(att, params.Resource, params.Action) { + return fmt.Errorf("token does not grant capability for %s:%s", params.Resource, params.Action) + } + } + + am, err := keybase.NewActionManager() + if err == nil { + if cid, ok := claims["cid"].(string); ok { + ctx := context.Background() + revoked, err := am.IsDelegationRevoked(ctx, cid) + if err == nil && revoked { + return errors.New("token has been revoked") + } + } + } + + pdk.Log(pdk.LogDebug, fmt.Sprintf("validateUCAN: validated token for %s:%s", params.Resource, params.Action)) + return nil +} + +func currentUnixTime() int64 { + return 0 +} + +func checkAttenuations(attenuations []any, resource, action string) bool { + for _, att := range attenuations { + attMap, ok := att.(map[string]any) + if !ok { + continue + } + + with, ok := attMap["with"].(string) + if !ok { + continue + } + + if !matchResource(with, resource) { + continue + } + + can := attMap["can"] + if canStr, ok := can.(string); ok { + if canStr == "*" || canStr == action { + return true + } + } else if canSlice, ok := can.([]any); ok { + for _, c := range canSlice { + if cStr, ok := c.(string); ok { + if cStr == "*" || cStr == action { + return true + } + } + } + } + } + return false +} + +func matchResource(pattern, resource string) bool { + if pattern == resource { + return true + } + + if strings.HasSuffix(pattern, "/*") { + prefix := strings.TrimSuffix(pattern, "/*") + return strings.HasPrefix(resource, prefix) + } + + if strings.Contains(pattern, "://") { + parts := strings.SplitN(pattern, "://", 2) + if len(parts) == 2 && parts[1] == resource { + return true + } + } + + return false +} + +func executeAction(params *types.FilterParams) (json.RawMessage, error) { + switch params.Resource { + case "accounts": + return executeAccountAction(params) + case "credentials": + return executeCredentialAction(params) + case "sessions": + return executeSessionAction(params) + case "grants": + return executeGrantAction(params) + case "key_shares": + return executeKeyShareAction(params) + case "ucans": + return executeUCANAction(params) + case "delegations": + return executeDelegationAction(params) + case "verification_methods": + return executeVerificationMethodAction(params) + case "services": + return executeServiceAction(params) + default: + return nil, fmt.Errorf("unknown resource: %s", params.Resource) + } +} + +func executeAccountAction(params *types.FilterParams) (json.RawMessage, error) { + am, err := keybase.NewActionManager() + if err != nil { + return nil, fmt.Errorf("action manager: %w", err) + } + + ctx := context.Background() + + switch params.Action { + case "list": + accounts, err := am.ListAccounts(ctx) + if err != nil { + return nil, fmt.Errorf("list accounts: %w", err) + } + return json.Marshal(accounts) + case "get": + if params.Subject == "" { + return nil, errors.New("subject (address) required for get action") + } + account, err := am.GetAccountByAddress(ctx, params.Subject) + if err != nil { + return nil, fmt.Errorf("get account: %w", err) + } + return json.Marshal(account) + case "balances": + return fetchAccountBalances(params.Subject) + case "sign": + return json.Marshal(map[string]string{"signature": "placeholder"}) + default: + return nil, fmt.Errorf("unknown action for accounts: %s", params.Action) + } +} + +func fetchAccountBalances(address string) (json.RawMessage, error) { + if address == "" { + address = state.GetDID() + } + + apiBase, ok := state.GetConfig("api_endpoint") + if !ok { + apiBase = "https://api.sonr.io" + } + + url := fmt.Sprintf("%s/cosmos/bank/v1beta1/balances/%s", apiBase, address) + pdk.Log(pdk.LogInfo, fmt.Sprintf("fetchAccountBalances: GET %s", url)) + + req := pdk.NewHTTPRequest(pdk.MethodGet, url) + req.SetHeader("Accept", "application/json") + + res := req.Send() + status := res.Status() + + if status < 200 || status >= 300 { + pdk.Log(pdk.LogError, fmt.Sprintf("fetchAccountBalances: HTTP %d", status)) + return json.Marshal(map[string]any{ + "error": "failed to fetch balances", + "status": status, + "address": address, + }) + } + + body := res.Body() + pdk.Log(pdk.LogDebug, fmt.Sprintf("fetchAccountBalances: received %d bytes", len(body))) + + return body, nil +} + +func executeCredentialAction(params *types.FilterParams) (json.RawMessage, error) { + am, err := keybase.NewActionManager() + if err != nil { + return nil, fmt.Errorf("action manager: %w", err) + } + + ctx := context.Background() + + switch params.Action { + case "list": + credentials, err := am.ListCredentials(ctx) + if err != nil { + return nil, fmt.Errorf("list credentials: %w", err) + } + return json.Marshal(credentials) + case "get": + if params.Subject == "" { + return nil, errors.New("subject (credential_id) required for get action") + } + credential, err := am.GetCredentialByID(ctx, params.Subject) + if err != nil { + return nil, fmt.Errorf("get credential: %w", err) + } + return json.Marshal(credential) + default: + return nil, fmt.Errorf("unknown action for credentials: %s", params.Action) + } +} + +func executeSessionAction(params *types.FilterParams) (json.RawMessage, error) { + am, err := keybase.NewActionManager() + if err != nil { + return nil, fmt.Errorf("action manager: %w", err) + } + + ctx := context.Background() + + switch params.Action { + case "list": + sessions, err := am.ListSessions(ctx) + if err != nil { + return nil, fmt.Errorf("list sessions: %w", err) + } + return json.Marshal(sessions) + case "revoke": + if params.Subject == "" { + return nil, errors.New("subject (session_id) required for revoke action") + } + if err := am.RevokeSession(ctx, params.Subject); err != nil { + return nil, fmt.Errorf("revoke session: %w", err) + } + return json.Marshal(map[string]bool{"revoked": true}) + default: + return nil, fmt.Errorf("unknown action for sessions: %s", params.Action) + } +} + +func executeGrantAction(params *types.FilterParams) (json.RawMessage, error) { + am, err := keybase.NewActionManager() + if err != nil { + return nil, fmt.Errorf("action manager: %w", err) + } + + ctx := context.Background() + + switch params.Action { + case "list": + grants, err := am.ListGrants(ctx) + if err != nil { + return nil, fmt.Errorf("list grants: %w", err) + } + return json.Marshal(grants) + case "revoke": + if params.Subject == "" { + return nil, errors.New("subject (grant_id) required for revoke action") + } + var grantID int64 + if _, err := fmt.Sscanf(params.Subject, "%d", &grantID); err != nil { + return nil, fmt.Errorf("invalid grant_id: %w", err) + } + if err := am.RevokeGrant(ctx, grantID); err != nil { + return nil, fmt.Errorf("revoke grant: %w", err) + } + return json.Marshal(map[string]bool{"revoked": true}) + default: + return nil, fmt.Errorf("unknown action for grants: %s", params.Action) + } +} + +func executeKeyShareAction(params *types.FilterParams) (json.RawMessage, error) { + am, err := keybase.NewActionManager() + if err != nil { + return nil, fmt.Errorf("action manager: %w", err) + } + + ctx := context.Background() + + switch params.Action { + case "list": + shares, err := am.ListKeyShares(ctx) + if err != nil { + return nil, fmt.Errorf("list key shares: %w", err) + } + return json.Marshal(shares) + case "get": + if params.Subject == "" { + return nil, errors.New("subject (share_id) required for get action") + } + share, err := am.GetKeyShareByID(ctx, params.Subject) + if err != nil { + return nil, fmt.Errorf("get key share: %w", err) + } + return json.Marshal(share) + case "rotate": + if params.Subject == "" { + return nil, errors.New("subject (share_id) required for rotate action") + } + if err := am.RotateKeyShare(ctx, params.Subject); err != nil { + return nil, fmt.Errorf("rotate key share: %w", err) + } + return json.Marshal(map[string]bool{"rotated": true}) + case "archive": + if params.Subject == "" { + return nil, errors.New("subject (share_id) required for archive action") + } + if err := am.ArchiveKeyShare(ctx, params.Subject); err != nil { + return nil, fmt.Errorf("archive key share: %w", err) + } + return json.Marshal(map[string]bool{"archived": true}) + case "delete": + if params.Subject == "" { + return nil, errors.New("subject (share_id) required for delete action") + } + if err := am.DeleteKeyShare(ctx, params.Subject); err != nil { + return nil, fmt.Errorf("delete key share: %w", err) + } + return json.Marshal(map[string]bool{"deleted": true}) + default: + return nil, fmt.Errorf("unknown action for key_shares: %s", params.Action) + } +} + +func executeUCANAction(params *types.FilterParams) (json.RawMessage, error) { + am, err := keybase.NewActionManager() + if err != nil { + return nil, fmt.Errorf("action manager: %w", err) + } + + ctx := context.Background() + + switch params.Action { + case "list": + delegations, err := am.ListDelegations(ctx) + if err != nil { + return nil, fmt.Errorf("list delegations: %w", err) + } + return json.Marshal(delegations) + case "get": + if params.Subject == "" { + return nil, errors.New("subject (cid) required for get action") + } + delegation, err := am.GetDelegationByCID(ctx, params.Subject) + if err != nil { + return nil, fmt.Errorf("get delegation: %w", err) + } + return json.Marshal(delegation) + case "revoke": + if params.Subject == "" { + return nil, errors.New("subject (cid) required for revoke action") + } + if err := am.RevokeDelegation(ctx, keybase.RevokeDelegationParams{ + DelegationCID: params.Subject, + RevokedBy: state.GetDID(), + Reason: "user revoked", + }); err != nil { + return nil, fmt.Errorf("revoke delegation: %w", err) + } + return json.Marshal(map[string]bool{"revoked": true}) + case "verify": + if params.Subject == "" { + return nil, errors.New("subject (cid) required for verify action") + } + revoked, err := am.IsDelegationRevoked(ctx, params.Subject) + if err != nil { + return nil, fmt.Errorf("check delegation: %w", err) + } + return json.Marshal(map[string]bool{"valid": !revoked, "revoked": revoked}) + case "cleanup": + if err := am.CleanExpiredDelegations(ctx); err != nil { + return nil, fmt.Errorf("cleanup delegations: %w", err) + } + return json.Marshal(map[string]bool{"cleaned": true}) + default: + return nil, fmt.Errorf("unknown action for ucans: %s", params.Action) + } +} + +func executeDelegationAction(params *types.FilterParams) (json.RawMessage, error) { + am, err := keybase.NewActionManager() + if err != nil { + return nil, fmt.Errorf("action manager: %w", err) + } + + ctx := context.Background() + + switch params.Action { + case "list": + if params.Subject == "" { + return nil, errors.New("subject (issuer DID) required for list action") + } + delegations, err := am.ListDelegationsByIssuer(ctx, params.Subject) + if err != nil { + return nil, fmt.Errorf("list delegations: %w", err) + } + return json.Marshal(delegations) + case "list_received": + if params.Subject == "" { + return nil, errors.New("subject (audience DID) required for list_received action") + } + delegations, err := am.ListDelegationsByAudience(ctx, params.Subject) + if err != nil { + return nil, fmt.Errorf("list received delegations: %w", err) + } + return json.Marshal(delegations) + case "list_command": + if params.Subject == "" { + return nil, errors.New("subject (command) required for list_command action") + } + delegations, err := am.ListDelegationsForCommand(ctx, params.Subject) + if err != nil { + return nil, fmt.Errorf("list delegations for command: %w", err) + } + return json.Marshal(delegations) + case "get": + if params.Subject == "" { + return nil, errors.New("subject (cid) required for get action") + } + delegation, err := am.GetDelegationByCID(ctx, params.Subject) + if err != nil { + return nil, fmt.Errorf("get delegation: %w", err) + } + return json.Marshal(delegation) + case "revoke": + if params.Subject == "" { + return nil, errors.New("subject (cid) required for revoke action") + } + if err := am.RevokeDelegation(ctx, keybase.RevokeDelegationParams{ + DelegationCID: params.Subject, + RevokedBy: state.GetDID(), + Reason: "user revoked", + }); err != nil { + return nil, fmt.Errorf("revoke delegation: %w", err) + } + return json.Marshal(map[string]bool{"revoked": true}) + case "verify": + if params.Subject == "" { + return nil, errors.New("subject (cid) required for verify action") + } + revoked, err := am.IsDelegationRevoked(ctx, params.Subject) + if err != nil { + return nil, fmt.Errorf("check delegation: %w", err) + } + return json.Marshal(map[string]bool{"valid": !revoked, "revoked": revoked}) + default: + return nil, fmt.Errorf("unknown action for delegations: %s", params.Action) + } +} + +func resolveDID(did string) (*types.QueryOutput, error) { + am, err := keybase.NewActionManager() + if err != nil { + return nil, fmt.Errorf("action manager: %w", err) + } + + ctx := context.Background() + doc, err := am.ResolveDID(ctx, did) + if err != nil { + return nil, fmt.Errorf("resolve DID: %w", err) + } + + vms := make([]types.VerificationMethod, len(doc.VerificationMethods)) + for i, vm := range doc.VerificationMethods { + vms[i] = types.VerificationMethod{ + ID: vm.ID, + Type: vm.Type, + Controller: vm.Controller, + PublicKey: vm.PublicKey, + Purpose: vm.Purpose, + } + } + + accounts := make([]types.Account, len(doc.Accounts)) + for i, acc := range doc.Accounts { + accounts[i] = types.Account{ + Address: acc.Address, + ChainID: acc.ChainID, + CoinType: int(acc.CoinType), + AccountIndex: int(acc.AccountIndex), + AddressIndex: int(acc.AddressIndex), + Label: acc.Label, + IsDefault: acc.IsDefault, + } + } + + credentials := make([]types.Credential, len(doc.Credentials)) + for i, cred := range doc.Credentials { + credentials[i] = types.Credential{ + CredentialID: cred.CredentialID, + DeviceName: cred.DeviceName, + DeviceType: cred.DeviceType, + Authenticator: cred.Authenticator, + Transports: cred.Transports, + CreatedAt: cred.CreatedAt, + LastUsed: cred.LastUsed, + } + } + + return &types.QueryOutput{ + DID: doc.DID, + Controller: doc.Controller, + VerificationMethods: vms, + Accounts: accounts, + Credentials: credentials, + }, nil +} + +func executeVerificationMethodAction(params *types.FilterParams) (json.RawMessage, error) { + am, err := keybase.NewActionManager() + if err != nil { + return nil, fmt.Errorf("action manager: %w", err) + } + + ctx := context.Background() + + switch params.Action { + case "list": + vms, err := am.ListVerificationMethodsFull(ctx) + if err != nil { + return nil, fmt.Errorf("list verification methods: %w", err) + } + return json.Marshal(vms) + case "get": + if params.Subject == "" { + return nil, errors.New("subject (method_id) required for get action") + } + vm, err := am.GetVerificationMethod(ctx, params.Subject) + if err != nil { + return nil, fmt.Errorf("get verification method: %w", err) + } + return json.Marshal(vm) + case "delete": + if params.Subject == "" { + return nil, errors.New("subject (method_id) required for delete action") + } + if err := am.DeleteVerificationMethod(ctx, params.Subject); err != nil { + return nil, fmt.Errorf("delete verification method: %w", err) + } + return json.Marshal(map[string]bool{"deleted": true}) + default: + return nil, fmt.Errorf("unknown action for verification_methods: %s", params.Action) + } +} + +func executeServiceAction(params *types.FilterParams) (json.RawMessage, error) { + am, err := keybase.NewActionManager() + if err != nil { + return nil, fmt.Errorf("action manager: %w", err) + } + + ctx := context.Background() + + switch params.Action { + case "list": + services, err := am.ListVerifiedServices(ctx) + if err != nil { + return nil, fmt.Errorf("list verified services: %w", err) + } + return json.Marshal(services) + case "get": + if params.Subject == "" { + return nil, errors.New("subject (origin) required for get action") + } + svc, err := am.GetServiceByOrigin(ctx, params.Subject) + if err != nil { + return nil, fmt.Errorf("get service: %w", err) + } + return json.Marshal(svc) + case "get_by_id": + if params.Subject == "" { + return nil, errors.New("subject (service_id) required for get_by_id action") + } + var serviceID int64 + if _, err := fmt.Sscanf(params.Subject, "%d", &serviceID); err != nil { + return nil, fmt.Errorf("invalid service_id: %w", err) + } + svc, err := am.GetServiceByID(ctx, serviceID) + if err != nil { + return nil, fmt.Errorf("get service by ID: %w", err) + } + return json.Marshal(svc) + default: + return nil, fmt.Errorf("unknown action for services: %s", params.Action) + } +} -- 2.43.0 From 16665cafc4389a2efaeb95a24cf6a237c4b5e962 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Fri, 9 Jan 2026 16:38:01 -0500 Subject: [PATCH 20/35] docs(migration): clarify migration guide diagrams and enclave schema --- MIGRATION.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index b60a63b..e82b60e 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -6,9 +6,9 @@ This document outlines the SQL schema design for the Nebula wallet's **encrypted ``` ┌─────────────────────────────────────────────────────────────────────┐ -│ NEBULA WALLET │ +│ NEBULA WALLET │ ├─────────────────────────────────────────────────────────────────────┤ -│ │ +│ │ │ ┌──────────────────────┐ ┌──────────────────────────────────┐ │ │ │ SQLite Enclave │ │ API Clients (Live Data) │ │ │ │ (Encrypted WASM) │ │ │ │ @@ -21,10 +21,10 @@ This document outlines the SQL schema design for the Nebula wallet's **encrypted │ │ • DID State │ │ • Network Status │ │ │ │ • Capability Delgs │ │ │ │ │ └──────────────────────┘ └──────────────────────────────────┘ │ -│ │ │ │ -│ │ Encrypted with │ REST/gRPC │ -│ │ WebAuthn-derived key │ │ -│ ▼ ▼ │ +│ │ │ │ +│ │ Encrypted with │ REST/gRPC │ +│ │ WebAuthn-derived key │ │ +│ ▼ ▼ │ │ ┌──────────────────────┐ ┌──────────────────────────────────┐ │ │ │ IndexedDB/OPFS │ │ Sonr Protocol / Indexers │ │ │ │ (Browser Storage) │ │ (PostgreSQL for live queries) │ │ @@ -32,8 +32,10 @@ This document outlines the SQL schema design for the Nebula wallet's **encrypted └─────────────────────────────────────────────────────────────────────┘ ``` + ### What Goes in the Enclave (SQLite) + | Data Type | Rationale | |-----------|-----------| | WebAuthn Credentials | Device authentication, never leaves device | @@ -46,6 +48,7 @@ This document outlines the SQL schema design for the Nebula wallet's **encrypted ### What Comes from APIs (NOT in SQLite) + | Data Type | Source | |-----------|--------| | Token Balances | Chain RPC / Indexer API | -- 2.43.0 From 5728e9cf6c04ae9e713950ec29a3f018ef33c7da Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Fri, 9 Jan 2026 19:19:59 -0500 Subject: [PATCH 21/35] chore(config): add repository configuration file --- .github/Repo.toml.migrated.20260109_191955 | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .github/Repo.toml.migrated.20260109_191955 diff --git a/.github/Repo.toml.migrated.20260109_191955 b/.github/Repo.toml.migrated.20260109_191955 new file mode 100644 index 0000000..6da103c --- /dev/null +++ b/.github/Repo.toml.migrated.20260109_191955 @@ -0,0 +1,4 @@ +[scopes] +docs = ["MIGRATION.md", "README.md"] +db = ["db"] +config = [".github"] -- 2.43.0 From 1c9067b91ac86553984e476110e3004934527a28 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Fri, 9 Jan 2026 19:20:01 -0500 Subject: [PATCH 22/35] chore(github): remove Repo.toml configuration file --- .github/Repo.toml | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 .github/Repo.toml diff --git a/.github/Repo.toml b/.github/Repo.toml deleted file mode 100644 index 6da103c..0000000 --- a/.github/Repo.toml +++ /dev/null @@ -1,4 +0,0 @@ -[scopes] -docs = ["MIGRATION.md", "README.md"] -db = ["db"] -config = [".github"] -- 2.43.0 From a633631dcfab66be1ae7748ba1945eb7ac5b6ec5 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Sat, 10 Jan 2026 11:50:59 -0500 Subject: [PATCH 23/35] feat(enclave): complete encryption strategy and database serialization --- TODO.md | 198 +++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 132 insertions(+), 66 deletions(-) diff --git a/TODO.md b/TODO.md index fc94c26..45bedad 100644 --- a/TODO.md +++ b/TODO.md @@ -10,11 +10,11 @@ Remaining tasks from [MIGRATION.md](./MIGRATION.md) for the Nebula Key Enclave. | SQLC Queries | Complete | `internal/migrations/query.sql` - CID-based queries added | | Generated Code | Complete | `internal/keybase/*.go` | | Basic Plugin Functions | Complete | `generate`, `load`, `exec`, `query`, `ping` | -| Encryption | Not Started | WebAuthn PRF key derivation needed | +| **Encryption** | **Complete** | `internal/enclave/` - WebAuthn PRF key derivation + AES-256-GCM | | **UCAN v1.0.0-rc.1** | **Complete** | Core types, builders, policies, DB actions all complete | | UCAN DB Actions | Complete | `actions_delegation.go`, `actions_invocation.go` | -| MPC Key Shares | Not Started | Key share management missing | -| Database Serialization | Incomplete | Export dumps comments only | +| MPC Key Shares | Complete | `actions_keyshare.go` - Full key share management | +| **Database Serialization** | **Complete** | Native SQLite serialization via `ncruces/go-sqlite3/ext/serdes` | --- @@ -184,45 +184,56 @@ The following files implement UCAN v1.0.0-rc.1 using the official go-ucan librar ## 2. Encryption Strategy > Reference: MIGRATION.md lines 770-814 +> **Status**: ✅ Complete - Implemented in `internal/enclave/` ### 2.1 WebAuthn PRF Key Derivation -- [ ] Implement `DeriveEncryptionKey(prfOutput []byte) ([]byte, error)` -- [ ] Use HKDF with SHA-256 to derive 256-bit encryption key -- [ ] Salt with `"nebula-enclave-v1"` as info parameter +- [x] Implement `DeriveEncryptionKey(prfOutput []byte) ([]byte, error)` +- [x] Use HKDF with SHA-256 to derive 256-bit encryption key +- [x] Salt with `"nebula-enclave-v1"` as info parameter +- [x] `DeriveKeyWithContext()` for purpose-specific key derivation ### 2.2 Database Encryption -- [ ] Implement application-level AES-GCM encryption for serialized pages -- [ ] Add encryption wrapper around `Serialize()` output -- [ ] Add decryption wrapper for `Load()` input -- [ ] Store encryption metadata (IV, auth tag) with serialized data +- [x] Implement application-level AES-256-GCM encryption for serialized pages +- [x] Add encryption wrapper around `Serialize()` output (`EncryptBytes()`) +- [x] Add decryption wrapper for `Load()` input (`DecryptBytes()`) +- [x] Store encryption metadata (version, nonce, auth tag) with serialized data +- [x] `SecureZero()` for memory clearing of sensitive data ### 2.3 Encrypted Database Wrapper -- [ ] Create `internal/enclave/enclave.go` - Encrypted database wrapper -- [ ] Create `internal/enclave/crypto.go` - WebAuthn PRF key derivation -- [ ] Integrate with existing `internal/keybase` package +- [x] Create `internal/enclave/enclave.go` - Encrypted database wrapper + - [x] `Enclave` struct wrapping `Keybase` with encryption key + - [x] `SerializeEncrypted()` - Export encrypted database + - [x] `LoadEncrypted()` - Load from encrypted bytes + - [x] `Export()` / `Import()` - Full bundle operations with DID + - [x] `EncryptedBundle` struct with JSON marshaling +- [x] Create `internal/enclave/crypto.go` - WebAuthn PRF key derivation + - [x] `Encrypt()` / `Decrypt()` with `EncryptedData` struct + - [x] `EncryptBytes()` / `DecryptBytes()` convenience functions + - [x] `GenerateNonce()` for secure random nonce generation +- [x] Integrate with existing `internal/keybase` package via `FromExisting()` --- ## 3. Database Serialization -> Current implementation in `conn.go:exportDump()` only outputs comments +> **Status**: ✅ Complete - Using native SQLite serialization via `ncruces/go-sqlite3/ext/serdes` -### 3.1 Proper Serialization +### 3.1 Native SQLite Serialization -- [ ] Implement full row export with proper SQL INSERT statements -- [ ] Handle JSON columns correctly (escape special characters) -- [ ] Include table creation order for foreign key constraints -- [ ] Add version header for migration compatibility +- [x] `Serialize()` using `serdes.Serialize(conn, "main")` - Binary database export +- [x] Full database state captured as byte slice +- [x] No SQL parsing needed - direct database format +- [x] Preserves all data types, indexes, and constraints -### 3.2 Proper Deserialization +### 3.2 Native SQLite Deserialization -- [ ] Parse serialized SQL dump in `Load()` -- [ ] Execute INSERT statements to restore data -- [ ] Validate data integrity after restore -- [ ] Handle schema version mismatches +- [x] `Load()` using `serdes.Deserialize(conn, "main", data)` - Binary import +- [x] `RestoreFromDump()` for encrypted bundle loading +- [x] Automatic DID context restoration after load +- [x] Integrated with `internal/enclave` for encrypted storage --- @@ -265,28 +276,28 @@ The following files implement UCAN v1.0.0-rc.1 using the official go-ucan librar ### 4.3 Verification Method Actions -- [ ] `CreateVerificationMethod(ctx, params) (*VerificationMethodResult, error)` -- [ ] `ListVerificationMethods(ctx) ([]VerificationMethodResult, error)` -- [ ] `GetVerificationMethod(ctx, methodID) (*VerificationMethodResult, error)` -- [ ] `DeleteVerificationMethod(ctx, methodID) error` +- [x] `CreateVerificationMethod(ctx, params) (*VerificationMethodResult, error)` +- [x] `ListVerificationMethodsFull(ctx) ([]VerificationMethodResult, error)` +- [x] `GetVerificationMethod(ctx, methodID) (*VerificationMethodResult, error)` +- [x] `DeleteVerificationMethod(ctx, methodID) error` ### 4.4 Service Actions -- [ ] `CreateService(ctx, params) (*ServiceResult, error)` -- [ ] `GetServiceByOrigin(ctx, origin) (*ServiceResult, error)` -- [ ] `GetServiceByID(ctx, serviceID) (*ServiceResult, error)` -- [ ] `UpdateService(ctx, params) error` -- [ ] `ListVerifiedServices(ctx) ([]ServiceResult, error)` +- [x] `CreateService(ctx, params) (*ServiceResult, error)` +- [x] `GetServiceByOrigin(ctx, origin) (*ServiceResult, error)` +- [x] `GetServiceByID(ctx, serviceID) (*ServiceResult, error)` +- [x] `UpdateService(ctx, params) error` +- [x] `ListVerifiedServices(ctx) ([]ServiceResult, error)` ### 4.5 Grant Actions (Extend Existing) -- [ ] `CreateGrant(ctx, params) (*GrantResult, error)` -- [ ] `GetGrantByService(ctx, serviceID) (*GrantResult, error)` -- [ ] `UpdateGrantScopes(ctx, grantID, scopes, accounts) error` -- [ ] `UpdateGrantLastUsed(ctx, grantID) error` -- [ ] `SuspendGrant(ctx, grantID) error` -- [ ] `ReactivateGrant(ctx, grantID) error` -- [ ] `CountActiveGrants(ctx) (int64, error)` +- [x] `CreateGrant(ctx, params) (*GrantResult, error)` +- [x] `GetGrantByService(ctx, serviceID) (*GrantResult, error)` +- [x] `UpdateGrantScopes(ctx, grantID, scopes, accounts) error` +- [x] `UpdateGrantLastUsed(ctx, grantID) error` +- [x] `SuspendGrant(ctx, grantID) error` +- [x] `ReactivateGrant(ctx, grantID) error` +- [x] `CountActiveGrants(ctx) (int64, error)` ### 4.6 Account Actions (Extend Existing) @@ -299,19 +310,19 @@ The following files implement UCAN v1.0.0-rc.1 using the official go-ucan librar ### 4.7 Credential Actions (Extend Existing) -- [ ] `CreateCredential(ctx, params) (*CredentialResult, error)` -- [ ] `UpdateCredentialCounter(ctx, credentialID, signCount) error` -- [ ] `RenameCredential(ctx, credentialID, name) error` -- [ ] `DeleteCredential(ctx, credentialID) error` -- [ ] `CountCredentialsByDID(ctx) (int64, error)` +- [x] `CreateCredential(ctx, params) (*CredentialResult, error)` +- [x] `UpdateCredentialCounter(ctx, credentialID, signCount) error` +- [x] `RenameCredential(ctx, credentialID, name) error` +- [x] `DeleteCredential(ctx, credentialID) error` +- [x] `CountCredentialsByDID(ctx) (int64, error)` ### 4.8 Session Actions (Extend Existing) -- [ ] `GetSessionByID(ctx, sessionID) (*SessionResult, error)` -- [ ] `GetCurrentSession(ctx) (*SessionResult, error)` -- [ ] `UpdateSessionActivity(ctx, sessionID) error` -- [ ] `SetCurrentSession(ctx, sessionID) error` -- [ ] `DeleteExpiredSessions(ctx) error` +- [x] `GetSessionByID(ctx, sessionID) (*SessionResult, error)` +- [x] `GetCurrentSession(ctx) (*SessionResult, error)` +- [x] `UpdateSessionActivity(ctx, sessionID) error` +- [x] `SetCurrentSession(ctx, sessionID) error` +- [x] `DeleteExpiredSessions(ctx) error` ### 4.9 Sync Checkpoint Actions @@ -355,12 +366,12 @@ The following files implement UCAN v1.0.0-rc.1 using the official go-ucan librar ### 6.1 Extend `exec` Resource Handlers -- [ ] Add `key_shares` resource handler -- [x] Add `ucans` resource handler (v1.0.0-rc.1 delegations) -- [x] Add `delegations` resource handler (v1.0.0-rc.1) +- [x] Add `key_shares` resource handler (list, get, rotate, archive, delete) +- [x] Add `ucans` resource handler (v1.0.0-rc.1 delegations - list, get, revoke, verify, cleanup) +- [x] Add `delegations` resource handler (v1.0.0-rc.1 - list, list_received, list_command, get, revoke, verify) - [ ] Add `invocations` resource handler (v1.0.0-rc.1) -- [x] Add `verification_methods` resource handler -- [x] Add `services` resource handler +- [x] Add `verification_methods` resource handler (list, get, delete) +- [x] Add `services` resource handler (list, get, get_by_id) - [ ] Add `sync_checkpoints` resource handler ### 6.2 Extend `generate` Function @@ -523,22 +534,23 @@ The following files implement UCAN v1.0.0-rc.1 using the official go-ucan librar - ~~Database integration (1.8)~~ ✅ Schema, queries, and actions complete - MPC signing integration (1.9) - Next priority -2. **High Priority (Core Functionality)** - - Database Serialization (3.1, 3.2) - - Credential Creation (6.2, 4.7) - - Key Share Actions (4.1) - - Account Actions (4.6) - - Delegation Loader for go-ucan (1.6) +2. **High Priority (Core Functionality)** - ✅ Mostly Complete + - ~~Database Serialization (3.1, 3.2)~~ ✅ Native SQLite serdes + - ~~Credential Actions (4.7)~~ ✅ All CRUD operations + - ~~Key Share Actions (4.1)~~ ✅ All operations + - ~~Account Actions (4.6)~~ ✅ All operations + - Delegation Loader for go-ucan (1.6) - Remaining + - Invocations exec handler (6.1) - Remaining -3. **Medium Priority (Authorization)** - - Revocation checker for go-ucan (1.7) - - MPC Signing (1.9) - - Encryption Strategy (2.1, 2.2) +3. **Medium Priority (Authorization)** - ✅ Partially Complete + - Revocation checker for go-ucan (1.7) - Remaining + - MPC Signing (1.9) - Remaining + - ~~Encryption Strategy (2.1, 2.2, 2.3)~~ ✅ Complete 4. **Lower Priority (Enhancement)** - TypeScript SDK (9.x) - DID State Sync (8.x) - - Additional exec handlers (6.1) + - Sync checkpoints handler (6.1) - Testing (10.x) - Security Hardening (11.x) @@ -546,6 +558,60 @@ The following files implement UCAN v1.0.0-rc.1 using the official go-ucan librar ## Completed Items +### Encryption & Serialization (January 2025) + +Full encryption layer and native SQLite serialization implemented: + +- ✅ `internal/enclave/crypto.go` - WebAuthn PRF key derivation + - `DeriveEncryptionKey()` using HKDF with SHA-256 + - `DeriveKeyWithContext()` for purpose-specific keys + - AES-256-GCM encryption/decryption (`Encrypt`, `Decrypt`) + - `EncryptBytes()` / `DecryptBytes()` convenience functions + - `SecureZero()` for memory clearing + +- ✅ `internal/enclave/enclave.go` - Encrypted database wrapper + - `Enclave` struct wrapping `Keybase` with encryption + - `SerializeEncrypted()` / `LoadEncrypted()` methods + - `Export()` / `Import()` with `EncryptedBundle` + - `FromExisting()` to wrap existing keybase + +- ✅ `internal/keybase/conn.go` - Native SQLite serialization + - `Serialize()` using `serdes.Serialize()` from ncruces/go-sqlite3 + - `Load()` using `serdes.Deserialize()` + - `RestoreFromDump()` for encrypted bundle loading + +### Action Manager Extensions (January 2025) + +All CRUD action handlers completed for remaining entities: + +- ✅ `internal/keybase/actions_verification.go` + - CreateVerificationMethod, ListVerificationMethodsFull + - GetVerificationMethod, DeleteVerificationMethod + +- ✅ `internal/keybase/actions_service.go` + - CreateService, GetServiceByOrigin, GetServiceByID + - UpdateService, ListVerifiedServices + +- ✅ `internal/keybase/actions_grant.go` + - CreateGrant, GetGrantByService, UpdateGrantScopes + - UpdateGrantLastUsed, SuspendGrant, ReactivateGrant, CountActiveGrants + +- ✅ `internal/keybase/actions_credential.go` + - CreateCredential, UpdateCredentialCounter, RenameCredential + - DeleteCredential, CountCredentialsByDID + +- ✅ `internal/keybase/actions_session.go` + - GetSessionByID, GetCurrentSession, UpdateSessionActivity + - SetCurrentSession, DeleteExpiredSessions + +### Plugin Exec Handlers (January 2025) + +Extended exec function with new resource handlers: + +- ✅ `key_shares` - list, get, rotate, archive, delete +- ✅ `verification_methods` - list, get, delete +- ✅ `services` - list, get, get_by_id + ### UCAN v1.0.0-rc.1 Database Integration (January 2025) Schema and action handlers for storing/querying UCAN delegations and invocations: -- 2.43.0 From 2dfd8b5a8fea1621578b4140b462f07891a818ce Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Sat, 10 Jan 2026 14:32:21 -0500 Subject: [PATCH 24/35] feat(enclave): add MPC enclave generation support --- cmd/enclave/main.go | 571 +++++---------------------- internal/keybase/actions.go | 17 +- internal/keybase/actions_account.go | 5 +- internal/keybase/actions_keyshare.go | 206 ---------- internal/keybase/conn.go | 268 ------------- internal/keybase/models.go | 110 +++++- internal/keybase/querier.go | 26 +- internal/keybase/query.sql.go | 455 +++++++++------------ internal/migrations/query.sql | 55 +-- internal/migrations/schema.sql | 82 ++-- internal/types/generate.go | 34 +- 11 files changed, 487 insertions(+), 1342 deletions(-) delete mode 100644 internal/keybase/actions_keyshare.go delete mode 100644 internal/keybase/conn.go diff --git a/cmd/enclave/main.go b/cmd/enclave/main.go index 3e75062..b252678 100644 --- a/cmd/enclave/main.go +++ b/cmd/enclave/main.go @@ -5,17 +5,19 @@ package main import ( "context" "encoding/base64" - "encoding/hex" "encoding/json" "errors" "fmt" "strings" + "enclave/internal/crypto/bip44" + "enclave/internal/crypto/mpc" "enclave/internal/keybase" "enclave/internal/state" "enclave/internal/types" "github.com/extism/go-pdk" + "github.com/sonr-io/crypto/core/protocol" ) func main() { state.Default() } @@ -51,7 +53,7 @@ func ping() int32 { //go:wasmexport generate func generate() int32 { - pdk.Log(pdk.LogInfo, "generate: starting database initialization") + pdk.Log(pdk.LogInfo, "generate: starting database initialization with MPC") var input types.GenerateInput if err := pdk.InputJSON(&input); err != nil { @@ -70,9 +72,9 @@ func generate() int32 { return 1 } - result, err := initializeDatabase(credentialBytes, input.KeyShare) + result, err := initializeWithMPC(credentialBytes) if err != nil { - pdk.SetError(fmt.Errorf("generate: failed to initialize database: %w", err)) + pdk.SetError(fmt.Errorf("generate: failed to initialize: %w", err)) return 1 } @@ -86,10 +88,11 @@ func generate() int32 { } output := types.GenerateOutput{ - DID: result.DID, - Database: dbBytes, - KeyShareID: result.KeyShareID, - Account: result.Account, + DID: result.DID, + Database: dbBytes, + EnclaveID: result.EnclaveID, + PublicKey: result.PublicKey, + Accounts: result.Accounts, } if err := pdk.OutputJSON(output); err != nil { @@ -97,7 +100,7 @@ func generate() int32 { return 1 } - pdk.Log(pdk.LogInfo, fmt.Sprintf("generate: created DID %s", result.DID)) + pdk.Log(pdk.LogInfo, fmt.Sprintf("generate: created DID %s with enclave %s", result.DID, result.EnclaveID)) return 0 } @@ -244,12 +247,13 @@ func query() int32 { } type initResult struct { - DID string - KeyShareID string - Account *types.AccountInfo + DID string + EnclaveID string + PublicKey string + Accounts []types.AccountInfo } -func initializeDatabase(credentialBytes []byte, keyShareInput *types.KeyShareInput) (*initResult, error) { +func initializeWithMPC(credentialBytes []byte) (*initResult, error) { kb, err := keybase.Open() if err != nil { return nil, fmt.Errorf("open database: %w", err) @@ -261,88 +265,95 @@ func initializeDatabase(credentialBytes []byte, keyShareInput *types.KeyShareInp return nil, fmt.Errorf("initialize: %w", err) } - result := &initResult{DID: did} - - if keyShareInput != nil { - keyShareID, account, err := createInitialKeyShare(ctx, keyShareInput) - if err != nil { - pdk.Log(pdk.LogWarn, fmt.Sprintf("initializeDatabase: failed to create keyshare: %s", err)) - } else { - result.KeyShareID = keyShareID - result.Account = account - pdk.Log(pdk.LogInfo, fmt.Sprintf("initializeDatabase: created keyshare %s", keyShareID)) - } + pdk.Log(pdk.LogInfo, "initializeWithMPC: generating MPC enclave") + enclave, err := mpc.NewEnclave() + if err != nil { + return nil, fmt.Errorf("generate MPC enclave: %w", err) } - pdk.Log(pdk.LogDebug, "initializeDatabase: created schema and initial records") - return result, nil -} + enclaveData := enclave.GetData() + enclaveID := fmt.Sprintf("enc_%x", credentialBytes[:8]) + + valShareStr, err := protocol.EncodeMessage(enclaveData.ValShare) + if err != nil { + return nil, fmt.Errorf("encode val share: %w", err) + } + userShareStr, err := protocol.EncodeMessage(enclaveData.UserShare) + if err != nil { + return nil, fmt.Errorf("encode user share: %w", err) + } -func createInitialKeyShare(ctx context.Context, input *types.KeyShareInput) (string, *types.AccountInfo, error) { am, err := keybase.NewActionManager() if err != nil { - return "", nil, fmt.Errorf("action manager: %w", err) + return nil, fmt.Errorf("action manager: %w", err) } - ks, err := am.CreateKeyShare(ctx, keybase.NewKeyShareInput{ - KeyID: input.KeyID, - PartyIndex: input.PartyIndex, - Threshold: input.Threshold, - TotalParties: input.TotalParties, - Curve: input.Curve, - ShareData: input.ShareData, - PublicKey: input.PublicKey, - ChainCode: input.ChainCode, - DerivationPath: input.DerivationPath, + enc, err := am.CreateEnclave(ctx, keybase.NewEnclaveInput{ + EnclaveID: enclaveID, + PublicKeyHex: enclaveData.PubHex, + PublicKey: enclaveData.PubBytes, + ValShare: []byte(valShareStr), + UserShare: []byte(userShareStr), + Nonce: enclaveData.Nonce, + Curve: string(enclaveData.Curve), }) if err != nil { - return "", nil, fmt.Errorf("create keyshare: %w", err) + return nil, fmt.Errorf("store enclave: %w", err) } - account, err := createInitialAccount(ctx, am, ks.ID, input.PublicKey) + pdk.Log(pdk.LogInfo, fmt.Sprintf("initializeWithMPC: stored enclave %s", enclaveID)) + + accounts, err := createDefaultAccounts(ctx, am, enc.ID, enclaveData.PubBytes) if err != nil { - pdk.Log(pdk.LogWarn, fmt.Sprintf("createInitialKeyShare: failed to create account: %s", err)) - return ks.ShareID, nil, nil + pdk.Log(pdk.LogWarn, fmt.Sprintf("initializeWithMPC: failed to create accounts: %s", err)) + accounts = []types.AccountInfo{} } - return ks.ShareID, account, nil -} - -func createInitialAccount(ctx context.Context, am *keybase.ActionManager, keyShareID int64, publicKey string) (*types.AccountInfo, error) { - address := deriveCosmosAddress(publicKey) - if address == "" { - return nil, fmt.Errorf("failed to derive address from public key") - } - - acc, err := am.CreateAccount(ctx, keybase.NewAccountInput{ - KeyShareID: keyShareID, - Address: address, - ChainID: "sonr-testnet-1", - CoinType: 118, - AccountIndex: 0, - AddressIndex: 0, - Label: "Default Account", - }) - if err != nil { - return nil, fmt.Errorf("create account: %w", err) - } - - return &types.AccountInfo{ - Address: acc.Address, - ChainID: acc.ChainID, - CoinType: acc.CoinType, + return &initResult{ + DID: did, + EnclaveID: enclaveID, + PublicKey: enclaveData.PubHex, + Accounts: accounts, }, nil } -func deriveCosmosAddress(publicKeyHex string) string { - if publicKeyHex == "" { - return "" +func createDefaultAccounts(ctx context.Context, am *keybase.ActionManager, enclaveID int64, pubKeyBytes []byte) ([]types.AccountInfo, error) { + chains := []string{"bitcoin", "ethereum", "sonr"} + derivedAccounts, err := bip44.DeriveAccounts(pubKeyBytes, chains) + if err != nil { + return nil, fmt.Errorf("derive accounts: %w", err) } - pubBytes, err := hex.DecodeString(publicKeyHex) - if err != nil || len(pubBytes) < 20 { - return "" + + accounts := make([]types.AccountInfo, 0, len(derivedAccounts)) + for i, derived := range derivedAccounts { + isDefault := int64(0) + if i == 0 { + isDefault = 1 + } + + acc, err := am.CreateAccount(ctx, keybase.NewAccountInput{ + EnclaveID: enclaveID, + Address: derived.Address, + ChainID: derived.ChainID, + CoinType: int64(derived.CoinType), + AccountIndex: int64(derived.AccountIndex), + AddressIndex: int64(derived.AddressIndex), + Label: derived.ChainID, + IsDefault: isDefault, + }) + if err != nil { + pdk.Log(pdk.LogWarn, fmt.Sprintf("createDefaultAccounts: failed for %s: %s", derived.ChainID, err)) + continue + } + + accounts = append(accounts, types.AccountInfo{ + Address: acc.Address, + ChainID: acc.ChainID, + CoinType: acc.CoinType, + }) } - return fmt.Sprintf("snr1%x", pubBytes[:20]) + + return accounts, nil } func serializeDatabase() ([]byte, error) { @@ -523,61 +534,16 @@ func matchResource(pattern, resource string) bool { } func executeAction(params *types.FilterParams) (json.RawMessage, error) { - switch params.Resource { - case "accounts": - return executeAccountAction(params) - case "credentials": - return executeCredentialAction(params) - case "sessions": - return executeSessionAction(params) - case "grants": - return executeGrantAction(params) - case "key_shares": - return executeKeyShareAction(params) - case "ucans": - return executeUCANAction(params) - case "delegations": - return executeDelegationAction(params) - case "verification_methods": - return executeVerificationMethodAction(params) - case "services": - return executeServiceAction(params) - default: - return nil, fmt.Errorf("unknown resource: %s", params.Resource) - } -} - -func executeAccountAction(params *types.FilterParams) (json.RawMessage, error) { - am, err := keybase.NewActionManager() - if err != nil { - return nil, fmt.Errorf("action manager: %w", err) + if params.Resource == "accounts" { + switch params.Action { + case "balances": + return fetchAccountBalances(params.Subject) + case "sign": + return json.Marshal(map[string]string{"signature": "placeholder"}) + } } - ctx := context.Background() - - switch params.Action { - case "list": - accounts, err := am.ListAccounts(ctx) - if err != nil { - return nil, fmt.Errorf("list accounts: %w", err) - } - return json.Marshal(accounts) - case "get": - if params.Subject == "" { - return nil, errors.New("subject (address) required for get action") - } - account, err := am.GetAccountByAddress(ctx, params.Subject) - if err != nil { - return nil, fmt.Errorf("get account: %w", err) - } - return json.Marshal(account) - case "balances": - return fetchAccountBalances(params.Subject) - case "sign": - return json.Marshal(map[string]string{"signature": "placeholder"}) - default: - return nil, fmt.Errorf("unknown action for accounts: %s", params.Action) - } + return keybase.Exec(context.Background(), params.Resource, params.Action, params.Subject) } func fetchAccountBalances(address string) (json.RawMessage, error) { @@ -614,274 +580,6 @@ func fetchAccountBalances(address string) (json.RawMessage, error) { return body, nil } -func executeCredentialAction(params *types.FilterParams) (json.RawMessage, error) { - am, err := keybase.NewActionManager() - if err != nil { - return nil, fmt.Errorf("action manager: %w", err) - } - - ctx := context.Background() - - switch params.Action { - case "list": - credentials, err := am.ListCredentials(ctx) - if err != nil { - return nil, fmt.Errorf("list credentials: %w", err) - } - return json.Marshal(credentials) - case "get": - if params.Subject == "" { - return nil, errors.New("subject (credential_id) required for get action") - } - credential, err := am.GetCredentialByID(ctx, params.Subject) - if err != nil { - return nil, fmt.Errorf("get credential: %w", err) - } - return json.Marshal(credential) - default: - return nil, fmt.Errorf("unknown action for credentials: %s", params.Action) - } -} - -func executeSessionAction(params *types.FilterParams) (json.RawMessage, error) { - am, err := keybase.NewActionManager() - if err != nil { - return nil, fmt.Errorf("action manager: %w", err) - } - - ctx := context.Background() - - switch params.Action { - case "list": - sessions, err := am.ListSessions(ctx) - if err != nil { - return nil, fmt.Errorf("list sessions: %w", err) - } - return json.Marshal(sessions) - case "revoke": - if params.Subject == "" { - return nil, errors.New("subject (session_id) required for revoke action") - } - if err := am.RevokeSession(ctx, params.Subject); err != nil { - return nil, fmt.Errorf("revoke session: %w", err) - } - return json.Marshal(map[string]bool{"revoked": true}) - default: - return nil, fmt.Errorf("unknown action for sessions: %s", params.Action) - } -} - -func executeGrantAction(params *types.FilterParams) (json.RawMessage, error) { - am, err := keybase.NewActionManager() - if err != nil { - return nil, fmt.Errorf("action manager: %w", err) - } - - ctx := context.Background() - - switch params.Action { - case "list": - grants, err := am.ListGrants(ctx) - if err != nil { - return nil, fmt.Errorf("list grants: %w", err) - } - return json.Marshal(grants) - case "revoke": - if params.Subject == "" { - return nil, errors.New("subject (grant_id) required for revoke action") - } - var grantID int64 - if _, err := fmt.Sscanf(params.Subject, "%d", &grantID); err != nil { - return nil, fmt.Errorf("invalid grant_id: %w", err) - } - if err := am.RevokeGrant(ctx, grantID); err != nil { - return nil, fmt.Errorf("revoke grant: %w", err) - } - return json.Marshal(map[string]bool{"revoked": true}) - default: - return nil, fmt.Errorf("unknown action for grants: %s", params.Action) - } -} - -func executeKeyShareAction(params *types.FilterParams) (json.RawMessage, error) { - am, err := keybase.NewActionManager() - if err != nil { - return nil, fmt.Errorf("action manager: %w", err) - } - - ctx := context.Background() - - switch params.Action { - case "list": - shares, err := am.ListKeyShares(ctx) - if err != nil { - return nil, fmt.Errorf("list key shares: %w", err) - } - return json.Marshal(shares) - case "get": - if params.Subject == "" { - return nil, errors.New("subject (share_id) required for get action") - } - share, err := am.GetKeyShareByID(ctx, params.Subject) - if err != nil { - return nil, fmt.Errorf("get key share: %w", err) - } - return json.Marshal(share) - case "rotate": - if params.Subject == "" { - return nil, errors.New("subject (share_id) required for rotate action") - } - if err := am.RotateKeyShare(ctx, params.Subject); err != nil { - return nil, fmt.Errorf("rotate key share: %w", err) - } - return json.Marshal(map[string]bool{"rotated": true}) - case "archive": - if params.Subject == "" { - return nil, errors.New("subject (share_id) required for archive action") - } - if err := am.ArchiveKeyShare(ctx, params.Subject); err != nil { - return nil, fmt.Errorf("archive key share: %w", err) - } - return json.Marshal(map[string]bool{"archived": true}) - case "delete": - if params.Subject == "" { - return nil, errors.New("subject (share_id) required for delete action") - } - if err := am.DeleteKeyShare(ctx, params.Subject); err != nil { - return nil, fmt.Errorf("delete key share: %w", err) - } - return json.Marshal(map[string]bool{"deleted": true}) - default: - return nil, fmt.Errorf("unknown action for key_shares: %s", params.Action) - } -} - -func executeUCANAction(params *types.FilterParams) (json.RawMessage, error) { - am, err := keybase.NewActionManager() - if err != nil { - return nil, fmt.Errorf("action manager: %w", err) - } - - ctx := context.Background() - - switch params.Action { - case "list": - delegations, err := am.ListDelegations(ctx) - if err != nil { - return nil, fmt.Errorf("list delegations: %w", err) - } - return json.Marshal(delegations) - case "get": - if params.Subject == "" { - return nil, errors.New("subject (cid) required for get action") - } - delegation, err := am.GetDelegationByCID(ctx, params.Subject) - if err != nil { - return nil, fmt.Errorf("get delegation: %w", err) - } - return json.Marshal(delegation) - case "revoke": - if params.Subject == "" { - return nil, errors.New("subject (cid) required for revoke action") - } - if err := am.RevokeDelegation(ctx, keybase.RevokeDelegationParams{ - DelegationCID: params.Subject, - RevokedBy: state.GetDID(), - Reason: "user revoked", - }); err != nil { - return nil, fmt.Errorf("revoke delegation: %w", err) - } - return json.Marshal(map[string]bool{"revoked": true}) - case "verify": - if params.Subject == "" { - return nil, errors.New("subject (cid) required for verify action") - } - revoked, err := am.IsDelegationRevoked(ctx, params.Subject) - if err != nil { - return nil, fmt.Errorf("check delegation: %w", err) - } - return json.Marshal(map[string]bool{"valid": !revoked, "revoked": revoked}) - case "cleanup": - if err := am.CleanExpiredDelegations(ctx); err != nil { - return nil, fmt.Errorf("cleanup delegations: %w", err) - } - return json.Marshal(map[string]bool{"cleaned": true}) - default: - return nil, fmt.Errorf("unknown action for ucans: %s", params.Action) - } -} - -func executeDelegationAction(params *types.FilterParams) (json.RawMessage, error) { - am, err := keybase.NewActionManager() - if err != nil { - return nil, fmt.Errorf("action manager: %w", err) - } - - ctx := context.Background() - - switch params.Action { - case "list": - if params.Subject == "" { - return nil, errors.New("subject (issuer DID) required for list action") - } - delegations, err := am.ListDelegationsByIssuer(ctx, params.Subject) - if err != nil { - return nil, fmt.Errorf("list delegations: %w", err) - } - return json.Marshal(delegations) - case "list_received": - if params.Subject == "" { - return nil, errors.New("subject (audience DID) required for list_received action") - } - delegations, err := am.ListDelegationsByAudience(ctx, params.Subject) - if err != nil { - return nil, fmt.Errorf("list received delegations: %w", err) - } - return json.Marshal(delegations) - case "list_command": - if params.Subject == "" { - return nil, errors.New("subject (command) required for list_command action") - } - delegations, err := am.ListDelegationsForCommand(ctx, params.Subject) - if err != nil { - return nil, fmt.Errorf("list delegations for command: %w", err) - } - return json.Marshal(delegations) - case "get": - if params.Subject == "" { - return nil, errors.New("subject (cid) required for get action") - } - delegation, err := am.GetDelegationByCID(ctx, params.Subject) - if err != nil { - return nil, fmt.Errorf("get delegation: %w", err) - } - return json.Marshal(delegation) - case "revoke": - if params.Subject == "" { - return nil, errors.New("subject (cid) required for revoke action") - } - if err := am.RevokeDelegation(ctx, keybase.RevokeDelegationParams{ - DelegationCID: params.Subject, - RevokedBy: state.GetDID(), - Reason: "user revoked", - }); err != nil { - return nil, fmt.Errorf("revoke delegation: %w", err) - } - return json.Marshal(map[string]bool{"revoked": true}) - case "verify": - if params.Subject == "" { - return nil, errors.New("subject (cid) required for verify action") - } - revoked, err := am.IsDelegationRevoked(ctx, params.Subject) - if err != nil { - return nil, fmt.Errorf("check delegation: %w", err) - } - return json.Marshal(map[string]bool{"valid": !revoked, "revoked": revoked}) - default: - return nil, fmt.Errorf("unknown action for delegations: %s", params.Action) - } -} - func resolveDID(did string) (*types.QueryOutput, error) { am, err := keybase.NewActionManager() if err != nil { @@ -939,82 +637,3 @@ func resolveDID(did string) (*types.QueryOutput, error) { Credentials: credentials, }, nil } - -func executeVerificationMethodAction(params *types.FilterParams) (json.RawMessage, error) { - am, err := keybase.NewActionManager() - if err != nil { - return nil, fmt.Errorf("action manager: %w", err) - } - - ctx := context.Background() - - switch params.Action { - case "list": - vms, err := am.ListVerificationMethodsFull(ctx) - if err != nil { - return nil, fmt.Errorf("list verification methods: %w", err) - } - return json.Marshal(vms) - case "get": - if params.Subject == "" { - return nil, errors.New("subject (method_id) required for get action") - } - vm, err := am.GetVerificationMethod(ctx, params.Subject) - if err != nil { - return nil, fmt.Errorf("get verification method: %w", err) - } - return json.Marshal(vm) - case "delete": - if params.Subject == "" { - return nil, errors.New("subject (method_id) required for delete action") - } - if err := am.DeleteVerificationMethod(ctx, params.Subject); err != nil { - return nil, fmt.Errorf("delete verification method: %w", err) - } - return json.Marshal(map[string]bool{"deleted": true}) - default: - return nil, fmt.Errorf("unknown action for verification_methods: %s", params.Action) - } -} - -func executeServiceAction(params *types.FilterParams) (json.RawMessage, error) { - am, err := keybase.NewActionManager() - if err != nil { - return nil, fmt.Errorf("action manager: %w", err) - } - - ctx := context.Background() - - switch params.Action { - case "list": - services, err := am.ListVerifiedServices(ctx) - if err != nil { - return nil, fmt.Errorf("list verified services: %w", err) - } - return json.Marshal(services) - case "get": - if params.Subject == "" { - return nil, errors.New("subject (origin) required for get action") - } - svc, err := am.GetServiceByOrigin(ctx, params.Subject) - if err != nil { - return nil, fmt.Errorf("get service: %w", err) - } - return json.Marshal(svc) - case "get_by_id": - if params.Subject == "" { - return nil, errors.New("subject (service_id) required for get_by_id action") - } - var serviceID int64 - if _, err := fmt.Sscanf(params.Subject, "%d", &serviceID); err != nil { - return nil, fmt.Errorf("invalid service_id: %w", err) - } - svc, err := am.GetServiceByID(ctx, serviceID) - if err != nil { - return nil, fmt.Errorf("get service by ID: %w", err) - } - return json.Marshal(svc) - default: - return nil, fmt.Errorf("unknown action for services: %s", params.Action) - } -} diff --git a/internal/keybase/actions.go b/internal/keybase/actions.go index 54b6dd9..4bddbe3 100644 --- a/internal/keybase/actions.go +++ b/internal/keybase/actions.go @@ -72,7 +72,7 @@ func (am *ActionManager) ListAccounts(ctx context.Context) ([]AccountResult, err AddressIndex: row.AddressIndex, Label: label, IsDefault: row.IsDefault == 1, - PublicKey: row.SharePublicKey, + PublicKey: row.PublicKeyHex, Curve: row.Curve, CreatedAt: row.CreatedAt, } @@ -245,12 +245,19 @@ func (am *ActionManager) ListSessions(ctx context.Context) ([]SessionResult, err authenticator = *sess.Authenticator } + var deviceInfo json.RawMessage + if sess.DeviceInfo != nil { + deviceInfo = json.RawMessage(*sess.DeviceInfo) + } else { + deviceInfo = json.RawMessage(`{}`) + } + results[i] = SessionResult{ ID: sess.ID, SessionID: sess.SessionID, DeviceName: sess.DeviceName, Authenticator: authenticator, - DeviceInfo: sess.DeviceInfo, + DeviceInfo: deviceInfo, IsCurrent: sess.IsCurrent == 1, LastActivity: sess.LastActivity, ExpiresAt: sess.ExpiresAt, @@ -386,8 +393,8 @@ func (am *ActionManager) ListGrants(ctx context.Context) ([]GrantResult, error) ServiceName: g.ServiceName, ServiceOrigin: g.ServiceOrigin, ServiceLogo: serviceLogo, - Scopes: g.Scopes, - Accounts: g.Accounts, + Scopes: json.RawMessage(g.Scopes), + Accounts: json.RawMessage(g.Accounts), Status: g.Status, GrantedAt: g.GrantedAt, LastUsed: lastUsed, @@ -481,7 +488,7 @@ func (am *ActionManager) ResolveDID(ctx context.Context, did string) (*DIDDocume AddressIndex: row.AddressIndex, Label: label, IsDefault: row.IsDefault == 1, - PublicKey: row.SharePublicKey, + PublicKey: row.PublicKeyHex, Curve: row.Curve, CreatedAt: row.CreatedAt, } diff --git a/internal/keybase/actions_account.go b/internal/keybase/actions_account.go index 8c139c7..4265bfb 100644 --- a/internal/keybase/actions_account.go +++ b/internal/keybase/actions_account.go @@ -6,13 +6,14 @@ import ( ) type NewAccountInput struct { - KeyShareID int64 `json:"key_share_id"` + EnclaveID int64 `json:"enclave_id"` Address string `json:"address"` ChainID string `json:"chain_id"` CoinType int64 `json:"coin_type"` AccountIndex int64 `json:"account_index"` AddressIndex int64 `json:"address_index"` Label string `json:"label,omitempty"` + IsDefault int64 `json:"is_default,omitempty"` } func (am *ActionManager) CreateAccount(ctx context.Context, params NewAccountInput) (*AccountResult, error) { @@ -30,7 +31,7 @@ func (am *ActionManager) CreateAccount(ctx context.Context, params NewAccountInp acc, err := am.kb.queries.CreateAccount(ctx, CreateAccountParams{ DidID: am.kb.didID, - KeyShareID: params.KeyShareID, + EnclaveID: params.EnclaveID, Address: params.Address, ChainID: params.ChainID, CoinType: params.CoinType, diff --git a/internal/keybase/actions_keyshare.go b/internal/keybase/actions_keyshare.go deleted file mode 100644 index 974df80..0000000 --- a/internal/keybase/actions_keyshare.go +++ /dev/null @@ -1,206 +0,0 @@ -package keybase - -import ( - "context" - "crypto/rand" - "encoding/hex" - "fmt" -) - -type KeyShareResult struct { - ID int64 `json:"id"` - ShareID string `json:"share_id"` - KeyID string `json:"key_id"` - PartyIndex int64 `json:"party_index"` - Threshold int64 `json:"threshold"` - TotalParties int64 `json:"total_parties"` - Curve string `json:"curve"` - PublicKey string `json:"public_key"` - ChainCode string `json:"chain_code,omitempty"` - DerivationPath string `json:"derivation_path,omitempty"` - Status string `json:"status"` - CreatedAt string `json:"created_at"` - RotatedAt string `json:"rotated_at,omitempty"` -} - -type NewKeyShareInput struct { - KeyID string `json:"key_id"` - PartyIndex int64 `json:"party_index"` - Threshold int64 `json:"threshold"` - TotalParties int64 `json:"total_parties"` - Curve string `json:"curve"` - ShareData string `json:"share_data"` - PublicKey string `json:"public_key"` - ChainCode string `json:"chain_code,omitempty"` - DerivationPath string `json:"derivation_path,omitempty"` -} - -func (am *ActionManager) CreateKeyShare(ctx context.Context, params NewKeyShareInput) (*KeyShareResult, error) { - am.kb.mu.Lock() - defer am.kb.mu.Unlock() - - if am.kb.didID == 0 { - return nil, fmt.Errorf("DID not initialized") - } - - shareID := generateShareID() - - var chainCode, derivationPath *string - if params.ChainCode != "" { - chainCode = ¶ms.ChainCode - } - if params.DerivationPath != "" { - derivationPath = ¶ms.DerivationPath - } - - ks, err := am.kb.queries.CreateKeyShare(ctx, CreateKeyShareParams{ - DidID: am.kb.didID, - ShareID: shareID, - KeyID: params.KeyID, - PartyIndex: params.PartyIndex, - Threshold: params.Threshold, - TotalParties: params.TotalParties, - Curve: params.Curve, - ShareData: params.ShareData, - PublicKey: params.PublicKey, - ChainCode: chainCode, - DerivationPath: derivationPath, - }) - if err != nil { - return nil, fmt.Errorf("create key share: %w", err) - } - - return keyShareToResult(&ks), nil -} - -func (am *ActionManager) ListKeyShares(ctx context.Context) ([]KeyShareResult, error) { - am.kb.mu.RLock() - defer am.kb.mu.RUnlock() - - if am.kb.didID == 0 { - return []KeyShareResult{}, nil - } - - shares, err := am.kb.queries.ListKeySharesByDID(ctx, am.kb.didID) - if err != nil { - return nil, fmt.Errorf("list key shares: %w", err) - } - - results := make([]KeyShareResult, len(shares)) - for i, ks := range shares { - results[i] = *keyShareToResult(&ks) - } - - return results, nil -} - -func (am *ActionManager) GetKeyShareByID(ctx context.Context, shareID string) (*KeyShareResult, error) { - am.kb.mu.RLock() - defer am.kb.mu.RUnlock() - - ks, err := am.kb.queries.GetKeyShareByID(ctx, shareID) - if err != nil { - return nil, fmt.Errorf("get key share: %w", err) - } - - return keyShareToResult(&ks), nil -} - -func (am *ActionManager) GetKeyShareByKeyID(ctx context.Context, keyID string) (*KeyShareResult, error) { - am.kb.mu.RLock() - defer am.kb.mu.RUnlock() - - if am.kb.didID == 0 { - return nil, fmt.Errorf("DID not initialized") - } - - ks, err := am.kb.queries.GetKeyShareByKeyID(ctx, GetKeyShareByKeyIDParams{ - DidID: am.kb.didID, - KeyID: keyID, - }) - if err != nil { - return nil, fmt.Errorf("get key share by key ID: %w", err) - } - - return keyShareToResult(&ks), nil -} - -func (am *ActionManager) RotateKeyShare(ctx context.Context, shareID string) error { - am.kb.mu.Lock() - defer am.kb.mu.Unlock() - - ks, err := am.kb.queries.GetKeyShareByID(ctx, shareID) - if err != nil { - return fmt.Errorf("get key share: %w", err) - } - - return am.kb.queries.RotateKeyShare(ctx, ks.ID) -} - -func (am *ActionManager) ArchiveKeyShare(ctx context.Context, shareID string) error { - am.kb.mu.Lock() - defer am.kb.mu.Unlock() - - ks, err := am.kb.queries.GetKeyShareByID(ctx, shareID) - if err != nil { - return fmt.Errorf("get key share: %w", err) - } - - return am.kb.queries.ArchiveKeyShare(ctx, ks.ID) -} - -func (am *ActionManager) DeleteKeyShare(ctx context.Context, shareID string) error { - am.kb.mu.Lock() - defer am.kb.mu.Unlock() - - if am.kb.didID == 0 { - return fmt.Errorf("DID not initialized") - } - - ks, err := am.kb.queries.GetKeyShareByID(ctx, shareID) - if err != nil { - return fmt.Errorf("get key share: %w", err) - } - - return am.kb.queries.DeleteKeyShare(ctx, DeleteKeyShareParams{ - ID: ks.ID, - DidID: am.kb.didID, - }) -} - -func generateShareID() string { - b := make([]byte, 16) - rand.Read(b) - return "ks_" + hex.EncodeToString(b) -} - -func keyShareToResult(ks *KeyShare) *KeyShareResult { - chainCode := "" - if ks.ChainCode != nil { - chainCode = *ks.ChainCode - } - derivationPath := "" - if ks.DerivationPath != nil { - derivationPath = *ks.DerivationPath - } - rotatedAt := "" - if ks.RotatedAt != nil { - rotatedAt = *ks.RotatedAt - } - - return &KeyShareResult{ - ID: ks.ID, - ShareID: ks.ShareID, - KeyID: ks.KeyID, - PartyIndex: ks.PartyIndex, - Threshold: ks.Threshold, - TotalParties: ks.TotalParties, - Curve: ks.Curve, - PublicKey: ks.PublicKey, - ChainCode: chainCode, - DerivationPath: derivationPath, - Status: ks.Status, - CreatedAt: ks.CreatedAt, - RotatedAt: rotatedAt, - } -} diff --git a/internal/keybase/conn.go b/internal/keybase/conn.go deleted file mode 100644 index 43ed1c2..0000000 --- a/internal/keybase/conn.go +++ /dev/null @@ -1,268 +0,0 @@ -package keybase - -import ( - "context" - "database/sql" - "encoding/json" - "fmt" - "sync" - - "enclave/internal/migrations" - - "github.com/ncruces/go-sqlite3" - "github.com/ncruces/go-sqlite3/driver" - _ "github.com/ncruces/go-sqlite3/embed" - "github.com/ncruces/go-sqlite3/ext/hash" - "github.com/ncruces/go-sqlite3/ext/serdes" - "github.com/ncruces/go-sqlite3/ext/uuid" -) - -// Keybase encapsulates the encrypted key storage database. -type Keybase struct { - db *sql.DB - conn *sqlite3.Conn // raw connection for serdes - queries *Queries - did string - didID int64 - mu sync.RWMutex -} - -var ( - instance *Keybase - initMu sync.Mutex -) - -// Open creates or returns the singleton Keybase instance with an in-memory database. -func Open() (*Keybase, error) { - initMu.Lock() - defer initMu.Unlock() - - if instance != nil { - return instance, nil - } - - var rawConn *sqlite3.Conn - initCallback := func(conn *sqlite3.Conn) error { - rawConn = conn - if err := hash.Register(conn); err != nil { - return fmt.Errorf("register hash extension: %w", err) - } - if err := uuid.Register(conn); err != nil { - return fmt.Errorf("register uuid extension: %w", err) - } - return nil - } - - db, err := driver.Open(":memory:", initCallback) - if err != nil { - return nil, fmt.Errorf("keybase: open database: %w", err) - } - - if _, err := db.Exec(migrations.SchemaSQL); err != nil { - db.Close() - return nil, fmt.Errorf("keybase: init schema: %w", err) - } - - instance = &Keybase{ - db: db, - conn: rawConn, - queries: New(db), - } - - return instance, nil -} - -// Get returns the existing Keybase instance or nil if not initialized. -func Get() *Keybase { - initMu.Lock() - defer initMu.Unlock() - return instance -} - -// MustGet returns the existing Keybase instance or panics if not initialized. -func MustGet() *Keybase { - kb := Get() - if kb == nil { - panic("keybase: not initialized") - } - return kb -} - -// Close closes the database connection and clears the singleton. -func Close() error { - initMu.Lock() - defer initMu.Unlock() - - if instance == nil { - return nil - } - - err := instance.db.Close() - instance = nil - return err -} - -// Reset clears the singleton instance (useful for testing). -func Reset() { - initMu.Lock() - defer initMu.Unlock() - - if instance != nil { - instance.db.Close() - instance = nil - } -} - -// DB returns the underlying sql.DB connection. -func (k *Keybase) DB() *sql.DB { - k.mu.RLock() - defer k.mu.RUnlock() - return k.db -} - -// Queries returns the SQLC-generated query interface. -func (k *Keybase) Queries() *Queries { - k.mu.RLock() - defer k.mu.RUnlock() - return k.queries -} - -// DID returns the current DID identifier. -func (k *Keybase) DID() string { - k.mu.RLock() - defer k.mu.RUnlock() - return k.did -} - -// DIDID returns the database ID of the current DID. -func (k *Keybase) DIDID() int64 { - k.mu.RLock() - defer k.mu.RUnlock() - return k.didID -} - -// IsInitialized returns true if a DID has been set. -func (k *Keybase) IsInitialized() bool { - k.mu.RLock() - defer k.mu.RUnlock() - return k.did != "" -} - -// SetDID sets the current DID context. -func (k *Keybase) SetDID(did string, didID int64) { - k.mu.Lock() - defer k.mu.Unlock() - k.did = did - k.didID = didID -} - -// Initialize creates a new DID document from a WebAuthn credential. -func (k *Keybase) Initialize(ctx context.Context, credentialBytes []byte) (string, error) { - k.mu.Lock() - defer k.mu.Unlock() - - did := fmt.Sprintf("did:sonr:%x", credentialBytes[:16]) - docJSON, _ := json.Marshal(map[string]any{ - "@context": []string{"https://www.w3.org/ns/did/v1"}, - "id": did, - }) - - doc, err := k.queries.CreateDID(ctx, CreateDIDParams{ - Did: did, - Controller: did, - Document: docJSON, - Sequence: 0, - }) - if err != nil { - return "", fmt.Errorf("keybase: create DID: %w", err) - } - - k.did = did - k.didID = doc.ID - - return did, nil -} - -// Load restores the database state from serialized bytes and sets the current DID. -func (k *Keybase) Load(ctx context.Context, data []byte) (string, error) { - if len(data) < 100 { - return "", fmt.Errorf("keybase: invalid database format") - } - - k.mu.Lock() - defer k.mu.Unlock() - - if k.conn == nil { - return "", fmt.Errorf("keybase: database not initialized") - } - - if err := serdes.Deserialize(k.conn, "main", data); err != nil { - return "", fmt.Errorf("keybase: deserialize database: %w", err) - } - - docs, err := k.queries.ListAllDIDs(ctx) - if err != nil { - return "", fmt.Errorf("keybase: list DIDs: %w", err) - } - - if len(docs) == 0 { - return "", fmt.Errorf("keybase: no DID found in database") - } - - k.did = docs[0].Did - k.didID = docs[0].ID - - return k.did, nil -} - -// Serialize exports the database state as bytes using native SQLite serialization. -func (k *Keybase) Serialize() ([]byte, error) { - k.mu.RLock() - defer k.mu.RUnlock() - - if k.conn == nil { - return nil, fmt.Errorf("keybase: database not initialized") - } - - return serdes.Serialize(k.conn, "main") -} - -func (k *Keybase) RestoreFromDump(data []byte) error { - k.mu.Lock() - defer k.mu.Unlock() - - if k.conn == nil { - return fmt.Errorf("keybase: database not initialized") - } - - if err := serdes.Deserialize(k.conn, "main", data); err != nil { - return fmt.Errorf("keybase: deserialize database: %w", err) - } - - docs, err := k.queries.ListAllDIDs(context.Background()) - if err != nil { - return fmt.Errorf("keybase: failed to list DIDs: %w", err) - } - - if len(docs) > 0 { - k.did = docs[0].Did - k.didID = docs[0].ID - } - - return nil -} - -// WithTx executes a function within a database transaction. -func (k *Keybase) WithTx(ctx context.Context, fn func(*Queries) error) error { - tx, err := k.db.BeginTx(ctx, nil) - if err != nil { - return fmt.Errorf("keybase: begin tx: %w", err) - } - - if err := fn(k.queries.WithTx(tx)); err != nil { - tx.Rollback() - return err - } - - return tx.Commit() -} diff --git a/internal/keybase/models.go b/internal/keybase/models.go index 65b3e81..db6e7f6 100644 --- a/internal/keybase/models.go +++ b/internal/keybase/models.go @@ -11,7 +11,7 @@ import ( type Account struct { ID int64 `json:"id"` DidID int64 `json:"did_id"` - KeyShareID int64 `json:"key_share_id"` + EnclaveID int64 `json:"enclave_id"` Address string `json:"address"` ChainID string `json:"chain_id"` CoinType int64 `json:"coin_type"` @@ -64,22 +64,19 @@ type Grant struct { ExpiresAt *string `json:"expires_at"` } -type KeyShare struct { - ID int64 `json:"id"` - DidID int64 `json:"did_id"` - ShareID string `json:"share_id"` - KeyID string `json:"key_id"` - PartyIndex int64 `json:"party_index"` - Threshold int64 `json:"threshold"` - TotalParties int64 `json:"total_parties"` - Curve string `json:"curve"` - ShareData string `json:"share_data"` - PublicKey string `json:"public_key"` - ChainCode *string `json:"chain_code"` - DerivationPath *string `json:"derivation_path"` - Status string `json:"status"` - CreatedAt string `json:"created_at"` - RotatedAt *string `json:"rotated_at"` +type MpcEnclafe struct { + ID int64 `json:"id"` + DidID int64 `json:"did_id"` + EnclaveID string `json:"enclave_id"` + PublicKeyHex string `json:"public_key_hex"` + PublicKey []byte `json:"public_key"` + ValShare []byte `json:"val_share"` + UserShare []byte `json:"user_share"` + Nonce []byte `json:"nonce"` + Curve string `json:"curve"` + Status string `json:"status"` + CreatedAt string `json:"created_at"` + RotatedAt *string `json:"rotated_at"` } type Service struct { @@ -158,6 +155,85 @@ type UcanRevocation struct { RevokedAt string `json:"revoked_at"` } +type VAccount struct { + ID int64 `json:"id"` + DidID int64 `json:"did_id"` + EnclaveID int64 `json:"enclave_id"` + Address string `json:"address"` + ChainID string `json:"chain_id"` + CoinType int64 `json:"coin_type"` + AccountIndex int64 `json:"account_index"` + AddressIndex int64 `json:"address_index"` + Label *string `json:"label"` + IsDefault int64 `json:"is_default"` + CreatedAt string `json:"created_at"` + PublicKeyHex string `json:"public_key_hex"` + Curve string `json:"curve"` + EnclaveRef string `json:"enclave_ref"` +} + +type VActiveDelegation struct { + ID int64 `json:"id"` + DidID int64 `json:"did_id"` + Cid string `json:"cid"` + Envelope []byte `json:"envelope"` + Iss string `json:"iss"` + Aud string `json:"aud"` + Sub *string `json:"sub"` + Cmd string `json:"cmd"` + Pol *string `json:"pol"` + Nbf *string `json:"nbf"` + Exp *string `json:"exp"` + IsRoot int64 `json:"is_root"` + IsPowerline int64 `json:"is_powerline"` + CreatedAt string `json:"created_at"` +} + +type VActiveEnclafe struct { + ID int64 `json:"id"` + DidID int64 `json:"did_id"` + EnclaveID string `json:"enclave_id"` + PublicKeyHex string `json:"public_key_hex"` + PublicKey []byte `json:"public_key"` + ValShare []byte `json:"val_share"` + UserShare []byte `json:"user_share"` + Nonce []byte `json:"nonce"` + Curve string `json:"curve"` + Status string `json:"status"` + CreatedAt string `json:"created_at"` + RotatedAt *string `json:"rotated_at"` +} + +type VGrant struct { + ID int64 `json:"id"` + DidID int64 `json:"did_id"` + ServiceID int64 `json:"service_id"` + DelegationCid *string `json:"delegation_cid"` + Scopes string `json:"scopes"` + Accounts string `json:"accounts"` + Status string `json:"status"` + GrantedAt string `json:"granted_at"` + LastUsed *string `json:"last_used"` + ExpiresAt *string `json:"expires_at"` + ServiceName string `json:"service_name"` + ServiceOrigin string `json:"service_origin"` + ServiceLogo *string `json:"service_logo"` +} + +type VSession struct { + ID int64 `json:"id"` + DidID int64 `json:"did_id"` + CredentialID int64 `json:"credential_id"` + SessionID string `json:"session_id"` + DeviceInfo *string `json:"device_info"` + IsCurrent int64 `json:"is_current"` + LastActivity string `json:"last_activity"` + ExpiresAt string `json:"expires_at"` + CreatedAt string `json:"created_at"` + DeviceName string `json:"device_name"` + Authenticator *string `json:"authenticator"` +} + type VerificationMethod struct { ID int64 `json:"id"` DidID int64 `json:"did_id"` diff --git a/internal/keybase/querier.go b/internal/keybase/querier.go index 43e960b..958bd73 100644 --- a/internal/keybase/querier.go +++ b/internal/keybase/querier.go @@ -9,7 +9,7 @@ import ( ) type Querier interface { - ArchiveKeyShare(ctx context.Context, id int64) error + ArchiveEnclave(ctx context.Context, id int64) error CleanExpiredDelegations(ctx context.Context) error CleanOldInvocations(ctx context.Context) error CountActiveGrants(ctx context.Context, didID int64) (int64, error) @@ -18,9 +18,9 @@ type Querier interface { CreateCredential(ctx context.Context, arg CreateCredentialParams) (Credential, error) CreateDID(ctx context.Context, arg CreateDIDParams) (DidDocument, error) CreateDelegation(ctx context.Context, arg CreateDelegationParams) (UcanDelegation, error) + CreateEnclave(ctx context.Context, arg CreateEnclaveParams) (MpcEnclafe, error) CreateGrant(ctx context.Context, arg CreateGrantParams) (Grant, error) CreateInvocation(ctx context.Context, arg CreateInvocationParams) (UcanInvocation, error) - CreateKeyShare(ctx context.Context, arg CreateKeyShareParams) (KeyShare, error) // ============================================================================= // UCAN REVOCATION QUERIES // ============================================================================= @@ -31,8 +31,8 @@ type Querier interface { DeleteAccount(ctx context.Context, arg DeleteAccountParams) error DeleteCredential(ctx context.Context, arg DeleteCredentialParams) error DeleteDelegation(ctx context.Context, arg DeleteDelegationParams) error + DeleteEnclave(ctx context.Context, arg DeleteEnclaveParams) error DeleteExpiredSessions(ctx context.Context) error - DeleteKeyShare(ctx context.Context, arg DeleteKeyShareParams) error DeleteSession(ctx context.Context, id int64) error DeleteVerificationMethod(ctx context.Context, id int64) error GetAccountByAddress(ctx context.Context, address string) (Account, error) @@ -49,14 +49,14 @@ type Querier interface { // ============================================================================= GetDelegationByCID(ctx context.Context, cid string) (UcanDelegation, error) GetDelegationEnvelopeByCID(ctx context.Context, cid string) ([]byte, error) + GetEnclaveByID(ctx context.Context, enclaveID string) (MpcEnclafe, error) + GetEnclaveByPubKeyHex(ctx context.Context, publicKeyHex string) (MpcEnclafe, error) GetGrantByService(ctx context.Context, arg GetGrantByServiceParams) (Grant, error) // ============================================================================= // UCAN INVOCATION QUERIES (v1.0.0-rc.1) // ============================================================================= GetInvocationByCID(ctx context.Context, cid string) (UcanInvocation, error) GetInvocationEnvelopeByCID(ctx context.Context, cid string) ([]byte, error) - GetKeyShareByID(ctx context.Context, shareID string) (KeyShare, error) - GetKeyShareByKeyID(ctx context.Context, arg GetKeyShareByKeyIDParams) (KeyShare, error) GetRevocation(ctx context.Context, delegationCid string) (UcanRevocation, error) GetServiceByID(ctx context.Context, id int64) (Service, error) // ============================================================================= @@ -74,7 +74,7 @@ type Querier interface { // ============================================================================= // ACCOUNT QUERIES // ============================================================================= - ListAccountsByDID(ctx context.Context, didID int64) ([]ListAccountsByDIDRow, error) + ListAccountsByDID(ctx context.Context, didID int64) ([]VAccount, error) ListAllDIDs(ctx context.Context) ([]DidDocument, error) // ============================================================================= // CREDENTIAL QUERIES @@ -86,17 +86,17 @@ type Querier interface { ListDelegationsBySubject(ctx context.Context, sub *string) ([]UcanDelegation, error) ListDelegationsForCommand(ctx context.Context, arg ListDelegationsForCommandParams) ([]UcanDelegation, error) // ============================================================================= + // MPC ENCLAVE QUERIES + // ============================================================================= + ListEnclavesByDID(ctx context.Context, didID int64) ([]MpcEnclafe, error) + // ============================================================================= // GRANT QUERIES // ============================================================================= - ListGrantsByDID(ctx context.Context, didID int64) ([]ListGrantsByDIDRow, error) + ListGrantsByDID(ctx context.Context, didID int64) ([]VGrant, error) ListInvocationsByDID(ctx context.Context, arg ListInvocationsByDIDParams) ([]UcanInvocation, error) ListInvocationsByIssuer(ctx context.Context, arg ListInvocationsByIssuerParams) ([]UcanInvocation, error) ListInvocationsBySubject(ctx context.Context, arg ListInvocationsBySubjectParams) ([]UcanInvocation, error) ListInvocationsForCommand(ctx context.Context, arg ListInvocationsForCommandParams) ([]UcanInvocation, error) - // ============================================================================= - // KEY SHARE QUERIES - // ============================================================================= - ListKeySharesByDID(ctx context.Context, didID int64) ([]KeyShare, error) ListPendingInvocations(ctx context.Context, didID int64) ([]UcanInvocation, error) ListPowerlineDelegations(ctx context.Context, didID int64) ([]UcanDelegation, error) ListRevocationsByRevoker(ctx context.Context, revokedBy string) ([]UcanRevocation, error) @@ -104,7 +104,7 @@ type Querier interface { // ============================================================================= // SESSION QUERIES // ============================================================================= - ListSessionsByDID(ctx context.Context, didID int64) ([]ListSessionsByDIDRow, error) + ListSessionsByDID(ctx context.Context, didID int64) ([]VSession, error) ListSyncCheckpoints(ctx context.Context, didID int64) ([]SyncCheckpoint, error) // ============================================================================= // VERIFICATION METHOD QUERIES @@ -115,7 +115,7 @@ type Querier interface { ReactivateGrant(ctx context.Context, id int64) error RenameCredential(ctx context.Context, arg RenameCredentialParams) error RevokeGrant(ctx context.Context, id int64) error - RotateKeyShare(ctx context.Context, id int64) error + RotateEnclave(ctx context.Context, id int64) error SetCurrentSession(ctx context.Context, arg SetCurrentSessionParams) error SetDefaultAccount(ctx context.Context, arg SetDefaultAccountParams) error SuspendGrant(ctx context.Context, id int64) error diff --git a/internal/keybase/query.sql.go b/internal/keybase/query.sql.go index 289f411..1232add 100644 --- a/internal/keybase/query.sql.go +++ b/internal/keybase/query.sql.go @@ -10,12 +10,12 @@ import ( "encoding/json" ) -const archiveKeyShare = `-- name: ArchiveKeyShare :exec -UPDATE key_shares SET status = 'archived' WHERE id = ? +const archiveEnclave = `-- name: ArchiveEnclave :exec +UPDATE mpc_enclaves SET status = 'archived' WHERE id = ? ` -func (q *Queries) ArchiveKeyShare(ctx context.Context, id int64) error { - _, err := q.db.ExecContext(ctx, archiveKeyShare, id) +func (q *Queries) ArchiveEnclave(ctx context.Context, id int64) error { + _, err := q.db.ExecContext(ctx, archiveEnclave, id) return err } @@ -60,14 +60,14 @@ func (q *Queries) CountCredentialsByDID(ctx context.Context, didID int64) (int64 } const createAccount = `-- name: CreateAccount :one -INSERT INTO accounts (did_id, key_share_id, address, chain_id, coin_type, account_index, address_index, label) +INSERT INTO accounts (did_id, enclave_id, address, chain_id, coin_type, account_index, address_index, label) VALUES (?, ?, ?, ?, ?, ?, ?, ?) -RETURNING id, did_id, key_share_id, address, chain_id, coin_type, account_index, address_index, label, is_default, created_at +RETURNING id, did_id, enclave_id, address, chain_id, coin_type, account_index, address_index, label, is_default, created_at ` type CreateAccountParams struct { DidID int64 `json:"did_id"` - KeyShareID int64 `json:"key_share_id"` + EnclaveID int64 `json:"enclave_id"` Address string `json:"address"` ChainID string `json:"chain_id"` CoinType int64 `json:"coin_type"` @@ -79,7 +79,7 @@ type CreateAccountParams struct { func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) (Account, error) { row := q.db.QueryRowContext(ctx, createAccount, arg.DidID, - arg.KeyShareID, + arg.EnclaveID, arg.Address, arg.ChainID, arg.CoinType, @@ -91,7 +91,7 @@ func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) (A err := row.Scan( &i.ID, &i.DidID, - &i.KeyShareID, + &i.EnclaveID, &i.Address, &i.ChainID, &i.CoinType, @@ -254,6 +254,54 @@ func (q *Queries) CreateDelegation(ctx context.Context, arg CreateDelegationPara return i, err } +const createEnclave = `-- name: CreateEnclave :one +INSERT INTO mpc_enclaves ( + did_id, enclave_id, public_key_hex, public_key, val_share, user_share, nonce, curve +) +VALUES (?, ?, ?, ?, ?, ?, ?, ?) +RETURNING id, did_id, enclave_id, public_key_hex, public_key, val_share, user_share, nonce, curve, status, created_at, rotated_at +` + +type CreateEnclaveParams struct { + DidID int64 `json:"did_id"` + EnclaveID string `json:"enclave_id"` + PublicKeyHex string `json:"public_key_hex"` + PublicKey []byte `json:"public_key"` + ValShare []byte `json:"val_share"` + UserShare []byte `json:"user_share"` + Nonce []byte `json:"nonce"` + Curve string `json:"curve"` +} + +func (q *Queries) CreateEnclave(ctx context.Context, arg CreateEnclaveParams) (MpcEnclafe, error) { + row := q.db.QueryRowContext(ctx, createEnclave, + arg.DidID, + arg.EnclaveID, + arg.PublicKeyHex, + arg.PublicKey, + arg.ValShare, + arg.UserShare, + arg.Nonce, + arg.Curve, + ) + var i MpcEnclafe + err := row.Scan( + &i.ID, + &i.DidID, + &i.EnclaveID, + &i.PublicKeyHex, + &i.PublicKey, + &i.ValShare, + &i.UserShare, + &i.Nonce, + &i.Curve, + &i.Status, + &i.CreatedAt, + &i.RotatedAt, + ) + return i, err +} + const createGrant = `-- name: CreateGrant :one INSERT INTO grants (did_id, service_id, delegation_cid, scopes, accounts, expires_at) VALUES (?, ?, ?, ?, ?, ?) @@ -348,64 +396,6 @@ func (q *Queries) CreateInvocation(ctx context.Context, arg CreateInvocationPara return i, err } -const createKeyShare = `-- name: CreateKeyShare :one -INSERT INTO key_shares ( - did_id, share_id, key_id, party_index, threshold, total_parties, - curve, share_data, public_key, chain_code, derivation_path -) -VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) -RETURNING id, did_id, share_id, key_id, party_index, threshold, total_parties, curve, share_data, public_key, chain_code, derivation_path, status, created_at, rotated_at -` - -type CreateKeyShareParams struct { - DidID int64 `json:"did_id"` - ShareID string `json:"share_id"` - KeyID string `json:"key_id"` - PartyIndex int64 `json:"party_index"` - Threshold int64 `json:"threshold"` - TotalParties int64 `json:"total_parties"` - Curve string `json:"curve"` - ShareData string `json:"share_data"` - PublicKey string `json:"public_key"` - ChainCode *string `json:"chain_code"` - DerivationPath *string `json:"derivation_path"` -} - -func (q *Queries) CreateKeyShare(ctx context.Context, arg CreateKeyShareParams) (KeyShare, error) { - row := q.db.QueryRowContext(ctx, createKeyShare, - arg.DidID, - arg.ShareID, - arg.KeyID, - arg.PartyIndex, - arg.Threshold, - arg.TotalParties, - arg.Curve, - arg.ShareData, - arg.PublicKey, - arg.ChainCode, - arg.DerivationPath, - ) - var i KeyShare - err := row.Scan( - &i.ID, - &i.DidID, - &i.ShareID, - &i.KeyID, - &i.PartyIndex, - &i.Threshold, - &i.TotalParties, - &i.Curve, - &i.ShareData, - &i.PublicKey, - &i.ChainCode, - &i.DerivationPath, - &i.Status, - &i.CreatedAt, - &i.RotatedAt, - ) - return i, err -} - const createRevocation = `-- name: CreateRevocation :exec INSERT INTO ucan_revocations (delegation_cid, revoked_by, invocation_cid, reason) @@ -592,6 +582,20 @@ func (q *Queries) DeleteDelegation(ctx context.Context, arg DeleteDelegationPara return err } +const deleteEnclave = `-- name: DeleteEnclave :exec +DELETE FROM mpc_enclaves WHERE id = ? AND did_id = ? +` + +type DeleteEnclaveParams struct { + ID int64 `json:"id"` + DidID int64 `json:"did_id"` +} + +func (q *Queries) DeleteEnclave(ctx context.Context, arg DeleteEnclaveParams) error { + _, err := q.db.ExecContext(ctx, deleteEnclave, arg.ID, arg.DidID) + return err +} + const deleteExpiredSessions = `-- name: DeleteExpiredSessions :exec DELETE FROM sessions WHERE expires_at < datetime('now') ` @@ -601,20 +605,6 @@ func (q *Queries) DeleteExpiredSessions(ctx context.Context) error { return err } -const deleteKeyShare = `-- name: DeleteKeyShare :exec -DELETE FROM key_shares WHERE id = ? AND did_id = ? -` - -type DeleteKeyShareParams struct { - ID int64 `json:"id"` - DidID int64 `json:"did_id"` -} - -func (q *Queries) DeleteKeyShare(ctx context.Context, arg DeleteKeyShareParams) error { - _, err := q.db.ExecContext(ctx, deleteKeyShare, arg.ID, arg.DidID) - return err -} - const deleteSession = `-- name: DeleteSession :exec DELETE FROM sessions WHERE id = ? ` @@ -634,7 +624,7 @@ func (q *Queries) DeleteVerificationMethod(ctx context.Context, id int64) error } const getAccountByAddress = `-- name: GetAccountByAddress :one -SELECT id, did_id, key_share_id, address, chain_id, coin_type, account_index, address_index, label, is_default, created_at FROM accounts WHERE address = ? LIMIT 1 +SELECT id, did_id, enclave_id, address, chain_id, coin_type, account_index, address_index, label, is_default, created_at FROM accounts WHERE address = ? LIMIT 1 ` func (q *Queries) GetAccountByAddress(ctx context.Context, address string) (Account, error) { @@ -643,7 +633,7 @@ func (q *Queries) GetAccountByAddress(ctx context.Context, address string) (Acco err := row.Scan( &i.ID, &i.DidID, - &i.KeyShareID, + &i.EnclaveID, &i.Address, &i.ChainID, &i.CoinType, @@ -749,7 +739,7 @@ func (q *Queries) GetDIDByID(ctx context.Context, id int64) (DidDocument, error) } const getDefaultAccount = `-- name: GetDefaultAccount :one -SELECT id, did_id, key_share_id, address, chain_id, coin_type, account_index, address_index, label, is_default, created_at FROM accounts WHERE did_id = ? AND chain_id = ? AND is_default = 1 LIMIT 1 +SELECT id, did_id, enclave_id, address, chain_id, coin_type, account_index, address_index, label, is_default, created_at FROM accounts WHERE did_id = ? AND chain_id = ? AND is_default = 1 LIMIT 1 ` type GetDefaultAccountParams struct { @@ -763,7 +753,7 @@ func (q *Queries) GetDefaultAccount(ctx context.Context, arg GetDefaultAccountPa err := row.Scan( &i.ID, &i.DidID, - &i.KeyShareID, + &i.EnclaveID, &i.Address, &i.ChainID, &i.CoinType, @@ -817,6 +807,54 @@ func (q *Queries) GetDelegationEnvelopeByCID(ctx context.Context, cid string) ([ return envelope, err } +const getEnclaveByID = `-- name: GetEnclaveByID :one +SELECT id, did_id, enclave_id, public_key_hex, public_key, val_share, user_share, nonce, curve, status, created_at, rotated_at FROM mpc_enclaves WHERE enclave_id = ? LIMIT 1 +` + +func (q *Queries) GetEnclaveByID(ctx context.Context, enclaveID string) (MpcEnclafe, error) { + row := q.db.QueryRowContext(ctx, getEnclaveByID, enclaveID) + var i MpcEnclafe + err := row.Scan( + &i.ID, + &i.DidID, + &i.EnclaveID, + &i.PublicKeyHex, + &i.PublicKey, + &i.ValShare, + &i.UserShare, + &i.Nonce, + &i.Curve, + &i.Status, + &i.CreatedAt, + &i.RotatedAt, + ) + return i, err +} + +const getEnclaveByPubKeyHex = `-- name: GetEnclaveByPubKeyHex :one +SELECT id, did_id, enclave_id, public_key_hex, public_key, val_share, user_share, nonce, curve, status, created_at, rotated_at FROM mpc_enclaves WHERE public_key_hex = ? LIMIT 1 +` + +func (q *Queries) GetEnclaveByPubKeyHex(ctx context.Context, publicKeyHex string) (MpcEnclafe, error) { + row := q.db.QueryRowContext(ctx, getEnclaveByPubKeyHex, publicKeyHex) + var i MpcEnclafe + err := row.Scan( + &i.ID, + &i.DidID, + &i.EnclaveID, + &i.PublicKeyHex, + &i.PublicKey, + &i.ValShare, + &i.UserShare, + &i.Nonce, + &i.Curve, + &i.Status, + &i.CreatedAt, + &i.RotatedAt, + ) + return i, err +} + const getGrantByService = `-- name: GetGrantByService :one SELECT id, did_id, service_id, delegation_cid, scopes, accounts, status, granted_at, last_used, expires_at FROM grants WHERE did_id = ? AND service_id = ? LIMIT 1 ` @@ -885,65 +923,6 @@ func (q *Queries) GetInvocationEnvelopeByCID(ctx context.Context, cid string) ([ return envelope, err } -const getKeyShareByID = `-- name: GetKeyShareByID :one -SELECT id, did_id, share_id, key_id, party_index, threshold, total_parties, curve, share_data, public_key, chain_code, derivation_path, status, created_at, rotated_at FROM key_shares WHERE share_id = ? LIMIT 1 -` - -func (q *Queries) GetKeyShareByID(ctx context.Context, shareID string) (KeyShare, error) { - row := q.db.QueryRowContext(ctx, getKeyShareByID, shareID) - var i KeyShare - err := row.Scan( - &i.ID, - &i.DidID, - &i.ShareID, - &i.KeyID, - &i.PartyIndex, - &i.Threshold, - &i.TotalParties, - &i.Curve, - &i.ShareData, - &i.PublicKey, - &i.ChainCode, - &i.DerivationPath, - &i.Status, - &i.CreatedAt, - &i.RotatedAt, - ) - return i, err -} - -const getKeyShareByKeyID = `-- name: GetKeyShareByKeyID :one -SELECT id, did_id, share_id, key_id, party_index, threshold, total_parties, curve, share_data, public_key, chain_code, derivation_path, status, created_at, rotated_at FROM key_shares WHERE did_id = ? AND key_id = ? AND status = 'active' LIMIT 1 -` - -type GetKeyShareByKeyIDParams struct { - DidID int64 `json:"did_id"` - KeyID string `json:"key_id"` -} - -func (q *Queries) GetKeyShareByKeyID(ctx context.Context, arg GetKeyShareByKeyIDParams) (KeyShare, error) { - row := q.db.QueryRowContext(ctx, getKeyShareByKeyID, arg.DidID, arg.KeyID) - var i KeyShare - err := row.Scan( - &i.ID, - &i.DidID, - &i.ShareID, - &i.KeyID, - &i.PartyIndex, - &i.Threshold, - &i.TotalParties, - &i.Curve, - &i.ShareData, - &i.PublicKey, - &i.ChainCode, - &i.DerivationPath, - &i.Status, - &i.CreatedAt, - &i.RotatedAt, - ) - return i, err -} - const getRevocation = `-- name: GetRevocation :one SELECT id, delegation_cid, revoked_by, invocation_cid, reason, revoked_at FROM ucan_revocations WHERE delegation_cid = ? LIMIT 1 ` @@ -1093,7 +1072,7 @@ func (q *Queries) IsDelegationRevoked(ctx context.Context, delegationCid string) } const listAccountsByChain = `-- name: ListAccountsByChain :many -SELECT id, did_id, key_share_id, address, chain_id, coin_type, account_index, address_index, label, is_default, created_at FROM accounts WHERE did_id = ? AND chain_id = ? ORDER BY account_index, address_index +SELECT id, did_id, enclave_id, address, chain_id, coin_type, account_index, address_index, label, is_default, created_at FROM accounts WHERE did_id = ? AND chain_id = ? ORDER BY account_index, address_index ` type ListAccountsByChainParams struct { @@ -1113,7 +1092,7 @@ func (q *Queries) ListAccountsByChain(ctx context.Context, arg ListAccountsByCha if err := rows.Scan( &i.ID, &i.DidID, - &i.KeyShareID, + &i.EnclaveID, &i.Address, &i.ChainID, &i.CoinType, @@ -1138,45 +1117,25 @@ func (q *Queries) ListAccountsByChain(ctx context.Context, arg ListAccountsByCha const listAccountsByDID = `-- name: ListAccountsByDID :many -SELECT a.id, a.did_id, a.key_share_id, a.address, a.chain_id, a.coin_type, a.account_index, a.address_index, a.label, a.is_default, a.created_at, k.public_key as share_public_key, k.curve -FROM accounts a -JOIN key_shares k ON a.key_share_id = k.id -WHERE a.did_id = ? -ORDER BY a.is_default DESC, a.created_at +SELECT id, did_id, enclave_id, address, chain_id, coin_type, account_index, address_index, label, is_default, created_at, public_key_hex, curve, enclave_ref FROM v_accounts WHERE did_id = ? ORDER BY is_default DESC, created_at ` -type ListAccountsByDIDRow struct { - ID int64 `json:"id"` - DidID int64 `json:"did_id"` - KeyShareID int64 `json:"key_share_id"` - Address string `json:"address"` - ChainID string `json:"chain_id"` - CoinType int64 `json:"coin_type"` - AccountIndex int64 `json:"account_index"` - AddressIndex int64 `json:"address_index"` - Label *string `json:"label"` - IsDefault int64 `json:"is_default"` - CreatedAt string `json:"created_at"` - SharePublicKey string `json:"share_public_key"` - Curve string `json:"curve"` -} - // ============================================================================= // ACCOUNT QUERIES // ============================================================================= -func (q *Queries) ListAccountsByDID(ctx context.Context, didID int64) ([]ListAccountsByDIDRow, error) { +func (q *Queries) ListAccountsByDID(ctx context.Context, didID int64) ([]VAccount, error) { rows, err := q.db.QueryContext(ctx, listAccountsByDID, didID) if err != nil { return nil, err } defer rows.Close() - items := []ListAccountsByDIDRow{} + items := []VAccount{} for rows.Next() { - var i ListAccountsByDIDRow + var i VAccount if err := rows.Scan( &i.ID, &i.DidID, - &i.KeyShareID, + &i.EnclaveID, &i.Address, &i.ChainID, &i.CoinType, @@ -1185,8 +1144,9 @@ func (q *Queries) ListAccountsByDID(ctx context.Context, didID int64) ([]ListAcc &i.Label, &i.IsDefault, &i.CreatedAt, - &i.SharePublicKey, + &i.PublicKeyHex, &i.Curve, + &i.EnclaveRef, ); err != nil { return nil, err } @@ -1512,43 +1472,67 @@ func (q *Queries) ListDelegationsForCommand(ctx context.Context, arg ListDelegat return items, nil } -const listGrantsByDID = `-- name: ListGrantsByDID :many +const listEnclavesByDID = `-- name: ListEnclavesByDID :many -SELECT g.id, g.did_id, g.service_id, g.delegation_cid, g.scopes, g.accounts, g.status, g.granted_at, g.last_used, g.expires_at, s.name as service_name, s.origin as service_origin, s.logo_url as service_logo -FROM grants g -JOIN services s ON g.service_id = s.id -WHERE g.did_id = ? AND g.status = 'active' -ORDER BY g.last_used DESC NULLS LAST +SELECT id, did_id, enclave_id, public_key_hex, public_key, val_share, user_share, nonce, curve, status, created_at, rotated_at FROM mpc_enclaves WHERE did_id = ? AND status = 'active' ORDER BY created_at ` -type ListGrantsByDIDRow struct { - ID int64 `json:"id"` - DidID int64 `json:"did_id"` - ServiceID int64 `json:"service_id"` - DelegationCid *string `json:"delegation_cid"` - Scopes json.RawMessage `json:"scopes"` - Accounts json.RawMessage `json:"accounts"` - Status string `json:"status"` - GrantedAt string `json:"granted_at"` - LastUsed *string `json:"last_used"` - ExpiresAt *string `json:"expires_at"` - ServiceName string `json:"service_name"` - ServiceOrigin string `json:"service_origin"` - ServiceLogo *string `json:"service_logo"` +// ============================================================================= +// MPC ENCLAVE QUERIES +// ============================================================================= +func (q *Queries) ListEnclavesByDID(ctx context.Context, didID int64) ([]MpcEnclafe, error) { + rows, err := q.db.QueryContext(ctx, listEnclavesByDID, didID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []MpcEnclafe{} + for rows.Next() { + var i MpcEnclafe + if err := rows.Scan( + &i.ID, + &i.DidID, + &i.EnclaveID, + &i.PublicKeyHex, + &i.PublicKey, + &i.ValShare, + &i.UserShare, + &i.Nonce, + &i.Curve, + &i.Status, + &i.CreatedAt, + &i.RotatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil } +const listGrantsByDID = `-- name: ListGrantsByDID :many + +SELECT id, did_id, service_id, delegation_cid, scopes, accounts, status, granted_at, last_used, expires_at, service_name, service_origin, service_logo FROM v_grants WHERE did_id = ? AND status = 'active' ORDER BY last_used DESC NULLS LAST +` + // ============================================================================= // GRANT QUERIES // ============================================================================= -func (q *Queries) ListGrantsByDID(ctx context.Context, didID int64) ([]ListGrantsByDIDRow, error) { +func (q *Queries) ListGrantsByDID(ctx context.Context, didID int64) ([]VGrant, error) { rows, err := q.db.QueryContext(ctx, listGrantsByDID, didID) if err != nil { return nil, err } defer rows.Close() - items := []ListGrantsByDIDRow{} + items := []VGrant{} for rows.Next() { - var i ListGrantsByDIDRow + var i VGrant if err := rows.Scan( &i.ID, &i.DidID, @@ -1778,53 +1762,6 @@ func (q *Queries) ListInvocationsForCommand(ctx context.Context, arg ListInvocat return items, nil } -const listKeySharesByDID = `-- name: ListKeySharesByDID :many - -SELECT id, did_id, share_id, key_id, party_index, threshold, total_parties, curve, share_data, public_key, chain_code, derivation_path, status, created_at, rotated_at FROM key_shares WHERE did_id = ? AND status = 'active' ORDER BY created_at -` - -// ============================================================================= -// KEY SHARE QUERIES -// ============================================================================= -func (q *Queries) ListKeySharesByDID(ctx context.Context, didID int64) ([]KeyShare, error) { - rows, err := q.db.QueryContext(ctx, listKeySharesByDID, didID) - if err != nil { - return nil, err - } - defer rows.Close() - items := []KeyShare{} - for rows.Next() { - var i KeyShare - if err := rows.Scan( - &i.ID, - &i.DidID, - &i.ShareID, - &i.KeyID, - &i.PartyIndex, - &i.Threshold, - &i.TotalParties, - &i.Curve, - &i.ShareData, - &i.PublicKey, - &i.ChainCode, - &i.DerivationPath, - &i.Status, - &i.CreatedAt, - &i.RotatedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - const listPendingInvocations = `-- name: ListPendingInvocations :many SELECT id, did_id, cid, envelope, iss, sub, aud, cmd, prf, exp, iat, executed_at, result_cid, created_at FROM ucan_invocations WHERE did_id = ? AND executed_at IS NULL AND (exp IS NULL OR exp > datetime('now')) @@ -1995,39 +1932,21 @@ func (q *Queries) ListRootDelegations(ctx context.Context, didID int64) ([]UcanD const listSessionsByDID = `-- name: ListSessionsByDID :many -SELECT s.id, s.did_id, s.credential_id, s.session_id, s.device_info, s.is_current, s.last_activity, s.expires_at, s.created_at, c.device_name, c.authenticator -FROM sessions s -JOIN credentials c ON s.credential_id = c.id -WHERE s.did_id = ? AND s.expires_at > datetime('now') -ORDER BY s.last_activity DESC +SELECT id, did_id, credential_id, session_id, device_info, is_current, last_activity, expires_at, created_at, device_name, authenticator FROM v_sessions WHERE did_id = ? ORDER BY last_activity DESC ` -type ListSessionsByDIDRow struct { - ID int64 `json:"id"` - DidID int64 `json:"did_id"` - CredentialID int64 `json:"credential_id"` - SessionID string `json:"session_id"` - DeviceInfo json.RawMessage `json:"device_info"` - IsCurrent int64 `json:"is_current"` - LastActivity string `json:"last_activity"` - ExpiresAt string `json:"expires_at"` - CreatedAt string `json:"created_at"` - DeviceName string `json:"device_name"` - Authenticator *string `json:"authenticator"` -} - // ============================================================================= // SESSION QUERIES // ============================================================================= -func (q *Queries) ListSessionsByDID(ctx context.Context, didID int64) ([]ListSessionsByDIDRow, error) { +func (q *Queries) ListSessionsByDID(ctx context.Context, didID int64) ([]VSession, error) { rows, err := q.db.QueryContext(ctx, listSessionsByDID, didID) if err != nil { return nil, err } defer rows.Close() - items := []ListSessionsByDIDRow{} + items := []VSession{} for rows.Next() { - var i ListSessionsByDIDRow + var i VSession if err := rows.Scan( &i.ID, &i.DidID, @@ -2213,14 +2132,14 @@ func (q *Queries) RevokeGrant(ctx context.Context, id int64) error { return err } -const rotateKeyShare = `-- name: RotateKeyShare :exec -UPDATE key_shares +const rotateEnclave = `-- name: RotateEnclave :exec +UPDATE mpc_enclaves SET status = 'rotating', rotated_at = datetime('now') WHERE id = ? ` -func (q *Queries) RotateKeyShare(ctx context.Context, id int64) error { - _, err := q.db.ExecContext(ctx, rotateKeyShare, id) +func (q *Queries) RotateEnclave(ctx context.Context, id int64) error { + _, err := q.db.ExecContext(ctx, rotateEnclave, id) return err } diff --git a/internal/migrations/query.sql b/internal/migrations/query.sql index 26abf31..ebfdffe 100644 --- a/internal/migrations/query.sql +++ b/internal/migrations/query.sql @@ -72,47 +72,42 @@ DELETE FROM credentials WHERE id = ? AND did_id = ?; SELECT COUNT(*) FROM credentials WHERE did_id = ?; -- ============================================================================= --- KEY SHARE QUERIES +-- MPC ENCLAVE QUERIES -- ============================================================================= --- name: ListKeySharesByDID :many -SELECT * FROM key_shares WHERE did_id = ? AND status = 'active' ORDER BY created_at; +-- name: ListEnclavesByDID :many +SELECT * FROM mpc_enclaves WHERE did_id = ? AND status = 'active' ORDER BY created_at; --- name: GetKeyShareByID :one -SELECT * FROM key_shares WHERE share_id = ? LIMIT 1; +-- name: GetEnclaveByID :one +SELECT * FROM mpc_enclaves WHERE enclave_id = ? LIMIT 1; --- name: GetKeyShareByKeyID :one -SELECT * FROM key_shares WHERE did_id = ? AND key_id = ? AND status = 'active' LIMIT 1; +-- name: GetEnclaveByPubKeyHex :one +SELECT * FROM mpc_enclaves WHERE public_key_hex = ? LIMIT 1; --- name: CreateKeyShare :one -INSERT INTO key_shares ( - did_id, share_id, key_id, party_index, threshold, total_parties, - curve, share_data, public_key, chain_code, derivation_path +-- name: CreateEnclave :one +INSERT INTO mpc_enclaves ( + did_id, enclave_id, public_key_hex, public_key, val_share, user_share, nonce, curve ) -VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING *; --- name: RotateKeyShare :exec -UPDATE key_shares +-- name: RotateEnclave :exec +UPDATE mpc_enclaves SET status = 'rotating', rotated_at = datetime('now') WHERE id = ?; --- name: ArchiveKeyShare :exec -UPDATE key_shares SET status = 'archived' WHERE id = ?; +-- name: ArchiveEnclave :exec +UPDATE mpc_enclaves SET status = 'archived' WHERE id = ?; --- name: DeleteKeyShare :exec -DELETE FROM key_shares WHERE id = ? AND did_id = ?; +-- name: DeleteEnclave :exec +DELETE FROM mpc_enclaves WHERE id = ? AND did_id = ?; -- ============================================================================= -- ACCOUNT QUERIES -- ============================================================================= -- name: ListAccountsByDID :many -SELECT a.*, k.public_key as share_public_key, k.curve -FROM accounts a -JOIN key_shares k ON a.key_share_id = k.id -WHERE a.did_id = ? -ORDER BY a.is_default DESC, a.created_at; +SELECT * FROM v_accounts WHERE did_id = ? ORDER BY is_default DESC, created_at; -- name: ListAccountsByChain :many SELECT * FROM accounts WHERE did_id = ? AND chain_id = ? ORDER BY account_index, address_index; @@ -124,7 +119,7 @@ SELECT * FROM accounts WHERE address = ? LIMIT 1; SELECT * FROM accounts WHERE did_id = ? AND chain_id = ? AND is_default = 1 LIMIT 1; -- name: CreateAccount :one -INSERT INTO accounts (did_id, key_share_id, address, chain_id, coin_type, account_index, address_index, label) +INSERT INTO accounts (did_id, enclave_id, address, chain_id, coin_type, account_index, address_index, label) VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING *; @@ -277,11 +272,7 @@ ORDER BY revoked_at DESC; -- ============================================================================= -- name: ListSessionsByDID :many -SELECT s.*, c.device_name, c.authenticator -FROM sessions s -JOIN credentials c ON s.credential_id = c.id -WHERE s.did_id = ? AND s.expires_at > datetime('now') -ORDER BY s.last_activity DESC; +SELECT * FROM v_sessions WHERE did_id = ? ORDER BY last_activity DESC; -- name: GetSessionByID :one SELECT * FROM sessions WHERE session_id = ? LIMIT 1; @@ -336,11 +327,7 @@ SELECT * FROM services WHERE is_verified = 1 ORDER BY name; -- ============================================================================= -- name: ListGrantsByDID :many -SELECT g.*, s.name as service_name, s.origin as service_origin, s.logo_url as service_logo -FROM grants g -JOIN services s ON g.service_id = s.id -WHERE g.did_id = ? AND g.status = 'active' -ORDER BY g.last_used DESC NULLS LAST; +SELECT * FROM v_grants WHERE did_id = ? AND status = 'active' ORDER BY last_used DESC NULLS LAST; -- name: GetGrantByService :one SELECT * FROM grants WHERE did_id = ? AND service_id = ? LIMIT 1; diff --git a/internal/migrations/schema.sql b/internal/migrations/schema.sql index 317e255..3166af5 100644 --- a/internal/migrations/schema.sql +++ b/internal/migrations/schema.sql @@ -66,46 +66,41 @@ CREATE INDEX idx_credentials_did_id ON credentials(did_id); CREATE INDEX idx_credentials_credential_id ON credentials(credential_id); -- ============================================================================= --- MPC KEY SHARES +-- MPC ENCLAVES -- ============================================================================= --- Key Shares: MPC/TSS key share storage -CREATE TABLE IF NOT EXISTS key_shares ( +CREATE TABLE IF NOT EXISTS mpc_enclaves ( id INTEGER PRIMARY KEY, did_id INTEGER NOT NULL REFERENCES did_documents(id) ON DELETE CASCADE, - share_id TEXT NOT NULL UNIQUE, -- Unique identifier for this share - key_id TEXT NOT NULL, -- Identifier for the full key (shared across parties) - party_index INTEGER NOT NULL, -- This party's index (1, 2, 3...) - threshold INTEGER NOT NULL, -- Minimum shares needed to sign - total_parties INTEGER NOT NULL, -- Total number of parties - curve TEXT NOT NULL DEFAULT 'secp256k1', -- secp256k1, ed25519 - share_data TEXT NOT NULL, -- Encrypted key share (base64) - public_key TEXT NOT NULL, -- Full public key (base64) - chain_code TEXT, -- BIP32 chain code for derivation - derivation_path TEXT, -- BIP44 path: m/44'/60'/0'/0 + enclave_id TEXT NOT NULL UNIQUE, + public_key_hex TEXT NOT NULL, + public_key BLOB NOT NULL, + val_share BLOB NOT NULL, + user_share BLOB NOT NULL, + nonce BLOB NOT NULL, + curve TEXT NOT NULL DEFAULT 'secp256k1', status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'rotating', 'archived')), created_at TEXT NOT NULL DEFAULT (datetime('now')), rotated_at TEXT, - UNIQUE(did_id, key_id, party_index) + UNIQUE(did_id, enclave_id) ); -CREATE INDEX idx_key_shares_did_id ON key_shares(did_id); -CREATE INDEX idx_key_shares_key_id ON key_shares(key_id); +CREATE INDEX idx_mpc_enclaves_did_id ON mpc_enclaves(did_id); +CREATE INDEX idx_mpc_enclaves_public_key_hex ON mpc_enclaves(public_key_hex); --- Derived Accounts: Wallet accounts derived from key shares CREATE TABLE IF NOT EXISTS accounts ( id INTEGER PRIMARY KEY, did_id INTEGER NOT NULL REFERENCES did_documents(id) ON DELETE CASCADE, - key_share_id INTEGER NOT NULL REFERENCES key_shares(id) ON DELETE CASCADE, - address TEXT NOT NULL, -- Derived address - chain_id TEXT NOT NULL, -- sonr-mainnet-1, ethereum, etc. - coin_type INTEGER NOT NULL, -- BIP44 coin type (118=cosmos, 60=eth) - account_index INTEGER NOT NULL DEFAULT 0, -- BIP44 account index - address_index INTEGER NOT NULL DEFAULT 0, -- BIP44 address index - label TEXT DEFAULT '', -- User-assigned label + enclave_id INTEGER NOT NULL REFERENCES mpc_enclaves(id) ON DELETE CASCADE, + address TEXT NOT NULL, + chain_id TEXT NOT NULL, + coin_type INTEGER NOT NULL, + account_index INTEGER NOT NULL DEFAULT 0, + address_index INTEGER NOT NULL DEFAULT 0, + label TEXT DEFAULT '', is_default INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')), - UNIQUE(key_share_id, chain_id, account_index, address_index) + UNIQUE(enclave_id, chain_id, account_index, address_index) ); CREATE INDEX idx_accounts_did_id ON accounts(did_id); @@ -291,3 +286,40 @@ CREATE TRIGGER IF NOT EXISTS did_documents_updated_at BEGIN UPDATE did_documents SET updated_at = datetime('now') WHERE id = NEW.id; END; + +-- ============================================================================= +-- VIEWS (pre-computed JOINs for common queries) +-- ============================================================================= + +CREATE VIEW IF NOT EXISTS v_accounts AS +SELECT + a.id, a.did_id, a.enclave_id, a.address, a.chain_id, + a.coin_type, a.account_index, a.address_index, + a.label, a.is_default, a.created_at, + e.public_key_hex, e.curve, e.enclave_id as enclave_ref +FROM accounts a +JOIN mpc_enclaves e ON a.enclave_id = e.id; + +CREATE VIEW IF NOT EXISTS v_sessions AS +SELECT + s.id, s.did_id, s.credential_id, s.session_id, s.device_info, + s.is_current, s.last_activity, s.expires_at, s.created_at, + c.device_name, c.authenticator +FROM sessions s +JOIN credentials c ON s.credential_id = c.id +WHERE s.expires_at > datetime('now'); + +CREATE VIEW IF NOT EXISTS v_grants AS +SELECT + g.id, g.did_id, g.service_id, g.delegation_cid, g.scopes, + g.accounts, g.status, g.granted_at, g.last_used, g.expires_at, + s.name as service_name, s.origin as service_origin, s.logo_url as service_logo +FROM grants g +JOIN services s ON g.service_id = s.id; + +CREATE VIEW IF NOT EXISTS v_active_delegations AS +SELECT * FROM ucan_delegations +WHERE exp IS NULL OR exp > datetime('now'); + +CREATE VIEW IF NOT EXISTS v_active_enclaves AS +SELECT * FROM mpc_enclaves WHERE status = 'active'; diff --git a/internal/types/generate.go b/internal/types/generate.go index 2803f36..3f897e1 100644 --- a/internal/types/generate.go +++ b/internal/types/generate.go @@ -1,39 +1,17 @@ package types -// GenerateInput represents the input for the generate function type GenerateInput struct { - Credential string `json:"credential"` // Base64-encoded WebAuthn credential - - // MPC keyshare data (optional - if provided, creates initial keyshare and account) - KeyShare *KeyShareInput `json:"key_share,omitempty"` + Credential string `json:"credential"` } -// KeyShareInput represents MPC keyshare data for initialization -type KeyShareInput struct { - KeyID string `json:"key_id"` - PartyIndex int64 `json:"party_index"` - Threshold int64 `json:"threshold"` - TotalParties int64 `json:"total_parties"` - Curve string `json:"curve"` - ShareData string `json:"share_data"` - PublicKey string `json:"public_key"` - ChainCode string `json:"chain_code,omitempty"` - DerivationPath string `json:"derivation_path,omitempty"` -} - -// GenerateOutput represents the output of the generate function type GenerateOutput struct { - DID string `json:"did"` - Database []byte `json:"database"` - - // KeyShare info if a keyshare was provided - KeyShareID string `json:"key_share_id,omitempty"` - - // Account info if an account was created - Account *AccountInfo `json:"account,omitempty"` + DID string `json:"did"` + Database []byte `json:"database"` + EnclaveID string `json:"enclave_id"` + PublicKey string `json:"public_key"` + Accounts []AccountInfo `json:"accounts"` } -// AccountInfo represents created account information type AccountInfo struct { Address string `json:"address"` ChainID string `json:"chain_id"` -- 2.43.0 From 6f2a3c580033b9f1181094bfd638f0a171fff69f Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Sat, 10 Jan 2026 14:32:22 -0500 Subject: [PATCH 25/35] feat(keybase): add enclave management API --- internal/crypto/bip44/bip44.go | 278 ++++++++++++++++++++++ internal/keybase/actions_enclave.go | 144 ++++++++++++ internal/keybase/exec.go | 351 ++++++++++++++++++++++++++++ internal/keybase/functions.go | 206 ++++++++++++++++ internal/keybase/store.go | 211 +++++++++++++++++ 5 files changed, 1190 insertions(+) create mode 100644 internal/crypto/bip44/bip44.go create mode 100644 internal/keybase/actions_enclave.go create mode 100644 internal/keybase/exec.go create mode 100644 internal/keybase/functions.go create mode 100644 internal/keybase/store.go diff --git a/internal/crypto/bip44/bip44.go b/internal/crypto/bip44/bip44.go new file mode 100644 index 0000000..bf48e6b --- /dev/null +++ b/internal/crypto/bip44/bip44.go @@ -0,0 +1,278 @@ +// Package bip44 provides BIP44 address derivation using MPC public keys. +package bip44 + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + + "golang.org/x/crypto/ripemd160" + "golang.org/x/crypto/sha3" +) + +type CoinType uint32 + +const ( + CoinTypeBitcoin CoinType = 0 + CoinTypeEthereum CoinType = 60 + CoinTypeCosmos CoinType = 118 + CoinTypeSonr CoinType = 703 +) + +type ChainConfig struct { + CoinType CoinType + Prefix string + ChainID string +} + +var DefaultChains = map[string]ChainConfig{ + "bitcoin": { + CoinType: CoinTypeBitcoin, + Prefix: "bc1", + ChainID: "bitcoin-mainnet", + }, + "ethereum": { + CoinType: CoinTypeEthereum, + Prefix: "0x", + ChainID: "1", + }, + "sonr": { + CoinType: CoinTypeSonr, + Prefix: "snr", + ChainID: "sonr-testnet-1", + }, + "cosmos": { + CoinType: CoinTypeCosmos, + Prefix: "cosmos", + ChainID: "cosmoshub-4", + }, +} + +func DeriveAddress(pubKeyBytes []byte, chain string) (string, error) { + config, ok := DefaultChains[chain] + if !ok { + return "", fmt.Errorf("bip44: unknown chain: %s", chain) + } + return DeriveAddressWithConfig(pubKeyBytes, config) +} + +func DeriveAddressWithConfig(pubKeyBytes []byte, config ChainConfig) (string, error) { + if len(pubKeyBytes) == 0 { + return "", fmt.Errorf("bip44: public key cannot be empty") + } + + switch config.CoinType { + case CoinTypeBitcoin: + return deriveBitcoinAddress(pubKeyBytes, config.Prefix) + case CoinTypeEthereum: + return deriveEthereumAddress(pubKeyBytes) + case CoinTypeCosmos, CoinTypeSonr: + return deriveCosmosAddress(pubKeyBytes, config.Prefix) + default: + return "", fmt.Errorf("bip44: unsupported coin type: %d", config.CoinType) + } +} + +// deriveBitcoinAddress creates P2WPKH (native SegWit) address using SHA256+RIPEMD160 +func deriveBitcoinAddress(pubKeyBytes []byte, prefix string) (string, error) { + compressed := ensureCompressed(pubKeyBytes) + if compressed == nil { + return "", fmt.Errorf("bip44: invalid public key length: %d", len(pubKeyBytes)) + } + + sha256Hash := sha256.Sum256(compressed) + ripemd := ripemd160.New() + ripemd.Write(sha256Hash[:]) + pubKeyHash := ripemd.Sum(nil) + + return bech32Encode(prefix, pubKeyHash) +} + +// deriveEthereumAddress creates address using Keccak256 of uncompressed pubkey +func deriveEthereumAddress(pubKeyBytes []byte) (string, error) { + var keyData []byte + + switch len(pubKeyBytes) { + case 65: + keyData = pubKeyBytes[1:] // strip 0x04 prefix + case 64: + keyData = pubKeyBytes + default: + return "", fmt.Errorf("bip44: ethereum requires uncompressed key (64 or 65 bytes), got %d", len(pubKeyBytes)) + } + + hash := sha3.NewLegacyKeccak256() + hash.Write(keyData) + hashBytes := hash.Sum(nil) + + return "0x" + hex.EncodeToString(hashBytes[12:]), nil +} + +// deriveCosmosAddress creates bech32 address using SHA256+RIPEMD160 +func deriveCosmosAddress(pubKeyBytes []byte, prefix string) (string, error) { + compressed := ensureCompressed(pubKeyBytes) + if compressed == nil { + return "", fmt.Errorf("bip44: invalid public key length: %d", len(pubKeyBytes)) + } + + sha256Hash := sha256.Sum256(compressed) + ripemd := ripemd160.New() + ripemd.Write(sha256Hash[:]) + addressBytes := ripemd.Sum(nil) + + return bech32Encode(prefix, addressBytes) +} + +func ensureCompressed(pubKeyBytes []byte) []byte { + switch len(pubKeyBytes) { + case 33: + return pubKeyBytes + case 65: + return compressPublicKey(pubKeyBytes) + default: + return nil + } +} + +func compressPublicKey(uncompressed []byte) []byte { + if len(uncompressed) != 65 { + return uncompressed + } + + x := uncompressed[1:33] + y := uncompressed[33:65] + + compressed := make([]byte, 33) + if y[31]&1 == 0 { + compressed[0] = 0x02 + } else { + compressed[0] = 0x03 + } + copy(compressed[1:], x) + + return compressed +} + +func bech32Encode(hrp string, data []byte) (string, error) { + converted, err := convertBits(data, 8, 5, true) + if err != nil { + return "", fmt.Errorf("bip44: convert bits: %w", err) + } + return encode(hrp, converted) +} + +func convertBits(data []byte, fromBits, toBits uint8, pad bool) ([]byte, error) { + acc := uint32(0) + bits := uint8(0) + maxv := uint32(1<= toBits { + bits -= toBits + result = append(result, byte((acc>>bits)&maxv)) + } + } + + if pad { + if bits > 0 { + result = append(result, byte((acc<<(toBits-bits))&maxv)) + } + } else if bits >= fromBits { + return nil, fmt.Errorf("illegal zero padding") + } else if ((acc << (toBits - bits)) & maxv) != 0 { + return nil, fmt.Errorf("non-zero padding") + } + + return result, nil +} + +const charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" + +func encode(hrp string, data []byte) (string, error) { + checksum := createChecksum(hrp, data) + combined := append(data, checksum...) + + result := make([]byte, len(hrp)+1+len(combined)) + copy(result, hrp) + result[len(hrp)] = '1' + + for i, b := range combined { + result[len(hrp)+1+i] = charset[b] + } + + return string(result), nil +} + +func createChecksum(hrp string, data []byte) []byte { + values := append(hrpExpand(hrp), data...) + values = append(values, []byte{0, 0, 0, 0, 0, 0}...) + polymod := polymod(values) ^ 1 + + checksum := make([]byte, 6) + for i := 0; i < 6; i++ { + checksum[i] = byte((polymod >> (5 * (5 - i))) & 31) + } + return checksum +} + +func hrpExpand(hrp string) []byte { + result := make([]byte, len(hrp)*2+1) + for i, c := range hrp { + result[i] = byte(c >> 5) + result[i+len(hrp)+1] = byte(c & 31) + } + result[len(hrp)] = 0 + return result +} + +func polymod(values []byte) uint32 { + gen := []uint32{0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3} + chk := uint32(1) + for _, v := range values { + top := chk >> 25 + chk = (chk&0x1ffffff)<<5 ^ uint32(v) + for i := 0; i < 5; i++ { + if (top>>i)&1 == 1 { + chk ^= gen[i] + } + } + } + return chk +} + +type Account struct { + Address string + ChainID string + CoinType CoinType + AccountIndex uint32 + AddressIndex uint32 +} + +func DeriveAccounts(pubKeyBytes []byte, chains []string) ([]Account, error) { + accounts := make([]Account, 0, len(chains)) + + for _, chain := range chains { + config, ok := DefaultChains[chain] + if !ok { + continue + } + + addr, err := DeriveAddressWithConfig(pubKeyBytes, config) + if err != nil { + continue + } + + accounts = append(accounts, Account{ + Address: addr, + ChainID: config.ChainID, + CoinType: config.CoinType, + AccountIndex: 0, + AddressIndex: 0, + }) + } + + return accounts, nil +} diff --git a/internal/keybase/actions_enclave.go b/internal/keybase/actions_enclave.go new file mode 100644 index 0000000..088481c --- /dev/null +++ b/internal/keybase/actions_enclave.go @@ -0,0 +1,144 @@ +package keybase + +import ( + "context" + "fmt" +) + +type EnclaveResult struct { + ID int64 `json:"id"` + EnclaveID string `json:"enclave_id"` + PublicKeyHex string `json:"public_key_hex"` + Curve string `json:"curve"` + Status string `json:"status"` + CreatedAt string `json:"created_at"` + RotatedAt string `json:"rotated_at,omitempty"` +} + +type NewEnclaveInput struct { + EnclaveID string `json:"enclave_id"` + PublicKeyHex string `json:"public_key_hex"` + PublicKey []byte `json:"public_key"` + ValShare []byte `json:"val_share"` + UserShare []byte `json:"user_share"` + Nonce []byte `json:"nonce"` + Curve string `json:"curve"` +} + +func (am *ActionManager) CreateEnclave(ctx context.Context, params NewEnclaveInput) (*EnclaveResult, error) { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + if am.kb.didID == 0 { + return nil, fmt.Errorf("DID not initialized") + } + + enc, err := am.kb.queries.CreateEnclave(ctx, CreateEnclaveParams{ + DidID: am.kb.didID, + EnclaveID: params.EnclaveID, + PublicKeyHex: params.PublicKeyHex, + PublicKey: params.PublicKey, + ValShare: params.ValShare, + UserShare: params.UserShare, + Nonce: params.Nonce, + Curve: params.Curve, + }) + if err != nil { + return nil, fmt.Errorf("create enclave: %w", err) + } + + return enclaveToResult(&enc), nil +} + +func (am *ActionManager) ListEnclaves(ctx context.Context) ([]EnclaveResult, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + if am.kb.didID == 0 { + return []EnclaveResult{}, nil + } + + enclaves, err := am.kb.queries.ListEnclavesByDID(ctx, am.kb.didID) + if err != nil { + return nil, fmt.Errorf("list enclaves: %w", err) + } + + results := make([]EnclaveResult, len(enclaves)) + for i, enc := range enclaves { + results[i] = *enclaveToResult(&enc) + } + + return results, nil +} + +func (am *ActionManager) GetEnclaveByID(ctx context.Context, enclaveID string) (*EnclaveResult, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + enc, err := am.kb.queries.GetEnclaveByID(ctx, enclaveID) + if err != nil { + return nil, fmt.Errorf("get enclave: %w", err) + } + + return enclaveToResult(&enc), nil +} + +func (am *ActionManager) RotateEnclave(ctx context.Context, enclaveID string) error { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + enc, err := am.kb.queries.GetEnclaveByID(ctx, enclaveID) + if err != nil { + return fmt.Errorf("get enclave: %w", err) + } + + return am.kb.queries.RotateEnclave(ctx, enc.ID) +} + +func (am *ActionManager) ArchiveEnclave(ctx context.Context, enclaveID string) error { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + enc, err := am.kb.queries.GetEnclaveByID(ctx, enclaveID) + if err != nil { + return fmt.Errorf("get enclave: %w", err) + } + + return am.kb.queries.ArchiveEnclave(ctx, enc.ID) +} + +func (am *ActionManager) DeleteEnclave(ctx context.Context, enclaveID string) error { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + if am.kb.didID == 0 { + return fmt.Errorf("DID not initialized") + } + + enc, err := am.kb.queries.GetEnclaveByID(ctx, enclaveID) + if err != nil { + return fmt.Errorf("get enclave: %w", err) + } + + return am.kb.queries.DeleteEnclave(ctx, DeleteEnclaveParams{ + ID: enc.ID, + DidID: am.kb.didID, + }) +} + +func enclaveToResult(enc *MpcEnclafe) *EnclaveResult { + rotatedAt := "" + if enc.RotatedAt != nil { + rotatedAt = *enc.RotatedAt + } + + return &EnclaveResult{ + ID: enc.ID, + EnclaveID: enc.EnclaveID, + PublicKeyHex: enc.PublicKeyHex, + Curve: enc.Curve, + Status: enc.Status, + CreatedAt: enc.CreatedAt, + RotatedAt: rotatedAt, + } +} diff --git a/internal/keybase/exec.go b/internal/keybase/exec.go new file mode 100644 index 0000000..f30fd8b --- /dev/null +++ b/internal/keybase/exec.go @@ -0,0 +1,351 @@ +package keybase + +import ( + "context" + "encoding/json" + "errors" + "fmt" +) + +type HandlerFunc func(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) + +type resourceHandlers map[string]HandlerFunc + +var handlers = map[string]resourceHandlers{ + "accounts": { + "list": handleAccountList, + "get": handleAccountGet, + }, + "credentials": { + "list": handleCredentialList, + "get": handleCredentialGet, + }, + "sessions": { + "list": handleSessionList, + "revoke": handleSessionRevoke, + }, + "grants": { + "list": handleGrantList, + "revoke": handleGrantRevoke, + }, + "enclaves": { + "list": handleEnclaveList, + "get": handleEnclaveGet, + "rotate": handleEnclaveRotate, + "archive": handleEnclaveArchive, + "delete": handleEnclaveDelete, + }, + "delegations": { + "list": handleDelegationList, + "list_received": handleDelegationListReceived, + "list_command": handleDelegationListCommand, + "get": handleDelegationGet, + "revoke": handleDelegationRevoke, + "verify": handleDelegationVerify, + "cleanup": handleDelegationCleanup, + }, + "ucans": { + "list": handleDelegationList, + "get": handleDelegationGet, + "revoke": handleDelegationRevoke, + "verify": handleDelegationVerify, + "cleanup": handleDelegationCleanup, + }, + "verification_methods": { + "list": handleVerificationMethodList, + "get": handleVerificationMethodGet, + "delete": handleVerificationMethodDelete, + }, + "services": { + "list": handleServiceList, + "get": handleServiceGet, + "get_by_id": handleServiceGetByID, + }, +} + +func Exec(ctx context.Context, resource, action, subject string) (json.RawMessage, error) { + am, err := NewActionManager() + if err != nil { + return nil, fmt.Errorf("action manager: %w", err) + } + + resHandlers, ok := handlers[resource] + if !ok { + return nil, fmt.Errorf("unknown resource: %s", resource) + } + + handler, ok := resHandlers[action] + if !ok { + return nil, fmt.Errorf("unknown action for %s: %s", resource, action) + } + + return handler(ctx, am, subject) +} + +func handleAccountList(ctx context.Context, am *ActionManager, _ string) (json.RawMessage, error) { + accounts, err := am.ListAccounts(ctx) + if err != nil { + return nil, err + } + return json.Marshal(accounts) +} + +func handleAccountGet(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) { + if subject == "" { + return nil, errors.New("subject (address) required for get action") + } + account, err := am.GetAccountByAddress(ctx, subject) + if err != nil { + return nil, err + } + return json.Marshal(account) +} + +func handleCredentialList(ctx context.Context, am *ActionManager, _ string) (json.RawMessage, error) { + credentials, err := am.ListCredentials(ctx) + if err != nil { + return nil, err + } + return json.Marshal(credentials) +} + +func handleCredentialGet(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) { + if subject == "" { + return nil, errors.New("subject (credential_id) required for get action") + } + credential, err := am.GetCredentialByID(ctx, subject) + if err != nil { + return nil, err + } + return json.Marshal(credential) +} + +func handleSessionList(ctx context.Context, am *ActionManager, _ string) (json.RawMessage, error) { + sessions, err := am.ListSessions(ctx) + if err != nil { + return nil, err + } + return json.Marshal(sessions) +} + +func handleSessionRevoke(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) { + if subject == "" { + return nil, errors.New("subject (session_id) required for revoke action") + } + if err := am.RevokeSession(ctx, subject); err != nil { + return nil, err + } + return json.Marshal(map[string]bool{"revoked": true}) +} + +func handleGrantList(ctx context.Context, am *ActionManager, _ string) (json.RawMessage, error) { + grants, err := am.ListGrants(ctx) + if err != nil { + return nil, err + } + return json.Marshal(grants) +} + +func handleGrantRevoke(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) { + if subject == "" { + return nil, errors.New("subject (grant_id) required for revoke action") + } + var grantID int64 + if _, err := fmt.Sscanf(subject, "%d", &grantID); err != nil { + return nil, fmt.Errorf("invalid grant_id: %w", err) + } + if err := am.RevokeGrant(ctx, grantID); err != nil { + return nil, err + } + return json.Marshal(map[string]bool{"revoked": true}) +} + +func handleEnclaveList(ctx context.Context, am *ActionManager, _ string) (json.RawMessage, error) { + enclaves, err := am.ListEnclaves(ctx) + if err != nil { + return nil, err + } + return json.Marshal(enclaves) +} + +func handleEnclaveGet(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) { + if subject == "" { + return nil, errors.New("subject (enclave_id) required for get action") + } + enc, err := am.GetEnclaveByID(ctx, subject) + if err != nil { + return nil, err + } + return json.Marshal(enc) +} + +func handleEnclaveRotate(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) { + if subject == "" { + return nil, errors.New("subject (enclave_id) required for rotate action") + } + if err := am.RotateEnclave(ctx, subject); err != nil { + return nil, err + } + return json.Marshal(map[string]bool{"rotated": true}) +} + +func handleEnclaveArchive(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) { + if subject == "" { + return nil, errors.New("subject (enclave_id) required for archive action") + } + if err := am.ArchiveEnclave(ctx, subject); err != nil { + return nil, err + } + return json.Marshal(map[string]bool{"archived": true}) +} + +func handleEnclaveDelete(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) { + if subject == "" { + return nil, errors.New("subject (enclave_id) required for delete action") + } + if err := am.DeleteEnclave(ctx, subject); err != nil { + return nil, err + } + return json.Marshal(map[string]bool{"deleted": true}) +} + +func handleDelegationList(ctx context.Context, am *ActionManager, _ string) (json.RawMessage, error) { + delegations, err := am.ListDelegations(ctx) + if err != nil { + return nil, err + } + return json.Marshal(delegations) +} + +func handleDelegationListReceived(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) { + if subject == "" { + return nil, errors.New("subject (audience DID) required for list_received action") + } + delegations, err := am.ListDelegationsByAudience(ctx, subject) + if err != nil { + return nil, err + } + return json.Marshal(delegations) +} + +func handleDelegationListCommand(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) { + if subject == "" { + return nil, errors.New("subject (command) required for list_command action") + } + delegations, err := am.ListDelegationsForCommand(ctx, subject) + if err != nil { + return nil, err + } + return json.Marshal(delegations) +} + +func handleDelegationGet(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) { + if subject == "" { + return nil, errors.New("subject (cid) required for get action") + } + delegation, err := am.GetDelegationByCID(ctx, subject) + if err != nil { + return nil, err + } + return json.Marshal(delegation) +} + +func handleDelegationRevoke(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) { + if subject == "" { + return nil, errors.New("subject (cid) required for revoke action") + } + kb := Get() + revokedBy := "" + if kb != nil { + revokedBy = kb.DID() + } + if err := am.RevokeDelegation(ctx, RevokeDelegationParams{ + DelegationCID: subject, + RevokedBy: revokedBy, + Reason: "user revoked", + }); err != nil { + return nil, err + } + return json.Marshal(map[string]bool{"revoked": true}) +} + +func handleDelegationVerify(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) { + if subject == "" { + return nil, errors.New("subject (cid) required for verify action") + } + revoked, err := am.IsDelegationRevoked(ctx, subject) + if err != nil { + return nil, err + } + return json.Marshal(map[string]bool{"valid": !revoked, "revoked": revoked}) +} + +func handleDelegationCleanup(ctx context.Context, am *ActionManager, _ string) (json.RawMessage, error) { + if err := am.CleanExpiredDelegations(ctx); err != nil { + return nil, err + } + return json.Marshal(map[string]bool{"cleaned": true}) +} + +func handleVerificationMethodList(ctx context.Context, am *ActionManager, _ string) (json.RawMessage, error) { + vms, err := am.ListVerificationMethodsFull(ctx) + if err != nil { + return nil, err + } + return json.Marshal(vms) +} + +func handleVerificationMethodGet(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) { + if subject == "" { + return nil, errors.New("subject (method_id) required for get action") + } + vm, err := am.GetVerificationMethod(ctx, subject) + if err != nil { + return nil, err + } + return json.Marshal(vm) +} + +func handleVerificationMethodDelete(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) { + if subject == "" { + return nil, errors.New("subject (method_id) required for delete action") + } + if err := am.DeleteVerificationMethod(ctx, subject); err != nil { + return nil, err + } + return json.Marshal(map[string]bool{"deleted": true}) +} + +func handleServiceList(ctx context.Context, am *ActionManager, _ string) (json.RawMessage, error) { + services, err := am.ListVerifiedServices(ctx) + if err != nil { + return nil, err + } + return json.Marshal(services) +} + +func handleServiceGet(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) { + if subject == "" { + return nil, errors.New("subject (origin) required for get action") + } + svc, err := am.GetServiceByOrigin(ctx, subject) + if err != nil { + return nil, err + } + return json.Marshal(svc) +} + +func handleServiceGetByID(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) { + if subject == "" { + return nil, errors.New("subject (service_id) required for get_by_id action") + } + var serviceID int64 + if _, err := fmt.Sscanf(subject, "%d", &serviceID); err != nil { + return nil, fmt.Errorf("invalid service_id: %w", err) + } + svc, err := am.GetServiceByID(ctx, serviceID) + if err != nil { + return nil, err + } + return json.Marshal(svc) +} diff --git a/internal/keybase/functions.go b/internal/keybase/functions.go new file mode 100644 index 0000000..4ed8d64 --- /dev/null +++ b/internal/keybase/functions.go @@ -0,0 +1,206 @@ +package keybase + +import ( + "context" + "encoding/hex" + "fmt" + + "enclave/internal/crypto/mpc" + + "github.com/ncruces/go-sqlite3" + "github.com/sonr-io/crypto/core/protocol" +) + +// RegisterMPCFunctions registers custom SQLite functions for MPC operations: +// - mpc_sign(enclave_id TEXT, data BLOB) -> BLOB +// - mpc_verify(public_key_hex TEXT, data BLOB, signature BLOB) -> INTEGER (0 or 1) +// - mpc_refresh(enclave_id TEXT) -> TEXT +func RegisterMPCFunctions(conn *sqlite3.Conn) error { + if err := registerSignFunction(conn); err != nil { + return fmt.Errorf("register mpc_sign: %w", err) + } + if err := registerVerifyFunction(conn); err != nil { + return fmt.Errorf("register mpc_verify: %w", err) + } + if err := registerRefreshFunction(conn); err != nil { + return fmt.Errorf("register mpc_refresh: %w", err) + } + return nil +} + +func registerSignFunction(conn *sqlite3.Conn) error { + return conn.CreateFunction("mpc_sign", 2, sqlite3.DETERMINISTIC, func(ctx sqlite3.Context, args ...sqlite3.Value) { + if len(args) != 2 { + ctx.ResultError(fmt.Errorf("mpc_sign requires 2 arguments: enclave_id, data")) + return + } + + enclaveID := args[0].Text() + data := args[1].RawBlob() + + if enclaveID == "" { + ctx.ResultError(fmt.Errorf("mpc_sign: enclave_id cannot be empty")) + return + } + if len(data) == 0 { + ctx.ResultError(fmt.Errorf("mpc_sign: data cannot be empty")) + return + } + + enclave, err := loadEnclaveFromDB(enclaveID) + if err != nil { + ctx.ResultError(fmt.Errorf("mpc_sign: %w", err)) + return + } + + signature, err := enclave.Sign(data) + if err != nil { + ctx.ResultError(fmt.Errorf("mpc_sign: signing failed: %w", err)) + return + } + + ctx.ResultBlob(signature) + }) +} + +func registerVerifyFunction(conn *sqlite3.Conn) error { + return conn.CreateFunction("mpc_verify", 3, sqlite3.DETERMINISTIC|sqlite3.INNOCUOUS, func(ctx sqlite3.Context, args ...sqlite3.Value) { + if len(args) != 3 { + ctx.ResultError(fmt.Errorf("mpc_verify requires 3 arguments: public_key_hex, data, signature")) + return + } + + pubKeyHex := args[0].Text() + data := args[1].RawBlob() + signature := args[2].RawBlob() + + if pubKeyHex == "" { + ctx.ResultError(fmt.Errorf("mpc_verify: public_key_hex cannot be empty")) + return + } + if len(data) == 0 { + ctx.ResultError(fmt.Errorf("mpc_verify: data cannot be empty")) + return + } + if len(signature) == 0 { + ctx.ResultError(fmt.Errorf("mpc_verify: signature cannot be empty")) + return + } + + pubKeyBytes, err := hex.DecodeString(pubKeyHex) + if err != nil { + ctx.ResultError(fmt.Errorf("mpc_verify: invalid public key hex: %w", err)) + return + } + + valid, err := mpc.VerifyWithPubKey(pubKeyBytes, data, signature) + if err != nil { + ctx.ResultError(fmt.Errorf("mpc_verify: verification error: %w", err)) + return + } + + if valid { + ctx.ResultInt(1) + } else { + ctx.ResultInt(0) + } + }) +} + +func registerRefreshFunction(conn *sqlite3.Conn) error { + return conn.CreateFunction("mpc_refresh", 1, 0, func(ctx sqlite3.Context, args ...sqlite3.Value) { + if len(args) != 1 { + ctx.ResultError(fmt.Errorf("mpc_refresh requires 1 argument: enclave_id")) + return + } + + enclaveID := args[0].Text() + if enclaveID == "" { + ctx.ResultError(fmt.Errorf("mpc_refresh: enclave_id cannot be empty")) + return + } + + enclave, err := loadEnclaveFromDB(enclaveID) + if err != nil { + ctx.ResultError(fmt.Errorf("mpc_refresh: %w", err)) + return + } + + newEnclave, err := enclave.Refresh() + if err != nil { + ctx.ResultError(fmt.Errorf("mpc_refresh: refresh failed: %w", err)) + return + } + + if err := updateEnclaveInDB(enclaveID, newEnclave); err != nil { + ctx.ResultError(fmt.Errorf("mpc_refresh: update failed: %w", err)) + return + } + + ctx.ResultText(enclaveID) + }) +} + +func loadEnclaveFromDB(enclaveID string) (mpc.Enclave, error) { + kb := Get() + if kb == nil { + return nil, fmt.Errorf("keybase not initialized") + } + + dbEnc, err := kb.queries.GetEnclaveByID(context.Background(), enclaveID) + if err != nil { + return nil, fmt.Errorf("enclave not found: %w", err) + } + + valShare, err := protocol.DecodeMessage(string(dbEnc.ValShare)) + if err != nil { + return nil, fmt.Errorf("decode val_share: %w", err) + } + + userShare, err := protocol.DecodeMessage(string(dbEnc.UserShare)) + if err != nil { + return nil, fmt.Errorf("decode user_share: %w", err) + } + + return &mpc.EnclaveData{ + PubHex: dbEnc.PublicKeyHex, + PubBytes: dbEnc.PublicKey, + ValShare: valShare, + UserShare: userShare, + Nonce: dbEnc.Nonce, + Curve: mpc.CurveName(dbEnc.Curve), + }, nil +} + +func updateEnclaveInDB(enclaveID string, enclave mpc.Enclave) error { + kb := Get() + if kb == nil { + return fmt.Errorf("keybase not initialized") + } + + data := enclave.GetData() + + valShareStr, err := protocol.EncodeMessage(data.ValShare) + if err != nil { + return fmt.Errorf("encode val_share: %w", err) + } + + userShareStr, err := protocol.EncodeMessage(data.UserShare) + if err != nil { + return fmt.Errorf("encode user_share: %w", err) + } + + dbEnc, err := kb.queries.GetEnclaveByID(context.Background(), enclaveID) + if err != nil { + return fmt.Errorf("get enclave: %w", err) + } + + // Raw SQL required: SQLC-generated code lacks full enclave update query + _, err = kb.db.ExecContext(context.Background(), ` + UPDATE mpc_enclaves + SET val_share = ?, user_share = ?, nonce = ?, rotated_at = datetime('now') + WHERE id = ? + `, []byte(valShareStr), []byte(userShareStr), data.Nonce, dbEnc.ID) + + return err +} diff --git a/internal/keybase/store.go b/internal/keybase/store.go new file mode 100644 index 0000000..9db763b --- /dev/null +++ b/internal/keybase/store.go @@ -0,0 +1,211 @@ +package keybase + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "sync" + + "enclave/internal/migrations" + + "github.com/ncruces/go-sqlite3" + "github.com/ncruces/go-sqlite3/driver" + _ "github.com/ncruces/go-sqlite3/embed" + "github.com/ncruces/go-sqlite3/ext/hash" + "github.com/ncruces/go-sqlite3/ext/serdes" + "github.com/ncruces/go-sqlite3/ext/uuid" +) + +type Keybase struct { + db *sql.DB + conn *sqlite3.Conn + queries *Queries + did string + didID int64 + mu sync.RWMutex +} + +var ( + instance *Keybase + initMu sync.Mutex +) + +func Open() (*Keybase, error) { + initMu.Lock() + defer initMu.Unlock() + + if instance != nil { + return instance, nil + } + + var rawConn *sqlite3.Conn + initCallback := func(conn *sqlite3.Conn) error { + rawConn = conn + if err := hash.Register(conn); err != nil { + return fmt.Errorf("register hash extension: %w", err) + } + if err := uuid.Register(conn); err != nil { + return fmt.Errorf("register uuid extension: %w", err) + } + return nil + } + + db, err := driver.Open(":memory:", initCallback) + if err != nil { + return nil, fmt.Errorf("open database: %w", err) + } + + if _, err := db.Exec(migrations.SchemaSQL); err != nil { + db.Close() + return nil, fmt.Errorf("init schema: %w", err) + } + + instance = &Keybase{ + db: db, + conn: rawConn, + queries: New(db), + } + + if rawConn != nil { + if err := RegisterMPCFunctions(rawConn); err != nil { + db.Close() + instance = nil + return nil, fmt.Errorf("register mpc functions: %w", err) + } + } + + return instance, nil +} + +func Get() *Keybase { + initMu.Lock() + defer initMu.Unlock() + return instance +} + +func Close() error { + initMu.Lock() + defer initMu.Unlock() + + if instance == nil { + return nil + } + + err := instance.db.Close() + instance = nil + return err +} + +func (k *Keybase) DB() *sql.DB { + k.mu.RLock() + defer k.mu.RUnlock() + return k.db +} + +func (k *Keybase) Queries() *Queries { + k.mu.RLock() + defer k.mu.RUnlock() + return k.queries +} + +func (k *Keybase) DID() string { + k.mu.RLock() + defer k.mu.RUnlock() + return k.did +} + +func (k *Keybase) DIDID() int64 { + k.mu.RLock() + defer k.mu.RUnlock() + return k.didID +} + +func (k *Keybase) SetDID(did string, didID int64) { + k.mu.Lock() + defer k.mu.Unlock() + k.did = did + k.didID = didID +} + +func (k *Keybase) Initialize(ctx context.Context, credentialBytes []byte) (string, error) { + k.mu.Lock() + defer k.mu.Unlock() + + did := fmt.Sprintf("did:sonr:%x", credentialBytes[:16]) + docJSON, _ := json.Marshal(map[string]any{ + "@context": []string{"https://www.w3.org/ns/did/v1"}, + "id": did, + }) + + doc, err := k.queries.CreateDID(ctx, CreateDIDParams{ + Did: did, + Controller: did, + Document: docJSON, + Sequence: 0, + }) + if err != nil { + return "", fmt.Errorf("create DID: %w", err) + } + + k.did = did + k.didID = doc.ID + + return did, nil +} + +func (k *Keybase) Load(ctx context.Context, data []byte) (string, error) { + if len(data) < 100 { + return "", fmt.Errorf("invalid database format") + } + + k.mu.Lock() + defer k.mu.Unlock() + + if k.conn == nil { + return "", fmt.Errorf("database not initialized") + } + + if err := serdes.Deserialize(k.conn, "main", data); err != nil { + return "", fmt.Errorf("deserialize database: %w", err) + } + + docs, err := k.queries.ListAllDIDs(ctx) + if err != nil { + return "", fmt.Errorf("list DIDs: %w", err) + } + + if len(docs) == 0 { + return "", fmt.Errorf("no DID found in database") + } + + k.did = docs[0].Did + k.didID = docs[0].ID + + return k.did, nil +} + +func (k *Keybase) Serialize() ([]byte, error) { + k.mu.RLock() + defer k.mu.RUnlock() + + if k.conn == nil { + return nil, fmt.Errorf("database not initialized") + } + + return serdes.Serialize(k.conn, "main") +} + +func (k *Keybase) WithTx(ctx context.Context, fn func(*Queries) error) error { + tx, err := k.db.BeginTx(ctx, nil) + if err != nil { + return fmt.Errorf("begin tx: %w", err) + } + + if err := fn(k.queries.WithTx(tx)); err != nil { + tx.Rollback() + return err + } + + return tx.Commit() +} -- 2.43.0 From 766de75ba992cb4e89128f12d5f34553deb39e13 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Sat, 10 Jan 2026 14:41:58 -0500 Subject: [PATCH 26/35] refactor(example): overhaul example app UI and UX --- example/index.html | 573 +++++++++++++++++++++++++++++++++++++-------- example/main.js | 326 +++++++++++++++++++------- src/enclave.ts | 134 ++++++++--- src/types.ts | 12 + 4 files changed, 834 insertions(+), 211 deletions(-) diff --git a/example/index.html b/example/index.html index b6b10fd..fc308fd 100644 --- a/example/index.html +++ b/example/index.html @@ -3,108 +3,497 @@ - Motr Enclave + Motr Enclave Demo -

-

Motr Enclave

+
+ -
-

Status

-
- Loading... - -
-
- -
-
-

ping(message)

- -
- - -
-
- -
-
-

generate(credential)

- -
-
- - -
-
-
- -
-
-

load(database)

- -
-
- -
-
-
- -
-
-

exec(filter)

- -
- -
- - - - - -
-
-
- -
-
-

query(did)

- -
- - -
-
+
+
+

Setup & Initialize

+ +
+
+

ping() health check

+ +
+ +
+
+
+ +
+

generate() create wallet

+
+ + +
+
+
+
+ +
+

load() restore wallet

+
+ + + +
+
+
+
+ +
+

Resource Explorer

+ +
+

Select Resource

+
+ + + + + + + + +
+
+ +
+

Actions for accounts

+
+ + +
+ + + +
+ + +
+
+ +
+

Result

+
+ Execute an action to see results +
+
+
+ +
+

DID Query

+ +
+

query() resolve DID document

+ +
+ +
+
+ +
+

DID Document

+
+ Query a DID to see the document +
+
+
+
diff --git a/example/main.js b/example/main.js index 115ced2..97b527f 100644 --- a/example/main.js +++ b/example/main.js @@ -2,8 +2,22 @@ import { createEnclave } from '../dist/enclave.js'; let enclave = null; let lastDatabase = null; +let currentResource = 'accounts'; +let currentAction = 'list'; -const LogLevel = { INFO: 'info', OK: 'ok', ERR: 'err', DATA: 'data' }; +const RESOURCE_ACTIONS = { + accounts: ['list', 'get'], + enclaves: ['list', 'get', 'rotate', 'archive', 'delete'], + credentials: ['list', 'get'], + sessions: ['list', 'revoke'], + grants: ['list', 'revoke'], + delegations: ['list', 'list_received', 'list_command', 'get', 'revoke', 'verify', 'cleanup'], + ucans: ['list', 'get', 'revoke', 'verify', 'cleanup'], + verification_methods: ['list', 'get', 'delete'], + services: ['list', 'get', 'get_by_id'], +}; + +const ACTIONS_REQUIRING_SUBJECT = ['get', 'revoke', 'delete', 'verify', 'rotate', 'archive', 'list_received', 'list_command', 'get_by_id']; function log(card, level, message, data = null) { const el = document.getElementById(`log-${card}`); @@ -14,22 +28,30 @@ function log(card, level, message, data = null) { let entry = `
${time} ${message}`; if (data !== null) { const json = typeof data === 'string' ? data : JSON.stringify(data, null, 2); - entry += `${json}`; + entry += `${escapeHtml(json)}`; } entry += '
'; el.innerHTML += entry; - el.classList.add('has-content'); + el.classList.add('visible'); el.scrollTop = el.scrollHeight; console.log(`[${time}] [${card}] ${message}`, data ?? ''); } -function setStatus(id, ok, message) { - const el = document.getElementById(id); - if (el) { - el.textContent = message; - el.className = `status ${ok ? 'ok' : ok === false ? 'err' : 'wait'}`; +function escapeHtml(str) { + return str.replace(/&/g, '&').replace(//g, '>'); +} + +function setStatus(ready, message) { + const dot = document.getElementById('status-dot'); + const text = document.getElementById('status-text'); + + if (dot) { + dot.className = 'status-dot' + (ready === true ? ' ready' : ready === false ? ' error' : ''); + } + if (text) { + text.textContent = message; } } @@ -46,16 +68,16 @@ async function createWebAuthnCredential() { const userId = crypto.getRandomValues(new Uint8Array(16)); const challenge = crypto.getRandomValues(new Uint8Array(32)); - const publicKeyCredentialCreationOptions = { + const options = { challenge, rp: { - name: "Motr Enclave", + name: "Motr Enclave Demo", id: window.location.hostname, }, user: { id: userId, name: `user-${Date.now()}@motr.local`, - displayName: "Motr User", + displayName: "Motr Demo User", }, pubKeyCredParams: [ { alg: -7, type: "public-key" }, @@ -70,9 +92,7 @@ async function createWebAuthnCredential() { attestation: "none", }; - const credential = await navigator.credentials.create({ - publicKey: publicKeyCredentialCreationOptions, - }); + const credential = await navigator.credentials.create({ publicKey: options }); return { id: credential.id, @@ -87,60 +107,71 @@ async function createWebAuthnCredential() { async function init() { try { - log('generate', LogLevel.INFO, 'Loading enclave.wasm...'); - + setStatus(null, 'Loading...'); enclave = await createEnclave('./enclave.wasm', { debug: true }); - - setStatus('status', true, 'Ready'); - log('generate', LogLevel.OK, 'Plugin loaded'); + setStatus(true, 'Ready'); + log('generate', 'ok', 'Plugin loaded successfully'); } catch (err) { - setStatus('status', false, 'Failed'); - log('generate', LogLevel.ERR, `Load failed: ${err?.message || String(err)}`); + setStatus(false, 'Failed'); + log('generate', 'err', `Load failed: ${err?.message || String(err)}`); } } -window.testPing = async function() { - if (!enclave) return log('ping', LogLevel.ERR, 'Plugin not loaded'); +window.showSection = function(section) { + document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active')); + document.querySelectorAll('.nav-btn').forEach(el => el.classList.remove('active')); - const message = document.getElementById('ping-msg').value || 'hello'; - log('ping', LogLevel.INFO, `Sending: "${message}"`); + const sectionEl = document.getElementById(`section-${section}`); + if (sectionEl) sectionEl.classList.add('active'); + + event?.target?.classList.add('active'); +}; + +window.testPing = async function() { + if (!enclave) return log('ping', 'err', 'Plugin not loaded'); + + const message = document.getElementById('ping-msg')?.value || 'hello'; + log('ping', 'info', `Sending: "${message}"`); try { const result = await enclave.ping(message); if (result.success) { - log('ping', LogLevel.OK, `Response: "${result.echo}"`, result); + log('ping', 'ok', `Response: "${result.echo}"`, result); } else { - log('ping', LogLevel.ERR, result.message, result); + log('ping', 'err', result.message, result); } return result; } catch (err) { - log('ping', LogLevel.ERR, err?.message || String(err)); + log('ping', 'err', err?.message || String(err)); throw err; } }; window.testGenerate = async function() { - if (!enclave) return log('generate', LogLevel.ERR, 'Plugin not loaded'); + if (!enclave) return log('generate', 'err', 'Plugin not loaded'); if (!window.PublicKeyCredential) { - log('generate', LogLevel.ERR, 'WebAuthn not supported in this browser'); + log('generate', 'err', 'WebAuthn not supported in this browser'); return; } try { - log('generate', LogLevel.INFO, 'Requesting WebAuthn credential...'); - + log('generate', 'info', 'Requesting WebAuthn credential...'); const credential = await createWebAuthnCredential(); - log('generate', LogLevel.OK, `Credential created: ${credential.id.slice(0, 20)}...`); + log('generate', 'ok', `Credential created: ${credential.id.slice(0, 20)}...`); - const credentialJson = JSON.stringify(credential); - const credentialBase64 = btoa(credentialJson); + const credentialBase64 = btoa(JSON.stringify(credential)); - log('generate', LogLevel.INFO, 'Calling enclave.generate()...'); + log('generate', 'info', 'Calling enclave.generate()...'); const result = await enclave.generate(credentialBase64); - const logData = { did: result.did, dbSize: result.database?.length }; - log('generate', LogLevel.OK, `DID created: ${result.did}`, logData); + log('generate', 'ok', `DID created: ${result.did}`, { + did: result.did, + enclaveId: result.enclave_id, + publicKey: result.public_key?.slice(0, 20) + '...', + accounts: result.accounts?.length ?? 0, + dbSize: result.database?.length ?? 0, + }); if (result.database) { lastDatabase = result.database; @@ -148,16 +179,16 @@ window.testGenerate = async function() { return result; } catch (err) { if (err.name === 'NotAllowedError') { - log('generate', LogLevel.ERR, 'User cancelled or WebAuthn not allowed'); + log('generate', 'err', 'User cancelled or WebAuthn not allowed'); } else { - log('generate', LogLevel.ERR, err?.message || String(err)); + log('generate', 'err', err?.message || String(err)); } throw err; } }; window.testGenerateMock = async function() { - if (!enclave) return log('generate', LogLevel.ERR, 'Plugin not loaded'); + if (!enclave) return log('generate', 'err', 'Plugin not loaded'); const mockCredential = btoa(JSON.stringify({ id: `mock-${Date.now()}`, @@ -169,111 +200,246 @@ window.testGenerateMock = async function() { }, })); - log('generate', LogLevel.INFO, 'Using mock credential...'); + log('generate', 'info', 'Using mock credential...'); try { const result = await enclave.generate(mockCredential); - const logData = { did: result.did, dbSize: result.database?.length }; - log('generate', LogLevel.OK, `DID created: ${result.did}`, logData); + log('generate', 'ok', `DID created: ${result.did}`, { + did: result.did, + enclaveId: result.enclave_id, + publicKey: result.public_key?.slice(0, 20) + '...', + accounts: result.accounts?.length ?? 0, + dbSize: result.database?.length ?? 0, + }); if (result.database) { lastDatabase = result.database; } return result; } catch (err) { - log('generate', LogLevel.ERR, err?.message || String(err)); + log('generate', 'err', err?.message || String(err)); throw err; } }; window.testLoadFromBytes = async function() { - if (!enclave) return log('load', LogLevel.ERR, 'Plugin not loaded'); + if (!enclave) return log('load', 'err', 'Plugin not loaded'); if (!lastDatabase) { - return log('load', LogLevel.ERR, 'No database in memory - run generate first'); + return log('load', 'err', 'No database in memory - run generate first'); } - log('load', LogLevel.INFO, `Loading from bytes (${lastDatabase.length} bytes)...`); + log('load', 'info', `Loading from bytes (${lastDatabase.length} bytes)...`); try { const result = await enclave.load(new Uint8Array(lastDatabase)); if (result.success) { - log('load', LogLevel.OK, `Loaded DID: ${result.did}`, result); + log('load', 'ok', `Loaded DID: ${result.did}`, result); } else { - log('load', LogLevel.ERR, result.error, result); + log('load', 'err', result.error, result); } return result; } catch (err) { - log('load', LogLevel.ERR, err?.message || String(err)); + log('load', 'err', err?.message || String(err)); throw err; } }; -window.testExec = async function() { - if (!enclave) return log('exec', LogLevel.ERR, 'Plugin not loaded'); +window.downloadDatabase = function() { + if (!lastDatabase) { + alert('No database in memory. Run generate first.'); + return; + } - const filter = document.getElementById('filter').value; - if (!filter) return log('exec', LogLevel.ERR, 'Filter required'); + const blob = new Blob([new Uint8Array(lastDatabase)], { type: 'application/octet-stream' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `enclave-${Date.now()}.db`; + a.click(); + URL.revokeObjectURL(url); - log('exec', LogLevel.INFO, `Executing: ${filter}`); + log('load', 'ok', `Downloaded database (${lastDatabase.length} bytes)`); +}; + +window.uploadDatabase = async function(event) { + const file = event.target.files?.[0]; + if (!file) return; + + const buffer = await file.arrayBuffer(); + lastDatabase = Array.from(new Uint8Array(buffer)); + + log('load', 'info', `Uploaded ${file.name} (${lastDatabase.length} bytes)`); + + if (enclave) { + await testLoadFromBytes(); + } +}; + +window.selectResource = function(resource) { + currentResource = resource; + currentAction = 'list'; + + document.querySelectorAll('.resource-btn').forEach(el => el.classList.remove('active')); + event?.target?.closest('.resource-btn')?.classList.add('active'); + + document.getElementById('selected-resource').textContent = resource; + + const actions = RESOURCE_ACTIONS[resource] || ['list']; + const pillsContainer = document.getElementById('action-pills'); + pillsContainer.innerHTML = actions.map((action, i) => + `` + ).join(''); + + updateSubjectRow(); +}; + +window.selectAction = function(action) { + currentAction = action; + + document.querySelectorAll('.action-pill').forEach(el => el.classList.remove('active')); + event?.target?.classList.add('active'); + + updateSubjectRow(); +}; + +function updateSubjectRow() { + const subjectRow = document.getElementById('subject-row'); + const subjectInput = document.getElementById('subject-input'); + + if (ACTIONS_REQUIRING_SUBJECT.includes(currentAction)) { + subjectRow.style.display = 'block'; + + const placeholders = { + get: 'ID or address', + revoke: 'ID to revoke', + delete: 'ID to delete', + verify: 'CID to verify', + rotate: 'Enclave ID', + archive: 'Enclave ID', + list_received: 'Audience DID', + list_command: 'Command (e.g., msg/send)', + get_by_id: 'Service ID (number)', + }; + + subjectInput.placeholder = placeholders[currentAction] || 'Subject'; + } else { + subjectRow.style.display = 'none'; + subjectInput.value = ''; + } +} + +window.executeAction = async function() { + if (!enclave) { + showResult('Plugin not loaded', true); + return; + } + + const subject = document.getElementById('subject-input')?.value || ''; + + if (ACTIONS_REQUIRING_SUBJECT.includes(currentAction) && !subject) { + showResult(`Subject is required for action: ${currentAction}`, true); + return; + } + + let filter = `resource:${currentResource} action:${currentAction}`; + if (subject) { + filter += ` subject:${subject}`; + } try { const result = await enclave.exec(filter); if (result.success) { - log('exec', LogLevel.OK, 'Success', result); + showResult(JSON.stringify(result.result, null, 2), false); } else { - log('exec', LogLevel.ERR, result.error, result); + showResult(result.error, true); } - return result; } catch (err) { - log('exec', LogLevel.ERR, err?.message || String(err)); - throw err; + showResult(err?.message || String(err), true); } }; +window.quickExec = async function(resource, action) { + if (!enclave) { + alert('Plugin not loaded'); + return; + } + + showSection('explorer'); + selectResource(resource); + selectAction(action); + await executeAction(); +}; + +function showResult(content, isError = false) { + const resultEl = document.getElementById('exec-result'); + resultEl.textContent = content; + resultEl.className = 'result-panel' + (isError ? ' error' : ' success'); +} + +window.clearResult = function() { + const resultEl = document.getElementById('exec-result'); + resultEl.innerHTML = 'Execute an action to see results'; + resultEl.className = 'result-panel'; +}; + window.testQuery = async function() { - if (!enclave) return log('query', LogLevel.ERR, 'Plugin not loaded'); + if (!enclave) { + showQueryResult('Plugin not loaded', true); + return; + } - const did = document.getElementById('did').value; - log('query', LogLevel.INFO, did ? `Querying: ${did}` : 'Querying current DID...'); + const did = document.getElementById('did-input')?.value || ''; try { const result = await enclave.query(did); - log('query', LogLevel.OK, `Resolved: ${result.did}`, result); - return result; + showQueryResult(JSON.stringify(result, null, 2), false); } catch (err) { - log('query', LogLevel.ERR, err?.message || String(err)); - throw err; + showQueryResult(err?.message || String(err), true); } }; -window.setFilter = function(filter) { - document.getElementById('filter').value = filter; -}; +function showQueryResult(content, isError = false) { + const resultEl = document.getElementById('query-result'); + resultEl.textContent = content; + resultEl.className = 'result-panel' + (isError ? ' error' : ' success'); +} -window.clearCardLog = function(card) { - const el = document.getElementById(`log-${card}`); - if (el) { - el.innerHTML = ''; - el.classList.remove('has-content'); +window.resetEnclave = async function() { + if (!enclave) return; + + try { + await enclave.reset(); + lastDatabase = null; + log('generate', 'info', 'Enclave state reset'); + clearResult(); + document.getElementById('query-result').innerHTML = 'Query a DID to see the document'; + } catch (err) { + log('generate', 'err', `Reset failed: ${err?.message || String(err)}`); } }; window.runAllTests = async function() { - log('ping', LogLevel.INFO, '=== Running all tests ==='); + log('generate', 'info', '=== Running all tests ==='); try { await testPing(); await testGenerateMock(); await testLoadFromBytes(); - await testExec(); + + showSection('explorer'); + await quickExec('accounts', 'list'); + await quickExec('enclaves', 'list'); + + showSection('query'); await testQuery(); - log('query', LogLevel.OK, '=== All tests passed ==='); + + log('generate', 'ok', '=== All tests completed ==='); } catch (err) { - log('query', LogLevel.ERR, `Tests failed: ${err?.message || String(err)}`); + log('generate', 'err', `Tests failed: ${err?.message || String(err)}`); } }; diff --git a/src/enclave.ts b/src/enclave.ts index b9e4b56..7d6d2fc 100644 --- a/src/enclave.ts +++ b/src/enclave.ts @@ -8,6 +8,24 @@ import type { Resource, } from './types'; +function extractErrorMessage(err: unknown): string { + if (err instanceof Error) { + return err.message; + } + if (typeof err === 'object' && err !== null) { + const obj = err as Record; + if (typeof obj.message === 'string') return obj.message; + if (typeof obj.error === 'string') return obj.error; + if (typeof obj.msg === 'string') return obj.msg; + try { + return JSON.stringify(err); + } catch { + return '[unknown error object]'; + } + } + return String(err); +} + export class Enclave { private plugin: Plugin; private logger: EnclaveOptions['logger']; @@ -41,12 +59,20 @@ export class Enclave { this.log('generate: starting with credential'); const input = JSON.stringify({ credential }); - const result = await this.plugin.call('generate', input); - if (!result) throw new Error('generate: plugin returned no output'); - const output = result.json() as GenerateOutput; - - this.log(`generate: created DID ${output.did}`); - return output; + + try { + const result = await this.plugin.call('generate', input); + if (!result) { + throw new Error('generate: plugin returned no output'); + } + const output = result.json() as GenerateOutput; + this.log(`generate: created DID ${output.did}`); + return output; + } catch (err) { + const errMsg = extractErrorMessage(err); + this.log(`generate: failed - ${errMsg}`, 'error'); + throw new Error(`generate: ${errMsg}`); + } } async load(source: Uint8Array | number[]): Promise { @@ -59,38 +85,52 @@ export class Enclave { } else if (Array.isArray(source)) { database = source; } else { - throw new Error('load: invalid source type'); + return { success: false, error: 'invalid source type' }; } const input = JSON.stringify({ database }); - const result = await this.plugin.call('load', input); - if (!result) throw new Error('load: plugin returned no output'); - const output = result.json() as LoadOutput; - - if (output.success) { - this.log(`load: loaded database for DID ${output.did}`); - } else { - this.log(`load: failed - ${output.error}`, 'error'); + + try { + const result = await this.plugin.call('load', input); + if (!result) { + return { success: false, error: 'plugin returned no output' }; + } + const output = result.json() as LoadOutput; + if (output.success) { + this.log(`load: loaded database for DID ${output.did}`); + } else { + this.log(`load: failed - ${output.error}`, 'error'); + } + return output; + } catch (err) { + const errMsg = extractErrorMessage(err); + this.log(`load: failed - ${errMsg}`, 'error'); + return { success: false, error: errMsg }; } - - return output; } async exec(filter: string, token?: string): Promise { this.log(`exec: executing filter "${filter}"`); const input = JSON.stringify({ filter, token }); - const result = await this.plugin.call('exec', input); - if (!result) throw new Error('exec: plugin returned no output'); - const output = result.json() as ExecOutput; - - if (output.success) { - this.log('exec: completed successfully'); - } else { - this.log(`exec: failed - ${output.error}`, 'error'); + + try { + const result = await this.plugin.call('exec', input); + if (!result) { + return { success: false, error: 'plugin returned no output' }; + } + const output = result.json() as ExecOutput; + if (output.success) { + this.log('exec: completed successfully'); + } else { + this.log(`exec: failed - ${output.error}`, 'error'); + } + return output; + } catch (err) { + const errMsg = extractErrorMessage(err); + this.log(`exec: failed - ${errMsg}`, 'error'); + return { success: false, error: errMsg }; } - - return output; } async execute( @@ -109,24 +149,40 @@ export class Enclave { this.log(`query: resolving DID ${did || '(current)'}`); const input = JSON.stringify({ did }); - const result = await this.plugin.call('query', input); - if (!result) throw new Error('query: plugin returned no output'); - const output = result.json() as QueryOutput; - - this.log(`query: resolved DID ${output.did}`); - return output; + + try { + const result = await this.plugin.call('query', input); + if (!result) { + throw new Error('query: plugin returned no output'); + } + const output = result.json() as QueryOutput; + this.log(`query: resolved DID ${output.did}`); + return output; + } catch (err) { + const errMsg = extractErrorMessage(err); + this.log(`query: failed - ${errMsg}`, 'error'); + throw new Error(`query: ${errMsg}`); + } } async ping(message: string = 'hello'): Promise<{ success: boolean; message: string; echo: string }> { this.log(`ping: sending "${message}"`); const input = JSON.stringify({ message }); - const result = await this.plugin.call('ping', input); - if (!result) throw new Error('ping: plugin returned no output'); - const output = result.json() as { success: boolean; message: string; echo: string }; - - this.log(`ping: received ${output.success ? 'pong' : 'error'}`); - return output; + + try { + const result = await this.plugin.call('ping', input); + if (!result) { + throw new Error('ping: plugin returned no output'); + } + const output = result.json() as { success: boolean; message: string; echo: string }; + this.log(`ping: received ${output.success ? 'pong' : 'error'}`); + return output; + } catch (err) { + const errMsg = err instanceof Error ? err.message : String(err); + this.log(`ping: failed - ${errMsg}`, 'error'); + throw new Error(`ping: ${errMsg}`); + } } async reset(): Promise { diff --git a/src/types.ts b/src/types.ts index 5e0088c..e1c02ce 100644 --- a/src/types.ts +++ b/src/types.ts @@ -17,6 +17,18 @@ export interface GenerateOutput { did: string; /** Serialized database buffer for storage */ database: number[]; + /** The MPC enclave ID */ + enclave_id?: string; + /** The public key hex */ + public_key?: string; + /** Default accounts created */ + accounts?: AccountInfo[]; +} + +export interface AccountInfo { + address: string; + chain_id: string; + coin_type: number; } // ============================================================================ -- 2.43.0 From de89b627d395002981fdb6bdefd551ec2c22f846 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Sat, 10 Jan 2026 14:45:06 -0500 Subject: [PATCH 27/35] refactor(enclave): enhance error extraction and logging for better debugging --- src/enclave.ts | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/enclave.ts b/src/enclave.ts index 7d6d2fc..6e1cdea 100644 --- a/src/enclave.ts +++ b/src/enclave.ts @@ -17,11 +17,18 @@ function extractErrorMessage(err: unknown): string { if (typeof obj.message === 'string') return obj.message; if (typeof obj.error === 'string') return obj.error; if (typeof obj.msg === 'string') return obj.msg; - try { - return JSON.stringify(err); - } catch { - return '[unknown error object]'; + const allKeys = [...Object.keys(obj), ...Object.getOwnPropertyNames(obj)]; + for (const key of allKeys) { + const val = obj[key]; + if (typeof val === 'string' && val.length > 0) { + return `${key}: ${val}`; + } } + try { + const str = JSON.stringify(err, Object.getOwnPropertyNames(err)); + if (str !== '{}') return str; + } catch {} + return `[error object with keys: ${allKeys.join(', ') || 'none'}]`; } return String(err); } @@ -69,6 +76,14 @@ export class Enclave { this.log(`generate: created DID ${output.did}`); return output; } catch (err) { + console.error('[DEBUG] Raw error:', err); + console.error('[DEBUG] typeof:', typeof err); + console.error('[DEBUG] constructor:', err?.constructor?.name); + if (typeof err === 'object' && err !== null) { + console.error('[DEBUG] keys:', Object.keys(err)); + console.error('[DEBUG] ownProps:', Object.getOwnPropertyNames(err)); + console.error('[DEBUG] prototype:', Object.getPrototypeOf(err)); + } const errMsg = extractErrorMessage(err); this.log(`generate: failed - ${errMsg}`, 'error'); throw new Error(`generate: ${errMsg}`); -- 2.43.0 From 30e372d07f7ddc7e0b17b3147d609d7bcc696a72 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Sat, 10 Jan 2026 14:46:16 -0500 Subject: [PATCH 28/35] feat(enclave): add detailed logging for MPC initialization steps --- cmd/enclave/main.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cmd/enclave/main.go b/cmd/enclave/main.go index b252678..e9a208d 100644 --- a/cmd/enclave/main.go +++ b/cmd/enclave/main.go @@ -254,22 +254,28 @@ type initResult struct { } func initializeWithMPC(credentialBytes []byte) (*initResult, error) { + pdk.Log(pdk.LogInfo, "initializeWithMPC: step 1 - opening database") kb, err := keybase.Open() if err != nil { return nil, fmt.Errorf("open database: %w", err) } + pdk.Log(pdk.LogInfo, "initializeWithMPC: step 2 - database opened") ctx := context.Background() + pdk.Log(pdk.LogInfo, "initializeWithMPC: step 3 - initializing DID") did, err := kb.Initialize(ctx, credentialBytes) if err != nil { return nil, fmt.Errorf("initialize: %w", err) } + pdk.Log(pdk.LogInfo, fmt.Sprintf("initializeWithMPC: step 4 - DID initialized: %s", did)) - pdk.Log(pdk.LogInfo, "initializeWithMPC: generating MPC enclave") + pdk.Log(pdk.LogInfo, "initializeWithMPC: step 5 - generating MPC enclave") enclave, err := mpc.NewEnclave() if err != nil { + pdk.Log(pdk.LogError, fmt.Sprintf("initializeWithMPC: MPC enclave generation failed: %v", err)) return nil, fmt.Errorf("generate MPC enclave: %w", err) } + pdk.Log(pdk.LogInfo, "initializeWithMPC: step 6 - MPC enclave generated") enclaveData := enclave.GetData() enclaveID := fmt.Sprintf("enc_%x", credentialBytes[:8]) -- 2.43.0 From 634e62dc811e4c889b6eb127aaf03fa9fdd41f65 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Sat, 10 Jan 2026 14:52:13 -0500 Subject: [PATCH 29/35] feat(enclave): add test MPC function and refactor generate function to use DID initialization without MPC --- cmd/enclave/main.go | 63 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 11 deletions(-) diff --git a/cmd/enclave/main.go b/cmd/enclave/main.go index e9a208d..3d49f82 100644 --- a/cmd/enclave/main.go +++ b/cmd/enclave/main.go @@ -22,6 +22,38 @@ import ( func main() { state.Default() } +//go:wasmexport test_mpc +func testMPC() int32 { + pdk.Log(pdk.LogInfo, "test_mpc: starting MPC test") + + var result string + func() { + defer func() { + if r := recover(); r != nil { + result = fmt.Sprintf("PANIC: %v", r) + pdk.Log(pdk.LogError, result) + } + }() + + pdk.Log(pdk.LogInfo, "test_mpc: calling mpc.NewEnclave()") + enc, err := mpc.NewEnclave() + if err != nil { + result = fmt.Sprintf("ERROR: %v", err) + pdk.Log(pdk.LogError, result) + return + } + result = fmt.Sprintf("SUCCESS: pubkey=%s", enc.PubKeyHex()[:16]) + pdk.Log(pdk.LogInfo, result) + }() + + output := map[string]string{"result": result} + if err := pdk.OutputJSON(output); err != nil { + pdk.SetError(err) + return 1 + } + return 0 +} + //go:wasmexport ping func ping() int32 { pdk.Log(pdk.LogInfo, "ping: received request") @@ -53,7 +85,7 @@ func ping() int32 { //go:wasmexport generate func generate() int32 { - pdk.Log(pdk.LogInfo, "generate: starting database initialization with MPC") + pdk.Log(pdk.LogInfo, "generate: starting database initialization") var input types.GenerateInput if err := pdk.InputJSON(&input); err != nil { @@ -72,15 +104,27 @@ func generate() int32 { return 1 } - result, err := initializeWithMPC(credentialBytes) + pdk.Log(pdk.LogInfo, "generate: opening keybase") + kb, err := keybase.Open() if err != nil { - pdk.SetError(fmt.Errorf("generate: failed to initialize: %w", err)) + pdk.SetError(fmt.Errorf("generate: open database: %w", err)) return 1 } - state.SetInitialized(true) - state.SetDID(result.DID) + pdk.Log(pdk.LogInfo, "generate: initializing DID") + ctx := context.Background() + did, err := kb.Initialize(ctx, credentialBytes) + if err != nil { + pdk.SetError(fmt.Errorf("generate: initialize DID: %w", err)) + return 1 + } + pdk.Log(pdk.LogInfo, fmt.Sprintf("generate: DID created: %s", did)) + + state.SetInitialized(true) + state.SetDID(did) + + pdk.Log(pdk.LogInfo, "generate: serializing database") dbBytes, err := serializeDatabase() if err != nil { pdk.SetError(fmt.Errorf("generate: failed to serialize database: %w", err)) @@ -88,11 +132,8 @@ func generate() int32 { } output := types.GenerateOutput{ - DID: result.DID, - Database: dbBytes, - EnclaveID: result.EnclaveID, - PublicKey: result.PublicKey, - Accounts: result.Accounts, + DID: did, + Database: dbBytes, } if err := pdk.OutputJSON(output); err != nil { @@ -100,7 +141,7 @@ func generate() int32 { return 1 } - pdk.Log(pdk.LogInfo, fmt.Sprintf("generate: created DID %s with enclave %s", result.DID, result.EnclaveID)) + pdk.Log(pdk.LogInfo, fmt.Sprintf("generate: created DID %s (no MPC)", did)) return 0 } -- 2.43.0 From e7202256c35f6a17578725716acbd602b48b8f12 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Sat, 10 Jan 2026 14:54:39 -0500 Subject: [PATCH 30/35] feat(example): add MPC generation test interface --- example/index.html | 8 ++++++++ example/main.js | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/example/index.html b/example/index.html index fc308fd..14d6781 100644 --- a/example/index.html +++ b/example/index.html @@ -407,6 +407,14 @@
+ +
+

test_mpc() debug

+
+ +
+
+
diff --git a/example/main.js b/example/main.js index 97b527f..53c8e1c 100644 --- a/example/main.js +++ b/example/main.js @@ -147,6 +147,30 @@ window.testPing = async function() { } }; +window.testMPC = async function() { + if (!enclave) return log('mpc', 'err', 'Plugin not loaded'); + + log('mpc', 'info', 'Testing MPC key generation...'); + + try { + const result = await enclave.plugin.call('test_mpc', '{}'); + if (!result) { + log('mpc', 'err', 'No response from plugin'); + return; + } + const output = result.json(); + if (output.result?.startsWith('SUCCESS')) { + log('mpc', 'ok', output.result); + } else if (output.result?.startsWith('PANIC')) { + log('mpc', 'err', output.result); + } else { + log('mpc', 'err', output.result || 'Unknown error'); + } + } catch (err) { + log('mpc', 'err', `Exception: ${err?.message || JSON.stringify(err)}`); + } +}; + window.testGenerate = async function() { if (!enclave) return log('generate', 'err', 'Plugin not loaded'); -- 2.43.0 From 876deb298954db27071f16edf14560d0b5d23b44 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Sat, 10 Jan 2026 15:39:41 -0500 Subject: [PATCH 31/35] refactor(mpc): migrate to simple enclaves --- cmd/enclave/main.go | 69 ++----- example/index.html | 8 +- example/main.js | 24 --- internal/crypto/mpc/codec.go | 107 +--------- internal/crypto/mpc/codec_test.go | 178 ---------------- internal/crypto/mpc/enclave.go | 158 -------------- internal/crypto/mpc/enclave_test.go | 307 ---------------------------- internal/crypto/mpc/import.go | 140 ------------- internal/crypto/mpc/protocol.go | 91 --------- internal/crypto/mpc/utils.go | 127 ------------ internal/keybase/functions.go | 101 +++++---- 11 files changed, 77 insertions(+), 1233 deletions(-) delete mode 100644 internal/crypto/mpc/codec_test.go delete mode 100644 internal/crypto/mpc/enclave.go delete mode 100644 internal/crypto/mpc/enclave_test.go delete mode 100644 internal/crypto/mpc/import.go delete mode 100644 internal/crypto/mpc/protocol.go diff --git a/cmd/enclave/main.go b/cmd/enclave/main.go index 3d49f82..4d8bf74 100644 --- a/cmd/enclave/main.go +++ b/cmd/enclave/main.go @@ -17,43 +17,10 @@ import ( "enclave/internal/types" "github.com/extism/go-pdk" - "github.com/sonr-io/crypto/core/protocol" ) func main() { state.Default() } -//go:wasmexport test_mpc -func testMPC() int32 { - pdk.Log(pdk.LogInfo, "test_mpc: starting MPC test") - - var result string - func() { - defer func() { - if r := recover(); r != nil { - result = fmt.Sprintf("PANIC: %v", r) - pdk.Log(pdk.LogError, result) - } - }() - - pdk.Log(pdk.LogInfo, "test_mpc: calling mpc.NewEnclave()") - enc, err := mpc.NewEnclave() - if err != nil { - result = fmt.Sprintf("ERROR: %v", err) - pdk.Log(pdk.LogError, result) - return - } - result = fmt.Sprintf("SUCCESS: pubkey=%s", enc.PubKeyHex()[:16]) - pdk.Log(pdk.LogInfo, result) - }() - - output := map[string]string{"result": result} - if err := pdk.OutputJSON(output); err != nil { - pdk.SetError(err) - return 1 - } - return 0 -} - //go:wasmexport ping func ping() int32 { pdk.Log(pdk.LogInfo, "ping: received request") @@ -310,26 +277,16 @@ func initializeWithMPC(credentialBytes []byte) (*initResult, error) { } pdk.Log(pdk.LogInfo, fmt.Sprintf("initializeWithMPC: step 4 - DID initialized: %s", did)) - pdk.Log(pdk.LogInfo, "initializeWithMPC: step 5 - generating MPC enclave") - enclave, err := mpc.NewEnclave() + pdk.Log(pdk.LogInfo, "initializeWithMPC: step 5 - generating simple enclave") + simpleEnc, err := mpc.NewSimpleEnclave() if err != nil { - pdk.Log(pdk.LogError, fmt.Sprintf("initializeWithMPC: MPC enclave generation failed: %v", err)) - return nil, fmt.Errorf("generate MPC enclave: %w", err) + pdk.Log(pdk.LogError, fmt.Sprintf("initializeWithMPC: enclave generation failed: %v", err)) + return nil, fmt.Errorf("generate enclave: %w", err) } - pdk.Log(pdk.LogInfo, "initializeWithMPC: step 6 - MPC enclave generated") + pdk.Log(pdk.LogInfo, "initializeWithMPC: step 6 - enclave generated") - enclaveData := enclave.GetData() enclaveID := fmt.Sprintf("enc_%x", credentialBytes[:8]) - valShareStr, err := protocol.EncodeMessage(enclaveData.ValShare) - if err != nil { - return nil, fmt.Errorf("encode val share: %w", err) - } - userShareStr, err := protocol.EncodeMessage(enclaveData.UserShare) - if err != nil { - return nil, fmt.Errorf("encode user share: %w", err) - } - am, err := keybase.NewActionManager() if err != nil { return nil, fmt.Errorf("action manager: %w", err) @@ -337,12 +294,12 @@ func initializeWithMPC(credentialBytes []byte) (*initResult, error) { enc, err := am.CreateEnclave(ctx, keybase.NewEnclaveInput{ EnclaveID: enclaveID, - PublicKeyHex: enclaveData.PubHex, - PublicKey: enclaveData.PubBytes, - ValShare: []byte(valShareStr), - UserShare: []byte(userShareStr), - Nonce: enclaveData.Nonce, - Curve: string(enclaveData.Curve), + PublicKeyHex: simpleEnc.PubKeyHex(), + PublicKey: simpleEnc.PubKeyBytes(), + ValShare: simpleEnc.GetShare1(), + UserShare: simpleEnc.GetShare2(), + Nonce: simpleEnc.GetNonce(), + Curve: string(simpleEnc.GetCurve()), }) if err != nil { return nil, fmt.Errorf("store enclave: %w", err) @@ -350,7 +307,7 @@ func initializeWithMPC(credentialBytes []byte) (*initResult, error) { pdk.Log(pdk.LogInfo, fmt.Sprintf("initializeWithMPC: stored enclave %s", enclaveID)) - accounts, err := createDefaultAccounts(ctx, am, enc.ID, enclaveData.PubBytes) + accounts, err := createDefaultAccounts(ctx, am, enc.ID, simpleEnc.PubKeyBytes()) if err != nil { pdk.Log(pdk.LogWarn, fmt.Sprintf("initializeWithMPC: failed to create accounts: %s", err)) accounts = []types.AccountInfo{} @@ -359,7 +316,7 @@ func initializeWithMPC(credentialBytes []byte) (*initResult, error) { return &initResult{ DID: did, EnclaveID: enclaveID, - PublicKey: enclaveData.PubHex, + PublicKey: simpleEnc.PubKeyHex(), Accounts: accounts, }, nil } diff --git a/example/index.html b/example/index.html index 14d6781..768040a 100644 --- a/example/index.html +++ b/example/index.html @@ -408,13 +408,7 @@
-
-

test_mpc() debug

-
- -
-
-
+
diff --git a/example/main.js b/example/main.js index 53c8e1c..97b527f 100644 --- a/example/main.js +++ b/example/main.js @@ -147,30 +147,6 @@ window.testPing = async function() { } }; -window.testMPC = async function() { - if (!enclave) return log('mpc', 'err', 'Plugin not loaded'); - - log('mpc', 'info', 'Testing MPC key generation...'); - - try { - const result = await enclave.plugin.call('test_mpc', '{}'); - if (!result) { - log('mpc', 'err', 'No response from plugin'); - return; - } - const output = result.json(); - if (output.result?.startsWith('SUCCESS')) { - log('mpc', 'ok', output.result); - } else if (output.result?.startsWith('PANIC')) { - log('mpc', 'err', output.result); - } else { - log('mpc', 'err', output.result || 'Unknown error'); - } - } catch (err) { - log('mpc', 'err', `Exception: ${err?.message || JSON.stringify(err)}`); - } -}; - window.testGenerate = async function() { if (!enclave) return log('generate', 'err', 'Plugin not loaded'); diff --git a/internal/crypto/mpc/codec.go b/internal/crypto/mpc/codec.go index 305ac2d..ba564aa 100644 --- a/internal/crypto/mpc/codec.go +++ b/internal/crypto/mpc/codec.go @@ -1,110 +1,11 @@ -// Package mpc implements the Sonr MPC protocol package mpc -import ( - "crypto/rand" - - "github.com/sonr-io/crypto/core/curves" - "github.com/sonr-io/crypto/core/protocol" - "github.com/sonr-io/crypto/tecdsa/dklsv1/dkg" -) - type CurveName string const ( - K256Name CurveName = "secp256k1" - BLS12381G1Name CurveName = "BLS12381G1" - BLS12381G2Name CurveName = "BLS12381G2" - BLS12831Name CurveName = "BLS12831" - P256Name CurveName = "P-256" - ED25519Name CurveName = "ed25519" - PallasName CurveName = "pallas" - BLS12377G1Name CurveName = "BLS12377G1" - BLS12377G2Name CurveName = "BLS12377G2" - BLS12377Name CurveName = "BLS12377" + K256Name CurveName = "secp256k1" + P256Name CurveName = "P-256" + ED25519Name CurveName = "ed25519" ) -func (c CurveName) String() string { - return string(c) -} - -func (c CurveName) Curve() *curves.Curve { - switch c { - case K256Name: - return curves.K256() - case BLS12381G1Name: - return curves.BLS12381G1() - case BLS12381G2Name: - return curves.BLS12381G2() - case BLS12831Name: - return curves.BLS12381G1() - case P256Name: - return curves.P256() - case ED25519Name: - return curves.ED25519() - case PallasName: - return curves.PALLAS() - case BLS12377G1Name: - return curves.BLS12377G1() - case BLS12377G2Name: - return curves.BLS12377G2() - case BLS12377Name: - return curves.BLS12377G1() - default: - return curves.K256() - } -} - -// ╭───────────────────────────────────────────────────────────╮ -// │ Exported Generics │ -// ╰───────────────────────────────────────────────────────────╯ - -type ( - AliceOut *dkg.AliceOutput - BobOut *dkg.BobOutput - Point curves.Point - Role string // Role is the type for the role - Message *protocol.Message // Message is the protocol.Message that is used for MPC - Signature *curves.EcdsaSignature // Signature is the type for the signature - RefreshFunc interface{ protocol.Iterator } // RefreshFunc is the type for the refresh function - SignFunc interface{ protocol.Iterator } // SignFunc is the type for the sign function -) - -const ( - RoleVal = "validator" - RoleUser = "user" -) - -func randNonce() []byte { - nonce := make([]byte, 12) - rand.Read(nonce) - return nonce -} - -// Enclave defines the interface for key management operations -type Enclave interface { - GetData() *EnclaveData // GetData returns the data of the keyEnclave - GetEnclave() Enclave // GetEnclave returns the enclave of the keyEnclave - Decrypt( - key []byte, - encryptedData []byte, - ) ([]byte, error) // Decrypt returns decrypted enclave data - Encrypt( - key []byte, - ) ([]byte, error) // Encrypt returns encrypted enclave data - IsValid() bool // IsValid returns true if the keyEnclave is valid - PubKeyBytes() []byte // PubKeyBytes returns the public key of the keyEnclave - PubKeyHex() string // PubKeyHex returns the public key of the keyEnclave - Refresh() (Enclave, error) // Refresh returns a new keyEnclave - Marshal() ([]byte, error) // Serialize returns the serialized keyEnclave - Sign( - data []byte, - ) ([]byte, error) // Sign returns the signature of the data - Unmarshal( - data []byte, - ) error // Verify returns true if the signature is valid - Verify( - data []byte, - sig []byte, - ) (bool, error) // Verify returns true if the signature is valid -} +func (c CurveName) String() string { return string(c) } diff --git a/internal/crypto/mpc/codec_test.go b/internal/crypto/mpc/codec_test.go deleted file mode 100644 index 933b93a..0000000 --- a/internal/crypto/mpc/codec_test.go +++ /dev/null @@ -1,178 +0,0 @@ -package mpc - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestKeyShareGeneration(t *testing.T) { - t.Run("Generate Valid Enclave", func(t *testing.T) { - // Generate enclave - enclave, err := NewEnclave() - require.NoError(t, err) - require.NotNil(t, enclave) - - // Validate enclave contents - assert.True(t, enclave.IsValid()) - }) - - t.Run("Export and Import", func(t *testing.T) { - // Generate original enclave - original, err := NewEnclave() - require.NoError(t, err) - - // Test key for encryption/decryption (32 bytes) - testKey := []byte("test-key-12345678-test-key-123456") - - // Test Export/Import - t.Run("Full Enclave", func(t *testing.T) { - // Export enclave - data, err := original.Encrypt(testKey) - require.NoError(t, err) - require.NotEmpty(t, data) - - // Create new empty enclave - newEnclave, err := NewEnclave() - require.NoError(t, err) - - // Verify the imported enclave works by signing - testData := []byte("test message") - sig, err := newEnclave.Sign(testData) - require.NoError(t, err) - valid, err := newEnclave.Verify(testData, sig) - require.NoError(t, err) - assert.True(t, valid) - }) - }) - - t.Run("Encrypt and Decrypt", func(t *testing.T) { - // Generate original enclave - original, err := NewEnclave() - require.NoError(t, err) - require.NotNil(t, original) - - // Test key for encryption/decryption (32 bytes) - testKey := []byte("test-key-12345678-test-key-123456") - - // Test Encrypt - encrypted, err := original.Encrypt(testKey) - require.NoError(t, err) - require.NotEmpty(t, encrypted) - - // Test Decrypt - decrypted, err := original.Decrypt(testKey, encrypted) - require.NoError(t, err) - require.NotEmpty(t, decrypted) - - // Verify decrypted data matches original - originalData, err := original.Marshal() - require.NoError(t, err) - assert.Equal(t, originalData, decrypted) - - // Test with wrong key should fail - wrongKey := []byte("wrong-key-12345678-wrong-key-123456") - _, err = original.Decrypt(wrongKey, encrypted) - assert.Error(t, err, "Decryption with wrong key should fail") - }) -} - -func TestEnclaveOperations(t *testing.T) { - t.Run("Signing and Verification", func(t *testing.T) { - // Generate valid enclave - enclave, err := NewEnclave() - require.NoError(t, err) - - // Test signing - testData := []byte("test message") - signature, err := enclave.Sign(testData) - require.NoError(t, err) - require.NotNil(t, signature) - - // Verify the signature - valid, err := enclave.Verify(testData, signature) - require.NoError(t, err) - assert.True(t, valid) - - // Test invalid data verification - invalidData := []byte("wrong message") - valid, err = enclave.Verify(invalidData, signature) - require.NoError(t, err) - assert.False(t, valid) - }) - - t.Run("Refresh Operation", func(t *testing.T) { - enclave, err := NewEnclave() - require.NoError(t, err) - - // Test refresh - refreshedEnclave, err := enclave.Refresh() - require.NoError(t, err) - require.NotNil(t, refreshedEnclave) - - // Verify refreshed enclave is valid - assert.True(t, refreshedEnclave.IsValid()) - }) -} - -func TestEnclaveDataAccess(t *testing.T) { - t.Run("GetData", func(t *testing.T) { - // Generate enclave - enclave, err := NewEnclave() - require.NoError(t, err) - require.NotNil(t, enclave) - - // Get the enclave data - data := enclave.GetData() - require.NotNil(t, data, "GetData should return non-nil value") - - // Verify the data is valid - assert.True(t, data.IsValid(), "Enclave data should be valid") - - // Verify the public key in the data matches the enclave's public key - assert.Equal(t, enclave.PubKeyHex(), data.PubKeyHex(), "Public keys should match") - }) - - t.Run("PubKeyHex", func(t *testing.T) { - // Generate enclave - enclave, err := NewEnclave() - require.NoError(t, err) - require.NotNil(t, enclave) - - // Get the public key hex - pubKeyHex := enclave.PubKeyHex() - require.NotEmpty(t, pubKeyHex, "PubKeyHex should return non-empty string") - - // Check that it's a valid hex string (should be 66 chars for compressed point: 0x02/0x03 + 32 bytes) - assert.GreaterOrEqual( - t, - len(pubKeyHex), - 66, - "Public key hex should be at least 66 characters", - ) - assert.True(t, len(pubKeyHex)%2 == 0, "Hex string should have even length") - - // Compare with the enclave data's public key - data := enclave.GetData() - assert.Equal( - t, - data.PubKeyHex(), - pubKeyHex, - "Public key hex should match the one from GetData", - ) - - // Verify that two different enclaves have different public keys - enclave2, err := NewEnclave() - require.NoError(t, err) - require.NotNil(t, enclave2) - - pubKeyHex2 := enclave2.PubKeyHex() - assert.NotEqual( - t, - pubKeyHex, - pubKeyHex2, - "Different enclaves should have different public keys", - ) - }) -} diff --git a/internal/crypto/mpc/enclave.go b/internal/crypto/mpc/enclave.go deleted file mode 100644 index be7c941..0000000 --- a/internal/crypto/mpc/enclave.go +++ /dev/null @@ -1,158 +0,0 @@ -package mpc - -import ( - "crypto/aes" - "crypto/cipher" - "crypto/ecdsa" - "encoding/json" - "fmt" - - "github.com/sonr-io/crypto/core/curves" - "golang.org/x/crypto/sha3" -) - -// EnclaveData implements the Enclave interface -type EnclaveData struct { - PubHex string `json:"pub_hex"` // PubHex is the hex-encoded compressed public key - PubBytes []byte `json:"pub_bytes"` // PubBytes is the uncompressed public key - ValShare Message `json:"val_share"` - UserShare Message `json:"user_share"` - Nonce []byte `json:"nonce"` - Curve CurveName `json:"curve"` -} - -// GetData returns the data of the keyEnclave -func (k *EnclaveData) GetData() *EnclaveData { - return k -} - -// GetEnclave returns the enclave of the keyEnclave -func (k *EnclaveData) GetEnclave() Enclave { - return k -} - -// GetPubPoint returns the public point of the keyEnclave -func (k *EnclaveData) GetPubPoint() (curves.Point, error) { - curve := k.Curve.Curve() - return curve.NewIdentityPoint().FromAffineUncompressed(k.PubBytes) -} - -// PubKeyHex returns the public key of the keyEnclave -func (k *EnclaveData) PubKeyHex() string { - return k.PubHex -} - -// PubKeyBytes returns the public key of the keyEnclave -func (k *EnclaveData) PubKeyBytes() []byte { - return k.PubBytes -} - -// Decrypt returns decrypted enclave data -func (k *EnclaveData) Decrypt(key []byte, encryptedData []byte) ([]byte, error) { - hashedKey := GetHashKey(key) - block, err := aes.NewCipher(hashedKey) - if err != nil { - return nil, err - } - - aesgcm, err := cipher.NewGCM(block) - if err != nil { - return nil, err - } - - // Decrypt the data using AES-GCM - plaintext, err := aesgcm.Open(nil, k.Nonce, encryptedData, nil) - if err != nil { - return nil, fmt.Errorf("decryption failed: %w", err) - } - return plaintext, nil -} - -// Encrypt returns encrypted enclave data -func (k *EnclaveData) Encrypt(key []byte) ([]byte, error) { - data, err := k.Marshal() - if err != nil { - return nil, fmt.Errorf("failed to serialize enclave: %w", err) - } - - hashedKey := GetHashKey(key) - block, err := aes.NewCipher(hashedKey) - if err != nil { - return nil, err - } - - aesgcm, err := cipher.NewGCM(block) - if err != nil { - return nil, err - } - - return aesgcm.Seal(nil, k.Nonce, data, nil), nil -} - -// IsValid returns true if the keyEnclave is valid -func (k *EnclaveData) IsValid() bool { - return k.ValShare != nil && k.UserShare != nil -} - -// Refresh returns a new keyEnclave -func (k *EnclaveData) Refresh() (Enclave, error) { - refreshFuncVal, err := GetAliceRefreshFunc(k) - if err != nil { - return nil, err - } - refreshFuncUser, err := GetBobRefreshFunc(k) - if err != nil { - return nil, err - } - return ExecuteRefresh(refreshFuncVal, refreshFuncUser, k.Curve) -} - -// Sign returns the signature of the data -func (k *EnclaveData) Sign(data []byte) ([]byte, error) { - userSign, err := GetBobSignFunc(k, data) - if err != nil { - return nil, err - } - valSign, err := GetAliceSignFunc(k, data) - if err != nil { - return nil, err - } - return ExecuteSigning(valSign, userSign) -} - -// Verify returns true if the signature is valid -func (k *EnclaveData) Verify(data []byte, sig []byte) (bool, error) { - edSig, err := DeserializeSignature(sig) - if err != nil { - return false, err - } - ePub, err := GetECDSAPoint(k.PubBytes) - if err != nil { - return false, err - } - pk := &ecdsa.PublicKey{ - Curve: ePub.Curve, - X: ePub.X, - Y: ePub.Y, - } - - // Hash the message using SHA3-256 - hash := sha3.New256() - hash.Write(data) - digest := hash.Sum(nil) - - return ecdsa.Verify(pk, digest, edSig.R, edSig.S), nil -} - -// Marshal returns the JSON encoding of keyEnclave -func (k *EnclaveData) Marshal() ([]byte, error) { - return json.Marshal(k) -} - -// Unmarshal unmarshals the JSON encoding of keyEnclave -func (k *EnclaveData) Unmarshal(data []byte) error { - if err := json.Unmarshal(data, k); err != nil { - return err - } - return nil -} diff --git a/internal/crypto/mpc/enclave_test.go b/internal/crypto/mpc/enclave_test.go deleted file mode 100644 index 39dc115..0000000 --- a/internal/crypto/mpc/enclave_test.go +++ /dev/null @@ -1,307 +0,0 @@ -package mpc - -import ( - "bytes" - "encoding/hex" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestEnclaveData_GetData(t *testing.T) { - // Create a new enclave - enclave, err := NewEnclave() - require.NoError(t, err) - require.NotNil(t, enclave) - - // Get the data - data := enclave.GetData() - require.NotNil(t, data) - - // Ensure the data is the same instance - assert.Equal(t, enclave, data.GetEnclave()) - - // Ensure the data is valid - assert.True(t, data.IsValid()) -} - -func TestEnclaveData_GetEnclave(t *testing.T) { - // Create a new enclave - enclave, err := NewEnclave() - require.NoError(t, err) - require.NotNil(t, enclave) - - // Get the enclave data - data := enclave.GetData() - require.NotNil(t, data) - - // Get the enclave back - returnedEnclave := data.GetEnclave() - require.NotNil(t, returnedEnclave) - - // Verify the returned enclave is the same - assert.Equal(t, enclave, returnedEnclave) -} - -func TestEnclaveData_GetPubPoint(t *testing.T) { - // Create a new enclave - enclave, err := NewEnclave() - require.NoError(t, err) - require.NotNil(t, enclave) - - // Get the enclave data - data := enclave.GetData() - require.NotNil(t, data) - - // Get the public point - pubPoint, err := data.GetPubPoint() - require.NoError(t, err) - require.NotNil(t, pubPoint) - - // Verify the public point's serialization matches the stored public bytes - pointBytes := pubPoint.ToAffineUncompressed() - assert.Equal(t, data.PubBytes, pointBytes) -} - -func TestEnclaveData_PubKeyHex(t *testing.T) { - // Create a new enclave - enclave, err := NewEnclave() - require.NoError(t, err) - require.NotNil(t, enclave) - - // Get the enclave data - data := enclave.GetData() - require.NotNil(t, data) - - // Get the public key hex - pubKeyHex := data.PubKeyHex() - require.NotEmpty(t, pubKeyHex) - - // Verify it's a valid hex string - _, err = hex.DecodeString(pubKeyHex) - require.NoError(t, err) - - // Verify it matches the stored PubHex - assert.Equal(t, data.PubHex, pubKeyHex) -} - -func TestEnclaveData_PubKeyBytes(t *testing.T) { - // Create a new enclave - enclave, err := NewEnclave() - require.NoError(t, err) - require.NotNil(t, enclave) - - // Get the enclave data - data := enclave.GetData() - require.NotNil(t, data) - - // Get the public key bytes - pubKeyBytes := data.PubKeyBytes() - require.NotEmpty(t, pubKeyBytes) - - // Verify it matches the stored PubBytes - assert.Equal(t, data.PubBytes, pubKeyBytes) -} - -func TestEnclaveData_EncryptDecrypt(t *testing.T) { - // Create a new enclave - enclave, err := NewEnclave() - require.NoError(t, err) - require.NotNil(t, enclave) - - // Get the enclave data - data := enclave.GetData() - require.NotNil(t, data) - - // Test key for encryption/decryption - testKey := []byte("test-key-12345678-test-key-123456") - - // Test encryption - encrypted, err := data.Encrypt(testKey) - require.NoError(t, err) - require.NotEmpty(t, encrypted) - - // Test decryption - decrypted, err := data.Decrypt(testKey, encrypted) - require.NoError(t, err) - require.NotEmpty(t, decrypted) - - // Serialize the original data for comparison - originalData, err := data.Marshal() - require.NoError(t, err) - - // Verify the decrypted data matches the original - assert.Equal(t, originalData, decrypted) - - // Test decryption with wrong key (should fail) - wrongKey := []byte("wrong-key-12345678-wrong-key-123456") - _, err = data.Decrypt(wrongKey, encrypted) - assert.Error(t, err, "Decryption with wrong key should fail") -} - -func TestEnclaveData_IsValid(t *testing.T) { - // Create a new enclave - enclave, err := NewEnclave() - require.NoError(t, err) - require.NotNil(t, enclave) - - // Get the enclave data - data := enclave.GetData() - require.NotNil(t, data) - - // Verify it's valid - assert.True(t, data.IsValid()) - - // Create an invalid enclave - invalidEnclave := &EnclaveData{ - PubHex: "invalid", - PubBytes: []byte("invalid"), - Nonce: []byte("nonce"), - Curve: K256Name, - } - - // Verify it's invalid - assert.False(t, invalidEnclave.IsValid()) -} - -func TestEnclaveData_RefreshAndSign(t *testing.T) { - // Create a new enclave - enclave, err := NewEnclave() - require.NoError(t, err) - require.NotNil(t, enclave) - - // Get the original public key - originalPubKeyHex := enclave.PubKeyHex() - originalPubKeyBytes := enclave.PubKeyBytes() - require.NotEmpty(t, originalPubKeyHex) - require.NotEmpty(t, originalPubKeyBytes) - - // Sign a message with the original enclave to verify it works - testMessage := []byte("test message before refresh") - originalSignature, err := enclave.Sign(testMessage) - require.NoError(t, err) - require.NotEmpty(t, originalSignature) - - // Verify the original signature - valid, err := enclave.Verify(testMessage, originalSignature) - require.NoError(t, err) - assert.True(t, valid, "Original signature should be valid") - - // Refresh the enclave - refreshedEnclave, err := enclave.Refresh() - require.NoError(t, err) - require.NotNil(t, refreshedEnclave) - - // CRITICAL TEST: The public key should remain the same after refresh - refreshedPubKeyHex := refreshedEnclave.PubKeyHex() - refreshedPubKeyBytes := refreshedEnclave.PubKeyBytes() - - assert.Equal(t, originalPubKeyHex, refreshedPubKeyHex, - "Public key hex should not change after refresh") - assert.Equal(t, originalPubKeyBytes, refreshedPubKeyBytes, - "Public key bytes should not change after refresh") - - // Verify the refreshed enclave is valid - assert.True(t, refreshedEnclave.IsValid(), "Refreshed enclave should be valid") - - // Test that the refreshed enclave can still sign messages - testMessage2 := []byte("test message after refresh") - refreshedSignature, err := refreshedEnclave.Sign(testMessage2) - require.NoError(t, err) - require.NotEmpty(t, refreshedSignature) - - // Verify the signature from the refreshed enclave with its own key - valid, err = refreshedEnclave.Verify(testMessage2, refreshedSignature) - require.NoError(t, err) - assert.True(t, valid, "Signature from refreshed enclave should be valid") - - // CRITICAL TEST: The original enclave should be able to verify the signature - // from the refreshed enclave since they have the same public key - valid, err = enclave.Verify(testMessage2, refreshedSignature) - require.NoError(t, err) - assert.True(t, valid, "Original enclave should be able to verify refreshed enclave's signature") - - // CRITICAL TEST: The refreshed enclave should be able to verify the signature - // from the original enclave since they have the same public key - valid, err = refreshedEnclave.Verify(testMessage, originalSignature) - require.NoError(t, err) - assert.True(t, valid, "Refreshed enclave should be able to verify original enclave's signature") - - // Test with wrong message (should fail) - wrongMessage := []byte("wrong message") - valid, err = refreshedEnclave.Verify(wrongMessage, refreshedSignature) - require.NoError(t, err) - assert.False(t, valid, "Wrong message verification should fail") -} - -func TestEnclaveData_MarshalUnmarshal(t *testing.T) { - // Create a new enclave - enclave, err := NewEnclave() - require.NoError(t, err) - require.NotNil(t, enclave) - - // Get the enclave data - data := enclave.GetData() - require.NotNil(t, data) - - // Marshal the enclave - encoded, err := data.Marshal() - require.NoError(t, err) - require.NotEmpty(t, encoded) - - // Create a new empty enclave - newEnclave := &EnclaveData{} - - // Unmarshal the encoded data - err = newEnclave.Unmarshal(encoded) - require.NoError(t, err) - - // Verify the unmarshaled enclave matches the original - assert.Equal(t, data.PubHex, newEnclave.PubHex) - assert.Equal(t, data.Curve, newEnclave.Curve) - assert.True(t, bytes.Equal(data.PubBytes, newEnclave.PubBytes)) - assert.True(t, bytes.Equal(data.Nonce, newEnclave.Nonce)) - assert.True(t, newEnclave.IsValid()) - - // Verify the public key matches - assert.Equal(t, data.PubKeyHex(), newEnclave.PubKeyHex()) -} - -func TestEnclaveData_Verify(t *testing.T) { - // Create a new enclave - enclave, err := NewEnclave() - require.NoError(t, err) - require.NotNil(t, enclave) - - // Sign a message - testMessage := []byte("test message") - signature, err := enclave.Sign(testMessage) - require.NoError(t, err) - require.NotEmpty(t, signature) - - // Verify the signature - valid, err := enclave.Verify(testMessage, signature) - require.NoError(t, err) - assert.True(t, valid) - - // Verify with wrong message - wrongMessage := []byte("wrong message") - valid, err = enclave.Verify(wrongMessage, signature) - require.NoError(t, err) - assert.False(t, valid) - - // Corrupt the signature - corruptedSig := make([]byte, len(signature)) - copy(corruptedSig, signature) - corruptedSig[0] ^= 0x01 // flip a bit - - // Verify with corrupted signature (should fail) - valid, err = enclave.Verify(testMessage, corruptedSig) - require.NoError(t, err) - assert.False(t, valid) - - // We don't need to manually create ECDSA signatures here - // as we already verified the Sign and Verify functions work together. - // This completes the verification of the enclave's signature functionality. -} diff --git a/internal/crypto/mpc/import.go b/internal/crypto/mpc/import.go deleted file mode 100644 index ccc116d..0000000 --- a/internal/crypto/mpc/import.go +++ /dev/null @@ -1,140 +0,0 @@ -package mpc - -import ( - "encoding/hex" - "errors" - "fmt" -) - -// ImportEnclave creates an Enclave instance from various import options. -// It prioritizes enclave bytes over keyshares if both are provided. -func ImportEnclave(options ...ImportOption) (Enclave, error) { - if len(options) == 0 { - return nil, errors.New("no import options provided") - } - - opts := Options{} - for _, opt := range options { - opts = opt(opts) - } - return opts.Apply() -} - -// Options is a struct that holds the import options -type Options struct { - valKeyshare Message - userKeyshare Message - enclaveBytes []byte - enclaveData *EnclaveData - initialShares bool - isEncrypted bool - secretKey []byte - curve CurveName -} - -// ImportOption is a function that modifies the import options -type ImportOption func(Options) Options - -// WithInitialShares creates an option to import an enclave from validator and user keyshares. -func WithInitialShares(valKeyshare Message, userKeyshare Message, curve CurveName) ImportOption { - return func(opts Options) Options { - opts.valKeyshare = valKeyshare - opts.userKeyshare = userKeyshare - opts.initialShares = true - opts.curve = curve - return opts - } -} - -// WithEncryptedData creates an option to import an enclave from encrypted data. -func WithEncryptedData(data []byte, key []byte) ImportOption { - return func(opts Options) Options { - opts.enclaveBytes = data - opts.initialShares = false - opts.isEncrypted = true - opts.secretKey = key - return opts - } -} - -// WithEnclaveData creates an option to import an enclave from a data struct. -func WithEnclaveData(data *EnclaveData) ImportOption { - return func(opts Options) Options { - opts.enclaveData = data - opts.initialShares = false - return opts - } -} - -// Apply applies the import options to create an Enclave instance. -func (opts Options) Apply() (Enclave, error) { - // Load from encrypted data if provided - if opts.isEncrypted { - if len(opts.enclaveBytes) == 0 { - return nil, errors.New("enclave bytes cannot be empty") - } - return RestoreEncryptedEnclave(opts.enclaveBytes, opts.secretKey) - } - // Generate from keyshares if provided - if opts.initialShares { - // Then try to build from keyshares - if opts.valKeyshare == nil { - return nil, errors.New("validator share cannot be nil") - } - if opts.userKeyshare == nil { - return nil, errors.New("user share cannot be nil") - } - return BuildEnclave(opts.valKeyshare, opts.userKeyshare, opts) - } - // Load from enclave data if provided - return RestoreEnclaveFromData(opts.enclaveData) -} - -// BuildEnclave creates a new enclave from validator and user keyshares. -func BuildEnclave(valShare, userShare Message, options Options) (Enclave, error) { - if valShare == nil { - return nil, errors.New("validator share cannot be nil") - } - if userShare == nil { - return nil, errors.New("user share cannot be nil") - } - - pubPoint, err := GetAlicePublicPoint(valShare) - if err != nil { - return nil, fmt.Errorf("failed to get public point: %w", err) - } - return &EnclaveData{ - PubBytes: pubPoint.ToAffineUncompressed(), - PubHex: hex.EncodeToString(pubPoint.ToAffineCompressed()), - ValShare: valShare, - UserShare: userShare, - Nonce: randNonce(), - Curve: options.curve, - }, nil -} - -// RestoreEnclaveFromData deserializes an enclave from its data struct. -func RestoreEnclaveFromData(data *EnclaveData) (Enclave, error) { - if data == nil { - return nil, errors.New("enclave data cannot be nil") - } - return data, nil -} - -// RestoreEncryptedEnclave decrypts an enclave from its binary representation. and key -func RestoreEncryptedEnclave(data []byte, key []byte) (Enclave, error) { - keyclave := &EnclaveData{} - err := keyclave.Unmarshal(data) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal enclave: %w", err) - } - decryptedData, err := keyclave.Decrypt(key, data) - if err != nil { - return nil, fmt.Errorf("failed to decrypt enclave: %w", err) - } - err = keyclave.Unmarshal(decryptedData) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal decrypted enclave: %w", err) - } - return keyclave, nil -} diff --git a/internal/crypto/mpc/protocol.go b/internal/crypto/mpc/protocol.go deleted file mode 100644 index 3ff91df..0000000 --- a/internal/crypto/mpc/protocol.go +++ /dev/null @@ -1,91 +0,0 @@ -package mpc - -import ( - "github.com/sonr-io/crypto/core/protocol" - "github.com/sonr-io/crypto/tecdsa/dklsv1" -) - -// NewEnclave generates a new MPC keyshare -func NewEnclave() (Enclave, error) { - curve := K256Name.Curve() - valKs := dklsv1.NewAliceDkg(curve, protocol.Version1) - userKs := dklsv1.NewBobDkg(curve, protocol.Version1) - aErr, bErr := RunProtocol(userKs, valKs) - if err := CheckIteratedErrors(aErr, bErr); err != nil { - return nil, err - } - valRes, err := valKs.Result(protocol.Version1) - if err != nil { - return nil, err - } - userRes, err := userKs.Result(protocol.Version1) - if err != nil { - return nil, err - } - return ImportEnclave(WithInitialShares(valRes, userRes, K256Name)) -} - -// ExecuteSigning runs the MPC signing protocol -func ExecuteSigning(signFuncVal SignFunc, signFuncUser SignFunc) ([]byte, error) { - aErr, bErr := RunProtocol(signFuncVal, signFuncUser) - if err := CheckIteratedErrors(aErr, bErr); err != nil { - return nil, err - } - out, err := signFuncUser.Result(protocol.Version1) - if err != nil { - return nil, err - } - s, err := dklsv1.DecodeSignature(out) - if err != nil { - return nil, err - } - sig, err := SerializeSignature(s) - if err != nil { - return nil, err - } - return sig, nil -} - -// ExecuteRefresh runs the MPC refresh protocol -func ExecuteRefresh( - refreshFuncVal RefreshFunc, - refreshFuncUser RefreshFunc, - curve CurveName, -) (Enclave, error) { - aErr, bErr := RunProtocol(refreshFuncVal, refreshFuncUser) - if err := CheckIteratedErrors(aErr, bErr); err != nil { - return nil, err - } - valRefreshResult, err := refreshFuncVal.Result(protocol.Version1) - if err != nil { - return nil, err - } - userRefreshResult, err := refreshFuncUser.Result(protocol.Version1) - if err != nil { - return nil, err - } - return ImportEnclave(WithInitialShares(valRefreshResult, userRefreshResult, curve)) -} - -// RunProtocol runs the MPC protocol -func RunProtocol(firstParty protocol.Iterator, secondParty protocol.Iterator) (error, error) { - var ( - message *protocol.Message - aErr error - bErr error - ) - - for aErr != protocol.ErrProtocolFinished || bErr != protocol.ErrProtocolFinished { - // Crank each protocol forward one iteration - message, bErr = firstParty.Next(message) - if bErr != nil && bErr != protocol.ErrProtocolFinished { - return nil, bErr - } - - message, aErr = secondParty.Next(message) - if aErr != nil && aErr != protocol.ErrProtocolFinished { - return aErr, nil - } - } - return aErr, bErr -} diff --git a/internal/crypto/mpc/utils.go b/internal/crypto/mpc/utils.go index ef0c188..b0407b6 100644 --- a/internal/crypto/mpc/utils.go +++ b/internal/crypto/mpc/utils.go @@ -1,97 +1,12 @@ package mpc import ( - "crypto/aes" - "crypto/cipher" - "errors" "fmt" "math/big" "github.com/sonr-io/crypto/core/curves" - "github.com/sonr-io/crypto/core/protocol" - "github.com/sonr-io/crypto/tecdsa/dklsv1" - "golang.org/x/crypto/sha3" ) -func CheckIteratedErrors(aErr, bErr error) error { - if aErr == protocol.ErrProtocolFinished && bErr == protocol.ErrProtocolFinished { - return nil - } - if aErr != protocol.ErrProtocolFinished { - return aErr - } - if bErr != protocol.ErrProtocolFinished { - return bErr - } - return nil -} - -func GetHashKey(key []byte) []byte { - hash := sha3.New256() - hash.Write(key) - return hash.Sum(nil)[:32] // Use first 32 bytes of hash -} - -func DecryptKeyshare(msg []byte, key []byte, nonce []byte) ([]byte, error) { - hashedKey := GetHashKey(key) - block, err := aes.NewCipher(hashedKey) - if err != nil { - return nil, err - } - aesgcm, err := cipher.NewGCM(block) - if err != nil { - return nil, err - } - plaintext, err := aesgcm.Open(nil, nonce, msg, nil) - if err != nil { - return nil, err - } - return plaintext, nil -} - -func EncryptKeyshare(msg Message, key []byte, nonce []byte) ([]byte, error) { - hashedKey := GetHashKey(key) - msgBytes, err := protocol.EncodeMessage(msg) - if err != nil { - return nil, err - } - block, err := aes.NewCipher(hashedKey) - if err != nil { - return nil, err - } - aesgcm, err := cipher.NewGCM(block) - if err != nil { - return nil, err - } - ciphertext := aesgcm.Seal(nil, nonce, []byte(msgBytes), nil) - return ciphertext, nil -} - -func GetAliceOut(msg *protocol.Message) (AliceOut, error) { - return dklsv1.DecodeAliceDkgResult(msg) -} - -func GetAlicePublicPoint(msg *protocol.Message) (Point, error) { - out, err := dklsv1.DecodeAliceDkgResult(msg) - if err != nil { - return nil, err - } - return out.PublicKey, nil -} - -func GetBobOut(msg *protocol.Message) (BobOut, error) { - return dklsv1.DecodeBobDkgResult(msg) -} - -func GetBobPubPoint(msg *protocol.Message) (Point, error) { - out, err := dklsv1.DecodeBobDkgResult(msg) - if err != nil { - return nil, err - } - return out.PublicKey, nil -} - -// GetECDSAPoint builds an elliptic curve point from a compressed byte slice func GetECDSAPoint(pubKey []byte) (*curves.EcPoint, error) { crv := curves.K256() x := new(big.Int).SetBytes(pubKey[1:33]) @@ -103,28 +18,6 @@ func GetECDSAPoint(pubKey []byte) (*curves.EcPoint, error) { return &curves.EcPoint{X: x, Y: y, Curve: ecCurve}, nil } -func SerializeSignature(sig *curves.EcdsaSignature) ([]byte, error) { - if sig == nil { - return nil, errors.New("nil signature") - } - - rBytes := sig.R.Bytes() - sBytes := sig.S.Bytes() - - // Ensure both components are 32 bytes - rPadded := make([]byte, 32) - sPadded := make([]byte, 32) - copy(rPadded[32-len(rBytes):], rBytes) - copy(sPadded[32-len(sBytes):], sBytes) - - // Concatenate R and S - result := make([]byte, 64) - copy(result[0:32], rPadded) - copy(result[32:64], sPadded) - - return result, nil -} - func DeserializeSignature(sigBytes []byte) (*curves.EcdsaSignature, error) { if len(sigBytes) != 64 { return nil, fmt.Errorf("invalid signature length: expected 64 bytes, got %d", len(sigBytes)) @@ -138,23 +31,3 @@ func DeserializeSignature(sigBytes []byte) (*curves.EcdsaSignature, error) { S: s, }, nil } - -func GetAliceSignFunc(k *EnclaveData, bz []byte) (SignFunc, error) { - curve := k.Curve.Curve() - return dklsv1.NewAliceSign(curve, sha3.New256(), bz, k.ValShare, protocol.Version1) -} - -func GetAliceRefreshFunc(k *EnclaveData) (RefreshFunc, error) { - curve := k.Curve.Curve() - return dklsv1.NewAliceRefresh(curve, k.ValShare, protocol.Version1) -} - -func GetBobSignFunc(k *EnclaveData, bz []byte) (SignFunc, error) { - curve := curves.K256() - return dklsv1.NewBobSign(curve, sha3.New256(), bz, k.UserShare, protocol.Version1) -} - -func GetBobRefreshFunc(k *EnclaveData) (RefreshFunc, error) { - curve := curves.K256() - return dklsv1.NewBobRefresh(curve, k.UserShare, protocol.Version1) -} diff --git a/internal/keybase/functions.go b/internal/keybase/functions.go index 4ed8d64..becaa16 100644 --- a/internal/keybase/functions.go +++ b/internal/keybase/functions.go @@ -2,20 +2,19 @@ package keybase import ( "context" + "crypto/rand" "encoding/hex" "fmt" "enclave/internal/crypto/mpc" "github.com/ncruces/go-sqlite3" - "github.com/sonr-io/crypto/core/protocol" ) -// RegisterMPCFunctions registers custom SQLite functions for MPC operations: -// - mpc_sign(enclave_id TEXT, data BLOB) -> BLOB -// - mpc_verify(public_key_hex TEXT, data BLOB, signature BLOB) -> INTEGER (0 or 1) -// - mpc_refresh(enclave_id TEXT) -> TEXT func RegisterMPCFunctions(conn *sqlite3.Conn) error { + if err := registerGenerateFunction(conn); err != nil { + return fmt.Errorf("register mpc_generate: %w", err) + } if err := registerSignFunction(conn); err != nil { return fmt.Errorf("register mpc_sign: %w", err) } @@ -28,6 +27,48 @@ func RegisterMPCFunctions(conn *sqlite3.Conn) error { return nil } +func registerGenerateFunction(conn *sqlite3.Conn) error { + return conn.CreateFunction("mpc_generate", 0, 0, func(ctx sqlite3.Context, args ...sqlite3.Value) { + enc, err := mpc.NewSimpleEnclave() + if err != nil { + ctx.ResultError(fmt.Errorf("mpc_generate: %w", err)) + return + } + + idBytes := make([]byte, 8) + rand.Read(idBytes) + enclaveID := fmt.Sprintf("enc_%x", idBytes) + + kb := Get() + if kb == nil { + ctx.ResultError(fmt.Errorf("mpc_generate: keybase not initialized")) + return + } + + am, err := NewActionManager() + if err != nil { + ctx.ResultError(fmt.Errorf("mpc_generate: action manager: %w", err)) + return + } + + _, err = am.CreateEnclave(context.Background(), NewEnclaveInput{ + EnclaveID: enclaveID, + PublicKeyHex: enc.PubKeyHex(), + PublicKey: enc.PubKeyBytes(), + ValShare: enc.GetShare1(), + UserShare: enc.GetShare2(), + Nonce: enc.GetNonce(), + Curve: string(enc.GetCurve()), + }) + if err != nil { + ctx.ResultError(fmt.Errorf("mpc_generate: create enclave: %w", err)) + return + } + + ctx.ResultText(enclaveID) + }) +} + func registerSignFunction(conn *sqlite3.Conn) error { return conn.CreateFunction("mpc_sign", 2, sqlite3.DETERMINISTIC, func(ctx sqlite3.Context, args ...sqlite3.Value) { if len(args) != 2 { @@ -47,7 +88,7 @@ func registerSignFunction(conn *sqlite3.Conn) error { return } - enclave, err := loadEnclaveFromDB(enclaveID) + enclave, err := loadSimpleEnclaveFromDB(enclaveID) if err != nil { ctx.ResultError(fmt.Errorf("mpc_sign: %w", err)) return @@ -120,7 +161,7 @@ func registerRefreshFunction(conn *sqlite3.Conn) error { return } - enclave, err := loadEnclaveFromDB(enclaveID) + enclave, err := loadSimpleEnclaveFromDB(enclaveID) if err != nil { ctx.ResultError(fmt.Errorf("mpc_refresh: %w", err)) return @@ -132,7 +173,7 @@ func registerRefreshFunction(conn *sqlite3.Conn) error { return } - if err := updateEnclaveInDB(enclaveID, newEnclave); err != nil { + if err := updateSimpleEnclaveInDB(enclaveID, newEnclave); err != nil { ctx.ResultError(fmt.Errorf("mpc_refresh: update failed: %w", err)) return } @@ -141,7 +182,7 @@ func registerRefreshFunction(conn *sqlite3.Conn) error { }) } -func loadEnclaveFromDB(enclaveID string) (mpc.Enclave, error) { +func loadSimpleEnclaveFromDB(enclaveID string) (*mpc.SimpleEnclave, error) { kb := Get() if kb == nil { return nil, fmt.Errorf("keybase not initialized") @@ -152,55 +193,31 @@ func loadEnclaveFromDB(enclaveID string) (mpc.Enclave, error) { return nil, fmt.Errorf("enclave not found: %w", err) } - valShare, err := protocol.DecodeMessage(string(dbEnc.ValShare)) - if err != nil { - return nil, fmt.Errorf("decode val_share: %w", err) - } - - userShare, err := protocol.DecodeMessage(string(dbEnc.UserShare)) - if err != nil { - return nil, fmt.Errorf("decode user_share: %w", err) - } - - return &mpc.EnclaveData{ - PubHex: dbEnc.PublicKeyHex, - PubBytes: dbEnc.PublicKey, - ValShare: valShare, - UserShare: userShare, - Nonce: dbEnc.Nonce, - Curve: mpc.CurveName(dbEnc.Curve), - }, nil + return mpc.ImportSimpleEnclave( + dbEnc.PublicKey, + dbEnc.ValShare, + dbEnc.UserShare, + dbEnc.Nonce, + mpc.CurveName(dbEnc.Curve), + ) } -func updateEnclaveInDB(enclaveID string, enclave mpc.Enclave) error { +func updateSimpleEnclaveInDB(enclaveID string, enclave *mpc.SimpleEnclave) error { kb := Get() if kb == nil { return fmt.Errorf("keybase not initialized") } - data := enclave.GetData() - - valShareStr, err := protocol.EncodeMessage(data.ValShare) - if err != nil { - return fmt.Errorf("encode val_share: %w", err) - } - - userShareStr, err := protocol.EncodeMessage(data.UserShare) - if err != nil { - return fmt.Errorf("encode user_share: %w", err) - } - dbEnc, err := kb.queries.GetEnclaveByID(context.Background(), enclaveID) if err != nil { return fmt.Errorf("get enclave: %w", err) } - // Raw SQL required: SQLC-generated code lacks full enclave update query _, err = kb.db.ExecContext(context.Background(), ` UPDATE mpc_enclaves SET val_share = ?, user_share = ?, nonce = ?, rotated_at = datetime('now') WHERE id = ? - `, []byte(valShareStr), []byte(userShareStr), data.Nonce, dbEnc.ID) + `, enclave.GetShare1(), enclave.GetShare2(), enclave.GetNonce(), dbEnc.ID) return err } -- 2.43.0 From b39bd89b47dfbcbbaf4a4554395d52b94e3fa521 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Sat, 10 Jan 2026 15:39:43 -0500 Subject: [PATCH 32/35] feat(crypto): add simple enclave implementation for MPC operations --- internal/crypto/mpc/simple.go | 219 ++++++++++++++++++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 internal/crypto/mpc/simple.go diff --git a/internal/crypto/mpc/simple.go b/internal/crypto/mpc/simple.go new file mode 100644 index 0000000..ca21d51 --- /dev/null +++ b/internal/crypto/mpc/simple.go @@ -0,0 +1,219 @@ +package mpc + +import ( + "crypto/ecdsa" + "crypto/rand" + "encoding/hex" + "fmt" + "math/big" + + "github.com/sonr-io/crypto/core/curves" + "golang.org/x/crypto/sha3" +) + +type SimpleEnclave struct { + pubKey *ecdsa.PublicKey + pubHex string + pubBytes []byte + share1 []byte + share2 []byte + nonce []byte + curveName CurveName +} + +func NewSimpleEnclave() (*SimpleEnclave, error) { + curve := curves.K256() + ecCurve, err := curve.ToEllipticCurve() + if err != nil { + return nil, fmt.Errorf("get elliptic curve: %w", err) + } + + privKey, err := ecdsa.GenerateKey(ecCurve, rand.Reader) + if err != nil { + return nil, fmt.Errorf("generate key: %w", err) + } + + share1, share2, err := splitSecret(privKey.D, ecCurve.Params().N) + if err != nil { + return nil, fmt.Errorf("split secret: %w", err) + } + + pubBytes := make([]byte, 65) + pubBytes[0] = 0x04 + copy(pubBytes[1:33], padTo32(privKey.PublicKey.X.Bytes())) + copy(pubBytes[33:65], padTo32(privKey.PublicKey.Y.Bytes())) + + compressed := compressPubKey(pubBytes) + + nonce := make([]byte, 12) + rand.Read(nonce) + + return &SimpleEnclave{ + pubKey: &privKey.PublicKey, + pubHex: hex.EncodeToString(compressed), + pubBytes: pubBytes, + share1: share1, + share2: share2, + nonce: nonce, + curveName: K256Name, + }, nil +} + +func splitSecret(secret *big.Int, order *big.Int) ([]byte, []byte, error) { + share1Bytes := make([]byte, 32) + _, err := rand.Read(share1Bytes) + if err != nil { + return nil, nil, err + } + + share1 := new(big.Int).SetBytes(share1Bytes) + share1.Mod(share1, order) + + share2 := new(big.Int).Sub(secret, share1) + share2.Mod(share2, order) + + return padTo32(share1.Bytes()), padTo32(share2.Bytes()), nil +} + +func combineShares(share1, share2 []byte, order *big.Int) *big.Int { + s1 := new(big.Int).SetBytes(share1) + s2 := new(big.Int).SetBytes(share2) + result := new(big.Int).Add(s1, s2) + return result.Mod(result, order) +} + +func (e *SimpleEnclave) Sign(data []byte) ([]byte, error) { + curve := curves.K256() + ecCurve, err := curve.ToEllipticCurve() + if err != nil { + return nil, err + } + + privKeyD := combineShares(e.share1, e.share2, ecCurve.Params().N) + + privKey := &ecdsa.PrivateKey{ + PublicKey: *e.pubKey, + D: privKeyD, + } + + hash := sha3.New256() + hash.Write(data) + digest := hash.Sum(nil) + + r, s, err := ecdsa.Sign(rand.Reader, privKey, digest) + if err != nil { + return nil, err + } + + sig := make([]byte, 64) + copy(sig[0:32], padTo32(r.Bytes())) + copy(sig[32:64], padTo32(s.Bytes())) + return sig, nil +} + +func (e *SimpleEnclave) Verify(data []byte, sig []byte) (bool, error) { + if len(sig) != 64 { + return false, fmt.Errorf("invalid signature length: %d", len(sig)) + } + + r := new(big.Int).SetBytes(sig[:32]) + s := new(big.Int).SetBytes(sig[32:]) + + hash := sha3.New256() + hash.Write(data) + digest := hash.Sum(nil) + + return ecdsa.Verify(e.pubKey, digest, r, s), nil +} + +func (e *SimpleEnclave) PubKeyHex() string { return e.pubHex } +func (e *SimpleEnclave) PubKeyBytes() []byte { return e.pubBytes } +func (e *SimpleEnclave) IsValid() bool { return len(e.share1) > 0 && len(e.share2) > 0 } +func (e *SimpleEnclave) GetShare1() []byte { return e.share1 } +func (e *SimpleEnclave) GetShare2() []byte { return e.share2 } +func (e *SimpleEnclave) GetNonce() []byte { return e.nonce } +func (e *SimpleEnclave) GetCurve() CurveName { return e.curveName } + +func (e *SimpleEnclave) Refresh() (*SimpleEnclave, error) { + curve := curves.K256() + ecCurve, err := curve.ToEllipticCurve() + if err != nil { + return nil, err + } + + privKeyD := combineShares(e.share1, e.share2, ecCurve.Params().N) + newShare1, newShare2, err := splitSecret(privKeyD, ecCurve.Params().N) + if err != nil { + return nil, err + } + + newNonce := make([]byte, 12) + rand.Read(newNonce) + + return &SimpleEnclave{ + pubKey: e.pubKey, + pubHex: e.pubHex, + pubBytes: e.pubBytes, + share1: newShare1, + share2: newShare2, + nonce: newNonce, + curveName: e.curveName, + }, nil +} + +func ImportSimpleEnclave(pubBytes, share1, share2, nonce []byte, curveName CurveName) (*SimpleEnclave, error) { + if len(pubBytes) != 65 { + return nil, fmt.Errorf("invalid pubkey length: %d", len(pubBytes)) + } + + curve := curves.K256() + ecCurve, err := curve.ToEllipticCurve() + if err != nil { + return nil, err + } + + x := new(big.Int).SetBytes(pubBytes[1:33]) + y := new(big.Int).SetBytes(pubBytes[33:65]) + + pubKey := &ecdsa.PublicKey{ + Curve: ecCurve, + X: x, + Y: y, + } + + compressed := compressPubKey(pubBytes) + + return &SimpleEnclave{ + pubKey: pubKey, + pubHex: hex.EncodeToString(compressed), + pubBytes: pubBytes, + share1: share1, + share2: share2, + nonce: nonce, + curveName: curveName, + }, nil +} + +func padTo32(b []byte) []byte { + if len(b) >= 32 { + return b[:32] + } + padded := make([]byte, 32) + copy(padded[32-len(b):], b) + return padded +} + +func compressPubKey(uncompressed []byte) []byte { + if len(uncompressed) != 65 { + return uncompressed + } + + compressed := make([]byte, 33) + if uncompressed[64]&1 == 0 { + compressed[0] = 0x02 + } else { + compressed[0] = 0x03 + } + copy(compressed[1:], uncompressed[1:33]) + return compressed +} -- 2.43.0 From b6a01a07ae566b9ead43f08fd8d1d9ae9a8fb6ca Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Sat, 10 Jan 2026 15:40:54 -0500 Subject: [PATCH 33/35] refactor(enclave): remove enclave package --- internal/enclave/crypto.go | 219 ------------------------------------ internal/enclave/enclave.go | 184 ------------------------------ 2 files changed, 403 deletions(-) delete mode 100644 internal/enclave/crypto.go delete mode 100644 internal/enclave/enclave.go diff --git a/internal/enclave/crypto.go b/internal/enclave/crypto.go deleted file mode 100644 index 18ca7f9..0000000 --- a/internal/enclave/crypto.go +++ /dev/null @@ -1,219 +0,0 @@ -// Package enclave provides encrypted database operations with WebAuthn PRF key derivation. -package enclave - -import ( - "crypto/aes" - "crypto/cipher" - "crypto/rand" - "crypto/sha256" - "fmt" - "io" - - "golang.org/x/crypto/hkdf" -) - -const ( - // EnclaveSalt is the salt used for HKDF key derivation - EnclaveSalt = "nebula-enclave-v1" - - // KeySize is the size of the derived encryption key (256 bits) - KeySize = 32 - - // NonceSize is the size of the GCM nonce (96 bits) - NonceSize = 12 - - // AuthTagSize is the size of the GCM authentication tag (128 bits) - AuthTagSize = 16 -) - -// DeriveEncryptionKey derives a 256-bit encryption key from WebAuthn PRF output using HKDF. -// -// Parameters: -// - prfOutput: The raw PRF output from WebAuthn (typically 32 bytes) -// -// Returns: -// - A 32-byte key suitable for AES-256-GCM encryption -// - An error if key derivation fails -func DeriveEncryptionKey(prfOutput []byte) ([]byte, error) { - if len(prfOutput) == 0 { - return nil, fmt.Errorf("enclave: PRF output cannot be empty") - } - - salt := []byte(EnclaveSalt) - info := []byte("database-encryption") - - hkdfReader := hkdf.New(sha256.New, prfOutput, salt, info) - - key := make([]byte, KeySize) - if _, err := io.ReadFull(hkdfReader, key); err != nil { - return nil, fmt.Errorf("enclave: failed to derive key: %w", err) - } - - return key, nil -} - -// DeriveKeyWithContext derives an encryption key with additional context binding. -// This allows deriving different keys for different purposes from the same PRF output. -// -// Parameters: -// - prfOutput: The raw PRF output from WebAuthn -// - context: Additional context to bind the key to (e.g., "database", "mpc-share") -func DeriveKeyWithContext(prfOutput []byte, context string) ([]byte, error) { - if len(prfOutput) == 0 { - return nil, fmt.Errorf("enclave: PRF output cannot be empty") - } - if context == "" { - return nil, fmt.Errorf("enclave: context cannot be empty") - } - - salt := []byte(EnclaveSalt) - info := []byte(context) - - hkdfReader := hkdf.New(sha256.New, prfOutput, salt, info) - - key := make([]byte, KeySize) - if _, err := io.ReadFull(hkdfReader, key); err != nil { - return nil, fmt.Errorf("enclave: failed to derive key: %w", err) - } - - return key, nil -} - -// EncryptedData represents encrypted data with its metadata. -type EncryptedData struct { - // Nonce is the unique nonce used for this encryption (12 bytes) - Nonce []byte `json:"nonce"` - // Ciphertext is the encrypted data including the GCM authentication tag - Ciphertext []byte `json:"ciphertext"` - // Version indicates the encryption scheme version - Version int `json:"version"` -} - -// Encrypt encrypts plaintext using AES-256-GCM with the provided key. -// -// Parameters: -// - key: 32-byte encryption key (from DeriveEncryptionKey) -// - plaintext: The data to encrypt -// -// Returns: -// - EncryptedData containing nonce and ciphertext -// - An error if encryption fails -func Encrypt(key, plaintext []byte) (*EncryptedData, error) { - if len(key) != KeySize { - return nil, fmt.Errorf("enclave: invalid key size: got %d, want %d", len(key), KeySize) - } - - block, err := aes.NewCipher(key) - if err != nil { - return nil, fmt.Errorf("enclave: failed to create cipher: %w", err) - } - - gcm, err := cipher.NewGCM(block) - if err != nil { - return nil, fmt.Errorf("enclave: failed to create GCM: %w", err) - } - - nonce := make([]byte, NonceSize) - if _, err := io.ReadFull(rand.Reader, nonce); err != nil { - return nil, fmt.Errorf("enclave: failed to generate nonce: %w", err) - } - - ciphertext := gcm.Seal(nil, nonce, plaintext, nil) - - return &EncryptedData{ - Nonce: nonce, - Ciphertext: ciphertext, - Version: 1, - }, nil -} - -// Decrypt decrypts ciphertext using AES-256-GCM with the provided key. -// -// Parameters: -// - key: 32-byte encryption key (from DeriveEncryptionKey) -// - data: The EncryptedData to decrypt -// -// Returns: -// - The decrypted plaintext -// - An error if decryption fails (including authentication failure) -func Decrypt(key []byte, data *EncryptedData) ([]byte, error) { - if len(key) != KeySize { - return nil, fmt.Errorf("enclave: invalid key size: got %d, want %d", len(key), KeySize) - } - if data == nil { - return nil, fmt.Errorf("enclave: encrypted data cannot be nil") - } - if len(data.Nonce) != NonceSize { - return nil, fmt.Errorf("enclave: invalid nonce size: got %d, want %d", len(data.Nonce), NonceSize) - } - - block, err := aes.NewCipher(key) - if err != nil { - return nil, fmt.Errorf("enclave: failed to create cipher: %w", err) - } - - gcm, err := cipher.NewGCM(block) - if err != nil { - return nil, fmt.Errorf("enclave: failed to create GCM: %w", err) - } - - plaintext, err := gcm.Open(nil, data.Nonce, data.Ciphertext, nil) - if err != nil { - return nil, fmt.Errorf("enclave: decryption failed (authentication error): %w", err) - } - - return plaintext, nil -} - -// EncryptBytes is a convenience function that encrypts and returns serialized bytes. -// The format is: version (1 byte) + nonce (12 bytes) + ciphertext (variable) -func EncryptBytes(key, plaintext []byte) ([]byte, error) { - data, err := Encrypt(key, plaintext) - if err != nil { - return nil, err - } - - result := make([]byte, 1+NonceSize+len(data.Ciphertext)) - result[0] = byte(data.Version) - copy(result[1:1+NonceSize], data.Nonce) - copy(result[1+NonceSize:], data.Ciphertext) - - return result, nil -} - -// DecryptBytes is a convenience function that decrypts serialized encrypted bytes. -// Expected format: version (1 byte) + nonce (12 bytes) + ciphertext (variable) -func DecryptBytes(key, encryptedBytes []byte) ([]byte, error) { - if len(encryptedBytes) < 1+NonceSize+AuthTagSize { - return nil, fmt.Errorf("enclave: encrypted data too short") - } - - version := int(encryptedBytes[0]) - if version != 1 { - return nil, fmt.Errorf("enclave: unsupported encryption version: %d", version) - } - - data := &EncryptedData{ - Version: version, - Nonce: encryptedBytes[1 : 1+NonceSize], - Ciphertext: encryptedBytes[1+NonceSize:], - } - - return Decrypt(key, data) -} - -// GenerateNonce generates a cryptographically secure random nonce. -func GenerateNonce() ([]byte, error) { - nonce := make([]byte, NonceSize) - if _, err := io.ReadFull(rand.Reader, nonce); err != nil { - return nil, fmt.Errorf("enclave: failed to generate nonce: %w", err) - } - return nonce, nil -} - -// SecureZero zeros out a byte slice to prevent sensitive data from remaining in memory. -func SecureZero(b []byte) { - for i := range b { - b[i] = 0 - } -} diff --git a/internal/enclave/enclave.go b/internal/enclave/enclave.go deleted file mode 100644 index f013b15..0000000 --- a/internal/enclave/enclave.go +++ /dev/null @@ -1,184 +0,0 @@ -package enclave - -import ( - "encoding/json" - "fmt" - - "enclave/internal/keybase" -) - -// Enclave wraps a Keybase with encryption capabilities using WebAuthn PRF-derived keys. -type Enclave struct { - keybase *keybase.Keybase - encryptionKey []byte -} - -// Config holds enclave configuration options. -type Config struct { - PRFOutput []byte -} - -// New creates a new Enclave with the given PRF output for key derivation. -func New(prfOutput []byte) (*Enclave, error) { - if len(prfOutput) == 0 { - return nil, fmt.Errorf("enclave: PRF output required") - } - - key, err := DeriveEncryptionKey(prfOutput) - if err != nil { - return nil, fmt.Errorf("enclave: key derivation failed: %w", err) - } - - kb, err := keybase.Open() - if err != nil { - SecureZero(key) - return nil, fmt.Errorf("enclave: failed to open keybase: %w", err) - } - - return &Enclave{ - keybase: kb, - encryptionKey: key, - }, nil -} - -// Keybase returns the underlying keybase instance. -func (e *Enclave) Keybase() *keybase.Keybase { - return e.keybase -} - -// DID returns the current DID. -func (e *Enclave) DID() string { - return e.keybase.DID() -} - -// IsInitialized returns true if the enclave has been initialized with a DID. -func (e *Enclave) IsInitialized() bool { - return e.keybase.IsInitialized() -} - -// SerializeEncrypted exports the database state as encrypted bytes. -func (e *Enclave) SerializeEncrypted() ([]byte, error) { - plaintext, err := e.keybase.Serialize() - if err != nil { - return nil, fmt.Errorf("enclave: serialization failed: %w", err) - } - - encrypted, err := EncryptBytes(e.encryptionKey, plaintext) - if err != nil { - SecureZero(plaintext) - return nil, fmt.Errorf("enclave: encryption failed: %w", err) - } - - SecureZero(plaintext) - return encrypted, nil -} - -// LoadEncrypted loads the database state from encrypted bytes. -func (e *Enclave) LoadEncrypted(encryptedData []byte) error { - plaintext, err := DecryptBytes(e.encryptionKey, encryptedData) - if err != nil { - return fmt.Errorf("enclave: decryption failed: %w", err) - } - defer SecureZero(plaintext) - - return e.loadFromPlaintext(plaintext) -} - -// loadFromPlaintext parses and executes the SQL statements to restore database state. -func (e *Enclave) loadFromPlaintext(data []byte) error { - if len(data) == 0 { - return fmt.Errorf("enclave: empty data") - } - - return e.keybase.RestoreFromDump(data) -} - -// Close securely closes the enclave and zeros out sensitive data. -func (e *Enclave) Close() error { - SecureZero(e.encryptionKey) - return keybase.Close() -} - -// EncryptedBundle represents a complete encrypted database export. -type EncryptedBundle struct { - Version int `json:"version"` - DID string `json:"did"` - Ciphertext []byte `json:"ciphertext"` - Nonce []byte `json:"nonce"` -} - -// Export creates a complete encrypted bundle for storage. -func (e *Enclave) Export() (*EncryptedBundle, error) { - plaintext, err := e.keybase.Serialize() - if err != nil { - return nil, fmt.Errorf("enclave: serialization failed: %w", err) - } - defer SecureZero(plaintext) - - encData, err := Encrypt(e.encryptionKey, plaintext) - if err != nil { - return nil, fmt.Errorf("enclave: encryption failed: %w", err) - } - - return &EncryptedBundle{ - Version: encData.Version, - DID: e.keybase.DID(), - Ciphertext: encData.Ciphertext, - Nonce: encData.Nonce, - }, nil -} - -// Import loads an encrypted bundle. -func (e *Enclave) Import(bundle *EncryptedBundle) error { - if bundle == nil { - return fmt.Errorf("enclave: bundle cannot be nil") - } - - encData := &EncryptedData{ - Version: bundle.Version, - Ciphertext: bundle.Ciphertext, - Nonce: bundle.Nonce, - } - - plaintext, err := Decrypt(e.encryptionKey, encData) - if err != nil { - return fmt.Errorf("enclave: decryption failed: %w", err) - } - defer SecureZero(plaintext) - - return e.loadFromPlaintext(plaintext) -} - -// MarshalBundle serializes an encrypted bundle to JSON. -func (b *EncryptedBundle) Marshal() ([]byte, error) { - return json.Marshal(b) -} - -// UnmarshalBundle deserializes an encrypted bundle from JSON. -func UnmarshalBundle(data []byte) (*EncryptedBundle, error) { - var bundle EncryptedBundle - if err := json.Unmarshal(data, &bundle); err != nil { - return nil, fmt.Errorf("enclave: failed to unmarshal bundle: %w", err) - } - return &bundle, nil -} - -// FromExisting wraps an existing keybase with encryption capabilities. -func FromExisting(kb *keybase.Keybase, prfOutput []byte) (*Enclave, error) { - if kb == nil { - return nil, fmt.Errorf("enclave: keybase cannot be nil") - } - if len(prfOutput) == 0 { - return nil, fmt.Errorf("enclave: PRF output required") - } - - key, err := DeriveEncryptionKey(prfOutput) - if err != nil { - return nil, fmt.Errorf("enclave: key derivation failed: %w", err) - } - - return &Enclave{ - keybase: kb, - encryptionKey: key, - }, nil -} -- 2.43.0 From 659736ade089912a06d493de485f1f89a058ec16 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Sat, 10 Jan 2026 16:49:23 -0500 Subject: [PATCH 34/35] refactor(enclave): migrate to enclave signing with MPC --- cmd/enclave/main.go | 54 ++++---------- internal/crypto/mpc/utils.go | 33 --------- internal/crypto/mpc/verify.go | 108 ++++++++++++++++++++++++---- internal/crypto/ucan/delegation.go | 2 +- internal/crypto/ucan/invocation.go | 2 +- internal/crypto/ucan/policy.go | 2 +- internal/crypto/ucan/types.go | 2 +- internal/keybase/actions_enclave.go | 25 +++++++ internal/keybase/exec.go | 60 ++++++++++++++++ internal/keybase/functions.go | 88 +++++++++++++++++++++++ 10 files changed, 283 insertions(+), 93 deletions(-) delete mode 100644 internal/crypto/mpc/utils.go diff --git a/cmd/enclave/main.go b/cmd/enclave/main.go index 4d8bf74..6b53ed2 100644 --- a/cmd/enclave/main.go +++ b/cmd/enclave/main.go @@ -52,7 +52,7 @@ func ping() int32 { //go:wasmexport generate func generate() int32 { - pdk.Log(pdk.LogInfo, "generate: starting database initialization") + pdk.Log(pdk.LogInfo, "generate: starting") var input types.GenerateInput if err := pdk.InputJSON(&input); err != nil { @@ -71,44 +71,32 @@ func generate() int32 { return 1 } - pdk.Log(pdk.LogInfo, "generate: opening keybase") - kb, err := keybase.Open() + result, err := initializeWithMPC(credentialBytes) if err != nil { - pdk.SetError(fmt.Errorf("generate: open database: %w", err)) + pdk.SetError(fmt.Errorf("generate: %w", err)) return 1 } - pdk.Log(pdk.LogInfo, "generate: initializing DID") - ctx := context.Background() - did, err := kb.Initialize(ctx, credentialBytes) - if err != nil { - pdk.SetError(fmt.Errorf("generate: initialize DID: %w", err)) - return 1 - } - - pdk.Log(pdk.LogInfo, fmt.Sprintf("generate: DID created: %s", did)) - state.SetInitialized(true) - state.SetDID(did) + state.SetDID(result.DID) - pdk.Log(pdk.LogInfo, "generate: serializing database") dbBytes, err := serializeDatabase() if err != nil { - pdk.SetError(fmt.Errorf("generate: failed to serialize database: %w", err)) + pdk.SetError(fmt.Errorf("generate: serialize: %w", err)) return 1 } output := types.GenerateOutput{ - DID: did, + DID: result.DID, Database: dbBytes, } if err := pdk.OutputJSON(output); err != nil { - pdk.SetError(fmt.Errorf("generate: failed to output result: %w", err)) + pdk.SetError(fmt.Errorf("generate: output: %w", err)) return 1 } - pdk.Log(pdk.LogInfo, fmt.Sprintf("generate: created DID %s (no MPC)", did)) + pdk.Log(pdk.LogInfo, fmt.Sprintf("generate: created DID %s with enclave %s", result.DID, result.EnclaveID)) return 0 } @@ -262,28 +250,21 @@ type initResult struct { } func initializeWithMPC(credentialBytes []byte) (*initResult, error) { - pdk.Log(pdk.LogInfo, "initializeWithMPC: step 1 - opening database") kb, err := keybase.Open() if err != nil { return nil, fmt.Errorf("open database: %w", err) } - pdk.Log(pdk.LogInfo, "initializeWithMPC: step 2 - database opened") ctx := context.Background() - pdk.Log(pdk.LogInfo, "initializeWithMPC: step 3 - initializing DID") did, err := kb.Initialize(ctx, credentialBytes) if err != nil { - return nil, fmt.Errorf("initialize: %w", err) + return nil, fmt.Errorf("initialize DID: %w", err) } - pdk.Log(pdk.LogInfo, fmt.Sprintf("initializeWithMPC: step 4 - DID initialized: %s", did)) - pdk.Log(pdk.LogInfo, "initializeWithMPC: step 5 - generating simple enclave") simpleEnc, err := mpc.NewSimpleEnclave() if err != nil { - pdk.Log(pdk.LogError, fmt.Sprintf("initializeWithMPC: enclave generation failed: %v", err)) return nil, fmt.Errorf("generate enclave: %w", err) } - pdk.Log(pdk.LogInfo, "initializeWithMPC: step 6 - enclave generated") enclaveID := fmt.Sprintf("enc_%x", credentialBytes[:8]) @@ -305,11 +286,9 @@ func initializeWithMPC(credentialBytes []byte) (*initResult, error) { return nil, fmt.Errorf("store enclave: %w", err) } - pdk.Log(pdk.LogInfo, fmt.Sprintf("initializeWithMPC: stored enclave %s", enclaveID)) - accounts, err := createDefaultAccounts(ctx, am, enc.ID, simpleEnc.PubKeyBytes()) if err != nil { - pdk.Log(pdk.LogWarn, fmt.Sprintf("initializeWithMPC: failed to create accounts: %s", err)) + pdk.Log(pdk.LogWarn, fmt.Sprintf("createDefaultAccounts: %s", err)) accounts = []types.AccountInfo{} } @@ -322,7 +301,7 @@ func initializeWithMPC(credentialBytes []byte) (*initResult, error) { } func createDefaultAccounts(ctx context.Context, am *keybase.ActionManager, enclaveID int64, pubKeyBytes []byte) ([]types.AccountInfo, error) { - chains := []string{"bitcoin", "ethereum", "sonr"} + chains := []string{"sonr", "ethereum", "bitcoin"} derivedAccounts, err := bip44.DeriveAccounts(pubKeyBytes, chains) if err != nil { return nil, fmt.Errorf("derive accounts: %w", err) @@ -346,7 +325,6 @@ func createDefaultAccounts(ctx context.Context, am *keybase.ActionManager, encla IsDefault: isDefault, }) if err != nil { - pdk.Log(pdk.LogWarn, fmt.Sprintf("createDefaultAccounts: failed for %s: %s", derived.ChainID, err)) continue } @@ -538,15 +516,9 @@ func matchResource(pattern, resource string) bool { } func executeAction(params *types.FilterParams) (json.RawMessage, error) { - if params.Resource == "accounts" { - switch params.Action { - case "balances": - return fetchAccountBalances(params.Subject) - case "sign": - return json.Marshal(map[string]string{"signature": "placeholder"}) - } + if params.Resource == "accounts" && params.Action == "balances" { + return fetchAccountBalances(params.Subject) } - return keybase.Exec(context.Background(), params.Resource, params.Action, params.Subject) } diff --git a/internal/crypto/mpc/utils.go b/internal/crypto/mpc/utils.go deleted file mode 100644 index b0407b6..0000000 --- a/internal/crypto/mpc/utils.go +++ /dev/null @@ -1,33 +0,0 @@ -package mpc - -import ( - "fmt" - "math/big" - - "github.com/sonr-io/crypto/core/curves" -) - -func GetECDSAPoint(pubKey []byte) (*curves.EcPoint, error) { - crv := curves.K256() - x := new(big.Int).SetBytes(pubKey[1:33]) - y := new(big.Int).SetBytes(pubKey[33:]) - ecCurve, err := crv.ToEllipticCurve() - if err != nil { - return nil, fmt.Errorf("error converting curve: %v", err) - } - return &curves.EcPoint{X: x, Y: y, Curve: ecCurve}, nil -} - -func DeserializeSignature(sigBytes []byte) (*curves.EcdsaSignature, error) { - if len(sigBytes) != 64 { - return nil, fmt.Errorf("invalid signature length: expected 64 bytes, got %d", len(sigBytes)) - } - - r := new(big.Int).SetBytes(sigBytes[:32]) - s := new(big.Int).SetBytes(sigBytes[32:]) - - return &curves.EcdsaSignature{ - R: r, - S: s, - }, nil -} diff --git a/internal/crypto/mpc/verify.go b/internal/crypto/mpc/verify.go index 4163759..a4e3a45 100644 --- a/internal/crypto/mpc/verify.go +++ b/internal/crypto/mpc/verify.go @@ -2,28 +2,106 @@ package mpc import ( "crypto/ecdsa" + "crypto/elliptic" + "fmt" + "math/big" "golang.org/x/crypto/sha3" ) -func VerifyWithPubKey(pubKeyCompressed []byte, data []byte, sig []byte) (bool, error) { - edSig, err := DeserializeSignature(sig) - if err != nil { - return false, err - } - ePub, err := GetECDSAPoint(pubKeyCompressed) - if err != nil { - return false, err - } - pk := &ecdsa.PublicKey{ - Curve: ePub.Curve, - X: ePub.X, - Y: ePub.Y, +func VerifyWithPubKey(pubKey []byte, data []byte, sig []byte) (bool, error) { + if len(sig) != 64 { + return false, fmt.Errorf("invalid signature length: expected 64, got %d", len(sig)) } - // Hash the message using SHA3-256 + pk, err := parsePublicKey(pubKey) + if err != nil { + return false, err + } + + r := new(big.Int).SetBytes(sig[:32]) + s := new(big.Int).SetBytes(sig[32:]) + hash := sha3.New256() hash.Write(data) digest := hash.Sum(nil) - return ecdsa.Verify(pk, digest, edSig.R, edSig.S), nil + + return ecdsa.Verify(pk, digest, r, s), nil +} + +func parsePublicKey(pubKey []byte) (*ecdsa.PublicKey, error) { + curve := elliptic.P256() + // Use secp256k1 parameters manually since Go stdlib doesn't include it + curve = secp256k1Curve() + + switch len(pubKey) { + case 65: // uncompressed: 0x04 || x || y + if pubKey[0] != 0x04 { + return nil, fmt.Errorf("invalid uncompressed pubkey prefix: %x", pubKey[0]) + } + x := new(big.Int).SetBytes(pubKey[1:33]) + y := new(big.Int).SetBytes(pubKey[33:65]) + return &ecdsa.PublicKey{Curve: curve, X: x, Y: y}, nil + + case 33: // compressed: 0x02/0x03 || x + x, y := decompressPoint(curve, pubKey) + if x == nil { + return nil, fmt.Errorf("failed to decompress pubkey") + } + return &ecdsa.PublicKey{Curve: curve, X: x, Y: y}, nil + + default: + return nil, fmt.Errorf("invalid pubkey length: %d", len(pubKey)) + } +} + +func secp256k1Curve() elliptic.Curve { + return &secp256k1Params +} + +var secp256k1Params = elliptic.CurveParams{ + Name: "secp256k1", + BitSize: 256, + P: fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F"), + N: fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141"), + B: big.NewInt(7), + Gx: fromHex("79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798"), + Gy: fromHex("483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8"), +} + +func fromHex(s string) *big.Int { + i, _ := new(big.Int).SetString(s, 16) + return i +} + +func decompressPoint(curve elliptic.Curve, compressed []byte) (*big.Int, *big.Int) { + if len(compressed) != 33 { + return nil, nil + } + + prefix := compressed[0] + if prefix != 0x02 && prefix != 0x03 { + return nil, nil + } + + x := new(big.Int).SetBytes(compressed[1:]) + p := curve.Params().P + + // y² = x³ + 7 (for secp256k1) + x3 := new(big.Int).Mul(x, x) + x3.Mul(x3, x) + x3.Add(x3, big.NewInt(7)) + x3.Mod(x3, p) + + y := new(big.Int).ModSqrt(x3, p) + if y == nil { + return nil, nil + } + + // Check parity + if (y.Bit(0) == 1) != (prefix == 0x03) { + y.Sub(p, y) + } + + return x, y } diff --git a/internal/crypto/ucan/delegation.go b/internal/crypto/ucan/delegation.go index 7b42050..0857d20 100644 --- a/internal/crypto/ucan/delegation.go +++ b/internal/crypto/ucan/delegation.go @@ -6,10 +6,10 @@ import ( "code.sonr.org/go/did-it" "code.sonr.org/go/did-it/crypto" - "github.com/ipfs/go-cid" "code.sonr.org/go/ucan/pkg/command" "code.sonr.org/go/ucan/pkg/policy" "code.sonr.org/go/ucan/token/delegation" + "github.com/ipfs/go-cid" ) // DelegationBuilder provides a fluent API for creating UCAN delegations. diff --git a/internal/crypto/ucan/invocation.go b/internal/crypto/ucan/invocation.go index d925fd6..db17d0e 100644 --- a/internal/crypto/ucan/invocation.go +++ b/internal/crypto/ucan/invocation.go @@ -6,9 +6,9 @@ import ( "code.sonr.org/go/did-it" "code.sonr.org/go/did-it/crypto" - "github.com/ipfs/go-cid" "code.sonr.org/go/ucan/pkg/command" "code.sonr.org/go/ucan/token/invocation" + "github.com/ipfs/go-cid" ) // InvocationBuilder provides a fluent API for creating UCAN invocations. diff --git a/internal/crypto/ucan/policy.go b/internal/crypto/ucan/policy.go index e8ed4d9..0873516 100644 --- a/internal/crypto/ucan/policy.go +++ b/internal/crypto/ucan/policy.go @@ -1,9 +1,9 @@ package ucan import ( - "github.com/ipld/go-ipld-prime" "code.sonr.org/go/ucan/pkg/policy" "code.sonr.org/go/ucan/pkg/policy/literal" + "github.com/ipld/go-ipld-prime" ) // PolicyBuilder provides a fluent API for constructing UCAN policies. diff --git a/internal/crypto/ucan/types.go b/internal/crypto/ucan/types.go index 2589309..afb500e 100644 --- a/internal/crypto/ucan/types.go +++ b/internal/crypto/ucan/types.go @@ -1,8 +1,8 @@ package ucan import ( - "github.com/ipfs/go-cid" "code.sonr.org/go/ucan/pkg/policy" + "github.com/ipfs/go-cid" ) // ValidationErrorCode represents UCAN validation error types. diff --git a/internal/keybase/actions_enclave.go b/internal/keybase/actions_enclave.go index 088481c..e6f3cd0 100644 --- a/internal/keybase/actions_enclave.go +++ b/internal/keybase/actions_enclave.go @@ -3,6 +3,8 @@ package keybase import ( "context" "fmt" + + "enclave/internal/crypto/mpc" ) type EnclaveResult struct { @@ -126,6 +128,29 @@ func (am *ActionManager) DeleteEnclave(ctx context.Context, enclaveID string) er }) } +func (am *ActionManager) SignWithEnclave(ctx context.Context, enclaveID string, data []byte) ([]byte, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + enc, err := am.kb.queries.GetEnclaveByID(ctx, enclaveID) + if err != nil { + return nil, fmt.Errorf("get enclave: %w", err) + } + + simpleEnc, err := mpc.ImportSimpleEnclave( + enc.PublicKey, + enc.ValShare, + enc.UserShare, + enc.Nonce, + mpc.CurveName(enc.Curve), + ) + if err != nil { + return nil, fmt.Errorf("import enclave: %w", err) + } + + return simpleEnc.Sign(data) +} + func enclaveToResult(enc *MpcEnclafe) *EnclaveResult { rotatedAt := "" if enc.RotatedAt != nil { diff --git a/internal/keybase/exec.go b/internal/keybase/exec.go index f30fd8b..72db39b 100644 --- a/internal/keybase/exec.go +++ b/internal/keybase/exec.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "strings" ) type HandlerFunc func(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) @@ -15,6 +16,7 @@ var handlers = map[string]resourceHandlers{ "accounts": { "list": handleAccountList, "get": handleAccountGet, + "sign": handleAccountSign, }, "credentials": { "list": handleCredentialList, @@ -31,6 +33,7 @@ var handlers = map[string]resourceHandlers{ "enclaves": { "list": handleEnclaveList, "get": handleEnclaveGet, + "sign": handleEnclaveSign, "rotate": handleEnclaveRotate, "archive": handleEnclaveArchive, "delete": handleEnclaveDelete, @@ -101,6 +104,32 @@ func handleAccountGet(ctx context.Context, am *ActionManager, subject string) (j return json.Marshal(account) } +func handleAccountSign(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) { + if subject == "" { + return nil, errors.New("subject (hex-encoded data) required for sign action") + } + + enclaves, err := am.ListEnclaves(ctx) + if err != nil { + return nil, fmt.Errorf("list enclaves: %w", err) + } + if len(enclaves) == 0 { + return nil, errors.New("no enclave available for signing") + } + + enc := enclaves[0] + signature, err := am.SignWithEnclave(ctx, enc.EnclaveID, []byte(subject)) + if err != nil { + return nil, fmt.Errorf("sign: %w", err) + } + + return json.Marshal(map[string]string{ + "signature": fmt.Sprintf("%x", signature), + "enclave_id": enc.EnclaveID, + "public_key": enc.PublicKeyHex, + }) +} + func handleCredentialList(ctx context.Context, am *ActionManager, _ string) (json.RawMessage, error) { credentials, err := am.ListCredentials(ctx) if err != nil { @@ -179,6 +208,37 @@ func handleEnclaveGet(ctx context.Context, am *ActionManager, subject string) (j return json.Marshal(enc) } +func handleEnclaveSign(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) { + if subject == "" { + return nil, errors.New("subject (enclave_id:hex_data) required for sign action") + } + + parts := strings.SplitN(subject, ":", 2) + if len(parts) != 2 { + return nil, errors.New("subject must be enclave_id:hex_data format") + } + + enclaveID := parts[0] + data := []byte(parts[1]) + + signature, err := am.SignWithEnclave(ctx, enclaveID, data) + if err != nil { + return nil, err + } + + enc, _ := am.GetEnclaveByID(ctx, enclaveID) + pubKeyHex := "" + if enc != nil { + pubKeyHex = enc.PublicKeyHex + } + + return json.Marshal(map[string]string{ + "signature": fmt.Sprintf("%x", signature), + "enclave_id": enclaveID, + "public_key": pubKeyHex, + }) +} + func handleEnclaveRotate(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) { if subject == "" { return nil, errors.New("subject (enclave_id) required for rotate action") diff --git a/internal/keybase/functions.go b/internal/keybase/functions.go index becaa16..053833e 100644 --- a/internal/keybase/functions.go +++ b/internal/keybase/functions.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "fmt" + "enclave/internal/crypto/bip44" "enclave/internal/crypto/mpc" "github.com/ncruces/go-sqlite3" @@ -24,6 +25,12 @@ func RegisterMPCFunctions(conn *sqlite3.Conn) error { if err := registerRefreshFunction(conn); err != nil { return fmt.Errorf("register mpc_refresh: %w", err) } + if err := registerBIP44DeriveFunction(conn); err != nil { + return fmt.Errorf("register bip44_derive: %w", err) + } + if err := registerBIP44DeriveFromEnclaveFunction(conn); err != nil { + return fmt.Errorf("register bip44_derive_from_enclave: %w", err) + } return nil } @@ -221,3 +228,84 @@ func updateSimpleEnclaveInDB(enclaveID string, enclave *mpc.SimpleEnclave) error return err } + +// bip44_derive(pubkey_hex, chain) -> address +// Derives a blockchain address from a public key for the specified chain. +// Supported chains: bitcoin, ethereum, cosmos, sonr +func registerBIP44DeriveFunction(conn *sqlite3.Conn) error { + return conn.CreateFunction("bip44_derive", 2, sqlite3.DETERMINISTIC|sqlite3.INNOCUOUS, func(ctx sqlite3.Context, args ...sqlite3.Value) { + if len(args) != 2 { + ctx.ResultError(fmt.Errorf("bip44_derive requires 2 arguments: pubkey_hex, chain")) + return + } + + pubKeyHex := args[0].Text() + chain := args[1].Text() + + if pubKeyHex == "" { + ctx.ResultError(fmt.Errorf("bip44_derive: pubkey_hex cannot be empty")) + return + } + if chain == "" { + ctx.ResultError(fmt.Errorf("bip44_derive: chain cannot be empty")) + return + } + + pubKeyBytes, err := hex.DecodeString(pubKeyHex) + if err != nil { + ctx.ResultError(fmt.Errorf("bip44_derive: invalid pubkey hex: %w", err)) + return + } + + address, err := bip44.DeriveAddress(pubKeyBytes, chain) + if err != nil { + ctx.ResultError(fmt.Errorf("bip44_derive: %w", err)) + return + } + + ctx.ResultText(address) + }) +} + +// bip44_derive_from_enclave(enclave_id, chain) -> address +// Derives a blockchain address from an MPC enclave's public key. +func registerBIP44DeriveFromEnclaveFunction(conn *sqlite3.Conn) error { + return conn.CreateFunction("bip44_derive_from_enclave", 2, sqlite3.DETERMINISTIC, func(ctx sqlite3.Context, args ...sqlite3.Value) { + if len(args) != 2 { + ctx.ResultError(fmt.Errorf("bip44_derive_from_enclave requires 2 arguments: enclave_id, chain")) + return + } + + enclaveID := args[0].Text() + chain := args[1].Text() + + if enclaveID == "" { + ctx.ResultError(fmt.Errorf("bip44_derive_from_enclave: enclave_id cannot be empty")) + return + } + if chain == "" { + ctx.ResultError(fmt.Errorf("bip44_derive_from_enclave: chain cannot be empty")) + return + } + + kb := Get() + if kb == nil { + ctx.ResultError(fmt.Errorf("bip44_derive_from_enclave: keybase not initialized")) + return + } + + dbEnc, err := kb.queries.GetEnclaveByID(context.Background(), enclaveID) + if err != nil { + ctx.ResultError(fmt.Errorf("bip44_derive_from_enclave: enclave not found: %w", err)) + return + } + + address, err := bip44.DeriveAddress(dbEnc.PublicKey, chain) + if err != nil { + ctx.ResultError(fmt.Errorf("bip44_derive_from_enclave: %w", err)) + return + } + + ctx.ResultText(address) + }) +} -- 2.43.0 From 8ceb4d600f20bf6ddf10022f63bbcfcb86887878 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Sat, 10 Jan 2026 16:57:37 -0500 Subject: [PATCH 35/35] docs(dwn): remove UCAN_SCHEMA_PROPOSAL.md --- .github/Repo.toml.migrated.20260109_191955 | 4 - docs/UCAN_SCHEMA_PROPOSAL.md | 311 --------------------- example/main.js | 7 +- 3 files changed, 4 insertions(+), 318 deletions(-) delete mode 100644 .github/Repo.toml.migrated.20260109_191955 delete mode 100644 docs/UCAN_SCHEMA_PROPOSAL.md diff --git a/.github/Repo.toml.migrated.20260109_191955 b/.github/Repo.toml.migrated.20260109_191955 deleted file mode 100644 index 6da103c..0000000 --- a/.github/Repo.toml.migrated.20260109_191955 +++ /dev/null @@ -1,4 +0,0 @@ -[scopes] -docs = ["MIGRATION.md", "README.md"] -db = ["db"] -config = [".github"] diff --git a/docs/UCAN_SCHEMA_PROPOSAL.md b/docs/UCAN_SCHEMA_PROPOSAL.md deleted file mode 100644 index b7405ca..0000000 --- a/docs/UCAN_SCHEMA_PROPOSAL.md +++ /dev/null @@ -1,311 +0,0 @@ -# UCAN v1.0.0-rc.1 Schema Proposal - -## Overview - -This document proposes schema changes to migrate from JWT-based UCAN to v1.0.0-rc.1 envelope format. - -## Design Principles - -1. **Single Database** - All data in one SQLite for WASM portability -2. **CID-based Lookup** - Primary key is content identifier (immutable) -3. **Binary Storage** - DAG-CBOR envelopes stored as BLOBs -4. **Indexed Fields** - Extract key fields for efficient queries -5. **DID Ownership** - Foreign key to did_documents for access control - -## Architecture - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ WASM Plugin (Enclave) │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ WebAuthn │ │ DID │ │ UCAN │ │ -│ │ (AuthN) │ │ (Identity) │ │ (AuthZ) │ │ -│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ -│ │ │ │ │ -│ ▼ ▼ ▼ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ SQLite Database │ │ -│ │ ┌────────────┐ ┌────────────┐ ┌─────────────────────┐ │ │ -│ │ │credentials │ │did_documents│ │ ucan_delegations │ │ │ -│ │ │ │ │ │ │ ucan_invocations │ │ │ -│ │ │ │ │ │ │ ucan_revocations │ │ │ -│ │ └────────────┘ └────────────┘ └─────────────────────┘ │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -## Schema Changes - -### 1. Replace `ucan_tokens` with `ucan_delegations` - -```sql --- DROP TABLE IF EXISTS ucan_tokens; -- Migration step - --- UCAN Delegations: v1.0.0-rc.1 delegation envelopes -CREATE TABLE IF NOT EXISTS ucan_delegations ( - id INTEGER PRIMARY KEY, - did_id INTEGER NOT NULL REFERENCES did_documents(id) ON DELETE CASCADE, - - -- Content Identifier (immutable, unique) - cid TEXT NOT NULL UNIQUE, - - -- Sealed envelope (DAG-CBOR encoded) - envelope BLOB NOT NULL, - - -- Extracted fields for indexing/queries - iss TEXT NOT NULL, -- Issuer DID - aud TEXT NOT NULL, -- Audience DID - sub TEXT, -- Subject DID (null = powerline) - cmd TEXT NOT NULL, -- Command (e.g., "/vault/read") - - -- Policy stored as JSON for inspection (actual evaluation uses envelope) - pol TEXT DEFAULT '[]', -- Policy JSON - - -- Temporal fields - nbf TEXT, -- Not before (ISO8601) - exp TEXT, -- Expiration (ISO8601, null = never) - - -- Metadata - is_root INTEGER NOT NULL DEFAULT 0, -- iss == sub - is_powerline INTEGER NOT NULL DEFAULT 0, -- sub IS NULL - created_at TEXT NOT NULL DEFAULT (datetime('now')) -); - -CREATE INDEX idx_ucan_delegations_cid ON ucan_delegations(cid); -CREATE INDEX idx_ucan_delegations_did_id ON ucan_delegations(did_id); -CREATE INDEX idx_ucan_delegations_iss ON ucan_delegations(iss); -CREATE INDEX idx_ucan_delegations_aud ON ucan_delegations(aud); -CREATE INDEX idx_ucan_delegations_sub ON ucan_delegations(sub); -CREATE INDEX idx_ucan_delegations_cmd ON ucan_delegations(cmd); -CREATE INDEX idx_ucan_delegations_exp ON ucan_delegations(exp); -``` - -### 2. Add `ucan_invocations` Table - -```sql --- UCAN Invocations: v1.0.0-rc.1 invocation envelopes (audit log) -CREATE TABLE IF NOT EXISTS ucan_invocations ( - id INTEGER PRIMARY KEY, - did_id INTEGER NOT NULL REFERENCES did_documents(id) ON DELETE CASCADE, - - -- Content Identifier - cid TEXT NOT NULL UNIQUE, - - -- Sealed envelope (DAG-CBOR encoded) - envelope BLOB NOT NULL, - - -- Extracted fields for indexing - iss TEXT NOT NULL, -- Invoker DID - sub TEXT NOT NULL, -- Subject DID - aud TEXT, -- Executor DID (if different from sub) - cmd TEXT NOT NULL, -- Command invoked - - -- Proof chain (JSON array of delegation CIDs) - prf TEXT NOT NULL DEFAULT '[]', - - -- Temporal - exp TEXT, -- Expiration - iat TEXT, -- Issued at - - -- Execution tracking - executed_at TEXT, -- When actually executed - result_cid TEXT, -- CID of receipt (if executed) - - created_at TEXT NOT NULL DEFAULT (datetime('now')) -); - -CREATE INDEX idx_ucan_invocations_cid ON ucan_invocations(cid); -CREATE INDEX idx_ucan_invocations_iss ON ucan_invocations(iss); -CREATE INDEX idx_ucan_invocations_sub ON ucan_invocations(sub); -CREATE INDEX idx_ucan_invocations_cmd ON ucan_invocations(cmd); -``` - -### 3. Update `ucan_revocations` (Minor Changes) - -```sql --- UCAN Revocations: Track revoked delegations --- (Mostly unchanged, but add invocation reference) -CREATE TABLE IF NOT EXISTS ucan_revocations ( - id INTEGER PRIMARY KEY, - delegation_cid TEXT NOT NULL UNIQUE, -- CID of revoked delegation - revoked_by TEXT NOT NULL, -- Revoker DID - invocation_cid TEXT, -- CID of revocation invocation - reason TEXT, - revoked_at TEXT NOT NULL DEFAULT (datetime('now')), - - FOREIGN KEY (delegation_cid) REFERENCES ucan_delegations(cid) ON DELETE CASCADE -); - -CREATE INDEX idx_ucan_revocations_delegation_cid ON ucan_revocations(delegation_cid); -CREATE INDEX idx_ucan_revocations_revoked_by ON ucan_revocations(revoked_by); -``` - -### 4. Remove `delegations` Table - -The old `delegations` table extracted fields from `ucan_tokens`. In v1.0.0-rc.1, the delegation IS the token. The `ucan_delegations` table replaces both. - -```sql --- DROP TABLE IF EXISTS delegations; -- Migration step -``` - -## Query Examples - -### Get Delegation by CID (for go-ucan Loader) - -```sql --- name: GetDelegationByCID :one -SELECT envelope FROM ucan_delegations WHERE cid = ? LIMIT 1; -``` - -### List Delegations Granted TO a DID (audience) - -```sql --- name: ListDelegationsToAudience :many -SELECT * FROM ucan_delegations -WHERE aud = ? AND (exp IS NULL OR exp > datetime('now')) -ORDER BY created_at DESC; -``` - -### List Delegations Granted BY a DID (issuer) - -```sql --- name: ListDelegationsByIssuer :many -SELECT * FROM ucan_delegations -WHERE iss = ? AND (exp IS NULL OR exp > datetime('now')) -ORDER BY created_at DESC; -``` - -### Find Delegations for a Command - -```sql --- name: ListDelegationsForCommand :many -SELECT * FROM ucan_delegations -WHERE did_id = ? - AND (cmd = ? OR cmd = '/' OR ? LIKE cmd || '/%') - AND (exp IS NULL OR exp > datetime('now')) -ORDER BY created_at DESC; -``` - -### Check if Delegation is Revoked - -```sql --- name: IsDelegationRevoked :one -SELECT EXISTS(SELECT 1 FROM ucan_revocations WHERE delegation_cid = ?) as revoked; -``` - -## Go Integration - -### Delegation Loader Implementation - -```go -// internal/keybase/ucan_loader.go - -package keybase - -import ( - "context" - "fmt" - - "github.com/ipfs/go-cid" - "github.com/ucan-wg/go-ucan/token/delegation" -) - -// DelegationLoader implements delegation.Loader for go-ucan -type DelegationLoader struct { - queries *Queries -} - -func NewDelegationLoader(queries *Queries) *DelegationLoader { - return &DelegationLoader{queries: queries} -} - -// GetDelegation implements delegation.Loader -func (l *DelegationLoader) GetDelegation(c cid.Cid) (*delegation.Token, error) { - ctx := context.Background() - - envelope, err := l.queries.GetDelegationEnvelopeByCID(ctx, c.String()) - if err != nil { - return nil, fmt.Errorf("delegation not found: %s", c) - } - - // Decode DAG-CBOR envelope to delegation token - return delegation.FromSealed(envelope) -} -``` - -### Action Manager Integration - -```go -// internal/keybase/actions_delegation_v2.go - -type DelegationV2Result struct { - CID string `json:"cid"` - Issuer string `json:"iss"` - Audience string `json:"aud"` - Subject string `json:"sub,omitempty"` - Command string `json:"cmd"` - Policy string `json:"pol"` - ExpiresAt string `json:"exp,omitempty"` - IsRoot bool `json:"is_root"` - CreatedAt string `json:"created_at"` -} - -func (am *ActionManager) StoreDelegation(ctx context.Context, sealed []byte, c cid.Cid) (*DelegationV2Result, error) { - // Decode to extract indexed fields - token, err := delegation.FromSealed(sealed) - if err != nil { - return nil, err - } - - // Store in database - result, err := am.kb.queries.CreateDelegationV2(ctx, CreateDelegationV2Params{ - DidID: am.kb.didID, - Cid: c.String(), - Envelope: sealed, - Iss: token.Issuer().String(), - Aud: token.Audience().String(), - Sub: didToNullable(token.Subject()), - Cmd: token.Command().String(), - Pol: policyToJSON(token.Policy()), - Exp: timeToNullable(token.Expiration()), - Nbf: timeToNullable(token.NotBefore()), - IsRoot: boolToInt(token.IsRoot()), - IsPowerline: boolToInt(token.IsPowerline()), - }) - - return delegationV2ToResult(result), nil -} -``` - -## Migration Path - -1. **Create new tables** alongside old ones -2. **Migrate existing data** (if any JWT tokens exist) - - Parse old `raw_token` - - Re-encode as v1.0.0-rc.1 envelope (requires re-signing) - - Or: Mark old tokens as legacy, start fresh with v1.0.0-rc.1 -3. **Update ActionManager** to use new tables -4. **Drop old tables** after migration verified - -## Benefits - -| Aspect | Old (JWT) | New (v1.0.0-rc.1) | -|--------|-----------|-------------------| -| Storage | JSON text | Binary CBOR (smaller) | -| Verification | Parse JWT, verify sig | go-ucan handles it | -| Proof Chain | JSON array in token | Separate CID references | -| Policy | `capabilities` JSON | Structured `pol` field | -| Interop | Non-standard | Spec-compliant | - -## Conclusion - -Integrating UCAN v1.0.0-rc.1 into the keybase schema: - -1. **Maintains single-database portability** for WASM plugin -2. **Leverages existing infrastructure** (SQLC, ActionManager) -3. **Enables foreign key relationships** with DID documents -4. **Provides efficient queries** via indexed fields -5. **Supports go-ucan integration** via `delegation.Loader` diff --git a/example/main.js b/example/main.js index 97b527f..567235e 100644 --- a/example/main.js +++ b/example/main.js @@ -6,8 +6,8 @@ let currentResource = 'accounts'; let currentAction = 'list'; const RESOURCE_ACTIONS = { - accounts: ['list', 'get'], - enclaves: ['list', 'get', 'rotate', 'archive', 'delete'], + accounts: ['list', 'get', 'sign'], + enclaves: ['list', 'get', 'sign', 'rotate', 'archive', 'delete'], credentials: ['list', 'get'], sessions: ['list', 'revoke'], grants: ['list', 'revoke'], @@ -17,7 +17,7 @@ const RESOURCE_ACTIONS = { services: ['list', 'get', 'get_by_id'], }; -const ACTIONS_REQUIRING_SUBJECT = ['get', 'revoke', 'delete', 'verify', 'rotate', 'archive', 'list_received', 'list_command', 'get_by_id']; +const ACTIONS_REQUIRING_SUBJECT = ['get', 'revoke', 'delete', 'verify', 'rotate', 'archive', 'list_received', 'list_command', 'get_by_id', 'sign']; function log(card, level, message, data = null) { const el = document.getElementById(`log-${card}`); @@ -322,6 +322,7 @@ function updateSubjectRow() { list_received: 'Audience DID', list_command: 'Command (e.g., msg/send)', get_by_id: 'Service ID (number)', + sign: currentResource === 'enclaves' ? 'enclave_id:data_to_sign' : 'Data to sign (text)', }; subjectInput.placeholder = placeholders[currentAction] || 'Subject'; -- 2.43.0