From 4c1e711ec444b4dd374eb2ec2ec485c4276b4e70 Mon Sep 17 00:00:00 2001 From: reinkrul Date: Tue, 31 Oct 2023 06:46:13 +0100 Subject: [PATCH] Support secp256k1 (#89) * Support secp256k1 * enable compile flag in workflow --- .github/workflows/go-test.yaml | 4 +-- README.md | 6 ++++ did/document.go | 37 +++++++++++++++++++++- did/document_test.go | 56 +++++++++++++++++++++++++++++----- 4 files changed, 92 insertions(+), 11 deletions(-) diff --git a/.github/workflows/go-test.yaml b/.github/workflows/go-test.yaml index 10eb4c8..73fd51e 100644 --- a/.github/workflows/go-test.yaml +++ b/.github/workflows/go-test.yaml @@ -27,6 +27,6 @@ jobs: - name: Display Go version run: go version - name: Build - run: go build -v ./... + run: go build --tags=jwx_es256k -v ./... - name: Test - run: go test -v ./... + run: go test --tags=jwx_es256k -v ./... diff --git a/README.md b/README.md index 8cb611a..efe0f7c 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,12 @@ Outputs: The library supports parsing of Verifiable Credentials and Verifiable Presentations in JSON-LD, and JWT proof format. Use `ParseVerifiableCredential(raw string)` and `ParseVerifiablePresentation(raw string)`. +## Supported key types + +- `JsonWebKey2020` +- `Ed25519VerificationKey2018` +- `EcdsaSecp256k1VerificationKey2019` (pass build tag to enable: `--tags=jwx_es256k`) + ## Installation ``` go get github.com/nuts-foundation/go-did diff --git a/did/document.go b/did/document.go index 66fe15b..a8544bc 100644 --- a/did/document.go +++ b/did/document.go @@ -1,13 +1,14 @@ package did import ( + "context" "crypto" "crypto/ed25519" "encoding/json" "errors" "fmt" + "github.com/lestrrat-go/jwx/jwa" "github.com/multiformats/go-multibase" - "github.com/nuts-foundation/go-did" "github.com/lestrrat-go/jwx/jwk" @@ -16,6 +17,9 @@ import ( "github.com/nuts-foundation/go-did/internal/marshal" ) +// lestrrat-go/jwx requires secp256k1 support to be enabled compile-time +var errP256k1NotSupported = errors.New("secp256k1 support is not enabled") + // ParseDocument parses a DID Document from a string. func ParseDocument(raw string) (*Document, error) { type Alias Document @@ -351,6 +355,20 @@ func NewVerificationMethod(id DID, keyType ssi.KeyType, controller DID, key cryp vm.PublicKeyJwk = keyAsMap } + if keyType == ssi.ECDSASECP256K1VerificationKey2019 { + if !secp256k1Supported() { + return nil, errP256k1NotSupported + } + keyAsJWK, err := jwk.New(key) + if err != nil { + return nil, err + } + jwkAsMap, err := keyAsJWK.AsMap(context.Background()) + if err != nil { + return nil, err + } + vm.PublicKeyJwk = jwkAsMap + } if keyType == ssi.ED25519VerificationKey2018 { ed25519Key, ok := key.(ed25519.PublicKey) if !ok { @@ -399,6 +417,14 @@ func (v VerificationMethod) PublicKey() (crypto.PublicKey, error) { return nil, errors.New("expected either publicKeyMultibase or publicKeyBase58 to be set") } return ed25519.PublicKey(keyBytes), err + case ssi.ECDSASECP256K1VerificationKey2019: + if !secp256k1Supported() { + return nil, errP256k1NotSupported + } + if v.PublicKeyJwk == nil { + return nil, errors.New("missing publicKeyJwk") + } + fallthrough case ssi.JsonWebKey2020: keyAsJWK, err := v.JWK() if err != nil { @@ -497,3 +523,12 @@ func resolveVerificationRelationship(reference DID, methods []*VerificationMetho } return nil } + +func secp256k1Supported() bool { + for _, alg := range jwa.EllipticCurveAlgorithms() { + if alg.String() == "secp256k1" { + return true + } + } + return false +} diff --git a/did/document_test.go b/did/document_test.go index bff43ea..c57dca6 100644 --- a/did/document_test.go +++ b/did/document_test.go @@ -6,6 +6,7 @@ import ( "crypto/elliptic" "crypto/rand" "encoding/json" + "github.com/decred/dcrd/dcrec/secp256k1/v4" ssi "github.com/nuts-foundation/go-did" "github.com/stretchr/testify/require" "testing" @@ -124,19 +125,58 @@ func Test_Document(t *testing.T) { t.Logf("resulting json:\n%s", didJson) }) - t.Run("it can add assertionMethods with ED25519VerificationKey2018", func(t *testing.T) { + t.Run("ED25519VerificationKey2018", func(t *testing.T) { id := actual.ID - id.Fragment = "added-assertion-method-1" + id.Fragment = "1" pubKey, _, _ := ed25519.GenerateKey(rand.Reader) vm, err := NewVerificationMethod(id, ssi.ED25519VerificationKey2018, actual.ID, pubKey) - if !assert.NoError(t, err) { - return - } + require.NoError(t, err) - actual.AddAssertionMethod(vm) - didJson, _ := json.MarshalIndent(actual, "", " ") - t.Logf("resulting json:\n%s", didJson) + publicKey, err := vm.PublicKey() + require.NoError(t, err) + require.NotNil(t, publicKey) + }) + + t.Run("ECDSASECP256K1VerificationKey2019", func(t *testing.T) { + t.Run("generated key", func(t *testing.T) { + id := actual.ID + id.Fragment = "1" + privateKey, err := secp256k1.GeneratePrivateKey() + require.NoError(t, err) + + vm, err := NewVerificationMethod(id, ssi.ECDSASECP256K1VerificationKey2019, actual.ID, privateKey.ToECDSA()) + require.NoError(t, err) + + publicKey, err := vm.PublicKey() + require.NoError(t, err) + require.NotNil(t, publicKey) + asJWK, err := vm.JWK() + require.NoError(t, err) + require.NotNil(t, asJWK) + }) + t.Run("static", func(t *testing.T) { + // copied from an online did:web source + const asJSON = `{ + "controller": "did:web:example.com", + "id": "did:web:example.com#xCPeUKv-0t4TPSlRnk61AqIK-DtH-riOvyx_Udk65XA", + "publicKeyJwk": { + "kty": "EC", + "x": "P_kEHfyV27kg5oxn1pzTTDAkNKqsH9QdfdADcKLlr4Y", + "y": "GE0tU43W30xT-DKmF75uWCWSnXid3kKhnYZbdWhiguE", + "crv": "secp256k1", + "kid": "did:web:example.com#xCPeUKv-0t4TPSlRnk61AqIK-DtH-riOvyx_Udk65XA" + }, + "type": "EcdsaSecp256k1VerificationKey2019" + }` + vm := VerificationMethod{} + err := json.Unmarshal([]byte(asJSON), &vm) + require.NoError(t, err) + + publicKey, err := vm.PublicKey() + require.NoError(t, err) + require.NotNil(t, publicKey) + }) }) t.Run("it can parse a jwk in a verification method", func(t *testing.T) {