From f792c43e7b284dc419046296d218aed935114548 Mon Sep 17 00:00:00 2001 From: Gabe <7622243+decentralgabe@users.noreply.github.com> Date: Tue, 2 Apr 2024 16:22:51 -0700 Subject: [PATCH] add new vector; update code for vectors besides gateways (#161) * add new vector; update code for vectors besides gateways * update vectors and spec error; make tests more robust * add gateway support --- impl/concurrencytest/main.go | 2 +- impl/go.mod | 2 +- impl/go.sum | 8 +- impl/integrationtest/main.go | 5 +- impl/internal/did/client.go | 16 +- impl/internal/did/client_test.go | 36 +-- impl/internal/did/did.go | 184 +++++++++++--- impl/internal/did/did_test.go | 229 +++++++++++++++--- .../did/testdata/vector-1-did-document.json | 2 +- .../did/testdata/vector-1-dns-records.json | 12 +- .../testdata/vector-1-public-key-jwk-1.json | 2 +- .../did/testdata/vector-2-did-document.json | 2 +- .../did/testdata/vector-2-dns-records.json | 39 ++- .../did/testdata/vector-3-did-document.json | 44 ++++ .../did/testdata/vector-3-dns-records.json | 26 ++ .../testdata/vector-3-public-key-jwk-1.json | 7 + .../testdata/vector-3-public-key-jwk-2.json | 7 + impl/internal/did/testdata_test.go | 5 + impl/pkg/dht/pkarr_test.go | 7 +- impl/pkg/pkarr/record_test.go | 7 +- impl/pkg/server/pkarr_test.go | 2 +- impl/pkg/service/pkarr_test.go | 8 +- impl/pkg/storage/db/bolt/bolt_test.go | 9 +- impl/pkg/storage/db/postgres/postgres_test.go | 6 +- spec/spec.md | 99 +++++++- 25 files changed, 627 insertions(+), 139 deletions(-) create mode 100644 impl/internal/did/testdata/vector-3-did-document.json create mode 100644 impl/internal/did/testdata/vector-3-dns-records.json create mode 100644 impl/internal/did/testdata/vector-3-public-key-jwk-1.json create mode 100644 impl/internal/did/testdata/vector-3-public-key-jwk-2.json diff --git a/impl/concurrencytest/main.go b/impl/concurrencytest/main.go index 8c8e53f9..74933928 100644 --- a/impl/concurrencytest/main.go +++ b/impl/concurrencytest/main.go @@ -114,7 +114,7 @@ func generateDIDPutRequest() (string, []byte, error) { return "", nil, err } - packet, err := did.DHT(doc.ID).ToDNSPacket(*doc, nil) + packet, err := did.DHT(doc.ID).ToDNSPacket(*doc, nil, nil) if err != nil { return "", nil, err } diff --git a/impl/go.mod b/impl/go.mod index 3b3e0e05..13658e50 100644 --- a/impl/go.mod +++ b/impl/go.mod @@ -4,7 +4,7 @@ go 1.22 require ( github.com/BurntSushi/toml v1.3.2 - github.com/TBD54566975/ssi-sdk v0.0.4-alpha.0.20240321215515-97ccd06a631d + github.com/TBD54566975/ssi-sdk v0.0.4-alpha.0.20240402005820-2c6b20991baa github.com/allegro/bigcache/v3 v3.1.0 github.com/anacrolix/dht/v2 v2.21.1 github.com/anacrolix/log v0.15.2 diff --git a/impl/go.sum b/impl/go.sum index 0bf2a8ad..8c461a06 100644 --- a/impl/go.sum +++ b/impl/go.sum @@ -24,8 +24,12 @@ github.com/RoaringBitmap/roaring v0.4.17/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrX github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= -github.com/TBD54566975/ssi-sdk v0.0.4-alpha.0.20240321215515-97ccd06a631d h1:lEekCCpwjxtQBNNUoUmPiDg35t3quQzDgtetug5xbx4= -github.com/TBD54566975/ssi-sdk v0.0.4-alpha.0.20240321215515-97ccd06a631d/go.mod h1:UoNlAhXuPb1VxsAkNbLyr4XYeyHhLvcwSbkmsaOeGjM= +github.com/TBD54566975/ssi-sdk v0.0.4-alpha.0.20240401231222-6550aeed8a9d h1:M/dYJOKyLNBDh97VYAyLvzO7sS01K1eIBwNcAK2SFDQ= +github.com/TBD54566975/ssi-sdk v0.0.4-alpha.0.20240401231222-6550aeed8a9d/go.mod h1:nyTjplXnrari2nQg63ztI4C0rgMb7Jjn3gfn0OM656g= +github.com/TBD54566975/ssi-sdk v0.0.4-alpha.0.20240402005210-f282aecaa186 h1:XIaS0WBSrg2wU00Cx45NO9H3x3ca7WEI35sSef6NC5c= +github.com/TBD54566975/ssi-sdk v0.0.4-alpha.0.20240402005210-f282aecaa186/go.mod h1:nyTjplXnrari2nQg63ztI4C0rgMb7Jjn3gfn0OM656g= +github.com/TBD54566975/ssi-sdk v0.0.4-alpha.0.20240402005820-2c6b20991baa h1:1kJozfMxe8fRI0jjKUbKhj7/o16d1oTDOfZJLOMTU28= +github.com/TBD54566975/ssi-sdk v0.0.4-alpha.0.20240402005820-2c6b20991baa/go.mod h1:nyTjplXnrari2nQg63ztI4C0rgMb7Jjn3gfn0OM656g= github.com/alecthomas/assert/v2 v2.0.0-alpha3 h1:pcHeMvQ3OMstAWgaeaXIAL8uzB9xMm2zlxt+/4ml8lk= github.com/alecthomas/assert/v2 v2.0.0-alpha3/go.mod h1:+zD0lmDXTeQj7TgDgCt0ePWxb0hMC1G+PGTsTCv1B9o= github.com/alecthomas/atomic v0.1.0-alpha2 h1:dqwXmax66gXvHhsOS4pGPZKqYOlTkapELkLb3MNdlH8= diff --git a/impl/integrationtest/main.go b/impl/integrationtest/main.go index 55cb4bde..954fa3f5 100644 --- a/impl/integrationtest/main.go +++ b/impl/integrationtest/main.go @@ -11,9 +11,10 @@ import ( "os/signal" "time" + "github.com/sirupsen/logrus" + "github.com/TBD54566975/did-dht-method/internal/did" "github.com/TBD54566975/did-dht-method/pkg/dht" - "github.com/sirupsen/logrus" ) var ( @@ -97,7 +98,7 @@ func generateDIDPutRequest() (string, []byte, error) { return "", nil, err } - packet, err := did.DHT(doc.ID).ToDNSPacket(*doc, nil) + packet, err := did.DHT(doc.ID).ToDNSPacket(*doc, nil, nil) if err != nil { return "", nil, err } diff --git a/impl/internal/did/client.go b/impl/internal/did/client.go index e5a71626..84ac209d 100644 --- a/impl/internal/did/client.go +++ b/impl/internal/did/client.go @@ -30,31 +30,31 @@ func NewGatewayClient(gatewayURL string) (*GatewayClient, error) { }, nil } -// GetDIDDocument gets a DID document and its types from a did:dht Gateway -func (c *GatewayClient) GetDIDDocument(id string) (*did.Document, []TypeIndex, error) { +// GetDIDDocument gets a DID document, its types, and authoritative gateways, from a did:dht Gateway +func (c *GatewayClient) GetDIDDocument(id string) (*did.Document, []TypeIndex, []AuthoritativeGateway, error) { d := DHT(id) if !d.IsValid() { - return nil, nil, errors.New("invalid did") + return nil, nil, nil, errors.New("invalid did") } suffix, err := d.Suffix() if err != nil { - return nil, nil, errors.Wrap(err, "failed to get suffix") + return nil, nil, nil, errors.Wrap(err, "failed to get suffix") } resp, err := http.Get(c.gatewayURL + "/" + suffix) if err != nil { - return nil, nil, errors.Wrap(err, "failed to get did document") + return nil, nil, nil, errors.Wrap(err, "failed to get did document") } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return nil, nil, errors.Errorf("failed to get did document, status code: %d", resp.StatusCode) + return nil, nil, nil, errors.Errorf("failed to get did document, status code: %d", resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { - return nil, nil, errors.Wrap(err, "failed to read response body") + return nil, nil, nil, errors.Wrap(err, "failed to read response body") } msg := new(dns.Msg) if err = msg.Unpack(body[72:]); err != nil { - return nil, nil, errors.Wrap(err, "failed to unpack records") + return nil, nil, nil, errors.Wrap(err, "failed to unpack records") } return d.FromDNSPacket(msg) } diff --git a/impl/internal/did/client_test.go b/impl/internal/did/client_test.go index ffc52a70..741b4882 100644 --- a/impl/internal/did/client_test.go +++ b/impl/internal/did/client_test.go @@ -23,7 +23,7 @@ func TestClient(t *testing.T) { require.NoError(t, err) require.NotEmpty(t, doc) - packet, err := DHT(doc.ID).ToDNSPacket(*doc, nil) + packet, err := DHT(doc.ID).ToDNSPacket(*doc, nil, nil) assert.NoError(t, err) assert.NotEmpty(t, packet) @@ -34,7 +34,7 @@ func TestClient(t *testing.T) { err = client.PutDocument(doc.ID, *bep44Put) assert.NoError(t, err) - gotDID, _, err := client.GetDIDDocument(doc.ID) + gotDID, _, _, err := client.GetDIDDocument(doc.ID) assert.NoError(t, err) assert.EqualValues(t, doc, gotDID) @@ -51,31 +51,35 @@ func TestClientInvalidGateway(t *testing.T) { func TestInvalidDIDDocument(t *testing.T) { client, err := NewGatewayClient("https://diddht.tbddev.test") require.NoError(t, err) - require.NotNil(t, client) + require.NotEmpty(t, client) - did, ty, err := client.GetDIDDocument("this is not a valid did") + did, types, gateways, err := client.GetDIDDocument("this is not a valid did") assert.Error(t, err) - assert.Nil(t, ty) - assert.Nil(t, did) + assert.Empty(t, did) + assert.Empty(t, types) + assert.Empty(t, gateways) - did, ty, err = client.GetDIDDocument("did:dht:example") + did, types, gateways, err = client.GetDIDDocument("did:dht:example") assert.EqualError(t, err, "invalid did") - assert.Nil(t, ty) - assert.Nil(t, did) + assert.Empty(t, did) + assert.Empty(t, types) + assert.Empty(t, gateways) - did, ty, err = client.GetDIDDocument("did:dht:i9xkp8ddcbcg8jwq54ox699wuzxyifsqx4jru45zodqu453ksz6y") + did, types, gateways, err = client.GetDIDDocument("did:dht:i9xkp8ddcbcg8jwq54ox699wuzxyifsqx4jru45zodqu453ksz6y") assert.Error(t, err) // this should error because the gateway URL is invalid - assert.Nil(t, ty) - assert.Nil(t, did) + assert.Empty(t, did) + assert.Empty(t, types) + assert.Empty(t, gateways) client, err = NewGatewayClient("https://tbd.website") require.NoError(t, err) - require.NotNil(t, client) + require.NotEmpty(t, client) - did, ty, err = client.GetDIDDocument("did:dht:i9xkp8ddcbcg8jwq54ox699wuzxyifsqx4jru45zodqu453ksz6y") + did, types, gateways, err = client.GetDIDDocument("did:dht:i9xkp8ddcbcg8jwq54ox699wuzxyifsqx4jru45zodqu453ksz6y") assert.Error(t, err) // this should error because the gateway URL will return a non-200 - assert.Nil(t, ty) - assert.Nil(t, did) + assert.Empty(t, did) + assert.Empty(t, types) + assert.Empty(t, gateways) err = client.PutDocument("did:dht:example", bep44.Put{}) assert.Error(t, err) diff --git a/impl/internal/did/did.go b/impl/internal/did/did.go index 8470d79c..f75e8a37 100644 --- a/impl/internal/did/did.go +++ b/impl/internal/did/did.go @@ -11,20 +11,22 @@ import ( "github.com/TBD54566975/ssi-sdk/crypto/jwx" "github.com/TBD54566975/ssi-sdk/cryptosuite" "github.com/TBD54566975/ssi-sdk/did" + "github.com/lestrrat-go/jwx/v2/jwa" "github.com/miekg/dns" + "github.com/pkg/errors" "github.com/tv42/zbase32" ) type ( - DHT string - TypeIndex int + DHT string + TypeIndex int + AuthoritativeGateway string ) const ( // Prefix did:dht prefix - Prefix = "did:dht" - DHTMethod did.Method = "dht" - JSONWebKeyType cryptosuite.LDKeyType = "JsonWebKey" + Prefix = "did:dht" + DHTMethod did.Method = "dht" // Version corresponds to the version fo the specification https://did-dht.com/#dids-as-dns-records Version int = 0 @@ -65,6 +67,8 @@ func (DHT) Method() did.Method { } type CreateDIDDHTOpts struct { + // AuthoritativeGateways is a list of authoritative gateways for the DID Document + AuthoritativeGateways []string // Controller is the DID Controller, can be a list of DIDs Controller []string // AlsoKnownAs is a list of alternative identifiers for the DID Document @@ -137,7 +141,7 @@ func CreateDIDDHTDID(pubKey ed25519.PublicKey, opts CreateDIDDHTOpts) (*did.Docu if seenIDs[vm.VerificationMethod.ID] { return nil, fmt.Errorf("verification method id %s is not unique", vm.VerificationMethod.ID) } - if vm.VerificationMethod.Type != JSONWebKeyType { + if vm.VerificationMethod.Type != cryptosuite.JSONWebKeyType { return nil, fmt.Errorf("verification method type %s is not supported", vm.VerificationMethod.Type) } if vm.VerificationMethod.PublicKeyJWK == nil { @@ -204,12 +208,14 @@ func CreateDIDDHTDID(pubKey ed25519.PublicKey, opts CreateDIDDHTOpts) (*did.Docu // create the did document kid := "0" key0JWK, err := jwx.PublicKeyToPublicKeyJWK(&kid, pubKey) + // temporary workaround until https://github.com/TBD54566975/ssi-sdk/issues/520 is in place + key0JWK.ALG = string(crypto.Ed25519DSA) if err != nil { return nil, err } vm0 := did.VerificationMethod{ ID: id + "#0", - Type: JSONWebKeyType, + Type: cryptosuite.JSONWebKeyType, Controller: id, PublicKeyJWK: key0JWK, } @@ -233,11 +239,16 @@ func GetDIDDHTIdentifier(pubKey []byte) string { } // ToDNSPacket converts a DID DHT Document to a DNS packet with an optional list of types to include -func (d DHT) ToDNSPacket(doc did.Document, types []TypeIndex) (*dns.Msg, error) { +func (d DHT) ToDNSPacket(doc did.Document, types []TypeIndex, gateways []AuthoritativeGateway) (*dns.Msg, error) { var records []dns.RR var rootRecord []string keyLookup := make(map[string]string) + suffix, err := d.Suffix() + if err != nil { + return nil, errors.Wrap(err, "failed to get suffix while decoding DNS packet") + } + // first append the version to the root record rootRecord = append(rootRecord, fmt.Sprintf("v=%d", Version)) @@ -281,15 +292,29 @@ func (d DHT) ToDNSPacket(doc did.Document, types []TypeIndex) (*dns.Msg, error) records = append(records, &akaAnswer) } + // add all gateways + for _, gateway := range gateways { + gatewayAnswer := dns.TXT{ + Hdr: dns.RR_Header{ + Name: fmt.Sprintf("_did.%s.", suffix), + Rrtype: dns.TypeNS, + Class: dns.ClassINET, + Ttl: 7200, + }, + Txt: []string{string(gateway)}, + } + records = append(records, &gatewayAnswer) + } + // build all key records var vmIDs []string for i, vm := range doc.VerificationMethod { recordIdentifier := fmt.Sprintf("k%d", i) keyLookup[vm.ID] = recordIdentifier - keyType := keyTypeByAlg(crypto.SignatureAlgorithm(vm.PublicKeyJWK.ALG)) + keyType := keyTypeForJWK(*vm.PublicKeyJWK) if keyType < 0 { - return nil, fmt.Errorf("unsupported key type given alg: %s", vm.PublicKeyJWK.ALG) + return nil, fmt.Errorf("+unsupported key type given alg: %s", vm.PublicKeyJWK.ALG) } // convert the public key to a base64url encoded string @@ -307,6 +332,13 @@ func (d DHT) ToDNSPacket(doc did.Document, types []TypeIndex) (*dns.Msg, error) keyBase64URL := base64.RawURLEncoding.EncodeToString(pubKeyBytes) vmKeyFragment := vm.ID[strings.LastIndex(vm.ID, "#")+1:] txtRecord := fmt.Sprintf("id=%s;t=%d;k=%s", vmKeyFragment, keyType, keyBase64URL) + + // only include the alg if it's not the default alg for the key type + forKeyType := algIsDefaultForJWK(*vm.PublicKeyJWK) + if !forKeyType { + txtRecord += fmt.Sprintf(";a=%s", vm.PublicKeyJWK.ALG) + } + // note the controller if it differs from the DID if vm.Controller != doc.ID { // handle the case where the controller of the identity key is not the DID itself @@ -409,7 +441,7 @@ func (d DHT) ToDNSPacket(doc did.Document, types []TypeIndex) (*dns.Msg, error) // add the root record rootAnswer := dns.TXT{ Hdr: dns.RR_Header{ - Name: "_did.", + Name: fmt.Sprintf("_did.%s.", suffix), Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 7200, @@ -472,18 +504,32 @@ func parseServiceData(serviceEndpoint any) string { } // FromDNSPacket converts a DNS packet to a DID DHT Document -func (d DHT) FromDNSPacket(msg *dns.Msg) (*did.Document, []TypeIndex, error) { +// Returns the DID Document, a list of types, a list of authoritative gateways, and an error +func (d DHT) FromDNSPacket(msg *dns.Msg) (*did.Document, []TypeIndex, []AuthoritativeGateway, error) { doc := did.Document{ ID: d.String(), } + suffix, err := d.Suffix() + if err != nil { + return nil, nil, nil, errors.Wrap(err, "failed to get suffix while decoding DNS packet") + } + + // track the authoritative gateways + var gateways []AuthoritativeGateway + // track the types var types []TypeIndex keyLookup := make(map[string]string) for _, rr := range msg.Answer { switch record := rr.(type) { case *dns.TXT: if strings.HasPrefix(record.Hdr.Name, "_cnt") { - doc.Controller = strings.Split(record.Txt[0], ",") + controllers := strings.Split(record.Txt[0], ",") + if len(controllers) == 1 { + doc.Controller = controllers[0] + } else { + doc.Controller = controllers + } } if strings.HasPrefix(record.Hdr.Name, "_aka") { doc.AlsoKnownAs = strings.Split(record.Txt[0], ",") @@ -494,6 +540,7 @@ func (d DHT) FromDNSPacket(msg *dns.Msg) (*did.Document, []TypeIndex, error) { keyType := keyTypeLookUp(data["t"]) keyBase64URL := data["k"] controller := data["c"] + alg := data["a"] // set the controller to the DID if it's not provided if controller == "" { @@ -503,21 +550,32 @@ func (d DHT) FromDNSPacket(msg *dns.Msg) (*did.Document, []TypeIndex, error) { // Convert keyBase64URL back to PublicKeyJWK pubKeyBytes, err := base64.RawURLEncoding.DecodeString(keyBase64URL) if err != nil { - return nil, nil, err + return nil, nil, nil, err } // as per the spec's guidance DNS representations use compressed keys, so we must unmarshall them as such pubKey, err := crypto.BytesToPubKey(pubKeyBytes, keyType, crypto.ECDSAUnmarshalCompressed) if err != nil { - return nil, nil, err + return nil, nil, nil, err } pubKeyJWK, err := jwx.PublicKeyToPublicKeyJWK(&vmID, pubKey) if err != nil { - return nil, nil, err + return nil, nil, nil, err + } + + // set the algorithm if it's not the default for the key type + if alg == "" { + defaultAlg := defaultAlgForJWK(*pubKeyJWK) + if defaultAlg == "" { + return nil, nil, nil, fmt.Errorf("unable to provide default alg for unsupported key type: %s", keyType) + } + pubKeyJWK.ALG = defaultAlg + } else { + pubKeyJWK.ALG = alg } // make sure the controller of the identity key matches the DID if vmID == "0" && controller != d.String() { - return nil, nil, fmt.Errorf("controller of identity key must be the DID itself, instead it is: %s", controller) + return nil, nil, nil, fmt.Errorf("controller of identity key must be the DID itself, instead it is: %s", controller) } // if the verification method ID is not set, set it to the thumbprint @@ -526,12 +584,12 @@ func (d DHT) FromDNSPacket(msg *dns.Msg) (*did.Document, []TypeIndex, error) { } if vmID != "0" && pubKeyJWK.KID != vmID { - return nil, nil, fmt.Errorf("verification method JWK KID must be set to its thumbprint") + return nil, nil, nil, fmt.Errorf("verification method JWK KID must be set to its thumbprint") } vm := did.VerificationMethod{ ID: d.String() + "#" + vmID, - Type: JSONWebKeyType, + Type: cryptosuite.JSONWebKeyType, Controller: d.String(), PublicKeyJWK: pubKeyJWK, } @@ -573,17 +631,22 @@ func (d DHT) FromDNSPacket(msg *dns.Msg) (*did.Document, []TypeIndex, error) { } else if record.Hdr.Name == "_typ._did." { if record.Txt[0] == "" || len(record.Txt) != 1 { - return nil, nil, fmt.Errorf("invalid types record") + return nil, nil, nil, fmt.Errorf("invalid types record") } typesStr := strings.Split(strings.TrimPrefix(record.Txt[0], "id="), ",") for _, t := range typesStr { tInt, err := strconv.Atoi(t) if err != nil { - return nil, nil, err + return nil, nil, nil, err } types = append(types, TypeIndex(tInt)) } - } else if record.Hdr.Name == "_did." { + } else if record.Hdr.Name == fmt.Sprintf("_did.%s.", suffix) && record.Hdr.Rrtype == dns.TypeNS { + if record.Txt[0] == "" || len(record.Txt) != 1 { + return nil, nil, nil, fmt.Errorf("invalid gateways record: %s", record.String()) + } + gateways = append(gateways, AuthoritativeGateway(record.Txt[0])) + } else if record.Hdr.Name == fmt.Sprintf("_did.%s.", suffix) && record.Hdr.Rrtype == dns.TypeTXT { rootData := strings.Join(record.Txt, ";") rootItems := strings.Split(rootData, ";") @@ -600,7 +663,7 @@ func (d DHT) FromDNSPacket(msg *dns.Msg) (*did.Document, []TypeIndex, error) { switch key { case "v": if len(valueItems) != 1 || valueItems[0] != strconv.Itoa(Version) { - return nil, nil, fmt.Errorf("invalid version: %s", values) + return nil, nil, nil, fmt.Errorf("invalid version: %s", values) } seenVersion = true case "auth": @@ -626,13 +689,13 @@ func (d DHT) FromDNSPacket(msg *dns.Msg) (*did.Document, []TypeIndex, error) { } } if !seenVersion { - return nil, nil, fmt.Errorf("root record missing version identifier") + return nil, nil, nil, fmt.Errorf("root record missing version identifier") } } } } - return &doc, types, nil + return &doc, types, gateways, nil } func parseTxtData(data string) map[string]string { @@ -647,6 +710,52 @@ func parseTxtData(data string) map[string]string { return result } +// algIsDefaultForJWK returns true if the given JWK ALG is the default for the given key type +// according to the key type index https://did-dht.com/registry/#key-type-index +func algIsDefaultForJWK(jwk jwx.PublicKeyJWK) bool { + // Ed25519 : Ed25519 + if jwk.CRV == crypto.Ed25519.String() && jwk.KTY == jwa.OKP.String() { + return jwk.ALG == string(crypto.Ed25519DSA) + } + // secp256k1 : ES256K + if jwk.CRV == crypto.SECP256k1.String() && jwk.KTY == jwa.EC.String() { + return jwk.ALG == string(crypto.ES256K) + } + // P-256 : ES256 + if jwk.CRV == crypto.P256.String() && jwk.KTY == jwa.EC.String() { + return jwk.ALG == string(crypto.ES256) + } + // X25519 : ECDH-ES+A256KW + if jwk.CRV == crypto.X25519.String() && jwk.KTY == jwa.OKP.String() { + return jwk.ALG == string(crypto.ECDHESA256KW) + } + return false +} + +// defaultAlgForJWK returns the default signature algorithm for the given JWK based on the key type index +// https://did-dht.com/registry/#key-type-index +func defaultAlgForJWK(jwk jwx.PublicKeyJWK) string { + // Ed25519 : Ed25519 + if jwk.CRV == crypto.Ed25519.String() && jwk.KTY == jwa.OKP.String() { + return string(crypto.Ed25519DSA) + } + // secp256k1 : ES256K + if jwk.CRV == crypto.SECP256k1.String() && jwk.KTY == jwa.EC.String() { + return string(crypto.ES256K) + } + // P-256 : ES256 + if jwk.CRV == crypto.P256.String() && jwk.KTY == jwa.EC.String() { + return string(crypto.ES256) + } + // X25519 : ECDH-ES+A256KW + if jwk.CRV == crypto.X25519.String() && jwk.KTY == jwa.OKP.String() { + return string(crypto.ECDHESA256KW) + } + return "" +} + +// keyTypeLookUp returns the key type for the given key type index +// https://did-dht.com/registry/#key-type-index func keyTypeLookUp(keyType string) crypto.KeyType { switch keyType { case "0": @@ -655,20 +764,31 @@ func keyTypeLookUp(keyType string) crypto.KeyType { return crypto.SECP256k1 case "2": return crypto.P256 + case "3": + return crypto.X25519 default: return "" } } -func keyTypeByAlg(alg crypto.SignatureAlgorithm) int { - switch alg { - case crypto.EdDSA: +// keyTypeForJWK returns the key type index for the given JWK according to the key type index +// https://did-dht.com/registry/#key-type-index +func keyTypeForJWK(jwk jwx.PublicKeyJWK) int { + // Ed25519 : Ed25519 : 0 + if jwk.CRV == crypto.Ed25519.String() && jwk.KTY == jwa.OKP.String() { return 0 - case crypto.ES256K: + } + // secp256k1 : ES256K : 1 + if jwk.CRV == crypto.SECP256k1.String() && jwk.KTY == jwa.EC.String() { return 1 - case crypto.ES256: + } + // P-256 : ES256 : 2 + if jwk.CRV == crypto.P256.String() && jwk.KTY == jwa.EC.String() { return 2 - default: - return -1 } + // X25519 : ECDH-ES+A256KW : 3 + if jwk.CRV == crypto.X25519.String() && jwk.KTY == jwa.OKP.String() { + return 3 + } + return -1 } diff --git a/impl/internal/did/did_test.go b/impl/internal/did/did_test.go index 2bd2f431..e4f9927b 100644 --- a/impl/internal/did/did_test.go +++ b/impl/internal/did/did_test.go @@ -3,8 +3,10 @@ package did import ( "crypto/ed25519" "fmt" + "strings" "testing" + "github.com/TBD54566975/ssi-sdk/cryptosuite" "github.com/goccy/go-json" "github.com/TBD54566975/ssi-sdk/crypto" @@ -55,7 +57,7 @@ func TestGenerateDIDDHT(t *testing.T) { { VerificationMethod: did.VerificationMethod{ ID: "key1", - Type: JSONWebKeyType, + Type: cryptosuite.JSONWebKeyType, Controller: "did:dht:123456789abcdefghi", PublicKeyJWK: pubKeyJWK, }, @@ -124,14 +126,15 @@ func TestToDNSPacket(t *testing.T) { require.NotEmpty(t, doc) didID := DHT(doc.ID) - packet, err := didID.ToDNSPacket(*doc, nil) + packet, err := didID.ToDNSPacket(*doc, nil, nil) require.NoError(t, err) require.NotEmpty(t, packet) - decodedDoc, types, err := didID.FromDNSPacket(packet) + decodedDoc, types, gateways, err := didID.FromDNSPacket(packet) require.NoError(t, err) require.NotEmpty(t, decodedDoc) require.Empty(t, types) + require.Empty(t, gateways) jsonDoc, err := json.Marshal(doc) require.NoError(t, err) @@ -142,22 +145,24 @@ func TestToDNSPacket(t *testing.T) { assert.JSONEq(t, string(jsonDoc), string(jsonDecodedDoc)) }) - t.Run("doc with types - test to dns packet round trip", func(t *testing.T) { + t.Run("doc with types and a gateway - test to dns packet round trip", func(t *testing.T) { privKey, doc, err := GenerateDIDDHT(CreateDIDDHTOpts{}) require.NoError(t, err) require.NotEmpty(t, privKey) require.NotEmpty(t, doc) didID := DHT(doc.ID) - packet, err := didID.ToDNSPacket(*doc, []TypeIndex{1, 2, 3}) + packet, err := didID.ToDNSPacket(*doc, []TypeIndex{1, 2, 3}, []AuthoritativeGateway{"gateway1.example-did-dht-gateway.com."}) require.NoError(t, err) require.NotEmpty(t, packet) - decodedDoc, types, err := didID.FromDNSPacket(packet) + decodedDoc, types, gateways, err := didID.FromDNSPacket(packet) require.NoError(t, err) require.NotEmpty(t, decodedDoc) require.NotEmpty(t, types) require.Equal(t, types, []TypeIndex{1, 2, 3}) + require.NotEmpty(t, gateways) + require.Equal(t, gateways, []AuthoritativeGateway{"gateway1.example-did-dht-gateway.com."}) assert.EqualValues(t, *doc, *decodedDoc) }) @@ -173,7 +178,7 @@ func TestToDNSPacket(t *testing.T) { { VerificationMethod: did.VerificationMethod{ ID: "key1", - Type: JSONWebKeyType, + Type: cryptosuite.JSONWebKeyType, PublicKeyJWK: pubKeyJWK, }, Purposes: []did.PublicKeyPurpose{did.AssertionMethod, did.CapabilityInvocation}, @@ -200,14 +205,15 @@ func TestToDNSPacket(t *testing.T) { require.NotEmpty(t, doc) didID := DHT(doc.ID) - packet, err := didID.ToDNSPacket(*doc, nil) + packet, err := didID.ToDNSPacket(*doc, nil, nil) require.NoError(t, err) require.NotEmpty(t, packet) - decodedDoc, types, err := didID.FromDNSPacket(packet) + decodedDoc, types, gateways, err := didID.FromDNSPacket(packet) require.NoError(t, err) require.NotEmpty(t, decodedDoc) require.Empty(t, types) + require.Empty(t, gateways) decodedJSON, err := json.Marshal(decodedDoc) require.NoError(t, err) @@ -221,6 +227,7 @@ func TestToDNSPacket(t *testing.T) { func TestVectors(t *testing.T) { type testVectorDNSRecord struct { + Name string `json:"name"` RecordType string `json:"type"` TTL string `json:"ttl"` Record string `json:"rdata"` @@ -239,25 +246,58 @@ func TestVectors(t *testing.T) { var expectedDIDDocument did.Document retrieveTestVectorAs(t, vector1DIDDocument, &expectedDIDDocument) - assert.EqualValues(t, expectedDIDDocument, *doc) + + docJSON, err := json.Marshal(doc) + require.NoError(t, err) + + expectedDIDDocJSON, err := json.Marshal(expectedDIDDocument) + require.NoError(t, err) + + assert.JSONEq(t, string(expectedDIDDocJSON), string(docJSON)) didID := DHT(doc.ID) - packet, err := didID.ToDNSPacket(*doc, nil) + packet, err := didID.ToDNSPacket(*doc, nil, nil) require.NoError(t, err) require.NotEmpty(t, packet) - var expectedDNSRecords map[string]testVectorDNSRecord + var expectedDNSRecords []testVectorDNSRecord retrieveTestVectorAs(t, vector1DNSRecords, &expectedDNSRecords) + // Initialize a map to track matched records + matchedRecords := make(map[int]bool) + for i := range expectedDNSRecords { + matchedRecords[i] = false // Initialize all expected records as unmatched + } + for _, record := range packet.Answer { - expectedRecord, ok := expectedDNSRecords[record.Header().Name] - require.True(t, ok) + for i, expectedRecord := range expectedDNSRecords { + if record.Header().Name == expectedRecord.Name { + s := record.String() + if strings.Contains(s, expectedRecord.RecordType) && + strings.Contains(s, expectedRecord.TTL) && + strings.Contains(s, expectedRecord.Record) { + matchedRecords[i] = true // Mark as matched + break + } + } + } + } - s := record.String() - assert.Contains(t, s, expectedRecord.RecordType) - assert.Contains(t, s, expectedRecord.TTL) - assert.Contains(t, s, expectedRecord.Record) + // Check if all expected records have been matched + for i, matched := range matchedRecords { + require.True(t, matched, fmt.Sprintf("Expected DNS record %d: %+v not matched", i, expectedDNSRecords[i])) } + + // Make sure going back to DID Document is consistent + decodedDoc, types, gateways, err := didID.FromDNSPacket(packet) + require.NoError(t, err) + require.NotEmpty(t, decodedDoc) + require.Empty(t, types) + require.Empty(t, gateways) + + decodedDocJSON, err := json.Marshal(decodedDoc) + require.NoError(t, err) + assert.JSONEq(t, string(expectedDIDDocJSON), string(decodedDocJSON)) }) t.Run("test vector 2", func(t *testing.T) { @@ -271,13 +311,17 @@ func TestVectors(t *testing.T) { retrieveTestVectorAs(t, vector2PublicKeyJWK2, &secpJWK) doc, err := CreateDIDDHTDID(pubKey.(ed25519.PublicKey), CreateDIDDHTOpts{ + AuthoritativeGateways: []string{ + "gateway1.example-did-dht-gateway.com.", + "gateway2.example-did-dht-gateway.com.", + }, Controller: []string{"did:example:abcd"}, AlsoKnownAs: []string{"did:example:efgh", "did:example:ijkl"}, VerificationMethods: []VerificationMethod{ { VerificationMethod: did.VerificationMethod{ ID: secpJWK.KID, - Type: JSONWebKeyType, + Type: cryptosuite.JSONWebKeyType, PublicKeyJWK: &secpJWK, }, Purposes: []did.PublicKeyPurpose{did.AssertionMethod, did.CapabilityInvocation}, @@ -306,22 +350,133 @@ func TestVectors(t *testing.T) { assert.JSONEq(t, string(expectedDIDDocJSON), string(docJSON)) didID := DHT(doc.ID) - packet, err := didID.ToDNSPacket(*doc, []TypeIndex{1, 2, 3}) + packet, err := didID.ToDNSPacket(*doc, []TypeIndex{1, 2, 3}, + []AuthoritativeGateway{"gateway1.example-did-dht-gateway.com.", "gateway2.example-did-dht-gateway.com."}) require.NoError(t, err) require.NotEmpty(t, packet) - var expectedDNSRecords map[string]testVectorDNSRecord + var expectedDNSRecords []testVectorDNSRecord retrieveTestVectorAs(t, vector2DNSRecords, &expectedDNSRecords) + // Initialize a map to track matched records + matchedRecords := make(map[int]bool) + for i := range expectedDNSRecords { + matchedRecords[i] = false // Initialize all expected records as unmatched + } + + for _, record := range packet.Answer { + for i, expectedRecord := range expectedDNSRecords { + if record.Header().Name == expectedRecord.Name { + s := record.String() + if strings.Contains(s, expectedRecord.RecordType) && + strings.Contains(s, expectedRecord.TTL) && + strings.Contains(s, expectedRecord.Record) { + matchedRecords[i] = true // Mark as matched + break + } + } + } + } + + // Check if all expected records have been matched + for i, matched := range matchedRecords { + require.True(t, matched, fmt.Sprintf("Expected DNS record %d: %+v not matched", i, expectedDNSRecords[i])) + } + + // Make sure going back to DID Document is consistent + decodedDoc, types, gateways, err := didID.FromDNSPacket(packet) + require.NoError(t, err) + require.NotEmpty(t, decodedDoc) + require.NotEmpty(t, types) + require.Equal(t, types, []TypeIndex{1, 2, 3}) + require.NotEmpty(t, gateways) + require.Equal(t, gateways, []AuthoritativeGateway{"gateway1.example-did-dht-gateway.com.", "gateway2.example-did-dht-gateway.com."}) + + decodedDocJSON, err := json.Marshal(decodedDoc) + require.NoError(t, err) + assert.JSONEq(t, string(expectedDIDDocJSON), string(decodedDocJSON)) + }) + + t.Run("test vector 3", func(t *testing.T) { + var pubKeyJWK jwx.PublicKeyJWK + retrieveTestVectorAs(t, vector3PublicKeyJWK1, &pubKeyJWK) + + pubKey, err := pubKeyJWK.ToPublicKey() + require.NoError(t, err) + + var x25519JWK jwx.PublicKeyJWK + retrieveTestVectorAs(t, vector3PublicKeyJWK2, &x25519JWK) + + doc, err := CreateDIDDHTDID(pubKey.(ed25519.PublicKey), CreateDIDDHTOpts{ + VerificationMethods: []VerificationMethod{ + { + VerificationMethod: did.VerificationMethod{ + ID: x25519JWK.KID, + Type: cryptosuite.JSONWebKeyType, + PublicKeyJWK: &x25519JWK, + }, + Purposes: []did.PublicKeyPurpose{did.KeyAgreement}, + }, + }, + }) + require.NoError(t, err) + require.NotEmpty(t, doc) + + var expectedDIDDocument did.Document + retrieveTestVectorAs(t, vector3DIDDocument, &expectedDIDDocument) + + docJSON, err := json.Marshal(doc) + require.NoError(t, err) + + expectedDIDDocJSON, err := json.Marshal(expectedDIDDocument) + require.NoError(t, err) + + assert.JSONEq(t, string(expectedDIDDocJSON), string(docJSON)) + + didID := DHT(doc.ID) + packet, err := didID.ToDNSPacket(*doc, nil, []AuthoritativeGateway{"gateway1.example-did-dht-gateway.com."}) + require.NoError(t, err) + require.NotEmpty(t, packet) + + var expectedDNSRecords []testVectorDNSRecord + retrieveTestVectorAs(t, vector3DNSRecords, &expectedDNSRecords) + + // Initialize a map to track matched records + matchedRecords := make(map[int]bool) + for i := range expectedDNSRecords { + matchedRecords[i] = false // Initialize all expected records as unmatched + } + for _, record := range packet.Answer { - expectedRecord, ok := expectedDNSRecords[record.Header().Name] - require.True(t, ok, "record not found: %s", record.Header().Name) + for i, expectedRecord := range expectedDNSRecords { + if record.Header().Name == expectedRecord.Name { + s := record.String() + if strings.Contains(s, expectedRecord.RecordType) && + strings.Contains(s, expectedRecord.TTL) && + strings.Contains(s, expectedRecord.Record) { + matchedRecords[i] = true // Mark as matched + break + } + } + } + } - s := record.String() - assert.Contains(t, s, expectedRecord.RecordType) - assert.Contains(t, s, expectedRecord.TTL) - assert.Contains(t, s, expectedRecord.Record) + // Check if all expected records have been matched + for i, matched := range matchedRecords { + require.True(t, matched, fmt.Sprintf("Expected DNS record %d: %+v not matched", i, expectedDNSRecords[i])) } + + // Make sure going back to DID Document is consistent + decodedDoc, types, gateways, err := didID.FromDNSPacket(packet) + require.NoError(t, err) + require.NotEmpty(t, decodedDoc) + require.Empty(t, types) + require.NotEmpty(t, gateways) + require.Equal(t, gateways, []AuthoritativeGateway{"gateway1.example-did-dht-gateway.com."}) + + decodedDocJSON, err := json.Marshal(decodedDoc) + require.NoError(t, err) + assert.JSONEq(t, string(expectedDIDDocJSON), string(decodedDocJSON)) }) } @@ -353,7 +508,7 @@ func TestMisc(t *testing.T) { { VerificationMethod: did.VerificationMethod{ ID: secpJWK.KID, - Type: JSONWebKeyType, + Type: cryptosuite.JSONWebKeyType, PublicKeyJWK: &secpJWK, }, Purposes: []did.PublicKeyPurpose{did.AssertionMethod, did.CapabilityInvocation}, @@ -378,7 +533,7 @@ func TestMisc(t *testing.T) { VerificationMethods: []VerificationMethod{ { VerificationMethod: did.VerificationMethod{ - Type: JSONWebKeyType, + Type: cryptosuite.JSONWebKeyType, PublicKeyJWK: &secpJWK, }, Purposes: []did.PublicKeyPurpose{did.AssertionMethod, did.CapabilityInvocation}, @@ -404,7 +559,7 @@ func TestMisc(t *testing.T) { { VerificationMethod: did.VerificationMethod{ ID: secpJWK.KID, - Type: JSONWebKeyType, + Type: cryptosuite.JSONWebKeyType, PublicKeyJWK: &secpJWK, }, Purposes: []did.PublicKeyPurpose{did.AssertionMethod, did.CapabilityInvocation}, @@ -430,7 +585,7 @@ func TestMisc(t *testing.T) { { VerificationMethod: did.VerificationMethod{ ID: "#key-1", - Type: JSONWebKeyType, + Type: cryptosuite.JSONWebKeyType, PublicKeyJWK: &secpJWK, }, Purposes: []did.PublicKeyPurpose{did.AssertionMethod, did.CapabilityInvocation}, @@ -456,7 +611,7 @@ func TestMisc(t *testing.T) { { VerificationMethod: did.VerificationMethod{ ID: secpJWK.KID, - Type: JSONWebKeyType, + Type: cryptosuite.JSONWebKeyType, PublicKeyJWK: &secpJWK, }, Purposes: []did.PublicKeyPurpose{did.Authentication, did.KeyAgreement, did.CapabilityDelegation}, @@ -493,7 +648,7 @@ func TestCreationFailures(t *testing.T) { { VerificationMethod: did.VerificationMethod{ ID: "#0", - Type: JSONWebKeyType, + Type: cryptosuite.JSONWebKeyType, PublicKeyJWK: &secpJWK, }, Purposes: []did.PublicKeyPurpose{did.AssertionMethod, did.CapabilityInvocation}, @@ -518,7 +673,7 @@ func TestCreationFailures(t *testing.T) { { VerificationMethod: did.VerificationMethod{ ID: secpJWK.KID, - Type: JSONWebKeyType, + Type: cryptosuite.JSONWebKeyType, PublicKeyJWK: &secpJWK, }, Purposes: []did.PublicKeyPurpose{did.AssertionMethod, did.CapabilityInvocation}, @@ -526,7 +681,7 @@ func TestCreationFailures(t *testing.T) { { VerificationMethod: did.VerificationMethod{ ID: secpJWK.KID, - Type: JSONWebKeyType, + Type: cryptosuite.JSONWebKeyType, PublicKeyJWK: &secpJWK, }, Purposes: []did.PublicKeyPurpose{did.AssertionMethod, did.CapabilityInvocation}, @@ -578,7 +733,7 @@ func TestCreationFailures(t *testing.T) { { VerificationMethod: did.VerificationMethod{ ID: secpJWK.KID, - Type: JSONWebKeyType, + Type: cryptosuite.JSONWebKeyType, PublicKeyJWK: nil, }, Purposes: []did.PublicKeyPurpose{did.AssertionMethod, did.CapabilityInvocation}, @@ -604,7 +759,7 @@ func TestCreationFailures(t *testing.T) { { VerificationMethod: did.VerificationMethod{ ID: secpJWK.KID, - Type: JSONWebKeyType, + Type: cryptosuite.JSONWebKeyType, PublicKeyJWK: &secpJWK, }, Purposes: []did.PublicKeyPurpose{"fake purpose"}, diff --git a/impl/internal/did/testdata/vector-1-did-document.json b/impl/internal/did/testdata/vector-1-did-document.json index 1a2c863b..11361fc1 100644 --- a/impl/internal/did/testdata/vector-1-did-document.json +++ b/impl/internal/did/testdata/vector-1-did-document.json @@ -9,7 +9,7 @@ "kty": "OKP", "crv": "Ed25519", "x": "YCcHYL2sYNPDlKaALcEmll2HHyT968M4UWbr-9CFGWE", - "alg": "EdDSA", + "alg": "Ed25519", "kid": "0" } } diff --git a/impl/internal/did/testdata/vector-1-dns-records.json b/impl/internal/did/testdata/vector-1-dns-records.json index 2fc1d911..84b9703c 100644 --- a/impl/internal/did/testdata/vector-1-dns-records.json +++ b/impl/internal/did/testdata/vector-1-dns-records.json @@ -1,12 +1,14 @@ -{ - "_did.": { +[ + { + "name": "_did.cyuoqaf7itop8ohww4yn5ojg13qaq83r9zihgqntc5i9zwrfdfoo.", "type": "TXT", "ttl": "7200", - "rdata": "vm=k0;auth=k0;asm=k0;inv=k0;del=k0" + "rdata": "v=0;vm=k0;auth=k0;asm=k0;inv=k0;del=k0" }, - "_k0._did.": { + { + "name": "_k0._did.", "type": "TXT", "ttl": "7200", "rdata": "id=0;t=0;k=YCcHYL2sYNPDlKaALcEmll2HHyT968M4UWbr-9CFGWE" } -} \ No newline at end of file +] \ No newline at end of file diff --git a/impl/internal/did/testdata/vector-1-public-key-jwk-1.json b/impl/internal/did/testdata/vector-1-public-key-jwk-1.json index eae78bb9..b7869103 100644 --- a/impl/internal/did/testdata/vector-1-public-key-jwk-1.json +++ b/impl/internal/did/testdata/vector-1-public-key-jwk-1.json @@ -2,6 +2,6 @@ "kty": "OKP", "crv": "Ed25519", "x": "YCcHYL2sYNPDlKaALcEmll2HHyT968M4UWbr-9CFGWE", - "alg": "EdDSA", + "alg": "Ed25519", "kid": "0" } \ No newline at end of file diff --git a/impl/internal/did/testdata/vector-2-did-document.json b/impl/internal/did/testdata/vector-2-did-document.json index 77dacc8c..8c303a82 100644 --- a/impl/internal/did/testdata/vector-2-did-document.json +++ b/impl/internal/did/testdata/vector-2-did-document.json @@ -11,7 +11,7 @@ "kty": "OKP", "crv": "Ed25519", "x": "YCcHYL2sYNPDlKaALcEmll2HHyT968M4UWbr-9CFGWE", - "alg": "EdDSA", + "alg": "Ed25519", "kid": "0" } }, diff --git a/impl/internal/did/testdata/vector-2-dns-records.json b/impl/internal/did/testdata/vector-2-dns-records.json index 0f3d1053..f81f9f5e 100644 --- a/impl/internal/did/testdata/vector-2-dns-records.json +++ b/impl/internal/did/testdata/vector-2-dns-records.json @@ -1,37 +1,56 @@ -{ - "_did.": { +[ + { + "name": "_did.cyuoqaf7itop8ohww4yn5ojg13qaq83r9zihgqntc5i9zwrfdfoo.", + "type": "NS", + "ttl": "7200", + "rdata": "gateway1.example-did-dht-gateway.com." + }, + { + "name": "_did.cyuoqaf7itop8ohww4yn5ojg13qaq83r9zihgqntc5i9zwrfdfoo.", + "type": "NS", + "ttl": "7200", + "rdata": "gateway2.example-did-dht-gateway.com." + }, + { + "name": "_did.cyuoqaf7itop8ohww4yn5ojg13qaq83r9zihgqntc5i9zwrfdfoo.", "type": "TXT", "ttl": "7200", - "rdata": "vm=k0,k1;svc=s0;auth=k0;asm=k0,k1;inv=k0,k1;del=k0" + "rdata": "v=0;vm=k0,k1;svc=s0;auth=k0;asm=k0,k1;inv=k0,k1;del=k0" }, - "_cnt._did.": { + { + "name": "_cnt._did.", "type": "TXT", "ttl": "7200", "rdata": "did:example:abcd" }, - "_aka._did.": { + { + "name": "_aka._did.", "type": "TXT", "ttl": "7200", "rdata": "did:example:efgh,did:example:ijkl" }, - "_k0._did.": { + { + "name": "_k0._did.", "type": "TXT", "ttl": "7200", "rdata": "id=0;t=0;k=YCcHYL2sYNPDlKaALcEmll2HHyT968M4UWbr-9CFGWE" }, - "_k1._did.": { + { + "name": "_k1._did.", "type": "TXT", "ttl": "7200", "rdata": "t=1;k=Atf6NCChxjWpnrfPt1WDVE4ipYVSvi4pXCq4SUjx0jT9" }, - "_s0._did.": { + { + "name": "_s0._did.", "type": "TXT", "ttl": "7200", "rdata": "id=service-1;t=TestService;se=https://test-service.com/1,https://test-service.com/2" }, - "_typ._did.": { + { + "name": "_typ._did.", "type": "TXT", "ttl": "7200", "rdata": "id=1,2,3" } -} \ No newline at end of file +] \ No newline at end of file diff --git a/impl/internal/did/testdata/vector-3-did-document.json b/impl/internal/did/testdata/vector-3-did-document.json new file mode 100644 index 00000000..fb40938f --- /dev/null +++ b/impl/internal/did/testdata/vector-3-did-document.json @@ -0,0 +1,44 @@ +{ + "id": "did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy", + "verificationMethod": [ + { + "id": "did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy#0", + "type": "JsonWebKey", + "controller": "did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy", + "publicKeyJwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "sTyTLYw-n1NI9X-84NaCuis1wZjAA8lku6f6Et5201g", + "alg": "Ed25519", + "kid": "0" + } + }, + { + "id": "did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy#WVy5IWMa36AoyAXZDvPd5j9zxt2t-GjifDEV-DwgIdQ", + "type": "JsonWebKey", + "controller": "did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy", + "publicKeyJwk": { + "kty": "OKP", + "crv": "X25519", + "x": "3POE0_i2mGeZ2qiQCA3KcLfi1fZo0311CXFSIwt1nB4", + "alg": "ECDH-ES+A128KW", + "kid": "WVy5IWMa36AoyAXZDvPd5j9zxt2t-GjifDEV-DwgIdQ" + } + } + ], + "authentication": [ + "did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy#0" + ], + "assertionMethod": [ + "did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy#0" + ], + "keyAgreement": [ + "did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy#WVy5IWMa36AoyAXZDvPd5j9zxt2t-GjifDEV-DwgIdQ" + ], + "capabilityInvocation": [ + "did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy#0" + ], + "capabilityDelegation": [ + "did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy#0" + ] +} \ No newline at end of file diff --git a/impl/internal/did/testdata/vector-3-dns-records.json b/impl/internal/did/testdata/vector-3-dns-records.json new file mode 100644 index 00000000..7db3bb88 --- /dev/null +++ b/impl/internal/did/testdata/vector-3-dns-records.json @@ -0,0 +1,26 @@ +[ + { + "name": "_did.sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy.", + "type": "NS", + "ttl": "7200", + "rdata": "gateway1.example-did-dht-gateway.com." + }, + { + "name": "_did.sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy.", + "type": "TXT", + "ttl": "7200", + "rdata": "v=0;vm=k0,k1;auth=k0;asm=k0;agm=k1;inv=k0;del=k0" + }, + { + "name": "_k0._did.", + "type": "TXT", + "ttl": "7200", + "rdata": "id=0;t=0;k=sTyTLYw-n1NI9X-84NaCuis1wZjAA8lku6f6Et5201g" + }, + { + "name": "_k1._did.", + "type": "TXT", + "ttl": "7200", + "rdata": "id=WVy5IWMa36AoyAXZDvPd5j9zxt2t-GjifDEV-DwgIdQ;t=3;k=3POE0_i2mGeZ2qiQCA3KcLfi1fZo0311CXFSIwt1nB4;a=ECDH-ES+A128KW" + } +] \ No newline at end of file diff --git a/impl/internal/did/testdata/vector-3-public-key-jwk-1.json b/impl/internal/did/testdata/vector-3-public-key-jwk-1.json new file mode 100644 index 00000000..6c131cd8 --- /dev/null +++ b/impl/internal/did/testdata/vector-3-public-key-jwk-1.json @@ -0,0 +1,7 @@ +{ + "kty": "OKP", + "crv": "Ed25519", + "x": "sTyTLYw-n1NI9X-84NaCuis1wZjAA8lku6f6Et5201g", + "alg": "Ed25519", + "kid": "0" +} \ No newline at end of file diff --git a/impl/internal/did/testdata/vector-3-public-key-jwk-2.json b/impl/internal/did/testdata/vector-3-public-key-jwk-2.json new file mode 100644 index 00000000..fcaab691 --- /dev/null +++ b/impl/internal/did/testdata/vector-3-public-key-jwk-2.json @@ -0,0 +1,7 @@ +{ + "kty": "OKP", + "crv": "X25519", + "x": "3POE0_i2mGeZ2qiQCA3KcLfi1fZo0311CXFSIwt1nB4", + "alg": "ECDH-ES+A128KW", + "kid": "WVy5IWMa36AoyAXZDvPd5j9zxt2t-GjifDEV-DwgIdQ" +} \ No newline at end of file diff --git a/impl/internal/did/testdata_test.go b/impl/internal/did/testdata_test.go index 92d4db52..1e39a36a 100644 --- a/impl/internal/did/testdata_test.go +++ b/impl/internal/did/testdata_test.go @@ -22,6 +22,11 @@ const ( vector2PublicKeyJWK2 string = "vector-2-public-key-jwk-2.json" vector2DIDDocument string = "vector-2-did-document.json" vector2DNSRecords string = "vector-2-dns-records.json" + + vector3PublicKeyJWK1 string = "vector-3-public-key-jwk-1.json" + vector3PublicKeyJWK2 string = "vector-3-public-key-jwk-2.json" + vector3DIDDocument string = "vector-3-did-document.json" + vector3DNSRecords string = "vector-3-dns-records.json" ) func getTestData(fileName string) ([]byte, error) { diff --git a/impl/pkg/dht/pkarr_test.go b/impl/pkg/dht/pkarr_test.go index 75d96d0d..d6392335 100644 --- a/impl/pkg/dht/pkarr_test.go +++ b/impl/pkg/dht/pkarr_test.go @@ -6,6 +6,7 @@ import ( "github.com/TBD54566975/ssi-sdk/crypto" "github.com/TBD54566975/ssi-sdk/crypto/jwx" + "github.com/TBD54566975/ssi-sdk/cryptosuite" didsdk "github.com/TBD54566975/ssi-sdk/did" "github.com/miekg/dns" "github.com/stretchr/testify/assert" @@ -71,7 +72,7 @@ func TestGetPutDIDDHT(t *testing.T) { { VerificationMethod: didsdk.VerificationMethod{ ID: "key1", - Type: did.JSONWebKeyType, + Type: cryptosuite.JSONWebKeyType, Controller: "did:dht:123456789abcdefghi", PublicKeyJWK: pubKeyJWK, }, @@ -97,7 +98,7 @@ func TestGetPutDIDDHT(t *testing.T) { require.NotEmpty(t, doc) didID := did.DHT(doc.ID) - didDocPacket, err := didID.ToDNSPacket(*doc, nil) + didDocPacket, err := didID.ToDNSPacket(*doc, nil, nil) require.NoError(t, err) putReq, err := CreatePKARRPublishRequest(privKey, *didDocPacket) @@ -116,7 +117,7 @@ func TestGetPutDIDDHT(t *testing.T) { require.NotEmpty(t, gotMsg.Answer) d := did.DHT("did:dht:" + gotID) - gotDoc, _, err := d.FromDNSPacket(gotMsg) + gotDoc, _, _, err := d.FromDNSPacket(gotMsg) require.NoError(t, err) require.NotEmpty(t, gotDoc) } diff --git a/impl/pkg/pkarr/record_test.go b/impl/pkg/pkarr/record_test.go index b5e8d98b..4f53a737 100644 --- a/impl/pkg/pkarr/record_test.go +++ b/impl/pkg/pkarr/record_test.go @@ -4,11 +4,12 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/TBD54566975/did-dht-method/internal/did" "github.com/TBD54566975/did-dht-method/pkg/dht" "github.com/TBD54566975/did-dht-method/pkg/pkarr" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestNewRecord(t *testing.T) { @@ -22,7 +23,7 @@ func TestNewRecord(t *testing.T) { require.NoError(t, err) require.NotEmpty(t, doc) - packet, err := did.DHT(doc.ID).ToDNSPacket(*doc, nil) + packet, err := did.DHT(doc.ID).ToDNSPacket(*doc, nil, nil) assert.NoError(t, err) assert.NotEmpty(t, packet) diff --git a/impl/pkg/server/pkarr_test.go b/impl/pkg/server/pkarr_test.go index a18bb2d5..264fdff1 100644 --- a/impl/pkg/server/pkarr_test.go +++ b/impl/pkg/server/pkarr_test.go @@ -168,7 +168,7 @@ func generateDIDPutRequest(t *testing.T) (string, []byte) { require.NoError(t, err) require.NotEmpty(t, doc) - packet, err := did.DHT(doc.ID).ToDNSPacket(*doc, nil) + packet, err := did.DHT(doc.ID).ToDNSPacket(*doc, nil, nil) assert.NoError(t, err) assert.NotEmpty(t, packet) diff --git a/impl/pkg/service/pkarr_test.go b/impl/pkg/service/pkarr_test.go index 31aab30f..c6a29cee 100644 --- a/impl/pkg/service/pkarr_test.go +++ b/impl/pkg/service/pkarr_test.go @@ -46,7 +46,7 @@ func TestPKARRService(t *testing.T) { require.NotEmpty(t, doc) d := did.DHT(doc.ID) - packet, err := d.ToDNSPacket(*doc, nil) + packet, err := d.ToDNSPacket(*doc, nil, nil) assert.NoError(t, err) assert.NotEmpty(t, packet) @@ -73,7 +73,7 @@ func TestPKARRService(t *testing.T) { require.NotEmpty(t, doc) d := did.DHT(doc.ID) - packet, err := d.ToDNSPacket(*doc, nil) + packet, err := d.ToDNSPacket(*doc, nil, nil) assert.NoError(t, err) assert.NotEmpty(t, packet) @@ -101,7 +101,7 @@ func TestPKARRService(t *testing.T) { require.NotEmpty(t, doc) d := did.DHT(doc.ID) - packet, err := d.ToDNSPacket(*doc, nil) + packet, err := d.ToDNSPacket(*doc, nil, nil) require.NoError(t, err) require.NotEmpty(t, packet) @@ -135,7 +135,7 @@ func TestDHT(t *testing.T) { require.NoError(t, err) require.NotEmpty(t, doc) d := did.DHT(doc.ID) - packet, err := d.ToDNSPacket(*doc, nil) + packet, err := d.ToDNSPacket(*doc, nil, nil) require.NoError(t, err) require.NotEmpty(t, packet) putMsg, err := dht.CreatePKARRPublishRequest(sk, *packet) diff --git a/impl/pkg/storage/db/bolt/bolt_test.go b/impl/pkg/storage/db/bolt/bolt_test.go index 577e5359..74ae425e 100644 --- a/impl/pkg/storage/db/bolt/bolt_test.go +++ b/impl/pkg/storage/db/bolt/bolt_test.go @@ -5,10 +5,11 @@ import ( "os" "testing" + "github.com/goccy/go-json" + "github.com/TBD54566975/did-dht-method/internal/did" "github.com/TBD54566975/did-dht-method/pkg/dht" "github.com/TBD54566975/did-dht-method/pkg/pkarr" - "github.com/goccy/go-json" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -114,7 +115,7 @@ func TestReadWrite(t *testing.T) { require.NoError(t, err) require.NotEmpty(t, doc) - packet, err := did.DHT(doc.ID).ToDNSPacket(*doc, nil) + packet, err := did.DHT(doc.ID).ToDNSPacket(*doc, nil, nil) require.NoError(t, err) require.NotEmpty(t, packet) @@ -153,7 +154,7 @@ func TestDBPagination(t *testing.T) { require.NoError(t, err) require.NotEmpty(t, doc) - packet, err := did.DHT(doc.ID).ToDNSPacket(*doc, nil) + packet, err := did.DHT(doc.ID).ToDNSPacket(*doc, nil, nil) assert.NoError(t, err) assert.NotEmpty(t, packet) @@ -174,7 +175,7 @@ func TestDBPagination(t *testing.T) { require.NoError(t, err) require.NotEmpty(t, doc) - packet, err := did.DHT(doc.ID).ToDNSPacket(*doc, nil) + packet, err := did.DHT(doc.ID).ToDNSPacket(*doc, nil, nil) assert.NoError(t, err) assert.NotEmpty(t, packet) diff --git a/impl/pkg/storage/db/postgres/postgres_test.go b/impl/pkg/storage/db/postgres/postgres_test.go index 9f1118ad..c3d4f042 100644 --- a/impl/pkg/storage/db/postgres/postgres_test.go +++ b/impl/pkg/storage/db/postgres/postgres_test.go @@ -42,7 +42,7 @@ func TestReadWrite(t *testing.T) { require.NoError(t, err) require.NotEmpty(t, doc) - packet, err := did.DHT(doc.ID).ToDNSPacket(*doc, nil) + packet, err := did.DHT(doc.ID).ToDNSPacket(*doc, nil, nil) require.NoError(t, err) require.NotEmpty(t, packet) @@ -81,7 +81,7 @@ func TestDBPagination(t *testing.T) { require.NoError(t, err) require.NotEmpty(t, doc) - packet, err := did.DHT(doc.ID).ToDNSPacket(*doc, nil) + packet, err := did.DHT(doc.ID).ToDNSPacket(*doc, nil, nil) assert.NoError(t, err) assert.NotEmpty(t, packet) @@ -102,7 +102,7 @@ func TestDBPagination(t *testing.T) { require.NoError(t, err) require.NotEmpty(t, doc) - packet, err := did.DHT(doc.ID).ToDNSPacket(*doc, nil) + packet, err := did.DHT(doc.ID).ToDNSPacket(*doc, nil, nil) assert.NoError(t, err) assert.NotEmpty(t, packet) diff --git a/spec/spec.md b/spec/spec.md index 122319e9..973f90d2 100644 --- a/spec/spec.md +++ b/spec/spec.md @@ -9,7 +9,7 @@ The DID DHT Method Specification 1.0 **Draft Created:** October 20, 2023 -**Latest Update:** March 29, 2024 +**Latest Update:** April 1, 2024 **Editors:** ~ [Gabe Cohen](https://github.com/decentralgabe) @@ -1078,8 +1078,8 @@ A minimal DID Document. #### Vector 2 -A DID Document with two keys ([[ref:Identity Key]] and an uncompressed secp256k1 key), a service with multiple endpoints, a gateway, -two types to index, an aka, and controller properties. +A DID Document with two keys ([[ref:Identity Key]] and an uncompressed secp256k1 key), a service with multiple +endpoints, a gateway, two types to index, an aka, and controller properties. **Identity Public Key JWK:** @@ -1191,10 +1191,101 @@ With controller: `did:dht:i9xkp8ddcbcg8jwq54ox699wuzxyifsqx4jru45zodqu453ksz6y`. | _cnt.did. | TXT | 7200 | did:example:abcd | | _aka.did. | TXT | 7200 | did:example:efgh,did:example:ijkl | | _k0.did. | TXT | 7200 | id=0;t=0;k=YCcHYL2sYNPDlKaALcEmll2HHyT968M4UWbr-9CFGWE;c=did:example:abcd | -| _k1.did. | TXT | 7200 | t=1;k=Atf6NCChxjWpnrfPt1WDVE4ipYVSvi4pXCq4SUjx0jT9;a=ES256K | +| _k1.did. | TXT | 7200 | t=1;k=Atf6NCChxjWpnrfPt1WDVE4ipYVSvi4pXCq4SUjx0jT9 | | _s0.did. | TXT | 7200 | id=service-1;t=TestService;se=https://test-service.com/1,https://test-service.com/2 | | _typ.did. | TXT | 7200 | id=1,2,3 | +#### Vector 3 + +A DID Document with two keys -- the [[ref:Identity Key]] and an X25519 key used with a different `alg` value than +what is specified in the registry. The DID also has two gateway records. + +**Identity Public Key JWK:** + +```json +{ + "kid": "0", + "alg": "Ed25519", + "crv": "Ed25519", + "kty": "OKP", + "x": "sTyTLYw-n1NI9X-84NaCuis1wZjAA8lku6f6Et5201g" +} +``` + +**X25519 Public Key JWK:** + +```json +{ + "kid": "WVy5IWMa36AoyAXZDvPd5j9zxt2t-GjifDEV-DwgIdQ", + "alg": "ECDH-ES+A128KW", + "crv": "X25519", + "kty": "OKP", + "x": "3POE0_i2mGeZ2qiQCA3KcLfi1fZo0311CXFSIwt1nB4" +} +``` + +**Key Purposes:** `Key Agreement`. + +**Gateways:**: `gateway1.example-did-dht-gateway.com.`, `gateway2.example-did-dht-gateway.com.`. + +**DID Document:** + +```json +{ + "id": "did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy", + "verificationMethod": [ + { + "id": "did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy#0", + "type": "JsonWebKey", + "controller": "did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy", + "publicKeyJwk": { + "kid": "0", + "alg": "Ed25519", + "crv": "Ed25519", + "kty": "OKP", + "x": "sTyTLYw-n1NI9X-84NaCuis1wZjAA8lku6f6Et5201g" + } + }, + { + "id": "did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy#WVy5IWMa36AoyAXZDvPd5j9zxt2t-GjifDEV-DwgIdQ", + "type": "JsonWebKey", + "controller": "did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy", + "publicKeyJwk": { + "kid": "WVy5IWMa36AoyAXZDvPd5j9zxt2t-GjifDEV-DwgIdQ", + "alg": "ECDH-ES+A128KW", + "crv": "X25519", + "kty": "OKP", + "x": "3POE0_i2mGeZ2qiQCA3KcLfi1fZo0311CXFSIwt1nB4" + } + } + ], + "authentication": [ + "did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy#0" + ], + "assertionMethod": [ + "did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy#0" + ], + "keyAgreement": [ + "did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy#WVy5IWMa36AoyAXZDvPd5j9zxt2t-GjifDEV-DwgIdQ" + ], + "capabilityInvocation": [ + "did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy#0" + ], + "capabilityDelegation": [ + "did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy#0" + ] +} +``` + +**DNS Resource Records:** + +| Name | Type | TTL | Rdata | +| --------- | ---- | ---- | ----------- | +| _did.sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy. | NS | 7200 | gateway1.example-did-dht-gateway.com. | +| _did.sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy. | TXT | 7200 | v=0;vm=k0,k1;auth=k0;asm=k0;agm=k1;inv=k0;del=k0 | +| _k0.did. | TXT | 7200 | id=0;t=0;k=sTyTLYw-n1NI9X-84NaCuis1wZjAA8lku6f6Et5201g | +| _k1.did. | TXT | 7200 | id=WVy5IWMa36AoyAXZDvPd5j9zxt2t-GjifDEV-DwgIdQ;t=3;k=3POE0_i2mGeZ2qiQCA3KcLfi1fZo0311CXFSIwt1nB4;a=ECDH-ES+A128KW | + ### Open API Definition ```yaml