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

Address TXT records > 255 characters; make language more consistent; separate section for root records #162

Merged
merged 9 commits into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
79 changes: 50 additions & 29 deletions impl/internal/did/did.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ func (d DHT) ToDNSPacket(doc did.Document, types []TypeIndex, gateways []Authori
Class: dns.ClassINET,
Ttl: 7200,
},
Txt: []string{controllerTxt},
Txt: chunkTextRecord(controllerTxt),
}
records = append(records, &controllerAnswer)
}
Expand All @@ -287,7 +287,7 @@ func (d DHT) ToDNSPacket(doc did.Document, types []TypeIndex, gateways []Authori
Class: dns.ClassINET,
Ttl: 7200,
},
Txt: []string{akaTxt},
Txt: chunkTextRecord(akaTxt),
}
records = append(records, &akaAnswer)
}
Expand All @@ -301,7 +301,7 @@ func (d DHT) ToDNSPacket(doc did.Document, types []TypeIndex, gateways []Authori
Class: dns.ClassINET,
Ttl: 7200,
},
Txt: []string{string(gateway)},
Txt: chunkTextRecord(string(gateway)),
}
records = append(records, &gatewayAnswer)
}
Expand Down Expand Up @@ -347,14 +347,18 @@ func (d DHT) ToDNSPacket(doc did.Document, types []TypeIndex, gateways []Authori
}
txtRecord += fmt.Sprintf(";c=%s", vm.Controller)
}

if len(txtRecord) > 255 {
return nil, fmt.Errorf("key value exceeds 255 characters")
}
keyRecord := dns.TXT{
Hdr: dns.RR_Header{
Name: fmt.Sprintf("_%s._did.", recordIdentifier),
Rrtype: dns.TypeTXT,
Class: dns.ClassINET,
Ttl: 7200,
},
Txt: []string{txtRecord},
Txt: chunkTextRecord(txtRecord),
}

records = append(records, &keyRecord)
Expand Down Expand Up @@ -386,16 +390,12 @@ func (d DHT) ToDNSPacket(doc did.Document, types []TypeIndex, gateways []Authori
Class: dns.ClassINET,
Ttl: 7200,
},
Txt: []string{svcTxt},
Txt: chunkTextRecord(svcTxt),
}

records = append(records, &serviceRecord)
svcIDs = append(svcIDs, recordIdentifier)
}
// add services to the root record
if len(svcIDs) != 0 {
rootRecord = append(rootRecord, fmt.Sprintf("svc=%s", strings.Join(svcIDs, ",")))
}

// add verification relationships to the root record
var authIDs []string
Expand Down Expand Up @@ -438,6 +438,11 @@ func (d DHT) ToDNSPacket(doc did.Document, types []TypeIndex, gateways []Authori
rootRecord = append(rootRecord, fmt.Sprintf("del=%s", strings.Join(capabilityDelegationIDs, ",")))
}

// add services to the root record
if len(svcIDs) != 0 {
rootRecord = append(rootRecord, fmt.Sprintf("svc=%s", strings.Join(svcIDs, ",")))
}

