This repository has been archived by the owner on Dec 12, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 55
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
1 parent
d5c8f79
commit 804491d
Showing
7 changed files
with
426 additions
and
6 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
Oops, something went wrong.