Skip to content

Commit

Permalink
update to latest spec
Browse files Browse the repository at this point in the history
  • Loading branch information
gabe committed Jan 9, 2024
1 parent cf247f7 commit 088d1a7
Show file tree
Hide file tree
Showing 8 changed files with 168 additions and 60 deletions.
4 changes: 2 additions & 2 deletions impl/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.21

require (
github.com/BurntSushi/toml v0.3.1
github.com/TBD54566975/ssi-sdk v0.0.4-alpha.0.20231121023732-c496504a93c4
github.com/TBD54566975/ssi-sdk v0.0.4-alpha.0.20240109225800-c9f99e5db02a
github.com/allegro/bigcache/v3 v3.1.0
github.com/anacrolix/dht/v2 v2.20.0
github.com/anacrolix/log v0.14.0
Expand Down Expand Up @@ -81,7 +81,7 @@ require (
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.4 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/jwx/v2 v2.0.16 // indirect
github.com/lestrrat-go/jwx/v2 v2.0.18 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
Expand Down
13 changes: 5 additions & 8 deletions impl/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ 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.20231121023732-c496504a93c4 h1:r0pRi+TDJEL5EaEY/VHzyMphNx5fNUmk0hsxzwU9EiI=
github.com/TBD54566975/ssi-sdk v0.0.4-alpha.0.20231121023732-c496504a93c4/go.mod h1:QmlzAUyFIChi0qqr3h5QvE9I8z0fc3U6A6hIQa2yr40=
github.com/TBD54566975/ssi-sdk v0.0.4-alpha.0.20240109225800-c9f99e5db02a h1:xvPnLvpf6zCNkgA5brq38wFOJBWoq3rMxXRH8xHrHp4=
github.com/TBD54566975/ssi-sdk v0.0.4-alpha.0.20240109225800-c9f99e5db02a/go.mod h1:v3JouHTB++xJi6zTC+hbtTeBZkfdp/orSHF/+inJozU=
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=
Expand Down Expand Up @@ -366,8 +366,8 @@ github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJG
github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
github.com/lestrrat-go/jwx/v2 v2.0.16 h1:TuH3dBkYTy2giQg/9D8f20znS3JtMRuQJ372boS3lWk=
github.com/lestrrat-go/jwx/v2 v2.0.16/go.mod h1:jBHyESp4e7QxfERM0UKkQ80/94paqNIEcdEfiUYz5zE=
github.com/lestrrat-go/jwx/v2 v2.0.18 h1:HHZkYS5wWDDyAiNBwztEtDoX07WDhGEdixm8G06R50o=
github.com/lestrrat-go/jwx/v2 v2.0.18/go.mod h1:fAJ+k5eTgKdDqanzCuK6DAt3W7n3cs2/FX7JhQdk83U=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
Expand Down Expand Up @@ -571,7 +571,7 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
Expand Down Expand Up @@ -731,14 +731,12 @@ golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
Expand All @@ -751,7 +749,6 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
Expand Down
164 changes: 133 additions & 31 deletions impl/internal/did/did.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ func (DHT) Method() did.Method {
}

type CreateDIDDHTOpts struct {
// Controller is the DID Controller, can be a list of DIDs
Controller []string
// AlsoKnownAs is a list of alternative identifiers for the DID Document
AlsoKnownAs []string
// VerificationMethods is a list of verification methods to include in the DID Document
// Cannot contain id #0 which is reserved for the identity key
VerificationMethods []VerificationMethod
Expand Down Expand Up @@ -91,8 +95,30 @@ func CreateDIDDHTDID(pubKey ed25519.PublicKey, opts CreateDIDDHTOpts) (*did.Docu
// generate the did:dht identifier
id := GetDIDDHTIdentifier(pubKey)

// validate opts and build verification methods, key purposes, and services
// validate opts and build controller, aka, verification methods, key purposes, and services
identityKeyID := id + "#0"

var controller any
if len(opts.Controller) != 0 {
if len(opts.Controller) == 1 {
// if there's only one controller, set it to the first controller
controller = opts.Controller[0]
} else {
// if there's more than one controller, set it to the list of controllers
controller = opts.Controller
}
}
var aka any
if len(opts.AlsoKnownAs) != 0 {
if len(opts.AlsoKnownAs) == 1 {
// if there's only one aka, set it to the first aka
aka = opts.AlsoKnownAs[0]
} else {
// if there's more than one aka, set it to the list of akas
aka = opts.AlsoKnownAs
}
}

var vms []did.VerificationMethod
var keyAgreement []did.VerificationMethodSet
authentication := []did.VerificationMethodSet{identityKeyID}
Expand All @@ -103,7 +129,7 @@ func CreateDIDDHTDID(pubKey ed25519.PublicKey, opts CreateDIDDHTOpts) (*did.Docu
seenIDs := make(map[string]bool)
for _, vm := range opts.VerificationMethods {
if vm.VerificationMethod.ID == identityKeyID || vm.VerificationMethod.ID == "#0" || vm.VerificationMethod.ID == "0" {
return nil, fmt.Errorf("verification method id #0 is reserved for the identity key")
return nil, fmt.Errorf("verification method id 0 is reserved for the identity key")
}
if seenIDs[vm.VerificationMethod.ID] {
return nil, fmt.Errorf("verification method id %s is not unique", vm.VerificationMethod.ID)
Expand All @@ -118,27 +144,34 @@ func CreateDIDDHTDID(pubKey ed25519.PublicKey, opts CreateDIDDHTOpts) (*did.Docu
// mark as seen
seenIDs[vm.VerificationMethod.ID] = true

// update ID and controller in place
if strings.Contains(vm.VerificationMethod.ID, "#") {
return nil, fmt.Errorf("verification method id %s is invalid", vm.VerificationMethod.ID)
}
// set to thumbprint if none is provided
// update ID and controller in place, setting to thumbprint if none is provided

// e.g. nothing -> did:dht:123456789abcdefghi#<jwk key id>
if vm.VerificationMethod.ID == "" {
vm.VerificationMethod.ID = id + "#" + vm.VerificationMethod.PublicKeyJWK.KID
} else {
// make sure the verification method ID and KID match
vm.VerificationMethod.PublicKeyJWK.KID = vm.VerificationMethod.ID
}
vm.VerificationMethod.ID = id + "#" + vm.VerificationMethod.ID

// e.g. #key-1 -> did:dht:123456789abcdefghi#key-1
if strings.HasPrefix(vm.VerificationMethod.ID, "#") {
vm.VerificationMethod.ID = id + vm.VerificationMethod.ID
}

// e.g. key-1 -> did:dht:123456789abcdefghi#key-1
if !strings.Contains(vm.VerificationMethod.ID, "#") {
vm.VerificationMethod.ID = id + "#" + vm.VerificationMethod.ID
}

// if there's no controller, set it to the DID itself
if vm.VerificationMethod.Controller != "" {
if vm.VerificationMethod.Controller == "" {
vm.VerificationMethod.Controller = id
}
vms = append(vms, vm.VerificationMethod)

// add purposes
vmID := vm.VerificationMethod.ID[strings.LastIndex(vm.VerificationMethod.ID, "#"):]
vmID := vm.VerificationMethod.ID
for _, purpose := range vm.Purposes {
switch purpose {
case did.Authentication:
Expand Down Expand Up @@ -186,6 +219,8 @@ func CreateDIDDHTDID(pubKey ed25519.PublicKey, opts CreateDIDDHTOpts) (*did.Docu
}
return &did.Document{
ID: id,
Controller: controller,
AlsoKnownAs: aka,
VerificationMethod: append([]did.VerificationMethod{vm0}, vms...),
Services: opts.Services,
Authentication: authentication,
Expand All @@ -207,6 +242,46 @@ func (d DHT) ToDNSPacket(doc did.Document, types []TypeIndex) (*dns.Msg, error)
var rootRecord []string
keyLookup := make(map[string]string)

// build controller and aka records
if doc.Controller != nil {
var controllerTxt string
switch c := doc.Controller.(type) {
case string:
controllerTxt = c
case []string:
controllerTxt = strings.Join(c, ",")
}
controllerAnswer := dns.TXT{
Hdr: dns.RR_Header{
Name: "_cnt._did.",
Rrtype: dns.TypeTXT,
Class: dns.ClassINET,
Ttl: 7200,
},
Txt: []string{controllerTxt},
}
records = append(records, &controllerAnswer)
}
if doc.AlsoKnownAs != nil {
var akaTxt string
switch a := doc.AlsoKnownAs.(type) {
case string:
akaTxt = a
case []string:
akaTxt = strings.Join(a, ",")
}
akaAnswer := dns.TXT{
Hdr: dns.RR_Header{
Name: "_aka._did.",
Rrtype: dns.TypeTXT,
Class: dns.ClassINET,
Ttl: 7200,
},
Txt: []string{akaTxt},
}
records = append(records, &akaAnswer)
}

// build all key records
var vmIDs []string
for i, vm := range doc.VerificationMethod {
Expand All @@ -229,7 +304,7 @@ func (d DHT) ToDNSPacket(doc did.Document, types []TypeIndex) (*dns.Msg, error)
}
keyBase64Url := base64.RawURLEncoding.EncodeToString(pubKeyBytes)

vmKeyFragment := vm.ID[strings.LastIndex(vm.ID, "#"):]
vmKeyFragment := vm.ID[strings.LastIndex(vm.ID, "#")+1:]
keyRecord := dns.TXT{
Hdr: dns.RR_Header{
Name: fmt.Sprintf("_%s._did.", recordIdentifier),
Expand All @@ -254,14 +329,21 @@ func (d DHT) ToDNSPacket(doc did.Document, types []TypeIndex) (*dns.Msg, error)
sID = sID[strings.LastIndex(service.ID, "#")+1:]
}

svcTxt := fmt.Sprintf("id=%s;t=%s;se=%s", sID, service.Type, parseServiceData(service.ServiceEndpoint))
if service.Sig != nil {
svcTxt += fmt.Sprintf(";sig=%s", parseServiceData(service.Sig))
}
if service.Enc != nil {
svcTxt += fmt.Sprintf(";enc=%s", parseServiceData(service.Enc))
}
serviceRecord := dns.TXT{
Hdr: dns.RR_Header{
Name: fmt.Sprintf("_%s._did.", recordIdentifier),
Rrtype: dns.TypeTXT,
Class: dns.ClassINET,
Ttl: 7200,
},
Txt: []string{fmt.Sprintf("id=%s;t=%s;se=%s", sID, service.Type, parseServiceEndpoint(service.ServiceEndpoint))},
Txt: []string{svcTxt},
}

records = append(records, &serviceRecord)
Expand Down Expand Up @@ -354,8 +436,9 @@ func (d DHT) ToDNSPacket(doc did.Document, types []TypeIndex) (*dns.Msg, error)
}, nil
}

// make a best-effort to parse a service endpoint which we expect as either a single string value or an array of strings
func parseServiceEndpoint(serviceEndpoint any) string {
// make a best-effort to parse a service endpoints and other service data which we expect as either a single string
// value or an array of strings
func parseServiceData(serviceEndpoint any) string {
switch se := serviceEndpoint.(type) {
case string:
return se
Expand Down Expand Up @@ -388,6 +471,12 @@ func (d DHT) FromDNSPacket(msg *dns.Msg) (*did.Document, []TypeIndex, error) {
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], ",")
}
if strings.HasPrefix(record.Hdr.Name, "_aka") {
doc.AlsoKnownAs = strings.Split(record.Txt[0], ",")
}
if strings.HasPrefix(record.Hdr.Name, "_k") {
data := parseTxtData(strings.Join(record.Txt, ","))
vmID := data["id"]
Expand All @@ -409,25 +498,44 @@ func (d DHT) FromDNSPacket(msg *dns.Msg) (*did.Document, []TypeIndex, error) {
}

vm := did.VerificationMethod{
ID: d.String() + vmID,
ID: d.String() + "#" + vmID,
Type: JSONWebKeyType,
Controller: d.String(),
PublicKeyJWK: pubKeyJWK,
}
doc.VerificationMethod = append(doc.VerificationMethod, vm)

// add to key lookup (e.g. "#k1" -> "#key1")
// add to key lookup (e.g. "k1" -> "key1")
keyLookup[strings.Split(record.Hdr.Name, ".")[0][1:]] = vmID
} else if strings.HasPrefix(record.Hdr.Name, "_s") {
data := parseTxtData(strings.Join(record.Txt, ","))
sID := data["id"]
serviceType := data["t"]
serviceEndpoint := data["uri"]

serviceEndpoint := data["se"]
var serviceEndpointValue any
if strings.Contains(serviceEndpoint, ",") {
serviceEndpointValue = strings.Split(serviceEndpoint, ",")
} else {
serviceEndpointValue = serviceEndpoint
}
service := did.Service{
ID: d.String() + sID,
ID: d.String() + "#" + sID,
Type: serviceType,
ServiceEndpoint: serviceEndpoint,
ServiceEndpoint: serviceEndpointValue,
}
if data["sig"] != "" {
if strings.Contains(data["sig"], ",") {
service.Sig = strings.Split(data["sig"], ",")
} else {
service.Sig = data["sig"]
}
}
if data["enc"] != "" {
if strings.Contains(data["enc"], ",") {
service.Enc = strings.Split(data["enc"], ",")
} else {
service.Enc = data["enc"]
}
}
doc.Services = append(doc.Services, service)

Expand Down Expand Up @@ -457,31 +565,25 @@ func (d DHT) FromDNSPacket(msg *dns.Msg) (*did.Document, []TypeIndex, error) {
valueItems := strings.Split(values, ",")

switch key {
case "vm":
// These are already processed in the "_k" prefix case
continue
case "srv":
// These are already processed in the "_s" prefix case
continue
case "auth":
for _, valueItem := range valueItems {
doc.Authentication = append(doc.Authentication, doc.ID+keyLookup[valueItem])
doc.Authentication = append(doc.Authentication, doc.ID+"#"+keyLookup[valueItem])
}
case "asm":
for _, valueItem := range valueItems {
doc.AssertionMethod = append(doc.AssertionMethod, doc.ID+keyLookup[valueItem])
doc.AssertionMethod = append(doc.AssertionMethod, doc.ID+"#"+keyLookup[valueItem])
}
case "agm":
for _, valueItem := range valueItems {
doc.KeyAgreement = append(doc.KeyAgreement, doc.ID+keyLookup[valueItem])
doc.KeyAgreement = append(doc.KeyAgreement, doc.ID+"#"+keyLookup[valueItem])
}
case "inv":
for _, valueItem := range valueItems {
doc.CapabilityInvocation = append(doc.CapabilityInvocation, doc.ID+keyLookup[valueItem])
doc.CapabilityInvocation = append(doc.CapabilityInvocation, doc.ID+"#"+keyLookup[valueItem])
}
case "del":
for _, valueItem := range valueItems {
doc.CapabilityDelegation = append(doc.CapabilityDelegation, doc.ID+keyLookup[valueItem])
doc.CapabilityDelegation = append(doc.CapabilityDelegation, doc.ID+"#"+keyLookup[valueItem])
}
}
}
Expand Down
Loading

0 comments on commit 088d1a7

Please sign in to comment.