Skip to content

Commit

Permalink
Add language around DID rotation; fix formatting (#200)
Browse files Browse the repository at this point in the history
* svc not srv

* formatting and some text

* formatting

* fix test

* prev did handling logic
  • Loading branch information
decentralgabe authored Apr 29, 2024
1 parent f2541b6 commit 953093a
Show file tree
Hide file tree
Showing 15 changed files with 746 additions and 483 deletions.
2 changes: 1 addition & 1 deletion impl/concurrencytest/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ func generateDIDPutRequest() (string, []byte, error) {
return "", nil, err
}

packet, err := did.DHT(doc.ID).ToDNSPacket(*doc, nil, nil)
packet, err := did.DHT(doc.ID).ToDNSPacket(*doc, nil, nil, nil)
if err != nil {
return "", nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion impl/integrationtest/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func generateDIDPutRequest() (string, []byte, error) {
return "", nil, err
}

packet, err := did.DHT(doc.ID).ToDNSPacket(*doc, nil, nil)
packet, err := did.DHT(doc.ID).ToDNSPacket(*doc, nil, nil, nil)
if err != nil {
return "", nil, err
}
Expand Down
15 changes: 7 additions & 8 deletions impl/internal/did/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"net/url"
"time"

"github.com/TBD54566975/ssi-sdk/did"
"github.com/anacrolix/dht/v2/bep44"
"github.com/miekg/dns"
"github.com/pkg/errors"
Expand All @@ -34,30 +33,30 @@ func NewGatewayClient(gatewayURL string) (*GatewayClient, 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) {
func (c *GatewayClient) GetDIDDocument(id string) (*DIDDHTDocument, error) {
d := DHT(id)
if !d.IsValid() {
return nil, nil, nil, errors.New("invalid did")
return nil, errors.New("invalid did")
}
suffix, err := d.Suffix()
if err != nil {
return nil, nil, nil, errors.Wrap(err, "failed to get suffix")
return nil, errors.Wrap(err, "failed to get suffix")
}
resp, err := http.Get(c.gatewayURL + "/" + suffix)
if err != nil {
return nil, nil, nil, errors.Wrap(err, "failed to get did document")
return nil, errors.Wrap(err, "failed to get did document")
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, nil, nil, errors.Errorf("failed to get did document, status code: %d", resp.StatusCode)
return 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, nil, errors.Wrap(err, "failed to read response body")
return nil, errors.Wrap(err, "failed to read response body")
}
msg := new(dns.Msg)
if err = msg.Unpack(body[72:]); err != nil {
return nil, nil, nil, errors.Wrap(err, "failed to unpack records")
return nil, errors.Wrap(err, "failed to unpack records")
}
return d.FromDNSPacket(msg)
}
Expand Down
30 changes: 11 additions & 19 deletions impl/internal/did/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, nil)
packet, err := DHT(doc.ID).ToDNSPacket(*doc, nil, nil, nil)
assert.NoError(t, err)
assert.NotEmpty(t, packet)

Expand All @@ -34,9 +34,9 @@ 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)
assert.EqualValues(t, *doc, gotDID.Doc)

since := time.Since(start)
t.Logf("time to put and get: %s", since)
Expand All @@ -53,33 +53,25 @@ func TestInvalidDIDDocument(t *testing.T) {
require.NoError(t, err)
require.NotEmpty(t, client)

did, types, gateways, err := client.GetDIDDocument("this is not a valid did")
gotDID, err := client.GetDIDDocument("this is not a valid did")
assert.Error(t, err)
assert.Empty(t, did)
assert.Empty(t, types)
assert.Empty(t, gateways)
assert.Empty(t, gotDID)

did, types, gateways, err = client.GetDIDDocument("did:dht:example")
gotDID, err = client.GetDIDDocument("did:dht:example")
assert.EqualError(t, err, "invalid did")
assert.Empty(t, did)
assert.Empty(t, types)
assert.Empty(t, gateways)
assert.Empty(t, gotDID)

did, types, gateways, err = client.GetDIDDocument("did:dht:i9xkp8ddcbcg8jwq54ox699wuzxyifsqx4jru45zodqu453ksz6y")
gotDID, err = client.GetDIDDocument("did:dht:i9xkp8ddcbcg8jwq54ox699wuzxyifsqx4jru45zodqu453ksz6y")
assert.Error(t, err) // this should error because the gateway URL is invalid
assert.Empty(t, did)
assert.Empty(t, types)
assert.Empty(t, gateways)
assert.Empty(t, gotDID)

client, err = NewGatewayClient("https://tbd.website")
require.NoError(t, err)
require.NotEmpty(t, client)

did, types, gateways, err = client.GetDIDDocument("did:dht:i9xkp8ddcbcg8jwq54ox699wuzxyifsqx4jru45zodqu453ksz6y")
gotDID, err = client.GetDIDDocument("did:dht:i9xkp8ddcbcg8jwq54ox699wuzxyifsqx4jru45zodqu453ksz6y")
assert.Error(t, err) // this should error because the gateway URL will return a non-200
assert.Empty(t, did)
assert.Empty(t, types)
assert.Empty(t, gateways)
assert.Empty(t, gotDID)

err = client.PutDocument("did:dht:example", bep44.Put{})
assert.Error(t, err)
Expand Down
129 changes: 112 additions & 17 deletions impl/internal/did/did.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ type (
AuthoritativeGateway string
)

type PreviousDID struct {
PreviousDID DHT `json:"did"`
Signature string `json:"signature"`
}

const (
// Prefix did:dht prefix
Prefix = "did:dht"
Expand Down Expand Up @@ -62,13 +67,26 @@ func (d DHT) Suffix() (string, error) {
return "", fmt.Errorf("invalid did:dht prefix: %s", d)
}

// IdentityKey returns the ed25519 public key for the DHT identifier https://did-dht.com/#identity-key
func (d DHT) IdentityKey() (ed25519.PublicKey, error) {
suffix, err := d.Suffix()
if err != nil {
return nil, err
}
pk, err := zbase32.DecodeString(suffix)
if err != nil {
return nil, err
}
return pk, nil
}

func (DHT) Method() did.Method {
return DHTMethod
}

// CreateDIDDHTOpts is a set of options for creating a did:dht identifier
// Note: this does not include additional properties only present in the DNS representation (e.g. gateways, types)
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
Expand Down Expand Up @@ -239,7 +257,7 @@ 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, gateways []AuthoritativeGateway) (*dns.Msg, error) {
func (d DHT) ToDNSPacket(doc did.Document, types []TypeIndex, gateways []AuthoritativeGateway, previousDID *PreviousDID) (*dns.Msg, error) {
var records []dns.RR
var rootRecord []string
keyLookup := make(map[string]string)
Expand All @@ -249,6 +267,24 @@ func (d DHT) ToDNSPacket(doc did.Document, types []TypeIndex, gateways []Authori
return nil, errors.Wrap(err, "failed to get suffix while decoding DNS packet")
}

// handle the previous DID if it's present
if previousDID != nil {
// make sure it's valid
if err = ValidatePreviousDIDSignatureValid(d, *previousDID); err != nil {
return nil, err
}
// add it to the record set
records = append(records, &dns.TXT{
Hdr: dns.RR_Header{
Name: "_prv._did.",
Rrtype: dns.TypeTXT,
Class: dns.ClassINET,
Ttl: 7200,
},
Txt: chunkTextRecord(fmt.Sprintf("id=%s;s=%s", previousDID.PreviousDID, previousDID.Signature)),
})
}

// first append the version to the root record
rootRecord = append(rootRecord, fmt.Sprintf("v=%d", Version))

Expand Down Expand Up @@ -508,22 +544,32 @@ func parseServiceData(serviceEndpoint any) string {
return ""
}

// DIDDHTDocument is a DID DHT Document along with additional metadata the DID supports
type DIDDHTDocument struct {
Doc did.Document `json:"did,omitempty"`
Types []TypeIndex `json:"types,omitempty"`
Gateways []AuthoritativeGateway `json:"gateways,omitempty"`
PreviousDID *PreviousDID `json:"previousDid,omitempty"`
}

// FromDNSPacket converts a DNS packet to a DID DHT Document
// 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) {
func (d DHT) FromDNSPacket(msg *dns.Msg) (*DIDDHTDocument, 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")
return 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
// track the previous DID
var previousDID *PreviousDID
keyLookup := make(map[string]string)
for _, rr := range msg.Answer {
switch record := rr.(type) {
Expand Down Expand Up @@ -558,23 +604,23 @@ func (d DHT) FromDNSPacket(msg *dns.Msg) (*did.Document, []TypeIndex, []Authorit
// Convert keyBase64URL back to PublicKeyJWK
pubKeyBytes, err := base64.RawURLEncoding.DecodeString(keyBase64URL)
if err != nil {
return nil, nil, nil, err
return 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, nil, err
return nil, err
}
pubKeyJWK, err := jwx.PublicKeyToPublicKeyJWK(&vmID, pubKey)
if err != nil {
return nil, nil, nil, err
return 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)
return nil, fmt.Errorf("unable to provide default alg for unsupported key type: %s", keyType)
}
pubKeyJWK.ALG = defaultAlg
} else {
Expand All @@ -583,7 +629,7 @@ func (d DHT) FromDNSPacket(msg *dns.Msg) (*did.Document, []TypeIndex, []Authorit

// make sure the controller of the identity key matches the DID
if vmID == "0" && controller != d.String() {
return nil, nil, nil, fmt.Errorf("controller of identity key must be the DID itself, instead it is: %s", controller)
return 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
Expand All @@ -592,7 +638,7 @@ func (d DHT) FromDNSPacket(msg *dns.Msg) (*did.Document, []TypeIndex, []Authorit
}

if vmID != "0" && pubKeyJWK.KID != vmID {
return nil, nil, nil, fmt.Errorf("verification method JWK KID must be set to its thumbprint")
return nil, fmt.Errorf("verification method JWK KID must be set to its thumbprint")
}

vm := did.VerificationMethod{
Expand Down Expand Up @@ -634,23 +680,34 @@ func (d DHT) FromDNSPacket(msg *dns.Msg) (*did.Document, []TypeIndex, []Authorit

} else if record.Hdr.Name == "_typ._did." {
if record.Txt[0] == "" {
return nil, nil, nil, fmt.Errorf("types record is empty")
return nil, fmt.Errorf("types record is empty")
}
unchunkedTextRecord := unchunkTextRecord(record.Txt)
typesStr := strings.Split(strings.TrimPrefix(unchunkedTextRecord, "id="), ",")
for _, t := range typesStr {
tInt, err := strconv.Atoi(t)
if err != nil {
return nil, nil, nil, err
return nil, err
}
types = append(types, TypeIndex(tInt))
}
} else if record.Hdr.Name == fmt.Sprintf("_did.%s.", suffix) && record.Hdr.Rrtype == dns.TypeNS {
if record.Txt[0] == "" {
return nil, nil, nil, fmt.Errorf("gateway record is empty")
return nil, fmt.Errorf("gateway record is empty")
}
unchunkedTextRecord := unchunkTextRecord(record.Txt)
gateways = append(gateways, AuthoritativeGateway(unchunkedTextRecord))
} else if record.Hdr.Name == "_prv._did." && record.Hdr.Rrtype == dns.TypeTXT {
unchunkedTextRecord := unchunkTextRecord(record.Txt)
data := parseTxtData(unchunkedTextRecord)
previousDID = &PreviousDID{
PreviousDID: DHT(data["id"]),
Signature: data["s"],
}
// validate previous DID signature
if err = ValidatePreviousDIDSignatureValid(d, *previousDID); err != nil {
return nil, err
}
} else if record.Hdr.Name == fmt.Sprintf("_did.%s.", suffix) && record.Hdr.Rrtype == dns.TypeTXT {
unchunkedTextRecord := unchunkTextRecord(record.Txt)
rootItems := strings.Split(unchunkedTextRecord, ";")
Expand All @@ -668,7 +725,7 @@ func (d DHT) FromDNSPacket(msg *dns.Msg) (*did.Document, []TypeIndex, []Authorit
switch key {
case "v":
if len(valueItems) != 1 || valueItems[0] != strconv.Itoa(Version) {
return nil, nil, nil, fmt.Errorf("invalid version: %s", values)
return nil, fmt.Errorf("invalid version: %s", values)
}
seenVersion = true
case "auth":
Expand All @@ -694,13 +751,51 @@ func (d DHT) FromDNSPacket(msg *dns.Msg) (*did.Document, []TypeIndex, []Authorit
}
}
if !seenVersion {
return nil, nil, nil, fmt.Errorf("root record missing version identifier")
return nil, fmt.Errorf("root record missing version identifier")
}
}
}
}

return &doc, types, gateways, nil
return &DIDDHTDocument{
Doc: doc,
Types: types,
Gateways: gateways,
PreviousDID: previousDID,
}, nil
}

// CreatePreviousDIDRecord creates a PreviousDID record for the given previous DID and current DID
func CreatePreviousDIDRecord(previousDIDPrivateKey ed25519.PrivateKey, previousDID, currentDID DHT) (*PreviousDID, error) {
currentDIDIdentityKey, err := currentDID.IdentityKey()
if err != nil {
return nil, errors.Wrapf(err, "failed to get identity key from currentDID: %s", currentDID)
}
previousDIDSignature := ed25519.Sign(previousDIDPrivateKey, currentDIDIdentityKey)
return &PreviousDID{
PreviousDID: previousDID,
Signature: base64.RawURLEncoding.EncodeToString(previousDIDSignature),
}, nil
}

// ValidatePreviousDIDSignatureValid validates the signature of the previous DID over the current DID
func ValidatePreviousDIDSignatureValid(currentDID DHT, previousDID PreviousDID) error {
identityKey, err := currentDID.IdentityKey()
if err != nil {
return errors.Wrapf(err, "failed to get identity key from the current DID: %s", currentDID)
}
previousDIDKey, err := previousDID.PreviousDID.IdentityKey()
if err != nil {
return errors.Wrapf(err, "failed to get identity key from the previous DID: %s", previousDID.PreviousDID)
}
decodedSignature, err := base64.RawURLEncoding.DecodeString(previousDID.Signature)
if err != nil {
return errors.Wrap(err, "failed to decode the previous DID's signature")
}
if ok := ed25519.Verify(previousDIDKey, identityKey, decodedSignature); !ok {
return errors.New("the previous DID signature is invalid")
}
return nil
}

func parseTxtData(data string) map[string]string {
Expand Down
Loading

0 comments on commit 953093a

Please sign in to comment.