diff --git a/config/compose.toml b/config/compose.toml index 6e2a7acc5..1f53666de 100644 --- a/config/compose.toml +++ b/config/compose.toml @@ -38,10 +38,11 @@ password = "default-password" [services.did] name = "did" -methods = ["key", "web"] -resolution_methods = ["key", "web", "pkh", "peer"] +methods = ["key", "web", "ion"] +local_resolution_methods = ["key", "web", "pkh", "peer"] universal_resolver_url = "http://uni-resolver-web:8080" universal_resolver_methods = ["ion"] +ion_resolver_url = "https://tbdwebsiteonline.com" [services.schema] name = "schema" diff --git a/config/config.go b/config/config.go index c94e2f778..25a50a303 100644 --- a/config/config.go +++ b/config/config.go @@ -105,9 +105,10 @@ func (k *KeyStoreServiceConfig) IsEmpty() bool { type DIDServiceConfig struct { *BaseServiceConfig Methods []string `toml:"methods"` - ResolutionMethods []string `toml:"resolution_methods"` + LocalResolutionMethods []string `toml:"local_resolution_methods"` UniversalResolverURL string `toml:"universal_resolver_url"` UniversalResolverMethods []string `toml:"universal_resolver_methods"` + IONResolverURL string `toml:"ion_resolver_url"` } func (d *DIDServiceConfig) IsEmpty() bool { @@ -255,9 +256,9 @@ func loadDefaultServicesConfig(config *SSIServiceConfig) { ServiceKeyPassword: "default-password", }, DIDConfig: DIDServiceConfig{ - BaseServiceConfig: &BaseServiceConfig{Name: "did"}, - Methods: []string{"key", "web"}, - ResolutionMethods: []string{"key", "peer", "web", "pkh"}, + BaseServiceConfig: &BaseServiceConfig{Name: "did"}, + Methods: []string{"key", "web"}, + LocalResolutionMethods: []string{"key", "peer", "web", "pkh"}, }, SchemaConfig: SchemaServiceConfig{ BaseServiceConfig: &BaseServiceConfig{Name: "schema"}, diff --git a/config/config.toml b/config/config.toml index 8d3027105..7e928d268 100644 --- a/config/config.toml +++ b/config/config.toml @@ -38,8 +38,8 @@ password = "default-password" [services.did] name = "did" -methods = ["key", "web"] -resolution_methods = ["key", "web", "pkh", "peer"] +methods = ["key", "web", "ion"] +local_resolution_methods = ["key", "web", "pkh", "peer"] universal_resolver_url = "http://localhost:8088" universal_resolver_methods = ["ion"] diff --git a/config/config.toml.example b/config/config.toml.example index d9ddc5a24..75ba0b993 100644 --- a/config/config.toml.example +++ b/config/config.toml.example @@ -42,7 +42,7 @@ password = "default-password" [services.did] name = "did" methods = ["key", "web"] -resolution_methods = ["key", "web", "pkh", "peer"] +local_resolution_methods = ["key", "web", "pkh", "peer"] [services.schema] name = "schema" diff --git a/go.mod b/go.mod index ca2791a3d..7cd9aa97f 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.20 require ( github.com/BurntSushi/toml v1.2.1 - github.com/TBD54566975/ssi-sdk v0.0.3-alpha.0.20230328175208-0ce55b2b0262 + github.com/TBD54566975/ssi-sdk v0.0.3-alpha.0.20230401051839-bca28f8b0fea github.com/alicebob/miniredis/v2 v2.30.1 github.com/ardanlabs/conf v1.5.0 github.com/benbjohnson/clock v1.3.0 @@ -39,6 +39,7 @@ require ( go.opentelemetry.io/otel/trace v1.14.0 golang.org/x/crypto v0.7.0 gopkg.in/go-playground/validator.v9 v9.31.0 + gopkg.in/h2non/gock.v1 v1.1.2 ) replace github.com/dgraph-io/ristretto => github.com/ory/ristretto v0.1.1-0.20211108053508-297c39e6640f @@ -48,6 +49,8 @@ require ( github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect github.com/bits-and-blooms/bitset v1.5.0 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect + github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cristalhq/jwt/v4 v4.0.2 // indirect github.com/dave/jennifer v1.4.0 // indirect @@ -66,6 +69,8 @@ require ( github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/gorilla/websocket v1.4.2 // indirect + github.com/gowebpki/jcs v1.0.0 // indirect + github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect github.com/hashicorp/go-cleanhttp v0.5.1 // indirect github.com/hashicorp/go-retryablehttp v0.6.8 // indirect github.com/hashicorp/hcl v1.0.0 // indirect @@ -73,6 +78,7 @@ require ( github.com/hyperledger/aries-framework-go/spi v0.0.0-20221025204933-b807371b6f1e // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/kilic/bls12-381 v0.1.1-0.20210503002446-7b7597926c69 // indirect + github.com/klauspost/cpuid/v2 v2.0.9 // indirect github.com/leodido/go-urn v1.2.2 // indirect github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect github.com/lestrrat-go/blackmagic v1.0.1 // indirect @@ -83,11 +89,13 @@ require ( github.com/lestrrat-go/option v1.0.1 // indirect github.com/magiconair/properties v1.8.1 // indirect github.com/mattn/goveralls v0.0.6 // indirect + github.com/minio/sha256-simd v1.0.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/multiformats/go-base32 v0.1.0 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect github.com/multiformats/go-multicodec v0.8.1 // indirect + github.com/multiformats/go-multihash v0.2.1 // indirect github.com/ory/go-acc v0.2.6 // indirect github.com/ory/go-convenience v0.1.0 // indirect github.com/ory/viper v1.7.5 // indirect @@ -99,6 +107,7 @@ require ( github.com/pquerna/cachecontrol v0.1.0 // indirect github.com/redis/go-redis/extra/rediscmd/v9 v9.0.2 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.2.0 // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spf13/afero v1.3.2 // indirect github.com/spf13/cast v1.3.2-0.20200723214538-8d17101741c8 // indirect github.com/spf13/cobra v1.0.0 // indirect @@ -122,4 +131,5 @@ require ( gopkg.in/square/go-jose.v2 v2.5.2-0.20210529014059-a5c7eec3c614 // indirect gopkg.in/yaml.v2 v2.3.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + lukechampine.com/blake3 v1.1.6 // indirect ) diff --git a/go.sum b/go.sum index 4f5ef8418..2d58dcd63 100644 --- a/go.sum +++ b/go.sum @@ -27,8 +27,8 @@ github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= -github.com/TBD54566975/ssi-sdk v0.0.3-alpha.0.20230328175208-0ce55b2b0262 h1:mMd9kTYtqEHAbiYwtnH94qyY4Meanp0mWkOktrZyVv4= -github.com/TBD54566975/ssi-sdk v0.0.3-alpha.0.20230328175208-0ce55b2b0262/go.mod h1:Ro9+HDADuYcLnCYKIL8pu92XWITKetTPOqfwLThplO0= +github.com/TBD54566975/ssi-sdk v0.0.3-alpha.0.20230401051839-bca28f8b0fea h1:+0dl8nqYYkewqiiYKxhk3RK7ivOOzweeYa08DFOU3fE= +github.com/TBD54566975/ssi-sdk v0.0.3-alpha.0.20230401051839-bca28f8b0fea/go.mod h1:9SNGqEkRihpNbgilFDRRmgi8Gi/l7/l0eyO+3Prubro= github.com/ajg/form v0.0.0-20160822230020-523a5da1a92f/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -62,7 +62,10 @@ github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dR github.com/bsm/ginkgo/v2 v2.5.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w= github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= github.com/bsm/gomega v1.20.0/go.mod h1:JifAceMQ4crZIWYUKrlGcmbN3bqHogVTADMD2ATsbwk= -github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= +github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= +github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 h1:KdUfX2zKommPRa+PD0sWZUyXe9w277ABlgELO7H04IM= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce h1:YtWJF7RHm2pYCvA5t0RPmAaLUhREsKuKd+SLhxFbFeQ= github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= @@ -110,6 +113,7 @@ github.com/dave/jennifer v1.4.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhr github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= @@ -562,10 +566,13 @@ github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0U github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gotestyourself/gotestyourself v1.3.0/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY= github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY= +github.com/gowebpki/jcs v1.0.0 h1:0pZtOgGetfH/L7yXb4KWcJqIyZNA43WXFyMd7ftZACw= +github.com/gowebpki/jcs v1.0.0/go.mod h1:CID1cNZ+sHp1CCpAR8mPf6QRtagFBgPJE0FCUQ6+BrI= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= @@ -655,6 +662,9 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/knadh/koanf v0.14.1-0.20201201075439-e0853799f9ec/go.mod h1:H5mEFsTeWizwFXHKtsITL5ipsLTuAMQoGuQpp+1JL9U= github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -755,6 +765,8 @@ github.com/mattn/goveralls v0.0.6/go.mod h1:h8b4ow6FxSPMQHF6o2ve3qsclnffZjYTNEKm github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= @@ -780,9 +792,13 @@ github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivnc github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= github.com/multiformats/go-multicodec v0.8.1 h1:ycepHwavHafh3grIbR1jIXnKCsFm0fqsfEOsJ8NtKE8= github.com/multiformats/go-multicodec v0.8.1/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= +github.com/multiformats/go-multihash v0.2.1 h1:aem8ZT0VA2nCHHk7bPJ1BjUbHNciqZC/d16Vve9l108= +github.com/multiformats/go-multihash v0.2.1/go.mod h1:WxoMcYG85AZVQUyRyo9s4wULvW5qrI9vb2Lt6evduFc= github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= +github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/nicksnyder/go-i18n v1.10.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= @@ -957,6 +973,8 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.0/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= @@ -1449,6 +1467,7 @@ gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWd gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw= gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= +gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= @@ -1484,6 +1503,8 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= +lukechampine.com/blake3 v1.1.6 h1:H3cROdztr7RCfoaTpGZFQsrqvweFLrqS73j7L7cmR5c= +lukechampine.com/blake3 v1.1.6/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= diff --git a/integration/common.go b/integration/common.go index e861850b5..8e9f3ee92 100644 --- a/integration/common.go +++ b/integration/common.go @@ -17,8 +17,10 @@ import ( "github.com/oliveagle/jsonpath" "github.com/pkg/errors" "github.com/sirupsen/logrus" + credmodel "github.com/tbd54566975/ssi-service/internal/credential" "github.com/tbd54566975/ssi-service/internal/keyaccess" + "github.com/tbd54566975/ssi-service/pkg/server/router" ) const ( @@ -45,7 +47,29 @@ func CreateDIDKey() (string, error) { func CreateDIDWeb() (string, error) { logrus.Println("\n\nCreate a did:web") - output, err := put(endpoint+version+"dids/web", getJSONFromFile("did-web-input.json")) + + var createDIDWebRequest router.CreateDIDByMethodRequest + inputJSON := getJSONFromFile("did-web-input.json") + if err := json.Unmarshal([]byte(inputJSON), &createDIDWebRequest); err != nil { + return "", errors.Wrap(err, "unmarshalling did:web request") + } + + createdRequestJSONBytes, err := json.Marshal(createDIDWebRequest) + if err != nil { + return "", errors.Wrap(err, "creating did:web request") + } + + output, err := put(endpoint+version+"dids/web", string(createdRequestJSONBytes)) + if err != nil { + return "", errors.Wrapf(err, "did endpoint with output: %s", output) + } + + return output, nil +} + +func CreateDIDION() (string, error) { + logrus.Println("\n\nCreate a did:ion") + output, err := put(endpoint+version+"dids/ion", getJSONFromFile("did-ion-input.json")) if err != nil { return "", errors.Wrapf(err, "did endpoint with output: %s", output) } diff --git a/integration/credential_revocation_integration_test.go b/integration/credential_revocation_integration_test.go index 5488e3fbc..acb3fabaa 100644 --- a/integration/credential_revocation_integration_test.go +++ b/integration/credential_revocation_integration_test.go @@ -24,7 +24,6 @@ func TestRevocationCreateIssuerDIDKeyIntegration(t *testing.T) { assert.NoError(t, err) assert.Contains(t, issuerDID, "did:key") - } func TestRevocationCreateSchemaIntegration(t *testing.T) { diff --git a/integration/didion_integration_test.go b/integration/didion_integration_test.go new file mode 100644 index 000000000..22e0ea1cd --- /dev/null +++ b/integration/didion_integration_test.go @@ -0,0 +1,211 @@ +package integration + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/tbd54566975/ssi-service/pkg/service/operation/storage" +) + +var didIONContext = NewTestContext("DIDION") + +func TestCreateIssuerDIDIONIntegration(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + didIONOutput, err := CreateDIDION() + assert.NoError(t, err) + + issuerDID, err := getJSONElement(didIONOutput, "$.did.id") + assert.NoError(t, err) + SetValue(didIONContext, "issuerDID", issuerDID) + + issuerKeyID, err := getJSONElement(didIONOutput, "$.did.verificationMethod[0].id") + SetValue(didIONContext, "issuerKeyID", issuerKeyID) + + assert.NoError(t, err) + assert.Contains(t, issuerDID, "did:ion") +} + +func TestCreateAliceDIDIONIntegration(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + didIONOutput, err := CreateDIDION() + assert.NoError(t, err) + assert.NotEmpty(t, didIONOutput) + + aliceDID, err := getJSONElement(didIONOutput, "$.did.id") + assert.NoError(t, err) + assert.NotEmpty(t, aliceDID) + SetValue(didIONContext, "aliceDID", aliceDID) + + aliceKeyID, err := getJSONElement(didIONOutput, "$.did.verificationMethod[0].id") + SetValue(didIONContext, "aliceKeyID", aliceKeyID) + assert.NoError(t, err) + assert.NotEmpty(t, aliceKeyID) + + assert.NoError(t, err) + assert.Contains(t, aliceDID, "did:ion") + + aliceDIDPrivateKey, err := getJSONElement(didIONOutput, "$.privateKeyBase58") + assert.NoError(t, err) + assert.NotEmpty(t, aliceDIDPrivateKey) + SetValue(didIONContext, "aliceDIDPrivateKey", aliceDIDPrivateKey) +} + +func TestDIDIONCreateSchemaIntegration(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + output, err := CreateKYCSchema() + assert.NoError(t, err) + + schemaID, err := getJSONElement(output, "$.id") + SetValue(didIONContext, "schemaID", schemaID) + + assert.NoError(t, err) + assert.NotEmpty(t, schemaID) +} + +func TestDIDIONCreateVerifiableCredentialIntegration(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + issuerDID, err := GetValue(didIONContext, "issuerDID") + assert.NoError(t, err) + assert.NotEmpty(t, issuerDID) + + issuerKeyID, err := GetValue(didIONContext, "issuerKeyID") + assert.NoError(t, err) + assert.NotEmpty(t, issuerKeyID) + + schemaID, err := GetValue(didIONContext, "schemaID") + assert.NoError(t, err) + assert.NotEmpty(t, schemaID) + + vcOutput, err := CreateVerifiableCredential(credInputParams{ + IssuerID: issuerDID.(string) + "#" + issuerKeyID.(string), + SchemaID: schemaID.(string), + SubjectID: issuerDID.(string), + }, false) + assert.NoError(t, err) + assert.NotEmpty(t, vcOutput) + + credentialJWT, err := getJSONElement(vcOutput, "$.credentialJwt") + SetValue(didIONContext, "credentialJWT", credentialJWT) + assert.NoError(t, err) + assert.NotEmpty(t, credentialJWT) +} + +func TestDIDIONCreateCredentialManifestIntegration(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + issuerDID, err := GetValue(didIONContext, "issuerDID") + assert.NoError(t, err) + assert.NotEmpty(t, issuerDID) + + issuerKeyID, err := GetValue(didIONContext, "issuerKeyID") + assert.NoError(t, err) + assert.NotEmpty(t, issuerKeyID) + + schemaID, err := GetValue(didIONContext, "schemaID") + assert.NoError(t, err) + assert.NotEmpty(t, schemaID) + + cmOutput, err := CreateCredentialManifest(credManifestParams{ + IssuerID: issuerDID.(string) + "#" + issuerKeyID.(string), + SchemaID: schemaID.(string), + }) + assert.NoError(t, err) + + presentationDefinitionID, err := getJSONElement(cmOutput, "$.credential_manifest.presentation_definition.id") + SetValue(didIONContext, "presentationDefinitionID", presentationDefinitionID) + assert.NoError(t, err) + assert.NotEmpty(t, presentationDefinitionID) + + manifestID, err := getJSONElement(cmOutput, "$.credential_manifest.id") + SetValue(didIONContext, "manifestID", manifestID) + assert.NoError(t, err) + assert.NotEmpty(t, manifestID) +} + +func TestDIDIONSubmitAndReviewApplicationIntegration(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + credentialJWT, err := GetValue(didIONContext, "credentialJWT") + assert.NoError(t, err) + assert.NotEmpty(t, credentialJWT) + + presentationDefinitionID, err := GetValue(didIONContext, "presentationDefinitionID") + assert.NoError(t, err) + assert.NotEmpty(t, presentationDefinitionID) + + manifestID, err := GetValue(didIONContext, "manifestID") + assert.NoError(t, err) + assert.NotEmpty(t, manifestID) + + aliceDID, err := GetValue(didIONContext, "aliceDID") + assert.NoError(t, err) + assert.NotEmpty(t, aliceDID) + + aliceKeyID, err := GetValue(didIONContext, "aliceKeyID") + assert.NoError(t, err) + assert.NotEmpty(t, aliceDID) + + aliceDIDPrivateKey, err := GetValue(didIONContext, "aliceDIDPrivateKey") + assert.NoError(t, err) + assert.NotEmpty(t, aliceDIDPrivateKey) + + credAppJWT, err := CreateCredentialApplicationJWT(credApplicationParams{ + DefinitionID: presentationDefinitionID.(string), + ManifestID: manifestID.(string), + }, credentialJWT.(string), aliceDID.(string)+"#"+aliceKeyID.(string), aliceDIDPrivateKey.(string)) + assert.NoError(t, err) + assert.NotEmpty(t, credAppJWT) + + submitApplicationOutput, err := SubmitApplication(applicationParams{ + ApplicationJWT: credAppJWT, + }) + assert.NoError(t, err) + assert.NotEmpty(t, submitApplicationOutput) + + isDone, err := getJSONElement(submitApplicationOutput, "$.done") + assert.NoError(t, err) + assert.Equal(t, "false", isDone) + opID, err := getJSONElement(submitApplicationOutput, "$.id") + assert.NoError(t, err) + + reviewApplicationOutput, err := ReviewApplication(reviewApplicationParams{ + ID: storage.StatusObjectID(opID), + Approved: true, + Reason: "oh yeah im testing", + }) + assert.NoError(t, err) + crManifestID, err := getJSONElement(reviewApplicationOutput, "$.credential_response.manifest_id") + assert.NoError(t, err) + assert.Equal(t, manifestID, crManifestID) + + vc, err := getJSONElement(reviewApplicationOutput, "$.verifiableCredentials[0]") + assert.NoError(t, err) + assert.NotEmpty(t, vc) + + operationOutput, err := get(endpoint + version + "operations/" + opID) + assert.NoError(t, err) + isDone, err = getJSONElement(operationOutput, "$.done") + assert.NoError(t, err) + assert.Equal(t, "true", isDone) + + opCredentialResponse, err := getJSONElement(operationOutput, "$.result.response") + assert.NoError(t, err) + assert.JSONEq(t, reviewApplicationOutput, opCredentialResponse) +} diff --git a/integration/didweb_integration_test.go b/integration/didweb_integration_test.go index faa61240a..0d912e018 100644 --- a/integration/didweb_integration_test.go +++ b/integration/didweb_integration_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/tbd54566975/ssi-service/pkg/service/operation/storage" ) @@ -22,7 +23,6 @@ func TestCreateIssuerDIDWebIntegration(t *testing.T) { assert.NoError(t, err) assert.Contains(t, issuerDID, "did:web") - } func TestCreateAliceDIDWebIntegration(t *testing.T) { @@ -44,7 +44,6 @@ func TestCreateAliceDIDWebIntegration(t *testing.T) { SetValue(didWebContext, "aliceDIDPrivateKey", aliceDIDPrivateKey) assert.NoError(t, err) assert.NotEmpty(t, aliceDID) - } func TestDIDWebCreateSchemaIntegration(t *testing.T) { diff --git a/integration/didweb_resolver_integration_test.go b/integration/didweb_resolver_integration_test.go index 4d6326189..224cec9c1 100644 --- a/integration/didweb_resolver_integration_test.go +++ b/integration/didweb_resolver_integration_test.go @@ -12,7 +12,7 @@ func TestResolveDIDWebIntegration(t *testing.T) { } // A .well-known file exists at https://tbd.website/.well-known/did.json - didWebOutput, err := put(endpoint+version+"dids/web", `{ "keyType":"Ed25519", "didWebId":"did:web:tbd.website"}`) + didWebOutput, err := put(endpoint+version+"dids/web", `{ "keyType":"Ed25519", "options": {"didWebId":"did:web:tbd.website"}}`) assert.NoError(t, err) did, err := getJSONElement(didWebOutput, "$.did.id") diff --git a/integration/testdata/did-ion-input.json b/integration/testdata/did-ion-input.json new file mode 100644 index 000000000..9d7bf8ea8 --- /dev/null +++ b/integration/testdata/did-ion-input.json @@ -0,0 +1,3 @@ +{ + "keyType":"Ed25519" +} \ No newline at end of file diff --git a/integration/testdata/did-web-input.json b/integration/testdata/did-web-input.json index fcb7f09d2..824b099b2 100644 --- a/integration/testdata/did-web-input.json +++ b/integration/testdata/did-web-input.json @@ -1,4 +1,6 @@ { "keyType":"Ed25519", - "didWebId":"did:web:tbd.website" + "options": { + "didWebId": "did:web:tbd.website" + } } \ No newline at end of file diff --git a/internal/did/resolver.go b/internal/did/resolver.go index 0b92845e4..fee47d49b 100644 --- a/internal/did/resolver.go +++ b/internal/did/resolver.go @@ -5,6 +5,7 @@ import ( didsdk "github.com/TBD54566975/ssi-sdk/did" "github.com/pkg/errors" + "github.com/sirupsen/logrus" ) // BuildMultiMethodResolver builds a multi method DID resolver from a list of methods to support resolution for @@ -16,7 +17,9 @@ func BuildMultiMethodResolver(methods []string) (*didsdk.MultiMethodResolver, er for _, method := range methods { resolver, err := getKnownResolver(method) if err != nil { - return nil, err + // if we can't create a resolver for a method, we just skip it since not all methods are supported locally + logrus.WithError(err).Errorf("failed to create resolver for method %s", method) + continue } resolvers = append(resolvers, resolver) } diff --git a/internal/did/resolver_test.go b/internal/did/resolver_test.go index 520aed2ac..2171a2ade 100644 --- a/internal/did/resolver_test.go +++ b/internal/did/resolver_test.go @@ -16,7 +16,7 @@ func TestResolver(t *testing.T) { // unsupported method _, err = BuildMultiMethodResolver([]string{"unsupported"}) assert.Error(t, err) - assert.Contains(t, err.Error(), "unsupported method: unsupported") + assert.Contains(t, err.Error(), "no resolvers created") // valid method resolver, err := BuildMultiMethodResolver([]string{"key"}) diff --git a/internal/util/util.go b/internal/util/util.go index b8b2f1edd..270296c39 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -2,6 +2,7 @@ package util import ( "fmt" + "reflect" "strings" didsdk "github.com/TBD54566975/ssi-sdk/did" @@ -9,6 +10,25 @@ import ( "github.com/sirupsen/logrus" ) +// IsStructPtr checks if the given object is a pointer to a struct +func IsStructPtr(obj any) bool { + if obj == nil { + return false + } + // make sure out is a ptr to a struct + outVal := reflect.ValueOf(obj) + if outVal.Kind() != reflect.Ptr { + return false + } + + // dereference the pointer + outValDeref := outVal.Elem() + if outValDeref.Kind() != reflect.Struct { + return false + } + return true +} + // GetMethodForDID gets a DID method from a did, the second part of the did (e.g. did:test:abcd, the method is 'test') func GetMethodForDID(did string) (didsdk.Method, error) { split := strings.Split(did, ":") diff --git a/pkg/authorizationserver/oauth2.go b/pkg/authorizationserver/oauth2.go index 72d60f600..2544a2287 100644 --- a/pkg/authorizationserver/oauth2.go +++ b/pkg/authorizationserver/oauth2.go @@ -16,6 +16,7 @@ import ( "github.com/ory/fosite/storage" "github.com/ory/fosite/token/jwt" "github.com/sirupsen/logrus" + "github.com/tbd54566975/ssi-service/config" "github.com/tbd54566975/ssi-service/pkg/server/framework" "github.com/tbd54566975/ssi-service/pkg/server/middleware" diff --git a/pkg/authorizationserver/oauth2_test.go b/pkg/authorizationserver/oauth2_test.go index bedc9da1b..6695de233 100644 --- a/pkg/authorizationserver/oauth2_test.go +++ b/pkg/authorizationserver/oauth2_test.go @@ -64,7 +64,7 @@ func TestCredentialIssuerMetadata(t *testing.T) { metadata, err := fetchMetadata(server.URL + "/oidc/issuer/.well-known/openid-credential-issuer") require.NoError(t, err) - // Check that the issuer matches the URL that was fetched + // Check that the issuer matches the DIDWebID that was fetched assert.JSONEq(t, string(expectedIssuerMetadata), string(metadata)) } diff --git a/pkg/server/router/did.go b/pkg/server/router/did.go index 94c22c0d9..db9b76973 100644 --- a/pkg/server/router/did.go +++ b/pkg/server/router/did.go @@ -7,9 +7,11 @@ import ( "github.com/TBD54566975/ssi-sdk/crypto" didsdk "github.com/TBD54566975/ssi-sdk/did" + "github.com/goccy/go-json" "github.com/pkg/errors" "github.com/sirupsen/logrus" + "github.com/tbd54566975/ssi-service/internal/util" "github.com/tbd54566975/ssi-service/pkg/server/framework" "github.com/tbd54566975/ssi-service/pkg/service/did" svcframework "github.com/tbd54566975/ssi-service/pkg/service/framework" @@ -34,9 +36,7 @@ func NewDIDRouter(s svcframework.Service) (*DIDRouter, error) { if !ok { return nil, fmt.Errorf("could not create DID router with service type: %s", s.Type()) } - return &DIDRouter{ - service: didService, - }, nil + return &DIDRouter{service: didService}, nil } type GetDIDMethodsResponse struct { @@ -60,11 +60,11 @@ func (dr DIDRouter) GetDIDMethods(ctx context.Context, w http.ResponseWriter, _ type CreateDIDByMethodRequest struct { // Identifies the cryptographic algorithm family to use when generating this key. - // One of the following: `"Ed25519","X25519","secp256k1","P-224","P-256","P-384","P-521","RSA"`. + // One of the following: "Ed25519", "X25519", "secp256k1", "P-224","P-256","P-384", "P-521", "RSA" KeyType crypto.KeyType `json:"keyType" validate:"required"` - // Required when creating a DID with the `web` did method. E.g. `did:web:identity.foundation`. - DIDWebID string `json:"didWebId"` + // Options for creating the DID. Implementation dependent on the method. + Options any `json:"options,omitempty"` } type CreateDIDByMethodResponse struct { @@ -111,8 +111,13 @@ func (dr DIDRouter) CreateDIDByMethod(ctx context.Context, w http.ResponseWriter } // TODO(gabe) check if the key type is supported for the method, to tell whether this is a bad req or internal error - createDIDRequest := did.CreateDIDRequest{Method: didsdk.Method(*method), KeyType: request.KeyType, DIDWebID: request.DIDWebID} - createDIDResponse, err := dr.service.CreateDIDByMethod(ctx, createDIDRequest) + createDIDRequest, err := toCreateDIDRequest(didsdk.Method(*method), request) + if err != nil { + errMsg := fmt.Sprintf("could not create DID for method<%s> with key type: %s", *method, request.KeyType) + logrus.WithError(err).Error(errMsg) + return framework.NewRequestError(errors.Wrap(err, invalidCreateDIDRequest), http.StatusBadRequest) + } + createDIDResponse, err := dr.service.CreateDIDByMethod(ctx, *createDIDRequest) if err != nil { errMsg := fmt.Sprintf("could not create DID for method<%s> with key type: %s", *method, request.KeyType) logrus.WithError(err).Error(errMsg) @@ -128,6 +133,56 @@ func (dr DIDRouter) CreateDIDByMethod(ctx context.Context, w http.ResponseWriter return framework.Respond(ctx, w, resp, http.StatusCreated) } +// toCreateDIDRequest converts CreateDIDByMethodRequest to did.CreateDIDRequest, parsing options according to method +func toCreateDIDRequest(m didsdk.Method, request CreateDIDByMethodRequest) (*did.CreateDIDRequest, error) { + createRequest := did.CreateDIDRequest{ + Method: m, + KeyType: request.KeyType, + } + + // check if options are present + if request.Options == nil { + return &createRequest, nil + } + + // parse options according to method + switch m { + case didsdk.IONMethod: + var opts did.CreateIONDIDOptions + if err := optionsToType(request.Options, &opts); err != nil { + return nil, errors.Wrap(err, "parsing ion options") + } + createRequest.Options = opts + case didsdk.WebMethod: + var opts did.CreateWebDIDOptions + if err := optionsToType(request.Options, &opts); err != nil { + return nil, errors.Wrap(err, "parsing web options") + } + createRequest.Options = opts + default: + if request.Options != nil { + return nil, fmt.Errorf("invalid options for method<%s>", m) + } + } + return &createRequest, nil +} + +// optionsToType converts options to the given type where options is a map[string]interface{} and optionType +// is a pointer to an empty struct of the desired type +func optionsToType(options any, out any) error { + if !util.IsStructPtr(out) { + return fmt.Errorf("output object must be a pointer to a struct") + } + optionBytes, err := json.Marshal(options) + if err != nil { + return errors.Wrap(err, "marshalling options") + } + if err = json.Unmarshal(optionBytes, out); err != nil { + return errors.Wrap(err, "unmarshalling options") + } + return nil +} + type GetDIDByMethodResponse struct { DID didsdk.Document `json:"did,omitempty"` } diff --git a/pkg/server/router/did_test.go b/pkg/server/router/did_test.go index 5e053c8c9..a879f4e9b 100644 --- a/pkg/server/router/did_test.go +++ b/pkg/server/router/did_test.go @@ -35,7 +35,7 @@ func TestDIDRouter(t *testing.T) { keyStoreService := testKeyStoreService(tt, db) methods := []string{didsdk.KeyMethod.String()} - serviceConfig := config.DIDServiceConfig{Methods: methods, ResolutionMethods: methods} + serviceConfig := config.DIDServiceConfig{Methods: methods, LocalResolutionMethods: methods} didService, err := did.NewDIDService(serviceConfig, db, keyStoreService) assert.NoError(tt, err) assert.NotEmpty(tt, didService) @@ -117,7 +117,7 @@ func TestDIDRouter(t *testing.T) { keyStoreService := testKeyStoreService(tt, db) methods := []string{didsdk.KeyMethod.String(), didsdk.WebMethod.String()} - serviceConfig := config.DIDServiceConfig{Methods: methods, ResolutionMethods: methods} + serviceConfig := config.DIDServiceConfig{Methods: methods, LocalResolutionMethods: methods} didService, err := did.NewDIDService(serviceConfig, db, keyStoreService) assert.NoError(tt, err) assert.NotEmpty(tt, didService) @@ -138,12 +138,13 @@ func TestDIDRouter(t *testing.T) { assert.ElementsMatch(tt, supported.Methods, []didsdk.Method{didsdk.KeyMethod, didsdk.WebMethod}) // bad key type - _, err = didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: didsdk.WebMethod, KeyType: "bad", DIDWebID: "did:web:example.com"}) + createOpts := did.CreateWebDIDOptions{DIDWebID: "did:web:example.com"} + _, err = didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: didsdk.WebMethod, KeyType: "bad", Options: createOpts}) assert.Error(tt, err) assert.Contains(tt, err.Error(), "could not generate key for did:web") // good key type - createDIDResponse, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: didsdk.WebMethod, KeyType: crypto.Ed25519, DIDWebID: "did:web:example.com"}) + createDIDResponse, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: didsdk.WebMethod, KeyType: crypto.Ed25519, Options: createOpts}) assert.NoError(tt, err) assert.NotEmpty(tt, createDIDResponse) @@ -159,7 +160,8 @@ func TestDIDRouter(t *testing.T) { assert.Equal(tt, createDIDResponse.DID.ID, getDIDResponse.DID.ID) // create a second DID - createDIDResponse2, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: didsdk.WebMethod, KeyType: crypto.Ed25519, DIDWebID: "did:web:tbd.website"}) + createOpts = did.CreateWebDIDOptions{DIDWebID: "did:web:tbd.website"} + createDIDResponse2, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: didsdk.WebMethod, KeyType: crypto.Ed25519, Options: createOpts}) assert.NoError(tt, err) assert.NotEmpty(tt, createDIDResponse2) diff --git a/pkg/server/router/keystore.go b/pkg/server/router/keystore.go index c59f8815d..00037ab6f 100644 --- a/pkg/server/router/keystore.go +++ b/pkg/server/router/keystore.go @@ -37,7 +37,7 @@ type StoreKeyRequest struct { ID string `json:"id" validate:"required"` // Identifies the cryptographic algorithm family used with the key. - // One of the following: `"Ed25519","X25519","secp256k1","P-224","P-256","P-384","P-521","RSA"`. + // One of the following: "Ed25519", "X25519", "secp256k1", "P-224", "P-256", "P-384", "P-521", "RSA". Type crypto.KeyType `json:"type,omitempty" validate:"required"` // See https://www.w3.org/TR/did-core/#did-controller diff --git a/pkg/server/router/testutils_test.go b/pkg/server/router/testutils_test.go index f5e79ff15..23297f88f 100644 --- a/pkg/server/router/testutils_test.go +++ b/pkg/server/router/testutils_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/stretchr/testify/require" + "github.com/tbd54566975/ssi-service/pkg/testutil" "github.com/tbd54566975/ssi-service/config" @@ -35,8 +36,8 @@ func testDIDService(t *testing.T, db storage.ServiceStorage, keyStore *keystore. BaseServiceConfig: &config.BaseServiceConfig{ Name: "did", }, - Methods: []string{"key"}, - ResolutionMethods: []string{"key"}, + Methods: []string{"key"}, + LocalResolutionMethods: []string{"key"}, } // create a did service didService, err := did.NewDIDService(serviceConfig, db, keyStore) diff --git a/pkg/server/server.go b/pkg/server/server.go index c0c095b5f..1f2fe13ac 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -229,7 +229,7 @@ func (s *SSIServer) OperationAPI(service svcframework.Service) (err error) { s.Handle(http.MethodGet, handlerPath, operationRouter.GetOperations) // See https://github.com/dimfeld/httptreemux#routing-rules for details on how the `*` works. - // In this case, it's used so that the operation id matches `presentations/submissions/{submission_id}` for the URL + // In this case, it's used so that the operation id matches `presentations/submissions/{submission_id}` for the DIDWebID // path `/v1/operations/cancel/presentations/submissions/{id}` s.Handle(http.MethodPut, path.Join(handlerPath, "/cancel/*id"), operationRouter.CancelOperation) s.Handle(http.MethodGet, path.Join(handlerPath, "/*id"), operationRouter.GetOperation) diff --git a/pkg/server/server_did_test.go b/pkg/server/server_did_test.go index 0f1497976..ce49ab23b 100644 --- a/pkg/server/server_did_test.go +++ b/pkg/server/server_did_test.go @@ -8,11 +8,14 @@ import ( "github.com/TBD54566975/ssi-sdk/crypto" didsdk "github.com/TBD54566975/ssi-sdk/did" + "github.com/TBD54566975/ssi-sdk/did/ion" "github.com/goccy/go-json" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "gopkg.in/h2non/gock.v1" "github.com/tbd54566975/ssi-service/pkg/server/router" + "github.com/tbd54566975/ssi-service/pkg/service/did" ) func TestDIDAPI(t *testing.T) { @@ -21,7 +24,7 @@ func TestDIDAPI(t *testing.T) { require.NotNil(tt, bolt) _, keyStoreService := testKeyStore(tt, bolt) - didService := testDIDRouter(tt, bolt, keyStoreService) + didService := testDIDRouter(tt, bolt, keyStoreService, []string{"key", "web", "ion"}) // get DID method req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids", nil) @@ -35,8 +38,10 @@ func TestDIDAPI(t *testing.T) { err = json.NewDecoder(w.Body).Decode(&resp) assert.NoError(tt, err) - assert.Len(tt, resp.DIDMethods, 1) - assert.Equal(tt, resp.DIDMethods[0], didsdk.KeyMethod) + assert.Len(tt, resp.DIDMethods, 3) + assert.Contains(tt, resp.DIDMethods, didsdk.KeyMethod) + assert.Contains(tt, resp.DIDMethods, didsdk.WebMethod) + assert.Contains(tt, resp.DIDMethods, didsdk.IONMethod) }) t.Run("Test Create DID By Method: Key", func(tt *testing.T) { @@ -44,7 +49,7 @@ func TestDIDAPI(t *testing.T) { require.NotNil(tt, bolt) _, keyStoreService := testKeyStore(tt, bolt) - didService := testDIDRouter(tt, bolt, keyStoreService) + didService := testDIDRouter(tt, bolt, keyStoreService, []string{"key"}) // create DID by method - key - missing body req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/key", nil) @@ -87,12 +92,160 @@ func TestDIDAPI(t *testing.T) { assert.Contains(tt, resp.DID.ID, didsdk.KeyMethod) }) + t.Run("Test Create DID By Method: Web", func(tt *testing.T) { + bolt := setupTestDB(tt) + require.NotNil(tt, bolt) + + _, keyStoreService := testKeyStore(tt, bolt) + didService := testDIDRouter(tt, bolt, keyStoreService, []string{"web"}) + + // create DID by method - web - missing body + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/web", nil) + w := httptest.NewRecorder() + params := map[string]string{ + "method": "web", + } + + err := didService.CreateDIDByMethod(newRequestContextWithParams(params), w, req) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "invalid create DID request") + + // reset recorder between calls + w.Flush() + + // with body, good key type, missing options + createDIDRequest := router.CreateDIDByMethodRequest{KeyType: crypto.Ed25519} + requestReader := newRequestValue(tt, createDIDRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/web", requestReader) + + err = didService.CreateDIDByMethod(newRequestContextWithParams(params), w, req) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "could not create DID for method with key type: Ed25519: options cannot be empty") + + // reset recorder between calls + w.Flush() + + // good options + options := did.CreateWebDIDOptions{DIDWebID: "did:web:example.com"} + + // with body, bad key type + createDIDRequest = router.CreateDIDByMethodRequest{KeyType: "bad", Options: options} + requestReader = newRequestValue(tt, createDIDRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/web", requestReader) + + err = didService.CreateDIDByMethod(newRequestContextWithParams(params), w, req) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "could not create DID for method with key type: bad") + + // reset recorder between calls + w.Flush() + + // with body, good key type with options + createDIDRequest = router.CreateDIDByMethodRequest{ + KeyType: crypto.Ed25519, + Options: options, + } + + requestReader = newRequestValue(tt, createDIDRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/web", requestReader) + + gock.New("https://example.com"). + Get("/.well-known/did.json"). + Reply(200). + BodyString(`{"didDocument": {"id": "did:web:example.com"}}`) + defer gock.Off() + + err = didService.CreateDIDByMethod(newRequestContextWithParams(params), w, req) + assert.NoError(tt, err) + + var resp router.CreateDIDByMethodResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + + assert.Contains(tt, resp.DID.ID, didsdk.WebMethod) + }) + + t.Run("Test Create DID By Method: ION", func(tt *testing.T) { + bolt := setupTestDB(tt) + require.NotNil(tt, bolt) + + _, keyStoreService := testKeyStore(tt, bolt) + didService := testDIDRouter(tt, bolt, keyStoreService, []string{"ion"}) + + // create DID by method - ion - missing body + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/ion", nil) + w := httptest.NewRecorder() + params := map[string]string{ + "method": "ion", + } + + err := didService.CreateDIDByMethod(newRequestContextWithParams(params), w, req) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "invalid create DID request") + + // reset recorder between calls + w.Flush() + + gock.New(testIONResolverURL). + Post("/operations"). + Reply(200) + defer gock.Off() + + // with body, good key type, no options + createDIDRequest := router.CreateDIDByMethodRequest{KeyType: crypto.Ed25519} + requestReader := newRequestValue(tt, createDIDRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/ion", requestReader) + + err = didService.CreateDIDByMethod(newRequestContextWithParams(params), w, req) + assert.NoError(tt, err) + + // reset recorder between calls + w.Flush() + + // good options + options := did.CreateIONDIDOptions{ServiceEndpoints: []ion.Service{{ID: "test", Type: "test", ServiceEndpoint: "test"}}} + + // with body, bad key type + createDIDRequest = router.CreateDIDByMethodRequest{KeyType: "bad", Options: options} + requestReader = newRequestValue(tt, createDIDRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/ion", requestReader) + + err = didService.CreateDIDByMethod(newRequestContextWithParams(params), w, req) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "could not create DID for method with key type: bad") + + // reset recorder between calls + w.Flush() + + // with body, good key type with options + createDIDRequest = router.CreateDIDByMethodRequest{ + KeyType: crypto.Ed25519, + Options: options, + } + requestReader = newRequestValue(tt, createDIDRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/ion", requestReader) + + gock.New(testIONResolverURL). + Post("/operations"). + Reply(200) + defer gock.Off() + + err = didService.CreateDIDByMethod(newRequestContextWithParams(params), w, req) + assert.NoError(tt, err) + + var resp router.CreateDIDByMethodResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + + assert.Contains(tt, resp.DID.ID, didsdk.IONMethod) + }) + t.Run("Test Get DID By Method", func(tt *testing.T) { bolt := setupTestDB(tt) require.NotNil(tt, bolt) _, keyStore := testKeyStore(tt, bolt) - didService := testDIDRouter(tt, bolt, keyStore) + didService := testDIDRouter(tt, bolt, keyStore, []string{"key"}) // get DID by method req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/bad/worse", nil) @@ -121,6 +274,7 @@ func TestDIDAPI(t *testing.T) { // reset recorder between calls w.Flush() + // store a DID createDIDRequest := router.CreateDIDByMethodRequest{KeyType: crypto.Ed25519} requestReader := newRequestValue(tt, createDIDRequest) @@ -161,7 +315,7 @@ func TestDIDAPI(t *testing.T) { require.NotNil(tt, bolt) _, keyStore := testKeyStore(tt, bolt) - didService := testDIDRouter(tt, bolt, keyStore) + didService := testDIDRouter(tt, bolt, keyStore, []string{"key"}) // soft delete DID by method req := httptest.NewRequest(http.MethodDelete, "https://ssi-service.com/v1/dids/bad/worse", nil) @@ -260,7 +414,7 @@ func TestDIDAPI(t *testing.T) { bolt := setupTestDB(tt) require.NotNil(tt, bolt) _, keyStore := testKeyStore(tt, bolt) - didService := testDIDRouter(tt, bolt, keyStore) + didService := testDIDRouter(tt, bolt, keyStore, []string{"key", "web"}) // get DIDs by method req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/bad", nil) @@ -342,7 +496,7 @@ func TestDIDAPI(t *testing.T) { require.NotNil(tt, bolt) _, keyStore := testKeyStore(tt, bolt) - didService := testDIDRouter(tt, bolt, keyStore) + didService := testDIDRouter(tt, bolt, keyStore, []string{"key", "web"}) // bad resolution request req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/resolver/bad", nil) diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go index 136917d62..bdd93af63 100644 --- a/pkg/server/server_test.go +++ b/pkg/server/server_test.go @@ -11,6 +11,7 @@ import ( "time" "github.com/TBD54566975/ssi-sdk/credential/exchange" + "github.com/tbd54566975/ssi-service/pkg/service/issuing" "github.com/tbd54566975/ssi-service/pkg/service/manifest/model" "github.com/tbd54566975/ssi-service/pkg/service/webhook" @@ -23,6 +24,7 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + credmodel "github.com/tbd54566975/ssi-service/internal/credential" "github.com/tbd54566975/ssi-service/config" @@ -37,6 +39,10 @@ import ( "github.com/tbd54566975/ssi-service/pkg/storage" ) +const ( + testIONResolverURL = "https://test-ion-resolver.com" +) + func TestMain(t *testing.M) { testutil.EnableSchemaCaching() os.Exit(t.Run()) @@ -53,7 +59,7 @@ func TestHealthCheckAPI(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/health", nil) w := httptest.NewRecorder() - err = router.Health(context.TODO(), w, req) + err = router.Health(context.Background(), w, req) assert.NoError(t, err) assert.Equal(t, http.StatusOK, w.Result().StatusCode) @@ -225,11 +231,15 @@ func testIssuanceService(t *testing.T, db storage.ServiceStorage) *issuing.Servi return s } -func testDIDService(t *testing.T, bolt storage.ServiceStorage, keyStore *keystore.Service) *did.Service { +func testDIDService(t *testing.T, bolt storage.ServiceStorage, keyStore *keystore.Service, methods ...string) *did.Service { + if methods == nil { + methods = []string{"key"} + } serviceConfig := config.DIDServiceConfig{ - BaseServiceConfig: &config.BaseServiceConfig{Name: "test-did"}, - Methods: []string{"key"}, - ResolutionMethods: []string{"key"}, + BaseServiceConfig: &config.BaseServiceConfig{Name: "test-did"}, + Methods: methods, + LocalResolutionMethods: []string{"key", "web", "peer", "pkh"}, + IONResolverURL: testIONResolverURL, } // create a did service @@ -239,8 +249,8 @@ func testDIDService(t *testing.T, bolt storage.ServiceStorage, keyStore *keystor return didService } -func testDIDRouter(t *testing.T, bolt storage.ServiceStorage, keyStore *keystore.Service) *router.DIDRouter { - didService := testDIDService(t, bolt, keyStore) +func testDIDRouter(t *testing.T, bolt storage.ServiceStorage, keyStore *keystore.Service, methods []string) *router.DIDRouter { + didService := testDIDService(t, bolt, keyStore, methods...) // create router for service didRouter, err := router.NewDIDRouter(didService) diff --git a/pkg/service/did/handler.go b/pkg/service/did/handler.go index 7ff548277..100c2485b 100644 --- a/pkg/service/did/handler.go +++ b/pkg/service/did/handler.go @@ -12,9 +12,11 @@ import ( // TODO(gabe) consider smaller/more composable interfaces and promoting reusability across methods // https://github.com/TBD54566975/ssi-service/issues/362 type MethodHandler interface { + GetMethod() didsdk.Method CreateDID(ctx context.Context, request CreateDIDRequest) (*CreateDIDResponse, error) + // TODO(gabe): support query parameters to get soft deleted and other DIDs https://github.com/TBD54566975/ssi-service/issues/364 GetDID(ctx context.Context, request GetDIDRequest) (*GetDIDResponse, error) - GetDIDs(ctx context.Context, method didsdk.Method) (*GetDIDsResponse, error) + GetDIDs(ctx context.Context) (*GetDIDsResponse, error) SoftDeleteDID(ctx context.Context, request DeleteDIDRequest) error } diff --git a/pkg/service/did/ion.go b/pkg/service/did/ion.go new file mode 100644 index 000000000..fbbc16782 --- /dev/null +++ b/pkg/service/did/ion.go @@ -0,0 +1,289 @@ +package did + +import ( + "context" + "fmt" + "net/http" + + "github.com/TBD54566975/ssi-sdk/crypto" + "github.com/TBD54566975/ssi-sdk/did" + "github.com/TBD54566975/ssi-sdk/did/ion" + "github.com/TBD54566975/ssi-sdk/util" + "github.com/google/uuid" + "github.com/mr-tron/base58" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + + "github.com/tbd54566975/ssi-service/pkg/service/keystore" +) + +const ( + updateKeySuffix string = "update" + recoverKeySuffix string = "recover" +) + +func NewIONHandler(baseURL string, s *Storage, ks *keystore.Service) (MethodHandler, error) { + if baseURL == "" { + return nil, errors.New("baseURL cannot be empty") + } + if s == nil { + return nil, errors.New("storage cannot be empty") + } + if ks == nil { + return nil, errors.New("keystore cannot be empty") + } + r, err := ion.NewIONResolver(http.DefaultClient, baseURL) + if err != nil { + return nil, errors.Wrap(err, "creating ion resolver") + } + return &ionHandler{method: did.IONMethod, resolver: r, storage: s, keyStore: ks}, nil +} + +type ionHandler struct { + method did.Method + resolver *ion.Resolver + storage *Storage + keyStore *keystore.Service +} + +type CreateIONDIDOptions struct { + // TODO(gabe) for now we only allow adding service endpoints upon creation. + // we do not allow adding external keys or other properties. + // Related: + // - https://github.com/TBD54566975/ssi-sdk/issues/336 + // - https://github.com/TBD54566975/ssi-sdk/issues/335 + ServiceEndpoints []ion.Service `json:"serviceEndpoints"` +} + +func (c CreateIONDIDOptions) Method() did.Method { + return did.IONMethod +} + +func (h *ionHandler) GetMethod() did.Method { + return h.method +} + +type ionStoredDID struct { + ID string `json:"id"` + DID did.Document `json:"did"` + SoftDeleted bool `json:"softDeleted"` + LongFormDID string `json:"longFormDID"` + Operations []any `json:"operations"` +} + +func (i ionStoredDID) GetID() string { + return i.ID +} + +func (i ionStoredDID) GetDocument() did.Document { + return i.DID +} + +func (i ionStoredDID) IsSoftDeleted() bool { + return i.SoftDeleted +} + +func (h *ionHandler) CreateDID(ctx context.Context, request CreateDIDRequest) (*CreateDIDResponse, error) { + // process options + var opts CreateIONDIDOptions + var ok bool + if request.Options != nil { + opts, ok = request.Options.(CreateIONDIDOptions) + if !ok || request.Options.Method() != did.IONMethod { + return nil, fmt.Errorf("invalid options for method, expected %s, got %s", did.IONMethod, request.Options.Method()) + } + if err := util.IsValidStruct(opts); err != nil { + return nil, errors.Wrap(err, "processing options") + } + } + + // create a key for the doc + _, privKey, err := crypto.GenerateKeyByKeyType(request.KeyType) + if err != nil { + return nil, errors.Wrap(err, "could not generate key for ion DID") + } + pubKeyJWK, privKeyJWK, err := crypto.PrivateKeyToPrivateKeyJWK(privKey) + if err != nil { + return nil, errors.Wrap(err, "could not convert key to JWK") + } + keyID := uuid.NewString() + pubKeys := []ion.PublicKey{ + { + ID: keyID, + Type: request.KeyType.String(), + PublicKeyJWK: *pubKeyJWK, + // TODO(gabe): configurable purposes + Purposes: []ion.PublicKeyPurpose{ion.Authentication, ion.AssertionMethod}, + }, + } + + // generate the did document's initial state + doc := ion.Document{PublicKeys: pubKeys, Services: opts.ServiceEndpoints} + ionDID, createOp, err := ion.NewIONDID(doc) + if err != nil { + return nil, errors.Wrap(err, "creating new ION DID") + } + + // submit the create operation to the ION service + if err = h.resolver.Anchor(ctx, createOp); err != nil { + return nil, errors.Wrap(err, "anchoring create operation") + } + + // construct first document state + ldKeyType, err := did.KeyTypeToLDKeyType(request.KeyType) + if err != nil { + return nil, errors.Wrap(err, "converting key type to LD key type") + } + + // TODO(gabe): move this to the SDK + didDoc := did.Document{ + ID: ionDID.ID(), + VerificationMethod: []did.VerificationMethod{ + { + ID: keyID, + Type: ldKeyType, + Controller: ionDID.ID(), + PublicKeyJWK: pubKeyJWK, + }, + }, + Authentication: []did.VerificationMethodSet{map[string]any{"id": keyID}}, + AssertionMethod: []did.VerificationMethodSet{map[string]any{"id": keyID}}, + } + for _, s := range opts.ServiceEndpoints { + didDoc.Services = append(didDoc.Services, did.Service{ + ID: s.ID, + Type: s.Type, + ServiceEndpoint: s.ServiceEndpoint, + }) + } + + // store the did document + storedDID := ionStoredDID{ + ID: ionDID.ID(), + DID: didDoc, + SoftDeleted: false, + LongFormDID: ionDID.LongForm(), + Operations: ionDID.Operations(), + } + if err = h.storage.StoreDID(ctx, storedDID); err != nil { + return nil, errors.Wrap(err, "storing ion did document") + } + + // store associated keys + // 1. update key + // 2. recovery key + // 3. key(s) in the did doc + updateStoreRequest, err := keyToStoreRequest(ionDID.ID()+"#"+updateKeySuffix, ionDID.GetUpdatePrivateKey(), ionDID.ID()) + if err != nil { + return nil, errors.Wrap(err, "converting update private key to store request") + } + if err = h.keyStore.StoreKey(ctx, *updateStoreRequest); err != nil { + return nil, errors.Wrap(err, "could not store did:ion update private key") + } + + recoveryStoreRequest, err := keyToStoreRequest(ionDID.ID()+"#"+recoverKeySuffix, ionDID.GetRecoveryPrivateKey(), ionDID.ID()) + if err != nil { + return nil, errors.Wrap(err, "converting recovery private key to store request") + } + if err = h.keyStore.StoreKey(ctx, *recoveryStoreRequest); err != nil { + return nil, errors.Wrap(err, "could not store did:ion recovery private key") + } + + keyStoreRequest, err := keyToStoreRequest(ionDID.ID()+"#"+keyID, *privKeyJWK, ionDID.ID()) + if err != nil { + return nil, errors.Wrap(err, "converting private key to store request") + } + if err = h.keyStore.StoreKey(ctx, *keyStoreRequest); err != nil { + return nil, errors.Wrap(err, "could not store did:ion private key") + } + + privKeyBytes, err := crypto.PrivKeyToBytes(privKey) + if err != nil { + return nil, errors.Wrap(err, "converting private key to bytes") + } + return &CreateDIDResponse{ + DID: didDoc, + PrivateKeyBase58: base58.Encode(privKeyBytes), + KeyType: request.KeyType, + }, nil +} + +func keyToStoreRequest(kid string, privateKeyJWK crypto.PrivateKeyJWK, controller string) (*keystore.StoreKeyRequest, error) { + privateKey, err := privateKeyJWK.ToPrivateKey() + if err != nil { + return nil, errors.Wrap(err, "getting private private key from JWK") + } + keyType, err := crypto.GetKeyTypeFromPrivateKey(privateKey) + if err != nil { + return nil, errors.Wrap(err, "getting private key type from private privateKeyJWK") + } + // convert to a serialized format + privateKeyBytes, err := crypto.PrivKeyToBytes(privateKey) + if err != nil { + return nil, errors.Wrap(err, "could not encode private key as base58 string") + } + privateKeyBase58 := base58.Encode(privateKeyBytes) + return &keystore.StoreKeyRequest{ + ID: kid, + Type: keyType, + Controller: controller, + PrivateKeyBase58: privateKeyBase58, + }, nil +} + +func (h *ionHandler) GetDID(ctx context.Context, request GetDIDRequest) (*GetDIDResponse, error) { + id := request.ID + + // TODO(gabe) as we are fully custodying ION DIDs this is fine; as we move to a more decentralized model we will + // need to either remove local storage or treat it as a cache with a TTL + + // first check if the DID is in the storage + gotDID := new(ionStoredDID) + err := h.storage.GetDID(ctx, id, gotDID) + if err == nil { + return &GetDIDResponse{DID: gotDID.DID}, nil + } + logrus.WithError(err).Warnf("error getting DID from storage: %s", id) + + // if not, resolve it from the network + resolved, err := h.resolver.Resolve(ctx, id, nil) + if err != nil { + return nil, errors.Wrap(err, "resolving DID from network") + } + return &GetDIDResponse{DID: resolved.Document}, nil +} + +// GetDIDs returns all DIDs we have in storage for ION, it is not feasible to get all DIDs from the network +func (h *ionHandler) GetDIDs(ctx context.Context) (*GetDIDsResponse, error) { + logrus.Debug("getting stored did:ion DIDs") + + gotDIDs, err := h.storage.GetDIDs(ctx, did.KeyMethod.String(), new(ionStoredDID)) + if err != nil { + return nil, fmt.Errorf("error getting did:ion DIDs") + } + dids := make([]did.Document, 0, len(gotDIDs)) + for _, gotDID := range gotDIDs { + if !gotDID.IsSoftDeleted() { + dids = append(dids, gotDID.GetDocument()) + } + } + return &GetDIDsResponse{DIDs: dids}, nil +} + +// SoftDeleteDID soft deletes a DID from storage but has no effect on the DID's state on the network +func (h *ionHandler) SoftDeleteDID(ctx context.Context, request DeleteDIDRequest) error { + logrus.Debugf("soft deleting DID: %+v", request) + + id := request.ID + gotDID := new(ionStoredDID) + if err := h.storage.GetDID(ctx, id, gotDID); err != nil { + return fmt.Errorf("error getting DID: %s", id) + } + if gotDID.GetID() == "" { + return fmt.Errorf("did with id<%s> could not be found", id) + } + + gotDID.SoftDeleted = true + + return h.storage.StoreDID(ctx, *gotDID) +} diff --git a/pkg/service/did/ion_test.go b/pkg/service/did/ion_test.go new file mode 100644 index 000000000..6a3dd1821 --- /dev/null +++ b/pkg/service/did/ion_test.go @@ -0,0 +1,181 @@ +package did + +import ( + "context" + "fmt" + "testing" + + "github.com/TBD54566975/ssi-sdk/crypto" + "github.com/TBD54566975/ssi-sdk/did" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/h2non/gock.v1" + + "github.com/tbd54566975/ssi-service/config" + "github.com/tbd54566975/ssi-service/pkg/service/keystore" + "github.com/tbd54566975/ssi-service/pkg/storage" +) + +func TestIONHandler(t *testing.T) { + t.Run("Test Create ION Handler", func(tt *testing.T) { + handler, err := NewIONHandler("", nil, nil) + assert.Error(tt, err) + assert.Empty(tt, handler) + assert.Contains(tt, err.Error(), "baseURL cannot be empty") + + s := setupTestDB(tt) + keystoreService := testKeyStoreService(tt, s) + didStorage, err := NewDIDStorage(s) + assert.NoError(tt, err) + handler, err = NewIONHandler("bad", nil, keystoreService) + assert.Error(tt, err) + assert.Empty(tt, handler) + assert.Contains(tt, err.Error(), "storage cannot be empty") + + handler, err = NewIONHandler("bad", didStorage, nil) + assert.Error(tt, err) + assert.Empty(tt, handler) + assert.Contains(tt, err.Error(), "keystore cannot be empty") + + handler, err = NewIONHandler("bad", didStorage, keystoreService) + assert.Error(tt, err) + assert.Empty(tt, handler) + assert.Contains(tt, err.Error(), "invalid resolver URL: parse \"bad\": invalid URI for request") + + handler, err = NewIONHandler("https://example.com", didStorage, keystoreService) + assert.NoError(tt, err) + assert.NotEmpty(tt, handler) + + assert.Equal(tt, handler.GetMethod(), did.IONMethod) + }) + + t.Run("Test Create DID", func(tt *testing.T) { + // create a handler + s := setupTestDB(tt) + keystoreService := testKeyStoreService(tt, s) + didStorage, err := NewDIDStorage(s) + assert.NoError(tt, err) + handler, err := NewIONHandler("https://test-ion-resolver.com", didStorage, keystoreService) + assert.NoError(tt, err) + assert.NotEmpty(tt, handler) + + gock.New("https://test-ion-resolver.com"). + Post("/operations"). + Reply(200) + defer gock.Off() + + // create a did + created, err := handler.CreateDID(context.Background(), CreateDIDRequest{ + Method: did.IONMethod, + KeyType: crypto.Ed25519, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, created) + assert.Equal(tt, crypto.Ed25519, created.KeyType) + }) + + t.Run("Test Create DID", func(tt *testing.T) { + // create a handler + s := setupTestDB(tt) + keystoreService := testKeyStoreService(tt, s) + didStorage, err := NewDIDStorage(s) + assert.NoError(tt, err) + handler, err := NewIONHandler("https://tbdwebsiteonline.com", didStorage, keystoreService) + assert.NoError(tt, err) + assert.NotEmpty(tt, handler) + + // create a did + created, err := handler.CreateDID(context.Background(), CreateDIDRequest{ + Method: did.IONMethod, + KeyType: crypto.Ed25519, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, created) + assert.Equal(tt, crypto.Ed25519, created.KeyType) + + // get the did + gotDID, err := handler.GetDID(context.Background(), GetDIDRequest{ + Method: did.IONMethod, + ID: created.DID.ID, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, gotDID) + }) + + t.Run("Test Get DID from storage", func(tt *testing.T) { + // create a handler + s := setupTestDB(tt) + keystoreService := testKeyStoreService(tt, s) + didStorage, err := NewDIDStorage(s) + assert.NoError(tt, err) + handler, err := NewIONHandler("https://test-ion-resolver.com", didStorage, keystoreService) + assert.NoError(tt, err) + assert.NotEmpty(tt, handler) + + gock.New("https://test-ion-resolver.com"). + Post("/operations"). + Reply(200) + defer gock.Off() + + // create a did + created, err := handler.CreateDID(context.Background(), CreateDIDRequest{ + Method: did.IONMethod, + KeyType: crypto.Ed25519, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, created) + assert.Equal(tt, crypto.Ed25519, created.KeyType) + + gock.New("https://test-ion-resolver.com"). + Get("/identifiers/" + created.DID.ID). + Reply(200).BodyString(fmt.Sprintf(`{"didDocument": {"id": "%s"}}`, created.DID.ID)) + defer gock.Off() + + // get the did + gotDID, err := handler.GetDID(context.Background(), GetDIDRequest{ + Method: did.IONMethod, + ID: created.DID.ID, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, gotDID) + assert.Equal(tt, created.DID.ID, gotDID.DID.ID) + }) + + t.Run("Test Get DID from resolver", func(tt *testing.T) { + // create a handler + s := setupTestDB(tt) + keystoreService := testKeyStoreService(tt, s) + didStorage, err := NewDIDStorage(s) + assert.NoError(tt, err) + handler, err := NewIONHandler("https://test-ion-resolver.com", didStorage, keystoreService) + assert.NoError(tt, err) + assert.NotEmpty(tt, handler) + + gock.New("https://test-ion-resolver.com"). + Get("/identifiers/did:ion:test"). + Reply(200).BodyString(`{"didDocument": {"id": "did:ion:test"}}`) + defer gock.Off() + + // get the did + gotDID, err := handler.GetDID(context.Background(), GetDIDRequest{ + Method: did.IONMethod, + ID: "did:ion:test", + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, gotDID) + assert.Equal(tt, "did:ion:test", gotDID.DID.ID) + }) +} + +func testKeyStoreService(t *testing.T, db storage.ServiceStorage) *keystore.Service { + serviceConfig := config.KeyStoreServiceConfig{ + BaseServiceConfig: &config.BaseServiceConfig{Name: "test-keystore"}, + ServiceKeyPassword: "test-password", + } + + // create a keystore service + keystoreService, err := keystore.NewKeyStoreService(serviceConfig, db) + require.NoError(t, err) + require.NotEmpty(t, keystoreService) + return keystoreService +} diff --git a/pkg/service/did/key.go b/pkg/service/did/key.go index a3e959235..dfb52a4f2 100644 --- a/pkg/service/did/key.go +++ b/pkg/service/did/key.go @@ -13,16 +13,27 @@ import ( "github.com/tbd54566975/ssi-service/pkg/service/keystore" ) -func NewKeyDIDHandler(s *Storage, ks *keystore.Service) MethodHandler { - return &keyDIDHandler{storage: s, keyStore: ks} +func NewKeyHandler(s *Storage, ks *keystore.Service) (MethodHandler, error) { + if s == nil { + return nil, errors.New("storage cannot be empty") + } + if ks == nil { + return nil, errors.New("keystore cannot be empty") + } + return &keyHandler{method: did.KeyMethod, storage: s, keyStore: ks}, nil } -type keyDIDHandler struct { +type keyHandler struct { + method did.Method storage *Storage keyStore *keystore.Service } -func (h *keyDIDHandler) CreateDID(ctx context.Context, request CreateDIDRequest) (*CreateDIDResponse, error) { +func (h *keyHandler) GetMethod() did.Method { + return h.method +} + +func (h *keyHandler) CreateDID(ctx context.Context, request CreateDIDRequest) (*CreateDIDResponse, error) { logrus.Debugf("creating DID: %+v", request) // create the DID @@ -39,7 +50,7 @@ func (h *keyDIDHandler) CreateDID(ctx context.Context, request CreateDIDRequest) // store metadata in DID storage id := doc.String() - storedDID := StoredDID{ + storedDID := DefaultStoredDID{ ID: id, DID: *expanded, SoftDeleted: false, @@ -74,11 +85,11 @@ func (h *keyDIDHandler) CreateDID(ctx context.Context, request CreateDIDRequest) }, nil } -func (h *keyDIDHandler) GetDID(ctx context.Context, request GetDIDRequest) (*GetDIDResponse, error) { +func (h *keyHandler) GetDID(ctx context.Context, request GetDIDRequest) (*GetDIDResponse, error) { logrus.Debugf("getting DID: %+v", request) id := request.ID - gotDID, err := h.storage.GetDID(ctx, id) + gotDID, err := h.storage.GetDIDDefault(ctx, id) if err != nil { return nil, fmt.Errorf("error getting DID: %s", id) } @@ -88,27 +99,27 @@ func (h *keyDIDHandler) GetDID(ctx context.Context, request GetDIDRequest) (*Get return &GetDIDResponse{DID: gotDID.DID}, nil } -func (h *keyDIDHandler) GetDIDs(ctx context.Context, method did.Method) (*GetDIDsResponse, error) { - logrus.Debugf("getting DIDs for method: %s", method) +func (h *keyHandler) GetDIDs(ctx context.Context) (*GetDIDsResponse, error) { + logrus.Debug("getting did:key DIDs") - gotDIDs, err := h.storage.GetDIDs(ctx, string(method)) + gotDIDs, err := h.storage.GetDIDsDefault(ctx, did.KeyMethod.String()) if err != nil { - return nil, fmt.Errorf("error getting DIDs for method: %s", method) + return nil, fmt.Errorf("error getting did:key DIDs") } dids := make([]did.Document, 0, len(gotDIDs)) for _, gotDID := range gotDIDs { - if !gotDID.SoftDeleted { - dids = append(dids, gotDID.DID) + if !gotDID.IsSoftDeleted() { + dids = append(dids, gotDID.GetDocument()) } } return &GetDIDsResponse{DIDs: dids}, nil } -func (h *keyDIDHandler) SoftDeleteDID(ctx context.Context, request DeleteDIDRequest) error { +func (h *keyHandler) SoftDeleteDID(ctx context.Context, request DeleteDIDRequest) error { logrus.Debugf("soft deleting DID: %+v", request) id := request.ID - gotStoredDID, err := h.storage.GetDID(ctx, id) + gotStoredDID, err := h.storage.GetDIDDefault(ctx, id) if err != nil { return fmt.Errorf("error getting DID: %s", id) } diff --git a/pkg/service/did/model.go b/pkg/service/did/model.go index 23456446b..b9d1cb583 100644 --- a/pkg/service/did/model.go +++ b/pkg/service/did/model.go @@ -21,18 +21,25 @@ type ResolveDIDResponse struct { DIDDocumentMetadata *didsdk.DocumentMetadata `json:"didDocumentMetadata,omitempty"` } +type CreateDIDRequestOptions interface { + Method() didsdk.Method +} + // CreateDIDRequest is the JSON-serializable request for creating a DID across DID method type CreateDIDRequest struct { - Method didsdk.Method `json:"method" validate:"required"` - KeyType crypto.KeyType `validate:"required"` - DIDWebID string `json:"didWebId"` + Method didsdk.Method `json:"method" validate:"required"` + KeyType crypto.KeyType `validate:"required"` + Options CreateDIDRequestOptions `json:"options"` } // CreateDIDResponse is the JSON-serializable response for creating a DID type CreateDIDResponse struct { - DID didsdk.Document `json:"did"` - PrivateKeyBase58 string `json:"base58PrivateKey"` - KeyType crypto.KeyType `json:"keyType"` + DID didsdk.Document `json:"did"` + // TODO(gabe): change to returning a set of public keys. private keys should be stored in the keystore, + // and stay within the service boundary. This will unify the solution for both custodial and non-custodial keys. + // https://github.com/TBD54566975/ssi-service/issues/371 + PrivateKeyBase58 string `json:"base58PrivateKey"` + KeyType crypto.KeyType `json:"keyType"` } type GetDIDRequest struct { diff --git a/pkg/service/did/resolution/resolver.go b/pkg/service/did/resolution/resolver.go index d8fca246a..f872a51d5 100644 --- a/pkg/service/did/resolution/resolver.go +++ b/pkg/service/did/resolution/resolver.go @@ -24,15 +24,15 @@ var _ didsdk.Resolver = (*ServiceResolver)(nil) // NewServiceResolver creates a new ServiceResolver instance which can resolve DIDs using a combination of local and // universal resolvers. -func NewServiceResolver(handlerResolver didsdk.Resolver, resolutionMethods []string, universalResolverURL string) (*ServiceResolver, error) { - if len(resolutionMethods) == 0 { - return nil, errors.New("no resolution methods configured") - } - - // instantiate sdk resolver - localResolver, err := didint.BuildMultiMethodResolver(resolutionMethods) - if err != nil { - return nil, errors.Wrap(err, "instantiating SDK DID resolver") +func NewServiceResolver(handlerResolver didsdk.Resolver, localResolutionMethods []string, universalResolverURL string) (*ServiceResolver, error) { + var lr didsdk.Resolver + var err error + if len(localResolutionMethods) > 0 { + // instantiate sdk resolver + lr, err = didint.BuildMultiMethodResolver(localResolutionMethods) + if err != nil { + return nil, errors.Wrap(err, "instantiating local DID resolver") + } } // instantiate universal resolver @@ -45,9 +45,9 @@ func NewServiceResolver(handlerResolver didsdk.Resolver, resolutionMethods []str } return &ServiceResolver{ - resolutionMethods: resolutionMethods, + resolutionMethods: localResolutionMethods, hr: handlerResolver, - lr: localResolver, + lr: lr, ur: ur, }, nil } diff --git a/pkg/service/did/service.go b/pkg/service/did/service.go index 42bbc48d1..90b5d51ff 100644 --- a/pkg/service/did/service.go +++ b/pkg/service/did/service.go @@ -74,6 +74,7 @@ func NewDIDService(config config.DIDServiceConfig, s storage.ServiceStorage, key } service := Service{ + config: config, storage: didStorage, handlers: make(map[didsdk.Method]MethodHandler), keyStore: keyStore, @@ -93,7 +94,7 @@ func NewDIDService(config config.DIDServiceConfig, s storage.ServiceStorage, key } // instantiate DID resolver - resolver, err := resolution.NewServiceResolver(hr, config.ResolutionMethods, config.UniversalResolverURL) + resolver, err := resolution.NewServiceResolver(hr, config.LocalResolutionMethods, config.UniversalResolverURL) if err != nil { return nil, errors.Wrap(err, "instantiating DID resolver") } @@ -105,12 +106,28 @@ func NewDIDService(config config.DIDServiceConfig, s storage.ServiceStorage, key return &service, nil } +// instantiateHandlerForMethod instantiates a handler for the given DID method. All handlers supported by the DID +// service must be instantiated here. func (s *Service) instantiateHandlerForMethod(method didsdk.Method) error { switch method { case didsdk.KeyMethod: - s.handlers[method] = NewKeyDIDHandler(s.storage, s.keyStore) + kh, err := NewKeyHandler(s.storage, s.keyStore) + if err != nil { + return errors.Wrap(err, "instantiating key handler") + } + s.handlers[method] = kh case didsdk.WebMethod: - s.handlers[method] = NewWebDIDHandler(s.storage, s.keyStore) + wh, err := NewWebHandler(s.storage, s.keyStore) + if err != nil { + return errors.Wrap(err, "instantiating web handler") + } + s.handlers[method] = wh + case didsdk.IONMethod: + ih, err := NewIONHandler(s.Config().IONResolverURL, s.storage, s.keyStore) + if err != nil { + return errors.Wrap(err, "instantiating ion handler") + } + s.handlers[method] = ih default: return util.LoggingNewErrorf("unsupported DID method: %s", method) } @@ -184,7 +201,7 @@ func (s *Service) GetDIDsByMethod(ctx context.Context, request GetDIDsRequest) ( if err != nil { return nil, util.LoggingErrorMsgf(err, "could not get handler for method<%s>", method) } - return handler.GetDIDs(ctx, method) + return handler.GetDIDs(ctx) } func (s *Service) SoftDeleteDIDByMethod(ctx context.Context, request DeleteDIDRequest) error { diff --git a/pkg/service/did/storage.go b/pkg/service/did/storage.go index 283ee50ca..49f848f68 100644 --- a/pkg/service/did/storage.go +++ b/pkg/service/did/storage.go @@ -3,6 +3,7 @@ package did import ( "context" "fmt" + "reflect" "github.com/TBD54566975/ssi-sdk/did" "github.com/goccy/go-json" @@ -17,21 +18,44 @@ const ( namespace = "did" keyNamespace = "key" webNamespace = "web" + ionNamespace = "ion" ) var ( didMethodToNamespace = map[string]string{ - "key": storage.MakeNamespace(namespace, keyNamespace), - "web": storage.MakeNamespace(namespace, webNamespace), + keyNamespace: storage.MakeNamespace(namespace, keyNamespace), + webNamespace: storage.MakeNamespace(namespace, webNamespace), + ionNamespace: storage.MakeNamespace(namespace, ionNamespace), } ) -type StoredDID struct { +// StoredDID is a DID that has been stored in the database. It is an interface to allow +// for different implementations of DID storage based on the DID method. +type StoredDID interface { + GetID() string + GetDocument() did.Document + IsSoftDeleted() bool +} + +// DefaultStoredDID is the default implementation of StoredDID if no other implementation requirements are needed. +type DefaultStoredDID struct { ID string `json:"id"` DID did.Document `json:"did"` SoftDeleted bool `json:"softDeleted"` } +func (d DefaultStoredDID) GetID() string { + return d.ID +} + +func (d DefaultStoredDID) GetDocument() did.Document { + return d.DID +} + +func (d DefaultStoredDID) IsSoftDeleted() bool { + return d.SoftDeleted +} + type Storage struct { db storage.ServiceStorage } @@ -44,8 +68,8 @@ func NewDIDStorage(db storage.ServiceStorage) (*Storage, error) { } func (ds *Storage) StoreDID(ctx context.Context, did StoredDID) error { - couldNotStoreDIDErr := fmt.Sprintf("could not store DID: %s", did.ID) - ns, err := getNamespaceForDID(did.ID) + couldNotStoreDIDErr := fmt.Sprintf("could not store DID: %s", did.GetID()) + ns, err := getNamespaceForDID(did.GetID()) if err != nil { return util.LoggingErrorMsg(err, couldNotStoreDIDErr) } @@ -53,32 +77,50 @@ func (ds *Storage) StoreDID(ctx context.Context, did StoredDID) error { if err != nil { return util.LoggingErrorMsg(err, couldNotStoreDIDErr) } - return ds.db.Write(ctx, ns, did.ID, didBytes) + return ds.db.Write(ctx, ns, did.GetID(), didBytes) } -func (ds *Storage) GetDID(ctx context.Context, id string) (*StoredDID, error) { +// GetDID attempts to get a DID from the database. It will return an error if it cannot. +// The out parameter must be a pointer to a struct that implements the StoredDID interface. +func (ds *Storage) GetDID(ctx context.Context, id string, out StoredDID) error { + if err := validateOut(out); err != nil { + return errors.Wrap(err, "validating out") + } couldNotGetDIDErr := fmt.Sprintf("could not get DID: %s", id) ns, err := getNamespaceForDID(id) if err != nil { - return nil, util.LoggingErrorMsg(err, couldNotGetDIDErr) + return util.LoggingErrorMsg(err, couldNotGetDIDErr) } docBytes, err := ds.db.Read(ctx, ns, id) if err != nil { - return nil, util.LoggingErrorMsg(err, couldNotGetDIDErr) + return util.LoggingErrorMsg(err, couldNotGetDIDErr) } if len(docBytes) == 0 { err = fmt.Errorf("did not found: %s", id) - return nil, util.LoggingErrorMsg(err, couldNotGetDIDErr) + return util.LoggingErrorMsg(err, couldNotGetDIDErr) } - var stored StoredDID - if err = json.Unmarshal(docBytes, &stored); err != nil { - return nil, util.LoggingErrorMsgf(err, "could not ummarshal stored DID: %s", id) + if err = json.Unmarshal(docBytes, out); err != nil { + return util.LoggingErrorMsgf(err, "could not ummarshal stored DID: %s", id) + } + return nil +} + +// GetDIDDefault is a convenience method for getting a DID that is stored as a DefaultStoredDID. +func (ds *Storage) GetDIDDefault(ctx context.Context, id string) (*DefaultStoredDID, error) { + outType := new(DefaultStoredDID) + if err := ds.GetDID(ctx, id, outType); err != nil { + return nil, err } - return &stored, nil + return outType, nil } // GetDIDs attempts to get all DIDs for a given method. It will return those it can even if it has trouble with some. -func (ds *Storage) GetDIDs(ctx context.Context, method string) ([]StoredDID, error) { +// The out parameter must be a pointer to a struct for a type that implement the StoredDID interface. +// The result is a slice of the type of the out parameter (an array of pointers to the type of the out parameter).) +func (ds *Storage) GetDIDs(ctx context.Context, method string, outType StoredDID) ([]StoredDID, error) { + if err := validateOut(outType); err != nil { + return nil, errors.Wrap(err, "validating the out type") + } couldNotGetDIDsErr := fmt.Sprintf("could not get DIDs for method: %s", method) ns, err := getNamespaceForMethod(method) if err != nil { @@ -92,14 +134,27 @@ func (ds *Storage) GetDIDs(ctx context.Context, method string) ([]StoredDID, err logrus.Infof("no DIDs found for method: %s", method) return nil, nil } - var stored []StoredDID + + out := make([]StoredDID, 0, len(gotDIDs)) for _, didBytes := range gotDIDs { - var nextDID StoredDID + nextDID := reflect.New(reflect.TypeOf(outType).Elem()).Interface() if err = json.Unmarshal(didBytes, &nextDID); err == nil { - stored = append(stored, nextDID) + out = append(out, nextDID.(StoredDID)) } } - return stored, nil + return out, nil +} + +func (ds *Storage) GetDIDsDefault(ctx context.Context, method string) ([]DefaultStoredDID, error) { + gotDIDs, err := ds.GetDIDs(ctx, method, new(DefaultStoredDID)) + if err != nil { + return nil, err + } + typedDIDs := make([]DefaultStoredDID, len(gotDIDs)) + for i, gotDID := range gotDIDs { + typedDIDs[i] = *gotDID.(*DefaultStoredDID) + } + return typedDIDs, nil } func (ds *Storage) DeleteDID(ctx context.Context, id string) error { @@ -114,6 +169,24 @@ func (ds *Storage) DeleteDID(ctx context.Context, id string) error { return nil } +func validateOut(out StoredDID) error { + if out == nil { + return errors.New("cannot be nil") + } + // make sure out is a ptr to a struct + outVal := reflect.ValueOf(out) + if outVal.Kind() != reflect.Ptr { + return fmt.Errorf("must be ptr to a struct; is %T", out) + } + + // dereference the pointer + outValDeref := outVal.Elem() + if outValDeref.Kind() != reflect.Struct { + return fmt.Errorf("must be ptr to a struct; is %T", out) + } + return nil +} + func getNamespaceForDID(id string) (string, error) { method, err := util.GetMethodForDID(id) if err != nil { diff --git a/pkg/service/did/storage_test.go b/pkg/service/did/storage_test.go new file mode 100644 index 000000000..a2b6c3f9a --- /dev/null +++ b/pkg/service/did/storage_test.go @@ -0,0 +1,245 @@ +package did + +import ( + "context" + "os" + "testing" + + didsdk "github.com/TBD54566975/ssi-sdk/did" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/tbd54566975/ssi-service/pkg/storage" +) + +func TestStorage(t *testing.T) { + t.Run("Create bad DID - no namespace", func(tt *testing.T) { + ds, err := NewDIDStorage(setupTestDB(tt)) + assert.NoError(tt, err) + assert.NotEmpty(tt, ds) + + // create a did + toStore := DefaultStoredDID{ + ID: "did:bad:test", + DID: didsdk.Document{ + ID: "did:bad:test", + }, + SoftDeleted: false, + } + + // store + err = ds.StoreDID(context.Background(), toStore) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "could not store DID") + }) + + t.Run("Get bad DID - namespace does not exist", func(tt *testing.T) { + ds, err := NewDIDStorage(setupTestDB(tt)) + assert.NoError(tt, err) + assert.NotEmpty(tt, ds) + + // store + gotDID, err := ds.GetDIDDefault(context.Background(), "did:test:bad") + assert.Error(tt, err) + assert.Empty(tt, gotDID) + assert.Contains(tt, err.Error(), "could not get DID: did:test:bad: no namespace found for DID method: test") + }) + + t.Run("Get bad DID - does not exist", func(tt *testing.T) { + ds, err := NewDIDStorage(setupTestDB(tt)) + assert.NoError(tt, err) + assert.NotEmpty(tt, ds) + + // store + gotDID, err := ds.GetDIDDefault(context.Background(), "did:key:bad") + assert.Error(tt, err) + assert.Empty(tt, gotDID) + assert.Contains(tt, err.Error(), "could not get DID: did:key:bad") + }) + + t.Run("Create and Get DID", func(tt *testing.T) { + ds, err := NewDIDStorage(setupTestDB(tt)) + assert.NoError(tt, err) + assert.NotEmpty(tt, ds) + + // create a did + toStore := DefaultStoredDID{ + ID: "did:key:test", + DID: didsdk.Document{ + ID: "did:key:test", + }, + SoftDeleted: false, + } + + // store + err = ds.StoreDID(context.Background(), toStore) + assert.NoError(tt, err) + + // get it back as a default + got, err := ds.GetDIDDefault(context.Background(), "did:key:test") + assert.NoError(tt, err) + assert.Equal(tt, toStore, *got) + + // get it back as a did + outDID := new(DefaultStoredDID) + err = ds.GetDID(context.Background(), "did:key:test", outDID) + assert.NoError(tt, err) + assert.Equal(tt, toStore, *outDID) + }) + + t.Run("Create and Get DID of a custom type", func(tt *testing.T) { + ds, err := NewDIDStorage(setupTestDB(tt)) + assert.NoError(tt, err) + assert.NotEmpty(tt, ds) + + // create a did + toStore := customStoredDID{ + ID: "did:key:test", + Party: false, + } + + // store + err = ds.StoreDID(context.Background(), toStore) + assert.NoError(tt, err) + + // get it back as a default - which won't be equal + got, err := ds.GetDIDDefault(context.Background(), "did:key:test") + assert.NoError(tt, err) + assert.NotEqual(tt, toStore, *got) + + // get it back as a custom did + outDID := new(customStoredDID) + err = ds.GetDID(context.Background(), "did:key:test", outDID) + assert.NoError(tt, err) + assert.Equal(tt, toStore, *outDID) + }) + + t.Run("Create and Get Multiple DIDs", func(tt *testing.T) { + ds, err := NewDIDStorage(setupTestDB(tt)) + assert.NoError(tt, err) + assert.NotEmpty(tt, ds) + + // create two dids + toStore1 := DefaultStoredDID{ + ID: "did:key:test-1", + DID: didsdk.Document{ + ID: "did:key:test-1", + }, + SoftDeleted: false, + } + + toStore2 := DefaultStoredDID{ + ID: "did:key:test-2", + DID: didsdk.Document{ + ID: "did:key:test-2", + }, + SoftDeleted: false, + } + + // store + err = ds.StoreDID(context.Background(), toStore1) + assert.NoError(tt, err) + + err = ds.StoreDID(context.Background(), toStore2) + assert.NoError(tt, err) + + // get both back as default + got, err := ds.GetDIDsDefault(context.Background(), didsdk.KeyMethod.String()) + assert.NoError(tt, err) + assert.Len(tt, got, 2) + assert.Contains(tt, got, toStore1) + assert.Contains(tt, got, toStore2) + + // get back as did + gotDIDs, err := ds.GetDIDs(context.Background(), didsdk.KeyMethod.String(), new(DefaultStoredDID)) + assert.NoError(tt, err) + assert.Len(tt, gotDIDs, 2) + assert.Contains(tt, gotDIDs, &toStore1) + assert.Contains(tt, gotDIDs, &toStore2) + }) + + t.Run("Soft delete DID", func(tt *testing.T) { + ds, err := NewDIDStorage(setupTestDB(tt)) + assert.NoError(tt, err) + assert.NotEmpty(tt, ds) + + // create two dids + toStore1 := DefaultStoredDID{ + ID: "did:key:test-1", + DID: didsdk.Document{ + ID: "did:key:test-1", + }, + SoftDeleted: false, + } + + toStore2 := DefaultStoredDID{ + ID: "did:key:test-2", + DID: didsdk.Document{ + ID: "did:key:test-2", + }, + SoftDeleted: false, + } + + // store + err = ds.StoreDID(context.Background(), toStore1) + assert.NoError(tt, err) + + err = ds.StoreDID(context.Background(), toStore2) + assert.NoError(tt, err) + + // get both and verify there are two + gotDIDs, err := ds.GetDIDsDefault(context.Background(), didsdk.KeyMethod.String()) + assert.NoError(tt, err) + assert.Len(tt, gotDIDs, 2) + + // soft delete one + err = ds.DeleteDID(context.Background(), "did:key:test-1") + assert.NoError(tt, err) + + // get it back + _, err = ds.GetDIDDefault(context.Background(), "did:key:test-1") + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "could not get DID: did:key:test-1") + + // get both and verify there is one + gotDIDs, err = ds.GetDIDsDefault(context.Background(), didsdk.KeyMethod.String()) + assert.NoError(tt, err) + assert.Len(tt, gotDIDs, 1) + assert.Contains(tt, gotDIDs, toStore2) + }) +} + +func setupTestDB(t *testing.T) storage.ServiceStorage { + file, err := os.CreateTemp("", "bolt") + require.NoError(t, err) + name := file.Name() + err = file.Close() + require.NoError(t, err) + s, err := storage.NewStorage(storage.Bolt, name) + require.NoError(t, err) + t.Cleanup(func() { + _ = s.Close() + _ = os.Remove(s.URI()) + }) + return s +} + +// new stored DID type +type customStoredDID struct { + ID string `json:"id,omitempty"` + Party bool `json:"party,omitempty"` +} + +func (d customStoredDID) GetID() string { + return d.ID +} + +func (d customStoredDID) GetDocument() didsdk.Document { + return didsdk.Document{ + ID: d.ID, + } +} + +func (d customStoredDID) IsSoftDeleted() bool { + return d.Party +} diff --git a/pkg/service/did/web.go b/pkg/service/did/web.go index 173f4363f..83eaf5e55 100644 --- a/pkg/service/did/web.go +++ b/pkg/service/did/web.go @@ -6,6 +6,7 @@ import ( "github.com/TBD54566975/ssi-sdk/crypto" "github.com/TBD54566975/ssi-sdk/did" + "github.com/TBD54566975/ssi-sdk/util" "github.com/mr-tron/base58" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -13,26 +14,54 @@ import ( "github.com/tbd54566975/ssi-service/pkg/service/keystore" ) -func NewWebDIDHandler(s *Storage, ks *keystore.Service) MethodHandler { - return &webDIDHandler{storage: s, keyStore: ks} +func NewWebHandler(s *Storage, ks *keystore.Service) (MethodHandler, error) { + if s == nil { + return nil, errors.New("storage cannot be empty") + } + if ks == nil { + return nil, errors.New("keystore cannot be empty") + } + return &webHandler{method: did.WebMethod, storage: s, keyStore: ks}, nil } -type webDIDHandler struct { +type webHandler struct { + method did.Method storage *Storage keyStore *keystore.Service } -func (h *webDIDHandler) CreateDID(ctx context.Context, request CreateDIDRequest) (*CreateDIDResponse, error) { +type CreateWebDIDOptions struct { + // e.g. did:web:example.com + DIDWebID string `json:"didWebId" validate:"required"` +} + +func (c CreateWebDIDOptions) Method() did.Method { + return did.WebMethod +} + +func (h *webHandler) GetMethod() did.Method { + return h.method +} + +func (h *webHandler) CreateDID(ctx context.Context, request CreateDIDRequest) (*CreateDIDResponse, error) { logrus.Debugf("creating DID: %+v", request) - if request.DIDWebID == "" { - return nil, errors.New("url is empty, cannot create did:web") + // process options + if request.Options == nil { + return nil, errors.New("options cannot be empty") + } + opts, ok := request.Options.(CreateWebDIDOptions) + if !ok || request.Options.Method() != did.WebMethod { + return nil, fmt.Errorf("invalid options for method, expected %s, got %s", did.WebMethod, request.Options.Method()) + } + if err := util.IsValidStruct(opts); err != nil { + return nil, errors.Wrap(err, "processing options") } - didWeb := did.DIDWeb(request.DIDWebID) + didWeb := did.DIDWeb(opts.DIDWebID) if !didWeb.IsValid() { - return nil, fmt.Errorf("did:web is not valid, could not resolve did:web DID: %s", didWeb) + return nil, fmt.Errorf("could not resolve did:web DID: %s", didWeb) } pubKey, privKey, err := crypto.GenerateKeyByKeyType(request.KeyType) @@ -52,7 +81,7 @@ func (h *webDIDHandler) CreateDID(ctx context.Context, request CreateDIDRequest) // store metadata in DID storage id := didWeb.String() - storedDID := StoredDID{ + storedDID := DefaultStoredDID{ ID: id, DID: *doc, SoftDeleted: false, @@ -70,6 +99,7 @@ func (h *webDIDHandler) CreateDID(ctx context.Context, request CreateDIDRequest) // store private key in key storage keyStoreRequest := keystore.StoreKeyRequest{ + // TODO(gabe): this should be a unique ID for the key ID: id, Type: request.KeyType, Controller: id, @@ -77,7 +107,7 @@ func (h *webDIDHandler) CreateDID(ctx context.Context, request CreateDIDRequest) } if err = h.keyStore.StoreKey(ctx, keyStoreRequest); err != nil { - return nil, errors.Wrap(err, "could not store did:key private key") + return nil, errors.Wrap(err, "could not store did:web private key") } return &CreateDIDResponse{ @@ -87,44 +117,43 @@ func (h *webDIDHandler) CreateDID(ctx context.Context, request CreateDIDRequest) }, nil } -func (h *webDIDHandler) GetDID(ctx context.Context, request GetDIDRequest) (*GetDIDResponse, error) { - +func (h *webHandler) GetDID(ctx context.Context, request GetDIDRequest) (*GetDIDResponse, error) { logrus.Debugf("getting DID: %+v", request) id := request.ID - gotDID, err := h.storage.GetDID(ctx, id) + gotDID, err := h.storage.GetDIDDefault(ctx, id) if err != nil { - return nil, fmt.Errorf("error getting DID: %s", id) + return nil, errors.Wrapf(err, "error getting DID: %s", id) } if gotDID == nil { return nil, fmt.Errorf("did with id<%s> could not be found", id) } - return &GetDIDResponse{DID: gotDID.DID}, nil + return &GetDIDResponse{DID: gotDID.GetDocument()}, nil } -func (h *webDIDHandler) GetDIDs(ctx context.Context, method did.Method) (*GetDIDsResponse, error) { - logrus.Debugf("getting DIDs for method: %s", method) +func (h *webHandler) GetDIDs(ctx context.Context) (*GetDIDsResponse, error) { + logrus.Debug("getting did:web DID") - gotDIDs, err := h.storage.GetDIDs(ctx, string(method)) + gotDIDs, err := h.storage.GetDIDsDefault(ctx, did.WebMethod.String()) if err != nil { - return nil, fmt.Errorf("error getting DIDs for method: %s", method) + return nil, errors.Wrap(err, "getting did:web DIDs") } dids := make([]did.Document, 0, len(gotDIDs)) for _, gotDID := range gotDIDs { - if !gotDID.SoftDeleted { - dids = append(dids, gotDID.DID) + if !gotDID.IsSoftDeleted() { + dids = append(dids, gotDID.GetDocument()) } } return &GetDIDsResponse{DIDs: dids}, nil } -func (h *webDIDHandler) SoftDeleteDID(ctx context.Context, request DeleteDIDRequest) error { +func (h *webHandler) SoftDeleteDID(ctx context.Context, request DeleteDIDRequest) error { logrus.Debugf("soft deleting DID: %+v", request) id := request.ID - gotStoredDID, err := h.storage.GetDID(ctx, id) + gotStoredDID, err := h.storage.GetDIDDefault(ctx, id) if err != nil { - return fmt.Errorf("error getting DID: %s", id) + return errors.Wrapf(err, "getting DID: %s", id) } if gotStoredDID == nil { return fmt.Errorf("did with id<%s> could not be found", id) diff --git a/pkg/service/keystore/service.go b/pkg/service/keystore/service.go index 32ce7daad..fdffb147a 100644 --- a/pkg/service/keystore/service.go +++ b/pkg/service/keystore/service.go @@ -10,9 +10,10 @@ import ( "github.com/mr-tron/base58" "github.com/pkg/errors" "github.com/sirupsen/logrus" - "github.com/tbd54566975/ssi-service/internal/keyaccess" "golang.org/x/crypto/chacha20poly1305" + "github.com/tbd54566975/ssi-service/internal/keyaccess" + "github.com/tbd54566975/ssi-service/config" "github.com/tbd54566975/ssi-service/internal/util" "github.com/tbd54566975/ssi-service/pkg/service/framework" @@ -73,7 +74,6 @@ func NewKeyStoreService(config config.KeyStoreServiceConfig, s storage.ServiceSt } func (s Service) StoreKey(ctx context.Context, request StoreKeyRequest) error { - logrus.Debugf("storing key: %+v", request) // check if the provided key type is supported. support entails being able to serialize/deserialize, in addition diff --git a/pkg/service/keystore/storage.go b/pkg/service/keystore/storage.go index 7ece85558..d16523215 100644 --- a/pkg/service/keystore/storage.go +++ b/pkg/service/keystore/storage.go @@ -106,6 +106,7 @@ func (kss *Storage) getAndSetServiceKey(ctx context.Context) ([]byte, error) { } func (kss *Storage) StoreKey(ctx context.Context, key StoredKey) error { + // TODO(gabe): conflict checking on key id id := key.ID if id == "" { return util.LoggingNewError("could not store key without an ID") diff --git a/pkg/service/webhook/model.go b/pkg/service/webhook/model.go index f88896e00..46e87d887 100644 --- a/pkg/service/webhook/model.go +++ b/pkg/service/webhook/model.go @@ -109,7 +109,7 @@ func (v Verb) isValid() bool { return false } -// isValidURL checks if there were any errors during parsing and if the parsed URL has a non-empty Scheme and Host. +// isValidURL checks if there were any errors during parsing and if the parsed DIDWebID has a non-empty Scheme and Host. // currently we support any scheme including http, https, ftp ... func isValidURL(urlStr string) bool { parsedURL, err := url.Parse(urlStr) diff --git a/pkg/service/webhook/service.go b/pkg/service/webhook/service.go index b7b10f283..a38bb2580 100644 --- a/pkg/service/webhook/service.go +++ b/pkg/service/webhook/service.go @@ -127,7 +127,7 @@ func (s Service) GetWebhooks(ctx context.Context) (*GetWebhooksResponse, error) return &GetWebhooksResponse{Webhooks: webhooks}, nil } -// DeleteWebhook deletes a webhook from the storage by removing a given URL from the list of URLs associated with the webhook. +// DeleteWebhook deletes a webhook from the storage by removing a given DIDWebID from the list of URLs associated with the webhook. // If there are no URLs left in the list, the entire webhook is deleted from storage. func (s Service) DeleteWebhook(ctx context.Context, request DeleteWebhookRequest) error { logrus.Debugf("deleting webhook: %s-%s", request.Noun, request.Verb) diff --git a/pkg/testutil/well_known_generation_test.go b/pkg/testutil/well_known_generation_test.go index 307480196..9a6897468 100644 --- a/pkg/testutil/well_known_generation_test.go +++ b/pkg/testutil/well_known_generation_test.go @@ -14,6 +14,7 @@ import ( "github.com/lestrrat-go/jwx/jwt" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/tbd54566975/ssi-service/config" "github.com/tbd54566975/ssi-service/internal/keyaccess" "github.com/tbd54566975/ssi-service/pkg/service/did" @@ -44,7 +45,8 @@ func TestWellKnownGenerationTest(t *testing.T) { createWellKnownDIDConfiguration(tt, didKey, gotKey, origin) - didWeb, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: "web", DIDWebID: "did:web:tbd.website", KeyType: "Ed25519"}) + createOpts := did.CreateWebDIDOptions{DIDWebID: "did:web:tbd.website"} + didWeb, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: "web", KeyType: "Ed25519", Options: createOpts}) assert.NoError(tt, err) assert.NotEmpty(tt, didWeb) @@ -167,8 +169,8 @@ func testDIDService(t *testing.T, db storage.ServiceStorage, keyStore *keystore. BaseServiceConfig: &config.BaseServiceConfig{ Name: "did", }, - Methods: []string{"key", "web"}, - ResolutionMethods: []string{"key"}, + Methods: []string{"key", "web"}, + LocalResolutionMethods: []string{"key"}, } // create a did service didService, err := did.NewDIDService(serviceConfig, db, keyStore)