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

Allow object in service endpoint #990

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 23 additions & 8 deletions packages/dids/src/methods/did-dht.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1092,10 +1092,18 @@ export class DidDhtDocument {
// other properties from the decoded TXT record data.
const { id, t, se, ...customProperties } = DidDhtUtils.parseTxtDataToObject(answer.data);

// if multi-values: 'a,b,c' -> ['a', 'b', 'c'], if single-value: 'a' -> ['a']
// NOTE: The service endpoint technically can either be a string or an array of strings,
// we enforce an array for single-value to simplify verification of vector 3 in the spec: https://did-dht.com/#vector-3
const serviceEndpoint = se.includes(VALUE_SEPARATOR) ? se.split(VALUE_SEPARATOR) : [se];
let serviceEndpoint;
try {
serviceEndpoint = Convert.base64Url(se).toObject();
} catch (error) {
// if the service endpoint is not a valid base64url encoded object, then we assume it is
// a string or array of strings separated by commas

// if multi-values: 'a,b,c' -> ['a', 'b', 'c'], if single-value: 'a' -> ['a']
// NOTE: The service endpoint technically can either be a string or an array of strings,
// we enforce an array for single-value to simplify verification of vector 3 in the spec: https://did-dht.com/#vector-3
serviceEndpoint = se.includes(VALUE_SEPARATOR) ? se.split(VALUE_SEPARATOR) : [se];
}

// Convert custom property values to either a string or an array of strings.
const serviceProperties = Object.fromEntries(Object.entries(customProperties).map(
Expand All @@ -1107,9 +1115,9 @@ export class DidDhtDocument {

didDocument.service.push({
...serviceProperties,
id : `${didUri}#${id}`,
type : t,
serviceEndpoint
id : `${didUri}#${id}`,
type : t,
serviceEndpoint : serviceEndpoint
});

break;
Expand Down Expand Up @@ -1268,7 +1276,14 @@ export class DidDhtDocument {
serviceIds.push(dnsRecordId);
let { id, type: t, serviceEndpoint: se, ...customProperties } = service;
id = extractDidFragment(id)!;
se = Array.isArray(se) ? se.join(',') : se;

if (typeof se === 'object') {
if (Array.isArray(se)) {
se = se.join(',');
} else {
se = Convert.object(se).toBase64Url();
}
}

// Define the data for the DNS TXT record.
const txtData = Object.entries({ id, t, se, ...customProperties }).map(
Expand Down
290 changes: 152 additions & 138 deletions packages/dids/tests/methods/did-dht.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import officialTestVector3 from '../fixtures/test-vectors/did-dht/vector-3.json'

import { Answer } from '@dnsquery/dns-packet';
import { Convert } from '@web5/common';
import { DidDocument } from '../../src/index.js';
import { DidDocument, DidService } from '../../src/index.js';
import { DidErrorCode } from '../../src/did-error.js';
import { DidDht, DidDhtDocument, DidDhtRegisteredDidType, DidDhtUtils } from '../../src/methods/did-dht.js';
import { expect, use } from 'chai';
Expand Down Expand Up @@ -915,78 +915,94 @@ describe('DidDhtDocument', () => {
});

describe('DNS Packet e2e tests', () => {
it('handles custom array for services', async () => {
let didUri: string;
let didDocument: DidDocument;

const didUri = 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery';
const dnsPacket = await DidDhtDocument.toDnsPacket({
didDocument: {
id : didUri,
verificationMethod : [
{
id : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#0',
type : 'JsonWebKey',
controller : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery',
publicKeyJwk : {
crv : 'Ed25519',
kty : 'OKP',
x : '2zHGF5m_DhcPbBZB6ooIxIOR-Vw-yJVYSPo2NgCMkgg',
kid : 'KDT9PKj4_z7gPk2s279Y-OGlMtt_L93oJzIaiVrrySU',
alg : 'EdDSA',
},
},
{
id : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#sig',
type : 'JsonWebKey',
controller : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery',
publicKeyJwk : {
crv : 'Ed25519',
kty : 'OKP',
x : 'FrrBhqvAWxE4lstj-IWgN8_5-O4L1KuZjdNjn5bX_dw',
kid : 'dRnxo2XQ7QT1is5WmpEefwEz3z4_4JdpGea6KWUn3ww',
alg : 'EdDSA',
},
beforeEach(() => {
didUri = 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery';
didDocument = {
id : didUri,
verificationMethod : [
{
id : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#0',
type : 'JsonWebKey',
controller : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery',
publicKeyJwk : {
crv : 'Ed25519',
kty : 'OKP',
x : '2zHGF5m_DhcPbBZB6ooIxIOR-Vw-yJVYSPo2NgCMkgg',
kid : 'KDT9PKj4_z7gPk2s279Y-OGlMtt_L93oJzIaiVrrySU',
alg : 'EdDSA',
},
{
id : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#enc',
type : 'JsonWebKey',
controller : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery',
publicKeyJwk : {
kty : 'EC',
crv : 'secp256k1',
x : 'e1_pCWZwI9cxdrotVKIT8t75itk22XkpalDPx7pVpYQ',
y : '5cAlBmnzzuwRNuFtLhyFNdy9v1rVEqEgrFEiiwKMx5I',
kid : 'jGYs9XgQMDH_PCDFWocTN0F06mTUOA1J1McVvluq4lM',
alg : 'ES256K',
},
},
{
id : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#sig',
type : 'JsonWebKey',
controller : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery',
publicKeyJwk : {
crv : 'Ed25519',
kty : 'OKP',
x : 'FrrBhqvAWxE4lstj-IWgN8_5-O4L1KuZjdNjn5bX_dw',
kid : 'dRnxo2XQ7QT1is5WmpEefwEz3z4_4JdpGea6KWUn3ww',
alg : 'EdDSA',
},
],
authentication: [
'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#0',
'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#sig',
],
assertionMethod: [
'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#0',
'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#sig',
],
capabilityDelegation: [
'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#0',
],
capabilityInvocation: [
'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#0',
],
keyAgreement: [
'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#enc',
],
service: [
{
id : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#dwn',
type : 'DecentralizedWebNode',
serviceEndpoint : 'https://example.com/dwn',
enc : '#enc',
sig : ['#sig', '#0'],
},
{
id : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#enc',
type : 'JsonWebKey',
controller : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery',
publicKeyJwk : {
kty : 'EC',
crv : 'secp256k1',
x : 'e1_pCWZwI9cxdrotVKIT8t75itk22XkpalDPx7pVpYQ',
y : '5cAlBmnzzuwRNuFtLhyFNdy9v1rVEqEgrFEiiwKMx5I',
kid : 'jGYs9XgQMDH_PCDFWocTN0F06mTUOA1J1McVvluq4lM',
alg : 'ES256K',
},
],
},
],
authentication: [
'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#0',
'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#sig',
],
assertionMethod: [
'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#0',
'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#sig',
],
capabilityDelegation: [
'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#0',
],
capabilityInvocation: [
'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#0',
],
keyAgreement: [
'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#enc',
],
service: [
{
id : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#dwn',
type : 'DecentralizedWebNode',
serviceEndpoint : 'https://example.com/dwn',
enc : '#enc',
sig : '#sig',
},
] as DidService[],
} satisfies DidDocument;
});

it('handles custom array for services', async () => {
didDocument.service = [
{
id : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#dwn',
type : 'DecentralizedWebNode',
serviceEndpoint : 'https://example.com/dwn',
enc : '#enc',
sig : ['#sig', '#0'],
},
];

const dnsPacket = await DidDhtDocument.toDnsPacket({
didDocument,
didMetadata: {
published: false,
}
Expand Down Expand Up @@ -1014,77 +1030,8 @@ describe('DidDhtDocument', () => {
});

it('handles custom string properties for services', async () => {
const didUri = 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery';

const dnsPacket = await DidDhtDocument.toDnsPacket({
didDocument: {
id : didUri,
verificationMethod : [
{
id : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#0',
type : 'JsonWebKey',
controller : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery',
publicKeyJwk : {
crv : 'Ed25519',
kty : 'OKP',
x : '2zHGF5m_DhcPbBZB6ooIxIOR-Vw-yJVYSPo2NgCMkgg',
kid : 'KDT9PKj4_z7gPk2s279Y-OGlMtt_L93oJzIaiVrrySU',
alg : 'EdDSA',
},
},
{
id : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#sig',
type : 'JsonWebKey',
controller : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery',
publicKeyJwk : {
crv : 'Ed25519',
kty : 'OKP',
x : 'FrrBhqvAWxE4lstj-IWgN8_5-O4L1KuZjdNjn5bX_dw',
kid : 'dRnxo2XQ7QT1is5WmpEefwEz3z4_4JdpGea6KWUn3ww',
alg : 'EdDSA',
},
},
{
id : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#enc',
type : 'JsonWebKey',
controller : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery',
publicKeyJwk : {
kty : 'EC',
crv : 'secp256k1',
x : 'e1_pCWZwI9cxdrotVKIT8t75itk22XkpalDPx7pVpYQ',
y : '5cAlBmnzzuwRNuFtLhyFNdy9v1rVEqEgrFEiiwKMx5I',
kid : 'jGYs9XgQMDH_PCDFWocTN0F06mTUOA1J1McVvluq4lM',
alg : 'ES256K',
},
},
],
authentication: [
'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#0',
'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#sig',
],
assertionMethod: [
'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#0',
'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#sig',
],
capabilityDelegation: [
'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#0',
],
capabilityInvocation: [
'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#0',
],
keyAgreement: [
'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#enc',
],
service: [
{
id : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#dwn',
type : 'DecentralizedWebNode',
serviceEndpoint : 'https://example.com/dwn',
enc : '#enc',
sig : '#sig',
},
],
},
didDocument,
didMetadata: {
published: false,
}
Expand All @@ -1111,6 +1058,73 @@ describe('DidDhtDocument', () => {
expect(didResolutionResult.didDocument!.service![0].enc).to.equal('#enc');
});

it('handles serviceEndpoint as an array of strings in services', async () => {
didDocument.service = [
{
id : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#dwn',
type : 'DecentralizedWebNode',
serviceEndpoint : ['https://example.com/dwn', 'https://example.com/dwn2'],
},
];

const dnsPacket = await DidDhtDocument.toDnsPacket({
didDocument,
didMetadata: {
published: false,
}
});

for (const record of dnsPacket.answers ?? []) {
if (record.name.startsWith('_s')) {
expect(record.data).to.include('id=dwn');
expect(record.data).to.include('t=DecentralizedWebNode');
expect(record.data).to.include('se=https://example.com/dwn,https://example.com/dwn2');
}
}

const didResolutionResult = await DidDhtDocument.fromDnsPacket({ didUri, dnsPacket });

expect(didResolutionResult).to.have.property('didDocument');
expect(didResolutionResult).to.have.property('didDocumentMetadata');
expect(didResolutionResult).to.have.property('didResolutionMetadata');

expect(didResolutionResult.didDocument).to.have.property('id', didUri);
expect(didResolutionResult.didDocument!.service![0].serviceEndpoint).to.deep.equal(['https://example.com/dwn', 'https://example.com/dwn2']);
});

it('handles serviceEndpoint as an object in services', async () => {
didDocument.service= [
{
id : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#didcomm',
type : 'DIDCommMessaging',
serviceEndpoint : { uri: 'https://example.com/dwn', routingKeys: ['did:example:somemediator#somekey'] },
},
];

const dnsPacket = await DidDhtDocument.toDnsPacket({
didDocument,
didMetadata: {
published: false,
}});

for (const record of dnsPacket.answers ?? []) {
if (record.name.startsWith('_s')) {
expect(record.data).to.include('id=didcomm');
expect(record.data).to.include('t=DIDCommMessaging');
expect(record.data).to.include('se=eyJ1cmkiOiJodHRwczovL2V4YW1wbGUuY29tL2R3biIsInJvdXRpbmdLZXlzIjpbImRpZDpleGFtcGxlOnNvbWVtZWRpYXRvciNzb21la2V5Il19');
}
}

const didResolutionResult = await DidDhtDocument.fromDnsPacket({ didUri, dnsPacket });

expect(didResolutionResult).to.have.property('didDocument');
expect(didResolutionResult).to.have.property('didDocumentMetadata');
expect(didResolutionResult).to.have.property('didResolutionMetadata');

expect(didResolutionResult.didDocument).to.have.property('id', didUri);
expect(didResolutionResult.didDocument!.service![0].serviceEndpoint).to.deep.equal({ uri: 'https://example.com/dwn', routingKeys: ['did:example:somemediator#somekey'] });
});

it('handles user defined Key Ids', async () => {
const didUri = 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery';
const dnsPacket = await DidDhtDocument.toDnsPacket({
Expand Down
Loading