// add the root record
rootAnswer := dns.TXT{
Hdr: dns.RR_Header{
Expand All @@ -463,7 +468,7 @@ func (d DHT) ToDNSPacket(doc did.Document, types []TypeIndex, gateways []Authori
Class: dns.ClassINET,
Ttl: 7200,
},
Txt: []string{"id=" + strings.Join(typesStr, ",")},
Txt: chunkTextRecord("id=" + strings.Join(typesStr, ",")),
}
records = append(records, &typesAnswer)
}
Expand Down Expand Up @@ -524,18 +529,21 @@ func (d DHT) FromDNSPacket(msg *dns.Msg) (*did.Document, []TypeIndex, []Authorit
switch record := rr.(type) {
case *dns.TXT:
if strings.HasPrefix(record.Hdr.Name, "_cnt") {
controllers := strings.Split(record.Txt[0], ",")
unchunkedTextRecord := unchunkTextRecord(record.Txt)
controllers := strings.Split(unchunkedTextRecord, ",")
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], ",")
unchunkedTextRecord := unchunkTextRecord(record.Txt)
doc.AlsoKnownAs = strings.Split(unchunkedTextRecord, ",")
}
if strings.HasPrefix(record.Hdr.Name, "_k") {
data := parseTxtData(strings.Join(record.Txt, ","))
unchunkedTextRecord := unchunkTextRecord(record.Txt)
data := parseTxtData(unchunkedTextRecord)
vmID := data["id"]
keyType := keyTypeLookUp(data["t"])
keyBase64URL := data["k"]
Expand Down Expand Up @@ -598,20 +606,15 @@ func (d DHT) FromDNSPacket(msg *dns.Msg) (*did.Document, []TypeIndex, []Authorit
// 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, ","))
unchunkedTextRecord := unchunkTextRecord(record.Txt)
data := parseTxtData(unchunkedTextRecord)
sID := data["id"]
serviceType := data["t"]
serviceEndpoint := data["se"]
var serviceEndpointValue any
if strings.Contains(serviceEndpoint, ",") {
serviceEndpointValue = strings.Split(serviceEndpoint, ",")
} else {
serviceEndpointValue = serviceEndpoint
}
service := did.Service{
ID: d.String() + "#" + sID,
Type: serviceType,
ServiceEndpoint: serviceEndpointValue,
ServiceEndpoint: strings.Split(serviceEndpoint, ","),
}
if data["sig"] != "" {
if strings.Contains(data["sig"], ",") {
Expand All @@ -630,10 +633,11 @@ func (d DHT) FromDNSPacket(msg *dns.Msg) (*did.Document, []TypeIndex, []Authorit
doc.Services = append(doc.Services, service)

} else if record.Hdr.Name == "_typ._did." {
if record.Txt[0] == "" || len(record.Txt) != 1 {
return nil, nil, nil, fmt.Errorf("invalid types record")
if record.Txt[0] == "" {
return nil, nil, nil, fmt.Errorf("types record is empty")
}
typesStr := strings.Split(strings.TrimPrefix(record.Txt[0], "id="), ",")
unchunkedTextRecord := unchunkTextRecord(record.Txt)
typesStr := strings.Split(strings.TrimPrefix(unchunkedTextRecord, "id="), ",")
for _, t := range typesStr {
tInt, err := strconv.Atoi(t)
if err != nil {
Expand All @@ -642,13 +646,14 @@ func (d DHT) FromDNSPacket(msg *dns.Msg) (*did.Document, []TypeIndex, []Authorit
types = append(types, TypeIndex(tInt))
}
} 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())
if record.Txt[0] == "" {
return nil, nil, nil, fmt.Errorf("gateway record is empty")
}
gateways = append(gateways, AuthoritativeGateway(record.Txt[0]))
unchunkedTextRecord := unchunkTextRecord(record.Txt)
gateways = append(gateways, AuthoritativeGateway(unchunkedTextRecord))
} else if record.Hdr.Name == fmt.Sprintf("_did.%s.", suffix) && record.Hdr.Rrtype == dns.TypeTXT {
rootData := strings.Join(record.Txt, ";")
rootItems := strings.Split(rootData, ";")
unchunkedTextRecord := unchunkTextRecord(record.Txt)
rootItems := strings.Split(unchunkedTextRecord, ";")

seenVersion := false
for _, item := range rootItems {
Expand Down Expand Up @@ -792,3 +797,19 @@ func keyTypeForJWK(jwk jwx.PublicKeyJWK) int {
}
return -1
}

// chunkTextRecord splits a text record into chunks of 255 characters
func chunkTextRecord(record string) []string {
var chunks []string
for len(record) > 255 {
chunks = append(chunks, record[:255])
record = record[255:]
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL (thanks chad!) that len(str) returns the number of bytes. This may not be equivalent to the number of chars. For instance, len("£") returns 2.

Given the above, if you want to actually do this into 255 chars, then see the function below.

// SplitString255 splits a string into substrings of up to 255 characters.
func SplitString255(input string) []string {
	var chunks []string
	runeArray := []rune(input) // Convert to rune slice to properly handle multi-byte characters

	for len(runeArray) > 0 {
		if len(runeArray) <= 255 {
			chunks = append(chunks, string(runeArray))
			break
		}

		chunks = append(chunks, string(runeArray[:255]))
		runeArray = runeArray[255:]
	}

	return chunks
}

But the real question is whether the library should allow setting a value in a DID Document which contains values outside the ASCII range. I think the library should. And thus, I believe the value should be base64url encoded before doing chunking, so there are no issues with existing DNS systems. If you agree, then my suggestion doesn't matter, as all ASCII chars are 1-byte when encoded in UTF-8.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a great catch about bytes vs character sizes - thank you
I am opposed to b64 encoding because of size increases

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am going to merge, but please feel free to open an issue to support non-ASCII characters.

chunks = append(chunks, record)
return chunks
}

// unchunkTextRecord joins chunks of a text record
func unchunkTextRecord(chunks []string) string {
return strings.Join(chunks, "")
}
31 changes: 21 additions & 10 deletions impl/internal/did/did_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ func TestToDNSPacket(t *testing.T) {
{
ID: "vcs",
Type: "VerifiableCredentialService",
ServiceEndpoint: "https://example.com/vc/",
ServiceEndpoint: []string{"https://example.com/vc/"},
Sig: []string{"1", "2"},
Enc: "3",
},
Expand Down Expand Up @@ -227,10 +227,10 @@ 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"`
Name string `json:"name"`
RecordType string `json:"type"`
TTL string `json:"ttl"`
Record []string `json:"rdata"`
}

t.Run("test vector 1", func(t *testing.T) {
Expand Down Expand Up @@ -275,7 +275,7 @@ func TestVectors(t *testing.T) {
s := record.String()
if strings.Contains(s, expectedRecord.RecordType) &&
strings.Contains(s, expectedRecord.TTL) &&
strings.Contains(s, expectedRecord.Record) {
strings.Contains(s, strings.Join(expectedRecord.Record, "")) {
matchedRecords[i] = true // Mark as matched
break
}
Expand Down Expand Up @@ -370,7 +370,7 @@ func TestVectors(t *testing.T) {
s := record.String()
if strings.Contains(s, expectedRecord.RecordType) &&
strings.Contains(s, expectedRecord.TTL) &&
strings.Contains(s, expectedRecord.Record) {
strings.Contains(s, strings.Join(expectedRecord.Record, "")) {
matchedRecords[i] = true // Mark as matched
break
}
Expand Down Expand Up @@ -418,6 +418,13 @@ func TestVectors(t *testing.T) {
Purposes: []did.PublicKeyPurpose{did.KeyAgreement},
},
},
Services: []did.Service{
{
ID: "service-1",
Type: "TestLongService",
ServiceEndpoint: []string{"https://test-lllllllllllllllllllllllllllllllllllooooooooooooooooooooonnnnnnnnnnnnnnnnnnngggggggggggggggggggggggggggggggggggggsssssssssssssssssssssssssseeeeeeeeeeeeeeeeeeerrrrrrrrrrrrrrrvvvvvvvvvvvvvvvvvvvviiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiccccccccccccccccccccccccccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee.com/1"},
},
},
})
require.NoError(t, err)
require.NotEmpty(t, doc)
Expand All @@ -432,7 +439,6 @@ func TestVectors(t *testing.T) {
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)
Expand All @@ -452,8 +458,13 @@ func TestVectors(t *testing.T) {
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) {
strings.Contains(s, expectedRecord.TTL) {
// make sure all parts of the record are contained within s
for _, r := range expectedRecord.Record {
if !strings.Contains(s, r) {
break
}
}
matchedRecords[i] = true // Mark as matched
break
}
Expand Down
4 changes: 2 additions & 2 deletions impl/internal/did/testdata/vector-1-dns-records.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
"name": "_did.cyuoqaf7itop8ohww4yn5ojg13qaq83r9zihgqntc5i9zwrfdfoo.",
"type": "TXT",
"ttl": "7200",
"rdata": "v=0;vm=k0;auth=k0;asm=k0;inv=k0;del=k0"
"rdata": ["v=0;vm=k0;auth=k0;asm=k0;inv=k0;del=k0"]
},
{
"name": "_k0._did.",
"type": "TXT",
"ttl": "7200",
"rdata": "id=0;t=0;k=YCcHYL2sYNPDlKaALcEmll2HHyT968M4UWbr-9CFGWE"
"rdata": ["id=0;t=0;k=YCcHYL2sYNPDlKaALcEmll2HHyT968M4UWbr-9CFGWE"]
}
]
18 changes: 9 additions & 9 deletions impl/internal/did/testdata/vector-2-dns-records.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,54 +3,54 @@
"name": "_did.cyuoqaf7itop8ohww4yn5ojg13qaq83r9zihgqntc5i9zwrfdfoo.",
"type": "NS",
"ttl": "7200",
"rdata": "gateway1.example-did-dht-gateway.com."
"rdata": ["gateway1.example-did-dht-gateway.com."]
},
{
"name": "_did.cyuoqaf7itop8ohww4yn5ojg13qaq83r9zihgqntc5i9zwrfdfoo.",
"type": "NS",
"ttl": "7200",
"rdata": "gateway2.example-did-dht-gateway.com."
"rdata": ["gateway2.example-did-dht-gateway.com."]
},
{
"name": "_did.cyuoqaf7itop8ohww4yn5ojg13qaq83r9zihgqntc5i9zwrfdfoo.",
"type": "TXT",
"ttl": "7200",
"rdata": "v=0;vm=k0,k1;svc=s0;auth=k0;asm=k0,k1;inv=k0,k1;del=k0"
"rdata": ["v=0;vm=k0,k1;auth=k0;asm=k0,k1;inv=k0,k1;del=k0;svc=s0"]
},
{
"name": "_cnt._did.",
"type": "TXT",
"ttl": "7200",
"rdata": "did:example:abcd"
"rdata": ["did:example:abcd"]
},
{
"name": "_aka._did.",
"type": "TXT",
"ttl": "7200",
"rdata": "did:example:efgh,did:example:ijkl"
"rdata": ["did:example:efgh,did:example:ijkl"]
},
{
"name": "_k0._did.",
"type": "TXT",
"ttl": "7200",
"rdata": "id=0;t=0;k=YCcHYL2sYNPDlKaALcEmll2HHyT968M4UWbr-9CFGWE"
"rdata": ["id=0;t=0;k=YCcHYL2sYNPDlKaALcEmll2HHyT968M4UWbr-9CFGWE"]
},
{
"name": "_k1._did.",
"type": "TXT",
"ttl": "7200",
"rdata": "t=1;k=Atf6NCChxjWpnrfPt1WDVE4ipYVSvi4pXCq4SUjx0jT9"
"rdata": ["t=1;k=Atf6NCChxjWpnrfPt1WDVE4ipYVSvi4pXCq4SUjx0jT9"]
},
{
"name": "_s0._did.",
"type": "TXT",
"ttl": "7200",
"rdata": "id=service-1;t=TestService;se=https://test-service.com/1,https://test-service.com/2"
"rdata": ["id=service-1;t=TestService;se=https://test-service.com/1,https://test-service.com/2"]
},
{
"name": "_typ._did.",
"type": "TXT",
"ttl": "7200",
"rdata": "id=1,2,3"
"rdata": ["id=1,2,3"]
}
]
23 changes: 15 additions & 8 deletions impl/internal/did/testdata/vector-3-did-document.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,23 @@
"type": "JsonWebKey",
"controller": "did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy",
"publicKeyJwk": {
"kty": "OKP",
"crv": "Ed25519",
"x": "sTyTLYw-n1NI9X-84NaCuis1wZjAA8lku6f6Et5201g",
"kid": "0",
"alg": "Ed25519",
"kid": "0"
"crv": "Ed25519",
"kty": "OKP",
"x": "sTyTLYw-n1NI9X-84NaCuis1wZjAA8lku6f6Et5201g"
}
},
{
"id": "did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy#WVy5IWMa36AoyAXZDvPd5j9zxt2t-GjifDEV-DwgIdQ",
"type": "JsonWebKey",
"controller": "did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy",
"publicKeyJwk": {
"kty": "OKP",
"crv": "X25519",
"x": "3POE0_i2mGeZ2qiQCA3KcLfi1fZo0311CXFSIwt1nB4",
"kid": "WVy5IWMa36AoyAXZDvPd5j9zxt2t-GjifDEV-DwgIdQ",
"alg": "ECDH-ES+A128KW",
"kid": "WVy5IWMa36AoyAXZDvPd5j9zxt2t-GjifDEV-DwgIdQ"
"crv": "X25519",
"kty": "OKP",
"x": "3POE0_i2mGeZ2qiQCA3KcLfi1fZo0311CXFSIwt1nB4"
}
}
],
Expand All @@ -40,5 +40,12 @@
],
"capabilityDelegation": [
"did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy#0"
],
"service": [
{
"id": "did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy#service-1",
"type": "TestLongService",
"serviceEndpoint": ["https://test-lllllllllllllllllllllllllllllllllllooooooooooooooooooooonnnnnnnnnnnnnnnnnnngggggggggggggggggggggggggggggggggggggsssssssssssssssssssssssssseeeeeeeeeeeeeeeeeeerrrrrrrrrrrrrrrvvvvvvvvvvvvvvvvvvvviiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiccccccccccccccccccccccccccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee.com/1"]
}
]
}
Loading
Loading