Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement did-pkh #168

Merged
merged 8 commits into from
Aug 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions did/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ type VerificationMethod struct {
PublicKeyJWK *cryptosuite.PublicKeyJWK `json:"publicKeyJwk,omitempty" validate:"omitempty,dive"`
// https://datatracker.ietf.org/doc/html/draft-multiformats-multibase-03
PublicKeyMultibase string `json:"publicKeyMultibase,omitempty"`
// for PKH DIDs - https://github.com/w3c-ccg/did-pkh/blob/90b28ad3c18d63822a8aab3c752302aa64fc9382/did-pkh-method-draft.md
BlockchainAccountID string `json:"blockchainAccountId,omitempty"`
}

// VerificationMethodSet is a union type supporting the `authentication`, `assertionMethod`, `keyAgreement`,
Expand Down
177 changes: 177 additions & 0 deletions did/pkh.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package did

import (
"fmt"
"github.com/TBD54566975/ssi-sdk/cryptosuite"
"github.com/TBD54566975/ssi-sdk/schema"
"github.com/TBD54566975/ssi-sdk/util"
"github.com/goccy/go-json"
"regexp"
"strings"
)

type (
DIDPKH string
Network string
)

const (
// DIDPKHPrefix did:pkh prefix
DIDPKHPrefix = "did:pkh"
pkhContextFilename = "did-pkh-context.json"
)

const (
Bitcoin Network = "Bitcoin"
Ethereum Network = "Ethereum"
Polygon Network = "Polygon"
)

var didPKHNetworkPrefixMap = map[Network][]string{
Bitcoin: {"bip122:000000000019d6689c085ae165831e93", "EcdsaSecp256k1RecoveryMethod2020"},
Ethereum: {"eip155:1", "EcdsaSecp256k1RecoveryMethod2020"},
Polygon: {"eip155:137", "EcdsaSecp256k1RecoveryMethod2020"},
}

// The following context should be manually inserted into each DID Document. This will likely change
// over time as new verification methods are supported, and general-purpose methods are specified.
var knownDIDPKHContext, _ = schema.GetKnownSchema(pkhContextFilename)

// CreateDIDPKHFromNetwork constructs a did:pkh from a network and the networks native address.
func CreateDIDPKHFromNetwork(network Network, address string) (*DIDPKH, error) {
if _, ok := didPKHNetworkPrefixMap[network]; ok {
split := strings.Split(didPKHNetworkPrefixMap[network][0], ":")
return CreateDIDPKH(split[0], split[1], address)
}

return nil, util.LoggingNewError(fmt.Sprintf("unsupported network: %s", string(network)))
}

// CreateDIDPKH constructs a did:pkh from a namespace, reference, and account address.
// Reference: did:pkh:namespace:reference:account_address
func CreateDIDPKH(namespace, reference, address string) (*DIDPKH, error) {
did := DIDPKH(fmt.Sprintf("%s:%s:%s:%s", DIDPKHPrefix, namespace, reference, address))

if !IsValidPKH(did) {
return nil, util.LoggingNewError(fmt.Sprintf("Pkh DID is not valid: %s", string(did)))
}

return &did, nil
}

// Parse returns the value without the `did:pkh` prefix
func (did DIDPKH) Parse() string {
split := strings.Split(string(did), DIDPKHPrefix+":")
if len(split) != 2 {
return ""
}
return split[1]
}

// GetNetwork returns the network by finding the network prefix in the did
func GetNetwork(didpkh DIDPKH) (*Network, error) {
for network, prefix := range didPKHNetworkPrefixMap {
if strings.Contains(string(didpkh), prefix[0]+":") {
return &network, nil
}
}

return nil, util.LoggingNewError("network not supported")
}

// Expand turns the DID key into a complaint DID Document
func (did DIDPKH) Expand() (*DIDDocument, error) {
verificationMethod, err := constructPKHVerificationMethod(did)

if err != nil {
return nil, util.LoggingErrorMsg(err, "could not construct verification method")
}

var knownDIDPKHContextJSON interface{}
if err := json.Unmarshal([]byte(knownDIDPKHContext), &knownDIDPKHContextJSON); err != nil {
return nil, util.LoggingErrorMsg(err, "could not unmarshal known context json")
}

verificationMethodSet := []VerificationMethodSet{
string(did) + "#blockchainAccountId",
}

return &DIDDocument{
Context: knownDIDPKHContextJSON,
ID: string(did),
VerificationMethod: []VerificationMethod{*verificationMethod},
Authentication: verificationMethodSet,
AssertionMethod: verificationMethodSet,
CapabilityDelegation: verificationMethodSet,
CapabilityInvocation: verificationMethodSet,
}, nil
}

