diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 467862bd..5fa10257 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: 1.20.7 + go-version: 1.21.0 - name: Install Mage run: go install github.com/magefile/mage @@ -38,7 +38,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: 1.20.7 + go-version: 1.21.0 - name: Install Mage run: go install github.com/magefile/mage diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 32327fcd..30d03e9b 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -16,7 +16,7 @@ jobs: steps: - uses: actions/setup-go@v3 with: - go-version: 1.20.7 + go-version: 1.21.0 - uses: actions/checkout@v3 - name: golangci-lint uses: golangci/golangci-lint-action@v3 diff --git a/.gitignore b/.gitignore index ee953ee1..415dd6ae 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,8 @@ .DS_Store # IDE -.idea/ \ No newline at end of file +.idea/ + +# Mobile +*.jar +*.aar \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 22d4e335..fde63855 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,7 @@ When you're ready you may: | Requirement | Tested Version | Installation Instructions | |-------------|----------------|--------------------------------------------------------| -| Go | 1.20.7 | [go.dev](https://go.dev/doc/tutorial/compile-install) | +| Go | 1.21.0 | [go.dev](https://go.dev/doc/tutorial/compile-install) | | Mage | 1.13.0-6 | [magefile.org](https://magefile.org/) | ### Go @@ -23,7 +23,7 @@ You may verify your `go` installation via the terminal: ``` $> go version -go version go1.20.7 darwin/amd64 +go version go1.21.0 darwin/amd64 ``` If you do not have go, we recommend installing it by: diff --git a/README.md b/README.md index b25e680d..fccff5a5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![godoc ssi-sdk](https://img.shields.io/badge/godoc-ssi--sdk-blue)](https://pkg.go.dev/github.com/TBD54566975/ssi-sdk) -[![go version 1.20.7](https://img.shields.io/badge/go_version-1.20.7-brightgreen)](https://golang.org/) +[![go version 1.21.0](https://img.shields.io/badge/go_version-1.21.0-brightgreen)](https://golang.org/) [![Go Report Card A+](https://goreportcard.com/badge/github.com/TBD54566975/ssi-sdk)](https://goreportcard.com/report/github.com/TBD54566975/ssi-sdk) [![license Apache 2](https://img.shields.io/badge/license-Apache%202-black)](https://github.com/TBD54566975/ssi-sdk/blob/main/LICENSE) [![issues](https://img.shields.io/github/issues/TBD54566975/ssi-sdk)](https://github.com/TBD54566975/ssi-sdk/issues) @@ -115,6 +115,11 @@ For information on versioning refer to our [versioning guide](doc/VERSIONING.md) The latest version is...nothing! No releases have been made. +# Mobile + +Using the [gomobile](https://pkg.go.dev/golang.org/x/mobile/cmd/gomobile) tool, we can generate a library that can be +used in mobile applications. For more information view the [mobile README](mobile/README.md). + # Examples A set of code examples can be found in the [examples directory](example). We welcome diff --git a/crypto/jwx/jwk.go b/crypto/jwx/jwk.go index d6a79c48..949458f3 100644 --- a/crypto/jwx/jwk.go +++ b/crypto/jwx/jwk.go @@ -176,7 +176,8 @@ func (k *PublicKeyJWK) ToPublicKey() (gocrypto.PublicKey, error) { } k.ALG = alg } - if IsSupportedJWXSigningVerificationAlgorithm(k.ALG) { + + if IsSupportedJWXSigningVerificationAlgorithm(k.ALG) || IsSupportedKeyAgreementType(k.ALG) { return k.toSupportedPublicKey() } if IsExperimentalJWXSigningVerificationAlgorithm(k.ALG) { diff --git a/crypto/jwx/jwk_test.go b/crypto/jwx/jwk_test.go index 6deede3f..0b8ddda2 100644 --- a/crypto/jwx/jwk_test.go +++ b/crypto/jwx/jwk_test.go @@ -115,6 +115,27 @@ func TestJWKToPublicKeyJWK(t *testing.T) { assert.Equal(tt, publicKey, gotPubKey) }) + t.Run("X25519", func(tt *testing.T) { + // known public key + publicKey, _, err := crypto.GenerateX25519Key() + assert.NoError(tt, err) + assert.NotEmpty(tt, publicKey) + + // to our representation of a jwk + pubKeyJWK, err := PublicKeyToPublicKeyJWK(testKID, publicKey) + assert.NoError(tt, err) + assert.NotEmpty(tt, pubKeyJWK) + + assert.Equal(tt, "OKP", pubKeyJWK.KTY) + assert.Equal(tt, "X25519", pubKeyJWK.CRV) + + // convert back + gotPubKey, err := pubKeyJWK.ToPublicKey() + assert.NoError(tt, err) + assert.NotEmpty(tt, gotPubKey) + assert.Equal(tt, publicKey, gotPubKey) + }) + t.Run("Dilithium 2", func(tt *testing.T) { // known private key publicKey, _, err := crypto.GenerateDilithiumKeyPair(dilithium.Mode2) diff --git a/crypto/jwx/jwt.go b/crypto/jwx/jwt.go index 8c51752c..524017f8 100644 --- a/crypto/jwx/jwt.go +++ b/crypto/jwx/jwt.go @@ -128,7 +128,7 @@ func jwxVerifier(id string, jwk PublicKeyJWK, key gocrypto.PublicKey) (*Verifier } jwk.ALG = alg } - if !IsSupportedJWXSigningVerificationAlgorithm(jwk.ALG) && !IsExperimentalJWXSigningVerificationAlgorithm(jwk.ALG) { + if !IsSupportedJWXSigningVerificationAlgorithm(jwk.ALG) && !IsSupportedKeyAgreementType(jwk.KTY) { return nil, fmt.Errorf("unsupported signing/verification algorithm: %s", jwk.ALG) } if convertedPubKey, ok := pubKeyForJWX(key); ok { @@ -280,6 +280,19 @@ func GetSupportedJWXSigningVerificationAlgorithms() []string { } } +func IsSupportedKeyAgreementType(keyAgreementType string) bool { + for _, supported := range GetSupportedKeyAgreementTypes() { + if keyAgreementType == supported { + return true + } + } + return false +} + +func GetSupportedKeyAgreementTypes() []string { + return []string{jwa.X25519.String()} +} + // IsExperimentalJWXSigningVerificationAlgorithm returns true if the algorithm is supported for experimental signing or verifying JWXs func IsExperimentalJWXSigningVerificationAlgorithm(algorithm string) bool { for _, supported := range GetExperimentalJWXSigningVerificationAlgorithms() { diff --git a/crypto/keys.go b/crypto/keys.go index 24e0e104..41c83c2a 100644 --- a/crypto/keys.go +++ b/crypto/keys.go @@ -9,7 +9,6 @@ import ( "crypto/rsa" "crypto/x509" "fmt" - "math/big" "reflect" "github.com/btcsuite/btcd/btcec/v2" @@ -61,13 +60,11 @@ func GenerateKeyByKeyType(kt KeyType) (crypto.PublicKey, crypto.PrivateKey, erro return nil, nil, fmt.Errorf("unsupported key type: %s", kt) } -type Option struct { - Name string - Value any -} +type Option int -var ( - ECDSAMarshalCompressed = Option{Name: "ecdsa-compressed", Value: true} +const ( + ECDSAMarshalCompressed Option = iota + ECDSAUnmarshalCompressed ) // PubKeyToBytes constructs a byte representation of a public key, for a set number of supported key types @@ -85,11 +82,25 @@ func PubKeyToBytes(key crypto.PublicKey, opts ...Option) ([]byte, error) { case secp.PublicKey: return k.SerializeCompressed(), nil case ecdsa.PublicKey: - curve := k.Curve - if len(opts) == 1 && opts[0].Name == "ecdsa-compressed" && opts[0].Value.(bool) { + if k.Curve == btcec.S256() { + x := new(btcec.FieldVal) + x.SetByteSlice(k.X.Bytes()) + y := new(btcec.FieldVal) + y.SetByteSlice(k.Y.Bytes()) + return btcec.NewPublicKey(x, y).SerializeCompressed(), nil + } + + // check if we should marshal the key in compressed form + if len(opts) == 1 && opts[0] == ECDSAMarshalCompressed { return elliptic.MarshalCompressed(k.Curve, k.X, k.Y), nil } - return elliptic.Marshal(curve, k.X, k.Y), nil + + // go from ecdsa public key to bytes + pk, err := x509.MarshalPKIXPublicKey(&k) + if err != nil { + return nil, err + } + return pk, nil case rsa.PublicKey: return x509.MarshalPKCS1PublicKey(&k), nil case dilithium.PublicKey: @@ -107,7 +118,7 @@ func PubKeyToBytes(key crypto.PublicKey, opts ...Option) ([]byte, error) { // BytesToPubKey reconstructs a public key given some bytes and a target key type // It is assumed the key was turned into byte form using the sibling method `PubKeyToBytes` -func BytesToPubKey(keyBytes []byte, kt KeyType) (crypto.PublicKey, error) { +func BytesToPubKey(keyBytes []byte, kt KeyType, opts ...Option) (crypto.PublicKey, error) { switch kt { case Ed25519: return ed25519.PublicKey(keyBytes), nil @@ -120,56 +131,35 @@ func BytesToPubKey(keyBytes []byte, kt KeyType) (crypto.PublicKey, error) { } return *pubKey, nil case SECP256k1ECDSA: - x, y := elliptic.Unmarshal(btcec.S256(), keyBytes) - return ecdsa.PublicKey{ - Curve: btcec.S256(), - X: x, - Y: y, - }, nil - case P224: - var x, y *big.Int - x, y = elliptic.Unmarshal(elliptic.P224(), keyBytes) - if x == nil || y == nil { - x, y = elliptic.UnmarshalCompressed(elliptic.P224(), keyBytes) - } - return ecdsa.PublicKey{ - Curve: elliptic.P224(), - X: x, - Y: y, - }, nil - case P256: - var x, y *big.Int - x, y = elliptic.Unmarshal(elliptic.P256(), keyBytes) - if x == nil || y == nil { - x, y = elliptic.UnmarshalCompressed(elliptic.P256(), keyBytes) + pk, err := secp.ParsePubKey(keyBytes) + if err != nil { + return nil, err } - return ecdsa.PublicKey{ - Curve: elliptic.P256(), - X: x, - Y: y, - }, nil - case P384: - var x, y *big.Int - x, y = elliptic.Unmarshal(elliptic.P384(), keyBytes) - if x == nil || y == nil { - x, y = elliptic.UnmarshalCompressed(elliptic.P384(), keyBytes) + return *pk.ToECDSA(), nil + case P224, P256, P384, P521: + // check if we should unmarshal the key in compressed form + if len(opts) == 1 && opts[0] == ECDSAUnmarshalCompressed { + switch kt { + case P224: + x, y := elliptic.UnmarshalCompressed(elliptic.P224(), keyBytes) + return ecdsa.PublicKey{Curve: elliptic.P224(), X: x, Y: y}, nil + case P256: + x, y := elliptic.UnmarshalCompressed(elliptic.P256(), keyBytes) + return ecdsa.PublicKey{Curve: elliptic.P256(), X: x, Y: y}, nil + case P384: + x, y := elliptic.UnmarshalCompressed(elliptic.P384(), keyBytes) + return ecdsa.PublicKey{Curve: elliptic.P384(), X: x, Y: y}, nil + case P521: + x, y := elliptic.UnmarshalCompressed(elliptic.P521(), keyBytes) + return ecdsa.PublicKey{Curve: elliptic.P521(), X: x, Y: y}, nil + } } - return ecdsa.PublicKey{ - Curve: elliptic.P384(), - X: x, - Y: y, - }, nil - case P521: - var x, y *big.Int - x, y = elliptic.Unmarshal(elliptic.P521(), keyBytes) - if x == nil || y == nil { - x, y = elliptic.UnmarshalCompressed(elliptic.P521(), keyBytes) + + key, err := x509.ParsePKIXPublicKey(keyBytes) + if err != nil { + return nil, err } - return ecdsa.PublicKey{ - Curve: elliptic.P521(), - X: x, - Y: y, - }, nil + return *key.(*ecdsa.PublicKey), nil case RSA: pubKey, err := x509.ParsePKCS1PublicKey(keyBytes) if err != nil { diff --git a/did/key/key.go b/did/key/key.go index 5fd7091f..593f6950 100644 --- a/did/key/key.go +++ b/did/key/key.go @@ -103,7 +103,7 @@ func GenerateDIDKey(kt crypto.KeyType) (gocrypto.PrivateKey, *DIDKey, error) { return nil, nil, errors.Wrap(err, "generating key for did:key") } - pubKeyBytes, err := crypto.PubKeyToBytes(pubKey) + pubKeyBytes, err := crypto.PubKeyToBytes(pubKey, crypto.ECDSAMarshalCompressed) if err != nil { return nil, nil, errors.Wrap(err, "converting public key to byte") } diff --git a/did/util.go b/did/util.go index 8238b3b7..8a075e0d 100644 --- a/did/util.go +++ b/did/util.go @@ -269,7 +269,8 @@ func DecodeMultibasePublicKeyWithType(data []byte) ([]byte, cryptosuite.LDKeyTyp // ConstructJWKVerificationMethod builds a DID verification method with a known LD key type as a JWK func ConstructJWKVerificationMethod(id, controller string, pubKeyBytes []byte, cryptoKeyType crypto.KeyType) (*VerificationMethod, error) { - pubKey, err := crypto.BytesToPubKey(pubKeyBytes, cryptoKeyType) + // TODO(gabe): consider exposing compression as an option instead of a default + pubKey, err := crypto.BytesToPubKey(pubKeyBytes, cryptoKeyType, crypto.ECDSAUnmarshalCompressed) if err != nil { return nil, errors.Wrap(err, "converting bytes to public key") } diff --git a/go.mod b/go.mod index ecb30cb8..f487b761 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/TBD54566975/ssi-sdk -go 1.19 +go 1.21 require ( github.com/bits-and-blooms/bitset v1.8.0 @@ -59,8 +59,11 @@ require ( github.com/segmentio/asm v1.2.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect golang.org/x/crypto v0.12.0 // indirect - golang.org/x/net v0.10.0 // indirect + golang.org/x/mobile v0.0.0-20230818142238-7088062f872d // indirect + golang.org/x/mod v0.12.0 // indirect + golang.org/x/net v0.14.0 // indirect golang.org/x/sys v0.11.0 // indirect + golang.org/x/tools v0.12.1-0.20230818130535-1517d1a3ba60 // 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 76514329..01138651 100644 --- a/go.sum +++ b/go.sum @@ -123,8 +123,12 @@ golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/mobile v0.0.0-20230818142238-7088062f872d h1:Ouem7YgI783/xoG5NZUHbg/ggHFOutUUoq1ZRlCCTbM= +golang.org/x/mobile v0.0.0-20230818142238-7088062f872d/go.mod h1:kQNMt2gXlYXNazoSeytBi7knmDN7YS/JzMKFYxgoNxc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -132,6 +136,7 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -165,6 +170,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.12.1-0.20230818130535-1517d1a3ba60 h1:o4bs4seAAlSiZQAZbO6/RP5XBCZCooQS3Pgc0AUjWts= +golang.org/x/tools v0.12.1-0.20230818130535-1517d1a3ba60/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= diff --git a/go.work b/go.work index 80036558..f97f8f00 100644 --- a/go.work +++ b/go.work @@ -1,6 +1,9 @@ -go 1.20 +go 1.21 + +toolchain go1.21.0 use ( - ./sd-jwt . -) \ No newline at end of file + ./sd-jwt + ./mobile +) diff --git a/go.work.sum b/go.work.sum index 017039b7..f3168ee9 100644 --- a/go.work.sum +++ b/go.work.sum @@ -238,16 +238,23 @@ golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU= +golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b h1:clP8eMhB30EHdc0bd2Twtq6kgU7yl5ub2cQLSdrv1Dg= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/magefile.go b/magefile.go index 5276f074..fcffa80f 100644 --- a/magefile.go +++ b/magefile.go @@ -25,7 +25,8 @@ import ( ) const ( - Go = "go" + Go = "go" + gomobile = "gomobile" ) // Build builds the library. @@ -305,3 +306,34 @@ func Vuln() error { func installGoVulnIfNotPresent() error { return installIfNotPresent("govulncheck", "golang.org/x/vuln/cmd/govulncheck@latest") } + +func installGoMobileIfNotPresent() error { + return installIfNotPresent(gomobile, "golang.org/x/mobile/cmd/gomobile@latest") +} + +// IOS Generates the iOS packages +// Note: this command also installs "gomobile" if not present +func IOS() error { + if err := installGoMobileIfNotPresent(); err != nil { + logrus.WithError(err).Fatal("Error installing gomobile") + return err + } + + println("Building iOS...") + bindIOS := sh.RunCmd(gomobile, "bind", "-target", "ios", "-tags", "jwx_es256k") + return bindIOS("./mobile") +} + +// Android Generates the Android packages +// Note: this command also installs "gomobile" if not present +func Android() error { + if err := installGoMobileIfNotPresent(); err != nil { + logrus.WithError(err).Fatal("Error installing gomobile") + return err + } + + apiLevel := "33" + println("Building Android - API Level: " + apiLevel + "...") + bindAndroid := sh.RunCmd("gomobile", "bind", "-target", "android", "-androidapi", "33", "-tags", "jwx_es256k") + return bindAndroid("./mobile") +} diff --git a/mobile/README.md b/mobile/README.md new file mode 100644 index 00000000..f64bd8ac --- /dev/null +++ b/mobile/README.md @@ -0,0 +1,17 @@ +# Mobile + +Mobile makes use of [Go Mobile](https://pkg.go.dev/golang.org/x/mobile/cmd/gomobile). Bindings are maintained in this +package. Mage targets are exposed to generate bindings for iOS and Android. At present, files must not be nested in +subdirectories for bindings to be generated correctly. + +## iOS + +``` +mage ios +``` + +## Android + +``` +mage android +``` \ No newline at end of file diff --git a/mobile/did_key.go b/mobile/did_key.go new file mode 100644 index 00000000..6431d5d9 --- /dev/null +++ b/mobile/did_key.go @@ -0,0 +1,152 @@ +package mobile + +import ( + "github.com/TBD54566975/ssi-sdk/crypto" + "github.com/TBD54566975/ssi-sdk/crypto/jwx" + "github.com/TBD54566975/ssi-sdk/did/key" + "github.com/goccy/go-json" + "github.com/sirupsen/logrus" +) + +// GetSupportedKeyTypes returns a list of supported key types as string values +func GetSupportedKeyTypes() []string { + keyTypes := make([]string, 0, len(key.GetSupportedDIDKeyTypes())) + for _, kt := range key.GetSupportedDIDKeyTypes() { + keyTypes = append(keyTypes, string(kt)) + } + return keyTypes +} + +// GenerateDIDKeyResult is a struct that contains the DID and JWK of a newly generated DID key +// It is returned as a result of the GenerateDIDKey function +type GenerateDIDKeyResult struct { + // DID is the string of the DID Key created, such as did:key:z6Mk... + DID string `json:"did"` + // JWK is the JSON Web Key (private key) of the newly created DID Key + JWK map[string]any `json:"jwk"` +} + +// GenerateDIDKey generates a new DID key and returns a JSON representation of GenerateDIDKeyResult +func GenerateDIDKey(kt string) ([]byte, error) { + privateKey, didKey, err := key.GenerateDIDKey(crypto.KeyType(kt)) + if err != nil { + logrus.WithError(err).Error("failed to generate did key") + return nil, err + } + + expanded, err := didKey.Expand() + if err != nil { + logrus.WithError(err).Error("failed to expand did key") + return nil, err + } + + _, jwkPrivateKey, err := jwx.PrivateKeyToPrivateKeyJWK(expanded.VerificationMethod[0].ID, privateKey) + if err != nil { + logrus.WithError(err).Error("failed to convert private key to jwk") + return nil, err + } + + jwkBytes, err := json.Marshal(jwkPrivateKey) + if err != nil { + logrus.WithError(err).Error("failed to marshal jwk") + return nil, err + } + + var jwk map[string]any + if err = json.Unmarshal(jwkBytes, &jwk); err != nil { + logrus.WithError(err).Error("failed to unmarshal jwk") + return nil, err + } + + result := GenerateDIDKeyResult{ + DID: didKey.String(), + JWK: jwk, + } + return json.Marshal(result) +} + +// CreateDIDKeyRequest is a struct that contains the key type and public key JWK of a DID key to be created +type CreateDIDKeyRequest struct { + // KeyType is the type of key to be created, such as "Ed25519VerificationKey2018" + KeyType string `json:"keyType"` + // PublicKeyJWK is the JSON Web Key (public key) of the DID Key to be created + PublicKeyJWK map[string]any `json:"publicKeyJwk"` +} + +// CreateDIDKeyResult is a struct that contains the DID of a newly created DID key +type CreateDIDKeyResult struct { + // DID is the string of the DID Key created, such as did:key:z6Mk... + DID string `json:"did"` +} + +// CreateDIDKey creates a new DID key from an existing public key, accepting a JSON representation of CreateDIDKeyRequest +// and returns a JSON representation of CreateDIDKeyResult which contains the DID of the newly created key as a string +func CreateDIDKey(requestBytes []byte) ([]byte, error) { + var request CreateDIDKeyRequest + if err := json.Unmarshal(requestBytes, &request); err != nil { + logrus.WithError(err).Error("failed to unmarshal request") + return nil, err + } + + // transform the json representation of the public key jwk into a public key + publicKeyBytes, err := json.Marshal(request.PublicKeyJWK) + if err != nil { + logrus.WithError(err).Error("failed to marshal public key jwk") + return nil, err + } + var publicKeyJWK jwx.PublicKeyJWK + if err = json.Unmarshal(publicKeyBytes, &publicKeyJWK); err != nil { + logrus.WithError(err).Error("failed to unmarshal public key jwk") + return nil, err + } + publicKey, err := publicKeyJWK.ToPublicKey() + if err != nil { + logrus.WithError(err).Error("failed to convert public key jwk to public key") + return nil, err + } + pubKeyBytes, err := crypto.PubKeyToBytes(publicKey, crypto.ECDSAMarshalCompressed) + if err != nil { + logrus.WithError(err).Error("failed to convert public key to bytes") + return nil, err + } + + didKey, err := key.CreateDIDKey(crypto.KeyType(request.KeyType), pubKeyBytes) + if err != nil { + logrus.WithError(err).Error("failed to create did key") + return nil, err + } + + result := CreateDIDKeyResult{DID: didKey.String()} + return json.Marshal(result) +} + +// Document is a struct that contains the DID document of a DID key +type Document struct { + // DIDDocument is the JSON representation of the DID document of a DID key + DIDDocument map[string]any `json:"didDocument"` +} + +// ExpandDIDKey expands a DID key string and returns a JSON representation of the expanded key +// Returns a JSON representation of Document +func ExpandDIDKey(didKey string) ([]byte, error) { + expanded, err := key.DIDKey(didKey).Expand() + if err != nil { + logrus.WithError(err).Error("failed to expand did key") + return nil, err + } + + expandedBytes, err := json.Marshal(expanded) + if err != nil { + logrus.WithError(err).Error("failed to marshal expanded did key") + return nil, err + } + + var didDocJSON map[string]any + if err = json.Unmarshal(expandedBytes, &didDocJSON); err != nil { + logrus.WithError(err).Error("failed to unmarshal did document") + return nil, err + } + + document := Document{DIDDocument: didDocJSON} + return json.Marshal(document) +} diff --git a/mobile/did_key_test.go b/mobile/did_key_test.go new file mode 100644 index 00000000..7ae55faf --- /dev/null +++ b/mobile/did_key_test.go @@ -0,0 +1,98 @@ +package mobile + +import ( + "testing" + + "github.com/TBD54566975/ssi-sdk/did/key" + "github.com/goccy/go-json" + "github.com/stretchr/testify/assert" +) + +func TestGetSupportedKeyTypes(t *testing.T) { + supportedKeyTypes := GetSupportedKeyTypes() + assert.NotEmpty(t, supportedKeyTypes) + assert.Equal(t, len(key.GetSupportedDIDKeyTypes()), len(supportedKeyTypes)) +} + +func TestGenerateDIDKey(t *testing.T) { + supportedKeyTypes := GetSupportedKeyTypes() + assert.NotEmpty(t, supportedKeyTypes) + + for _, kt := range supportedKeyTypes { + result, err := GenerateDIDKey(kt) + assert.NoError(t, err) + assert.NotEmpty(t, result) + + var didKeyResult GenerateDIDKeyResult + err = json.Unmarshal(result, &didKeyResult) + assert.NoError(t, err) + assert.NotEmpty(t, didKeyResult.DID) + assert.NotEmpty(t, didKeyResult.JWK) + } +} + +func TestCreateDIDKey(t *testing.T) { + supportedKeyTypes := GetSupportedKeyTypes() + assert.NotEmpty(t, supportedKeyTypes) + + didKeys := make(map[string]GenerateDIDKeyResult) + for _, kt := range supportedKeyTypes { + result, err := GenerateDIDKey(kt) + assert.NoError(t, err) + assert.NotEmpty(t, result) + + var didKeyResult GenerateDIDKeyResult + err = json.Unmarshal(result, &didKeyResult) + assert.NoError(t, err) + assert.NotEmpty(t, didKeyResult.DID) + assert.NotEmpty(t, didKeyResult.JWK) + didKeys[kt] = didKeyResult + } + + for kt, didKey := range didKeys { + createDIDKeyRequest := CreateDIDKeyRequest{ + KeyType: kt, + PublicKeyJWK: didKey.JWK, + } + reqBytes, err := json.Marshal(createDIDKeyRequest) + assert.NoError(t, err) + assert.NotEmpty(t, reqBytes) + + result, err := CreateDIDKey(reqBytes) + assert.NoError(t, err) + assert.NotEmpty(t, result) + + var createDIDKeyResult CreateDIDKeyResult + err = json.Unmarshal(result, &createDIDKeyResult) + assert.NoError(t, err) + + assert.NotEmpty(t, createDIDKeyResult.DID) + assert.Equal(t, didKey.DID, createDIDKeyResult.DID) + } +} + +func TestExpandDIDKey(t *testing.T) { + supportedKeyTypes := GetSupportedKeyTypes() + assert.NotEmpty(t, supportedKeyTypes) + + for _, kt := range supportedKeyTypes { + result, err := GenerateDIDKey(kt) + assert.NoError(t, err) + assert.NotEmpty(t, result) + + var didKeyResult GenerateDIDKeyResult + err = json.Unmarshal(result, &didKeyResult) + assert.NoError(t, err) + assert.NotEmpty(t, didKeyResult.DID) + assert.NotEmpty(t, didKeyResult.JWK) + + expandedDIDKey, err := ExpandDIDKey(didKeyResult.DID) + assert.NoError(t, err) + assert.NotEmpty(t, expandedDIDKey) + + var doc Document + err = json.Unmarshal(expandedDIDKey, &doc) + assert.NoError(t, err) + assert.Equal(t, didKeyResult.DID, doc.DIDDocument["id"]) + } +} diff --git a/mobile/go.mod b/mobile/go.mod new file mode 100644 index 00000000..23c2798e --- /dev/null +++ b/mobile/go.mod @@ -0,0 +1,8 @@ +module github.com/TBD54566975/ssi-sdk/mobile + +go 1.21 + +require ( + github.com/TBD54566975/ssi-sdk v0.0.4-alpha + github.com/goccy/go-json v0.10.2 +) \ No newline at end of file diff --git a/sd-jwt/README.md b/sd-jwt/README.md index f378af62..fdc556b8 100644 --- a/sd-jwt/README.md +++ b/sd-jwt/README.md @@ -1,5 +1,5 @@ [![godoc ssi-sdk](https://img.shields.io/badge/godoc-ssi--sdk-blue)](https://pkg.go.dev/github.com/TBD54566975/ssi-sdk/sd-jwt) -[![go version 1.20.6](https://img.shields.io/badge/go_version-1.20.6-brightgreen)](https://golang.org/) +[![go version 1.21.0](https://img.shields.io/badge/go_version-1.21.0-brightgreen)](https://golang.org/) [![Go Report Card A+](https://goreportcard.com/badge/github.com/TBD54566975/ssi-sdk/sd-jwt)](https://goreportcard.com/report/github.com/TBD54566975/ssi-sdk/sd-jwt) [![license Apache 2](https://img.shields.io/badge/license-Apache%202-black)](https://github.com/TBD54566975/ssi-sdk/blob/main/LICENSE) diff --git a/sd-jwt/go.mod b/sd-jwt/go.mod index 0bb519bf..3fbaeac0 100644 --- a/sd-jwt/go.mod +++ b/sd-jwt/go.mod @@ -1,6 +1,6 @@ module github.com/TBD54566975/ssi-sdk/sd-jwt -go 1.20 +go 1.21 require ( github.com/TBD54566975/ssi-sdk v0.0.4-alpha