Skip to content
This repository has been archived by the owner on Dec 12, 2024. It is now read-only.

Commit

Permalink
Implementation of did:jwk (#363)
Browse files Browse the repository at this point in the history
* jwk temp

* did jwk impl

* more tests

* more tests

* move ctx

* Apply suggestions from code review

Co-authored-by: Andres Uribe <[email protected]>

* pr comments

---------

Co-authored-by: Andres Uribe <[email protected]>
  • Loading branch information
decentralgabe and andresuribe87 authored May 1, 2023
1 parent d5c8f79 commit 804491d
Show file tree
Hide file tree
Showing 7 changed files with 426 additions and 6 deletions.
1 change: 0 additions & 1 deletion did/context/did-pkh-context-deref.json

This file was deleted.

1 change: 1 addition & 0 deletions did/did.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const (
PKHMethod Method = "pkh"
WebMethod Method = "web"
IONMethod Method = "ion"
JWKMethod Method = "jwk"
)

func (m Method) String() string {
Expand Down
179 changes: 179 additions & 0 deletions did/jwk.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package did

import (
"context"
gocrypto "crypto"
"encoding/base64"
"fmt"
"strings"

"github.com/TBD54566975/ssi-sdk/crypto"
"github.com/TBD54566975/ssi-sdk/cryptosuite"
"github.com/goccy/go-json"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/pkg/errors"
)

type (
DIDJWK string
)

const (
// JWKPrefix did:jwk prefix
JWKPrefix = "did:jwk"
JWS2020Context = "https://w3id.org/security/suites/jws-2020/v1"
)

func (d DIDJWK) IsValid() bool {
_, err := d.Expand()
return err == nil
}

func (d DIDJWK) String() string {
return string(d)
}

// Suffix returns the value without the `did:jwk` prefix
func (d DIDJWK) Suffix() (string, error) {
if suffix, ok := strings.CutPrefix(string(d), JWKPrefix+":"); ok {
return suffix, nil
}
return "", fmt.Errorf("invalid did:jwk: %s", d)
}

func (DIDJWK) Method() Method {
return JWKMethod
}

// GenerateDIDJWK takes in a key type value that this library supports and constructs a conformant did:jwk identifier.
func GenerateDIDJWK(kt crypto.KeyType) (gocrypto.PrivateKey, *DIDJWK, error) {
if !isSupportedJWKType(kt) {
return nil, nil, fmt.Errorf("unsupported did:jwk type: %s", kt)
}

// 1. Generate a JWK
pubKey, privKey, err := crypto.GenerateKeyByKeyType(kt)
if err != nil {
return nil, nil, errors.Wrap(err, "generating key for did:jwk")
}
pubKeyJWK, err := crypto.PublicKeyToJWK(pubKey)
if err != nil {
return nil, nil, errors.Wrap(err, "converting public key to JWK")
}

// 2. Serialize it into a UTF-8 string
// 3. Encode string using base64url
// 4. Prepend the string with the did:jwk prefix
didJWK, err := CreateDIDJWK(pubKeyJWK)
if err != nil {
return nil, nil, errors.Wrap(err, "creating did:jwk")
}
return privKey, didJWK, nil
}

// CreateDIDJWK creates a did:jwk from a JWK public key by following the steps in the spec:
// https://github.com/quartzjer/did-jwk/blob/main/spec.md
func CreateDIDJWK(publicKeyJWK jwk.Key) (*DIDJWK, error) {
// 2. Serialize it into a UTF-8 string
pubKeyJWKBytes, err := json.Marshal(publicKeyJWK)
if err != nil {
return nil, errors.Wrap(err, "marshalling public key JWK")
}
pubKeyJWKStr := string(pubKeyJWKBytes)

// 3. Encode string using base64url
encodedPubKeyJWKStr := base64.RawURLEncoding.EncodeToString([]byte(pubKeyJWKStr))

// 4. Prepend the string with the did:jwk prefix
didJWK := DIDJWK(fmt.Sprintf("%s:%s", JWKPrefix, encodedPubKeyJWKStr))
return &didJWK, nil
}

// Expand turns the DID JWK into a compliant DID Document
func (d DIDJWK) Expand() (*Document, error) {
id := d.String()

if !strings.HasPrefix(id, JWKPrefix) {
return nil, fmt.Errorf("not a did:jwk DID, invalid prefix: %s", id)
}

encodedJWK, err := d.Suffix()
if err != nil {
return nil, errors.Wrap(err, "reading suffix")
}
decodedPubKeyJWKStr, err := base64.RawURLEncoding.DecodeString(encodedJWK)
if err != nil {
return nil, errors.Wrap(err, "decoding did:jwk")
}

var pubKeyJWK crypto.PublicKeyJWK
if err = json.Unmarshal(decodedPubKeyJWKStr, &pubKeyJWK); err != nil {
return nil, errors.Wrap(err, "unmarshalling did:jwk")
}

keyReference := "#0"
keyID := id + keyReference

doc := Document{
Context: []string{KnownDIDContext, JWS2020Context},
ID: id,
VerificationMethod: []VerificationMethod{
{
ID: keyID,
Type: cryptosuite.JSONWebKey2020Type,
Controller: id,
PublicKeyJWK: &pubKeyJWK,
},
},
Authentication: []VerificationMethodSet{keyID},
AssertionMethod: []VerificationMethodSet{keyID},
KeyAgreement: []VerificationMethodSet{keyID},
CapabilityInvocation: []VerificationMethodSet{keyID},
CapabilityDelegation: []VerificationMethodSet{keyID},
}

// If the JWK contains a use property with the value "sig" then the keyAgreement property is not included in the
// DID Document. If the use value is "enc" then only the keyAgreement property is included in the DID Document.
switch pubKeyJWK.Use {
case "sig":
doc.KeyAgreement = nil
case "enc":
doc.Authentication = nil
doc.AssertionMethod = nil
doc.CapabilityInvocation = nil
doc.CapabilityDelegation = nil
}

return &doc, nil
}

func isSupportedJWKType(kt crypto.KeyType) bool {
jwkTypes := GetSupportedDIDJWKTypes()
for _, t := range jwkTypes {
if t == kt {
return true
}
}
return false
}

func GetSupportedDIDJWKTypes() []crypto.KeyType {
return []crypto.KeyType{crypto.Ed25519, crypto.X25519, crypto.SECP256k1, crypto.P256, crypto.P384, crypto.P521, crypto.RSA}
}

type JWKResolver struct{}

var _ Resolver = (*JWKResolver)(nil)

func (JWKResolver) Resolve(_ context.Context, did string, _ ...ResolutionOption) (*ResolutionResult, error) {
didJWK := DIDJWK(did)
doc, err := didJWK.Expand()
if err != nil {
return nil, errors.Wrap(err, "expanding did:jwk")
}
return &ResolutionResult{Document: *doc}, nil
}

func (JWKResolver) Methods() []Method {
return []Method{JWKMethod}
}
195 changes: 195 additions & 0 deletions did/jwk_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
package did

import (
"context"
"embed"
"strings"
"testing"

"github.com/TBD54566975/ssi-sdk/crypto"
"github.com/TBD54566975/ssi-sdk/cryptosuite"
"github.com/goccy/go-json"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/stretchr/testify/assert"
)

const (
P256Vector string = "did-jwk-p256.json"
X25519Vector string = "did-jwk-x25519.json"
)

var (
//go:embed testdata
jwkTestVectors embed.FS
jwkVectors = []string{P256Vector, X25519Vector}
)

// from https://github.com/quartzjer/did-jwk/blob/main/spec.md#examples
func TestDIDJWKVectors(t *testing.T) {
t.Run("P-256", func(tt *testing.T) {
did := "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9"
didJWK := DIDJWK(did)
valid := didJWK.IsValid()
assert.True(tt, valid)

gotTestVector, err := getTestVector(P256Vector)
assert.NoError(t, err)
var didDoc Document
err = json.Unmarshal([]byte(gotTestVector), &didDoc)
assert.NoError(tt, err)

ourDID, err := didJWK.Expand()
assert.NoError(tt, err)

// turn into json and compare
ourDIDJSON, err := json.Marshal(ourDID)
assert.NoError(tt, err)
didDocJSON, err := json.Marshal(didDoc)
assert.NoError(tt, err)
assert.JSONEq(tt, string(ourDIDJSON), string(didDocJSON))
})

t.Run("X25519", func(tt *testing.T) {
did := "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9"
didJWK := DIDJWK(did)
valid := didJWK.IsValid()
assert.True(tt, valid)

gotTestVector, err := getTestVector(X25519Vector)
assert.NoError(t, err)
var didDoc Document
err = json.Unmarshal([]byte(gotTestVector), &didDoc)
assert.NoError(tt, err)

ourDID, err := didJWK.Expand()
assert.NoError(tt, err)

// turn into json and compare
ourDIDJSON, err := json.Marshal(ourDID)
assert.NoError(tt, err)
didDocJSON, err := json.Marshal(didDoc)
assert.NoError(tt, err)

assert.JSONEq(tt, string(ourDIDJSON), string(didDocJSON))
})
}

func TestGenerateDIDJWK(t *testing.T) {
tests := []struct {
name string
keyType crypto.KeyType
expectErr bool
}{
{
name: "Ed25519",
keyType: crypto.Ed25519,
expectErr: false,
},
{
name: "x25519",
keyType: crypto.X25519,
expectErr: false,
},
{
name: "SECP256k1",
keyType: crypto.SECP256k1,
expectErr: false,
},
{
name: "P256",
keyType: crypto.P256,
expectErr: false,
},
{
name: "P384",
keyType: crypto.P384,
expectErr: false,
},
{
name: "P521",
keyType: crypto.P521,
expectErr: false,
},
{
name: "RSA",
keyType: crypto.RSA,
expectErr: false,
},
{
name: "Unsupported",
keyType: crypto.KeyType("unsupported"),
expectErr: true,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
privKey, didJWK, err := GenerateDIDJWK(test.keyType)

if test.expectErr {
assert.Error(t, err)
return
}

jsonWebKey, err := cryptosuite.JSONWebKey2020FromPrivateKey(privKey)
assert.NoError(t, err)
assert.NotEmpty(t, jsonWebKey)

assert.NoError(t, err)
assert.NotNil(t, didJWK)
assert.NotEmpty(t, privKey)

assert.True(t, strings.Contains(string(*didJWK), "did:jwk"))
})
}
}

func TestExpandDIDJWK(t *testing.T) {
t.Run("happy path", func(t *testing.T) {
pk, sk, err := crypto.GenerateEd25519Key()
assert.NoError(t, err)
assert.NotEmpty(t, pk)
assert.NotEmpty(t, sk)

gotJWK, err := jwk.FromRaw(pk)
assert.NoError(t, err)

didJWK, err := CreateDIDJWK(gotJWK)
assert.NoError(t, err)
assert.NotEmpty(t, didJWK)

doc, err := didJWK.Expand()
assert.NoError(t, err)
assert.NotEmpty(t, doc)
assert.NoError(t, doc.IsValid())
})

t.Run("bad DID returns error", func(t *testing.T) {
badDID := DIDJWK("bad")
_, err := badDID.Expand()
assert.Error(t, err)
assert.Contains(t, err.Error(), "not a did:jwk DID, invalid prefix: bad")
})

t.Run("DID but not a valid did:jwk", func(t *testing.T) {
badDID := DIDJWK("did:jwk:bad")
_, err := badDID.Expand()
assert.Error(t, err)
assert.Contains(t, err.Error(), "unmarshalling did:jwk")
})
}

func TestGenerateAndResolveDIDJWK(t *testing.T) {
resolvers := []Resolver{JWKResolver{}}
resolver, _ := NewResolver(resolvers...)

for _, kt := range GetSupportedDIDJWKTypes() {
_, didJWK, err := GenerateDIDJWK(kt)
assert.NoError(t, err)

doc, err := resolver.Resolve(context.Background(), didJWK.String())
assert.NoError(t, err)
assert.NotEmpty(t, doc)
assert.Equal(t, didJWK.String(), doc.Document.ID)
}
}
Loading

0 comments on commit 804491d

Please sign in to comment.