func constructPKHVerificationMethod(did DIDPKH) (*VerificationMethod, error) {
if !IsValidPKH(did) || did.Parse() == "" {
return nil, util.LoggingNewError("Pkh DID is not valid")
}

network, err := GetNetwork(did)
if err != nil {
return nil, util.LoggingErrorMsg(err, "could not find network")
}
verificationType := didPKHNetworkPrefixMap[*network][1]

return &VerificationMethod{
ID: string(did) + "#blockchainAccountId",
Type: cryptosuite.LDKeyType(verificationType),
Controller: string(did),
BlockchainAccountID: did.Parse(),
}, nil
}

// IsValidPKH checks if a pkh did is valid based on the following parameters:

// pkh-did = "did:pkh:" address
// address = account_id according to [CAIP-10]

// account_id: chain_id + ":" + account_address
// chain_id: [-a-z0-9]{3,8}:[-a-zA-Z0-9]{1,32}
// account_address: [a-zA-Z0-9]{1,64}

// chain_id: namespace + ":" + reference
// namespace: [-a-z0-9]{3,8}
// reference: [-a-zA-Z0-9]{1,32}
func IsValidPKH(did DIDPKH) bool {
split := strings.Split(string(did), ":")

if len(split) != 5 || (split[0]+":"+split[1]) != DIDPKHPrefix {
return false
}

// namespace
matched, err := regexp.MatchString(`[-a-z0-9]{3,8}`, split[2])
if !matched || err != nil {
return false
}

// reference
matched, err = regexp.MatchString(`[-a-zA-Z0-9]{1,32}`, split[3])
if !matched || err != nil {
return false
}

// account_address
matched, err = regexp.MatchString(`[a-zA-Z0-9]{1,64}`, split[4])
if !matched || err != nil {
return false
}

return true
}

func GetSupportedNetworks() []Network {
var networks []Network

for network := range didPKHNetworkPrefixMap {
networks = append(networks, network)
}

return networks
}
132 changes: 132 additions & 0 deletions did/pkh_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package did

import (
"embed"
"github.com/stretchr/testify/assert"
"strings"
"testing"

"github.com/goccy/go-json"
)

const (
testDataDirectory = "testdata"
)

var (
//go:embed testdata
testVectorPKHDIDFS embed.FS
)

var PKHTestVectors = map[Network][]string{
Bitcoin: {"bip122:000000000019d6689c085ae165831e93", "did-pkh-bitcoin-doc.json"},
Ethereum: {"eip155:1", "did-pkh-ethereum-doc.json"},
Polygon: {"eip155:137", "did-pkh-polygon-doc.json"},
}

func TestDIDPKHVectors(t *testing.T) {
// round trip serialize and de-serialize from json to our object model
for network, tv := range PKHTestVectors {
gotTestVector, err := testVectorPKHDIDFS.ReadFile(testDataDirectory + "/" + tv[1])
assert.NoError(t, err)

// Test Known DIDPKH
var knownDIDPKH DIDDocument
err = json.Unmarshal([]byte(gotTestVector), &knownDIDPKH)

assert.NoError(t, err)
assert.NoError(t, knownDIDPKH.IsValid())
assert.False(t, knownDIDPKH.IsEmpty())

knownDIDBytes, err := json.Marshal(knownDIDPKH)
assert.NoError(t, err)
assert.JSONEqf(t, string(gotTestVector), string(knownDIDBytes), "Known DID Serializtion error")

// Test Create DIDPKH With same ID as KnownDIDPKH
split := strings.Split(knownDIDPKH.ID, ":")
address := split[len(split)-1]
testDIDPKH, err := CreateDIDPKHFromNetwork(network, address)
assert.NoError(t, err)
assert.NotEmpty(t, testDIDPKH)

testDIDPKHDoc, err := testDIDPKH.Expand()

assert.NoError(t, err)
assert.NotEmpty(t, testDIDPKHDoc)
assert.Equal(t, string(*testDIDPKH), testDIDPKHDoc.ID)

// Compare Known and Testing DIDPKH Document. This compares the known PKH DID Document with the one we generate
generatedDIDBytes, err := json.Marshal(testDIDPKHDoc)
assert.NoError(t, err)
assert.JSONEqf(t, string(generatedDIDBytes), string(knownDIDBytes), "Generated DIDPKH does not match known DIDPKH")
}
}

func TestCreateDIDPKH(t *testing.T) {
address := "0xb9c5714089478a327f09197987f16f9e5d936e8a"
didPKH, err := CreateDIDPKHFromNetwork(Ethereum, address)
assert.NoError(t, err)
assert.NotEmpty(t, didPKH)
assert.Equal(t, string(*didPKH), "did:pkh:eip155:1:"+address)

didDoc, err := didPKH.Expand()

assert.NoError(t, err)
assert.NotEmpty(t, didDoc)
assert.Equal(t, string(*didPKH), didDoc.ID)

generatedDIDDocBytes, err := json.Marshal(didDoc)
assert.NoError(t, err)

testVectorDIDDoc, err := testVectorPKHDIDFS.ReadFile(testDataDirectory + "/" + PKHTestVectors[Ethereum][1])
assert.NoError(t, err)

var expandedTestDIDDoc DIDDocument
json.Unmarshal([]byte(testVectorDIDDoc), &expandedTestDIDDoc)
expandedTestDIDDocBytes, err := json.Marshal(expandedTestDIDDoc)
assert.NoError(t, err)

assert.Equal(t, string(generatedDIDDocBytes), string(expandedTestDIDDocBytes))
}

func TestIsValidPKH(t *testing.T) {
// Bitcoin
assert.True(t, IsValidPKH("did:pkh:bip122:000000000019d6689c085ae165831e93:128Lkh3S7CkDTBZ8W7BbpsN3YYizJMp8p6"))
// Dogecoin
assert.True(t, IsValidPKH("did:pkh:bip122:1a91e3dace36e2be3bf030a65679fe82:DH5yaieqoZN36fDVciNyRueRGvGLR3mr7L"))
// Ethereum
assert.True(t, IsValidPKH("did:pkh:eip155:1:0xb9c5714089478a327f09197987f16f9e5d936e8a"))
// Solana
assert.True(t, IsValidPKH("did:pkh:solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ:CKg5d12Jhpej1JqtmxLJgaFqqeYjxgPqToJ4LBdvG9Ev"))

// Invalid DIDs
assert.False(t, IsValidPKH(""))
assert.False(t, IsValidPKH("did:pkh::"))
assert.False(t, IsValidPKH("did:pkh:eip155:1:"))
assert.False(t, IsValidPKH("did:pkh:NOCAP:1:0xb9c5714089478a327f09197987f16f9e5d936e8a"))
}

func TestGetNetwork(t *testing.T) {
for network := range PKHTestVectors {
didPKH, err := CreateDIDPKHFromNetwork(network, "dummyaddress")
assert.NoError(t, err)

ntwrk, err := GetNetwork(*didPKH)
assert.NoError(t, err)

assert.Equal(t, network, *ntwrk)
}
}

func TestGetSupportedNetworks(t *testing.T) {
supportedNetworks := GetSupportedNetworks()

supportedNetworksSet := make(map[Network]bool)
for i := range supportedNetworks {
supportedNetworksSet[supportedNetworks[i]] = true
}

for network := range PKHTestVectors {
assert.True(t, supportedNetworksSet[network])
}
}
38 changes: 38 additions & 0 deletions did/testdata/did-pkh-bitcoin-doc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"@context": [
"https://www.w3.org/ns/did/v1",
{
"blockchainAccountId": "https://w3id.org/security#blockchainAccountId",
"publicKeyJwk": {
"@id": "https://w3id.org/security#publicKeyJwk",
"@type": "@json"
},
"Ed25519VerificationKey2018": "https://w3id.org/security#Ed25519VerificationKey2018",
"Ed25519PublicKeyBLAKE2BDigestSize20Base58CheckEncoded2021": "https://w3id.org/security#Ed25519PublicKeyBLAKE2BDigestSize20Base58CheckEncoded2021",
"P256PublicKeyBLAKE2BDigestSize20Base58CheckEncoded2021": "https://w3id.org/security#P256PublicKeyBLAKE2BDigestSize20Base58CheckEncoded2021",
"TezosMethod2021": "https://w3id.org/security#TezosMethod2021",
"EcdsaSecp256k1RecoveryMethod2020": "https://identity.foundation/EcdsaSecp256k1RecoverySignature2020#EcdsaSecp256k1RecoveryMethod2020"
}
],
"id": "did:pkh:bip122:000000000019d6689c085ae165831e93:128Lkh3S7CkDTBZ8W7BbpsN3YYizJMp8p6",
"verificationMethod": [
{
"id": "did:pkh:bip122:000000000019d6689c085ae165831e93:128Lkh3S7CkDTBZ8W7BbpsN3YYizJMp8p6#blockchainAccountId",
"type": "EcdsaSecp256k1RecoveryMethod2020",
"controller": "did:pkh:bip122:000000000019d6689c085ae165831e93:128Lkh3S7CkDTBZ8W7BbpsN3YYizJMp8p6",
"blockchainAccountId": "bip122:000000000019d6689c085ae165831e93:128Lkh3S7CkDTBZ8W7BbpsN3YYizJMp8p6"
}
],
"authentication": [
"did:pkh:bip122:000000000019d6689c085ae165831e93:128Lkh3S7CkDTBZ8W7BbpsN3YYizJMp8p6#blockchainAccountId"
],
"assertionMethod": [
"did:pkh:bip122:000000000019d6689c085ae165831e93:128Lkh3S7CkDTBZ8W7BbpsN3YYizJMp8p6#blockchainAccountId"
],
"capabilityDelegation": [
"did:pkh:bip122:000000000019d6689c085ae165831e93:128Lkh3S7CkDTBZ8W7BbpsN3YYizJMp8p6#blockchainAccountId"
],
"capabilityInvocation": [
"did:pkh:bip122:000000000019d6689c085ae165831e93:128Lkh3S7CkDTBZ8W7BbpsN3YYizJMp8p6#blockchainAccountId"
]
}
38 changes: 38 additions & 0 deletions did/testdata/did-pkh-ethereum-doc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"@context": [
"https://www.w3.org/ns/did/v1",
{
"blockchainAccountId": "https://w3id.org/security#blockchainAccountId",
"publicKeyJwk": {
"@id": "https://w3id.org/security#publicKeyJwk",
"@type": "@json"
},
"Ed25519VerificationKey2018": "https://w3id.org/security#Ed25519VerificationKey2018",
"Ed25519PublicKeyBLAKE2BDigestSize20Base58CheckEncoded2021": "https://w3id.org/security#Ed25519PublicKeyBLAKE2BDigestSize20Base58CheckEncoded2021",
"P256PublicKeyBLAKE2BDigestSize20Base58CheckEncoded2021": "https://w3id.org/security#P256PublicKeyBLAKE2BDigestSize20Base58CheckEncoded2021",
"TezosMethod2021": "https://w3id.org/security#TezosMethod2021",
"EcdsaSecp256k1RecoveryMethod2020": "https://identity.foundation/EcdsaSecp256k1RecoverySignature2020#EcdsaSecp256k1RecoveryMethod2020"
}
],
"id": "did:pkh:eip155:1:0xb9c5714089478a327f09197987f16f9e5d936e8a",
"verificationMethod": [
{
"id": "did:pkh:eip155:1:0xb9c5714089478a327f09197987f16f9e5d936e8a#blockchainAccountId",
"type": "EcdsaSecp256k1RecoveryMethod2020",
"controller": "did:pkh:eip155:1:0xb9c5714089478a327f09197987f16f9e5d936e8a",
"blockchainAccountId": "eip155:1:0xb9c5714089478a327f09197987f16f9e5d936e8a"
}
],
"authentication": [
"did:pkh:eip155:1:0xb9c5714089478a327f09197987f16f9e5d936e8a#blockchainAccountId"
],
"assertionMethod": [
"did:pkh:eip155:1:0xb9c5714089478a327f09197987f16f9e5d936e8a#blockchainAccountId"
],
"capabilityDelegation": [
"did:pkh:eip155:1:0xb9c5714089478a327f09197987f16f9e5d936e8a#blockchainAccountId"
],
"capabilityInvocation": [
"did:pkh:eip155:1:0xb9c5714089478a327f09197987f16f9e5d936e8a#blockchainAccountId"
]
}
Loading