Skip to content
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
3 changes: 3 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ export const BASE_CONTEXT = ['https://www.w3.org/ns/did/v1', 'https://w3id.org/s
export const METHOD_PROTOCOL_V0_5 = `did:${METHOD}:0.5`;
export const METHOD_PROTOCOL_V1_0 = `did:${METHOD}:1.0`;

// TTL default
export const DEFAULT_TTL_SECONDS = '3600';

// Method parameter keys
export const METHOD_PARAMETER_KEYS = {
scid: 'scid',
Expand Down
3 changes: 3 additions & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,15 @@ export type DidResolutionError = 'invalidDid' | 'invalidDidUrl' | 'invalidOption

export interface DIDResolutionMeta {
versionId: string;
versionTime: string;
created: string;
updated: string;
previousLogEntryHash?: string;
updateKeys: string[];
scid: string;
prerotation: boolean;
portable: boolean;
ttl?: string;
nextKeyHashes: string[];
deactivated: boolean;
witness?: WitnessParameterResolution;
Expand Down Expand Up @@ -159,6 +161,7 @@ export interface DIDLogEntry {
portable?: boolean;
witness?: WitnessParameter;
watchers?: string[] | null;
ttl?: string | number | null;
deactivated?: boolean;
};
state: DIDDoc;
Expand Down
43 changes: 41 additions & 2 deletions src/method_versions/method.v1.0.resolution.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { documentStateIsValid, hashChainIsValid, newKeysAreInNextKeys, scidIsFromHash } from '../assertions';
import { METHOD_PARAMETER_KEYS, METHOD_PROTOCOL_V0_5, METHOD_PROTOCOL_V1_0, SCID_PLACEHOLDER } from '../constants';
import {
DEFAULT_TTL_SECONDS,
METHOD_PARAMETER_KEYS,
METHOD_PROTOCOL_V0_5,
METHOD_PROTOCOL_V1_0,
SCID_PLACEHOLDER,
} from '../constants';
import { addDefaultDidWebvhServices } from '../did-document';
import type {
DIDDoc,
Expand Down Expand Up @@ -159,7 +165,14 @@ export const resolveV1Log = async (
throw new Error('DID resolution failed: No valid identifier found');
}

if (resolvedSnapshot.meta.deactivated && !hasExplicitHistoricalSelector) {
// Deactivation is a DID-global state. Historical selectors keep the historical
// document, while non-historical resolution nulls the document when deactivated.
if (hasExplicitHistoricalSelector) {
if (resolverContext.meta.deactivated) {
resolvedSnapshot = markResolvedSnapshotDeactivated({ resolvedSnapshot, resolverContext });
}
} else if (resolvedSnapshot.meta.deactivated) {
resolvedSnapshot = markResolvedSnapshotDeactivated({ resolvedSnapshot, resolverContext });
return {
did: resolvedSnapshot.did,
doc: null,
Expand All @@ -178,6 +191,24 @@ export const resolveV1Log = async (
};
};

const markResolvedSnapshotDeactivated = ({
resolvedSnapshot,
resolverContext,
}: {
resolvedSnapshot: ResolutionSnapshot;
resolverContext: ResolverContext;
}): ResolutionSnapshot => {
const nextSnapshot: ResolutionSnapshot = {
...resolvedSnapshot,
meta: {
...resolvedSnapshot.meta,
deactivated: true,
},
};
resolverContext.resolvedSnapshot = nextSnapshot;
return nextSnapshot;
};

const processResolvedLogEntries = async ({
resolverContext,
logEntries,
Expand Down Expand Up @@ -206,6 +237,7 @@ const processResolvedLogEntries = async ({

const previousWitness = resolverContext.meta.witness ? deepClone(resolverContext.meta.witness) : undefined;
resolverContext.meta.versionId = versionId;
resolverContext.meta.versionTime = versionTime;
resolverContext.previousVersionTime = entryContext.currentVersionTime;
resolverContext.meta.updated = versionTime;

Expand Down Expand Up @@ -298,6 +330,7 @@ const createInitialResolverContext = (): ResolverContext => {
return {
meta: {
versionId: '',
versionTime: '',
created: '',
updated: '',
deactivated: false,
Expand Down Expand Up @@ -388,6 +421,8 @@ const processV1GenesisEntry = async ({
// Always set witness: normalize null/undefined to {}, and preserve explicit config
resolverContext.meta.witness = resolvedGenesisWitness ?? {};
resolverContext.meta.watchers = parameters.watchers ?? null;
resolverContext.meta.ttl =
parameters.ttl !== undefined && parameters.ttl !== null ? String(parameters.ttl) : DEFAULT_TTL_SECONDS;

const logEntry = {
versionId: SCID_PLACEHOLDER,
Expand Down Expand Up @@ -520,6 +555,10 @@ const processV1SubsequentEntry = async ({
if (hasOwn(parameters, METHOD_PARAMETER_KEYS.watchers)) {
resolverContext.meta.watchers = parameters.watchers ?? null;
}
if (hasOwn(parameters, METHOD_PARAMETER_KEYS.ttl)) {
resolverContext.meta.ttl =
parameters.ttl !== undefined && parameters.ttl !== null ? String(parameters.ttl) : DEFAULT_TTL_SECONDS;
}

return sourceEntry.state;
};
Expand Down
13 changes: 11 additions & 2 deletions src/method_versions/method.v1.0.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SCID_PLACEHOLDER } from '../constants';
import { DEFAULT_TTL_SECONDS, SCID_PLACEHOLDER } from '../constants';
import { generateParallelDidWeb } from '../did-document';
import type {
CreateDIDInterface,
Expand Down Expand Up @@ -29,9 +29,14 @@ const buildMetaFromEntry = (entry: DIDLogEntry): DIDResolutionMeta => {
const resolvedWitness = resolveWitnessParameter(entry.parameters);
return {
versionId: entry.versionId,
versionTime: entry.versionTime,
created: entry.versionTime,
updated: entry.versionTime,
scid: entry.parameters.scid ?? '',
ttl:
entry.parameters.ttl !== undefined && entry.parameters.ttl !== null
? String(entry.parameters.ttl)
: DEFAULT_TTL_SECONDS,
updateKeys: entry.parameters.updateKeys ?? [],
portable: entry.parameters.portable ?? false,
nextKeyHashes: entry.parameters.nextKeyHashes ?? [],
Expand All @@ -54,13 +59,17 @@ const mergeMetaFromEntry = ({
deactivated?: boolean;
}): DIDResolutionMeta => {
const resolvedNextKeyHashes = nextKeyHashes ?? previousMeta.nextKeyHashes;

return {
...previousMeta,
versionId: entry.versionId,
versionTime: entry.versionTime,
updated: entry.versionTime,
updateKeys: entry.parameters.updateKeys ?? previousMeta.updateKeys,
portable: entry.parameters.portable ?? previousMeta.portable,
ttl:
entry.parameters.ttl !== undefined && entry.parameters.ttl !== null
? String(entry.parameters.ttl)
: previousMeta.ttl,
nextKeyHashes: resolvedNextKeyHashes,
prerotation: resolvedNextKeyHashes.length > 0,
witness: entry.parameters.witness ?? previousMeta.witness,
Expand Down
2 changes: 2 additions & 0 deletions src/resolver-result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ export interface WebvhResolutionMetadata extends DIDResolutionMetadata {
}

export interface WebvhDocumentMetadata extends DIDDocumentMetadata {
versionTime?: string;
scid?: string;
ttl?: string;
updateKeys?: string[];
nextKeyHashes?: string[];
prerotation?: boolean;
Expand Down
47 changes: 47 additions & 0 deletions test/backwards-compat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,53 @@ describe('Backwards Compatibility', () => {
expect(result.didDocumentMetadata.nextKeyHashes).toEqual([]);
});

test('v1.0 log entry with legacy ttl: null resolves and normalizes ttl to default', async () => {
const authKey = await generateTestVerificationMethod('assertionMethod', 'key-1');
const signer = createTestSigner(authKey);
const verifier = createTestVerifier(authKey);

const genesis = await createDID({
address: 'example.com',
signer,
updateKeys: [authKey.publicKeyMultibase!],
verificationMethods: asPublicVerificationMethods(authKey),
verifier,
created: '2024-01-01T00:00:00Z',
});

const previousEntry = genesis.log[0];
const entryWithoutProof: DIDLogEntry = {
versionId: previousEntry.versionId,
versionTime: '2024-01-01T00:00:01Z',
parameters: { ttl: null },
state: previousEntry.state,
};
const entryHash = await deriveHash({ ...entryWithoutProof, versionId: previousEntry.versionId });
const entryToSign: DIDLogEntry = {
...entryWithoutProof,
versionId: `2-${entryHash}`,
};
const proofTemplate = {
type: 'DataIntegrityProof' as const,
cryptosuite: 'eddsa-jcs-2022' as const,
verificationMethod: signer.getVerificationMethodId(),
created: '2024-01-01T00:00:01Z',
proofPurpose: 'assertionMethod' as const,
};
const signedProof = await signer.sign({ document: entryToSign, proof: proofTemplate });

const log1: DIDLog = [
...genesis.log,
{ ...entryToSign, proof: [{ ...proofTemplate, proofValue: signedProof.proofValue }] },
];
expect(log1[1].parameters.ttl).toBeNull();

const result = await resolveDIDFromLog(log1, { verifier });

expect(result.didDocument).not.toBeNull();
expect(result.didDocumentMetadata.ttl).toBe('3600');
});

test('carry-forward omitted updateKeys across multiple v0.5 entries', async () => {
const authKey = await generateTestVerificationMethod('assertionMethod', 'key-1');
const signer = createTestSigner(authKey);
Expand Down
111 changes: 111 additions & 0 deletions test/features.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,69 @@ test('Resolve DID latest', async () => {
expect(meta.versionId!.split('-')[0]).toBe('4');
});

test('Resolver metadata defaults ttl to 3600 when ttl is absent', async () => {
const authKey = await generateTestVerificationMethod();
const verifier = new TestCryptoImplementation({ verificationMethod: authKey });

const created = await createDID({
address: 'example.com',
signer: createTestSigner(authKey),
updateKeys: [authKey.publicKeyMultibase!],
verificationMethods: asPublicVerificationMethods(authKey),
verifier,
});

expect(created.meta.ttl).toBe('3600');

const resolved = await resolveDIDFromLog(created.log, { verifier });
expect(resolved.didDocumentMetadata.ttl).toBe('3600');
});

test('Resolver metadata emits configured ttl as string when ttl is explicitly set in a log entry', async () => {
const authKey = await generateTestVerificationMethod();
const verifier = new TestCryptoImplementation({ verificationMethod: authKey });
const signer = createTestSigner(authKey);

const created = await createDID({
address: 'example.com',
signer,
updateKeys: [authKey.publicKeyMultibase!],
verificationMethods: asPublicVerificationMethods(authKey),
verifier,
created: '2024-01-01T00:00:00Z',
});

const previousEntry = created.log[0];
const entryWithoutProof: DIDLogEntry = {
versionId: previousEntry.versionId,
versionTime: '2024-01-01T00:00:01Z',
parameters: { ttl: 7200 },
state: previousEntry.state,
};
const entryHash = await deriveHash({ ...entryWithoutProof, versionId: previousEntry.versionId });
const entryToSign: DIDLogEntry = {
...entryWithoutProof,
versionId: `2-${entryHash}`,
};
const proofTemplate = {
type: 'DataIntegrityProof' as const,
cryptosuite: 'eddsa-jcs-2022' as const,
verificationMethod: signer.getVerificationMethodId(),
created: '2024-01-01T00:00:01Z',
proofPurpose: 'assertionMethod' as const,
};
const signedProof = await signer.sign({ document: entryToSign, proof: proofTemplate });

const logWithTtl: DIDLog = [
...created.log,
{ ...entryToSign, proof: [{ ...proofTemplate, proofValue: signedProof.proofValue }] },
];
const resolved = await resolveDIDFromLog(logWithTtl, { verifier });

expect(resolved.didResolutionMetadata.error).toBeUndefined();
expect(resolved.didDocumentMetadata.ttl).toBe('7200');
});

test('Normal resolution path augments default #files and #whois services', async () => {
const key = await generateTestVerificationMethod();
const verifier = new TestCryptoImplementation({ verificationMethod: key });
Expand Down Expand Up @@ -262,6 +325,54 @@ test('deactivateDID with pre-rotation active produces a resolvable deactivated l
expect(meta.prerotation).toBe(false);
});

test('Historical versionId on a deactivated log keeps historical document and reports deactivated=true', async () => {
const authKey = await generateTestVerificationMethod();
const verifier = new TestCryptoImplementation({ verificationMethod: authKey });

const created = await createDID({
address: 'example.com',
signer: createTestSigner(authKey),
updateKeys: [authKey.publicKeyMultibase!],
verificationMethods: asPublicVerificationMethods(authKey),
verifier,
created: '2024-01-01T00:00:00Z',
});

const updated1 = await updateDID({
log: created.log,
signer: createTestSigner(authKey),
updateKeys: [authKey.publicKeyMultibase!],
verificationMethods: asPublicVerificationMethods(authKey),
verifier,
updated: '2024-01-01T00:00:01Z',
});

const updated2 = await updateDID({
log: updated1.log,
signer: createTestSigner(authKey),
updateKeys: [authKey.publicKeyMultibase!],
verificationMethods: asPublicVerificationMethods(authKey),
verifier,
updated: '2024-01-01T00:00:02Z',
});

const deactivated = await deactivateDID({
log: updated2.log,
signer: createTestSigner(authKey),
verifier,
});

const historical = await resolveDIDFromLog(deactivated.log, {
versionId: updated1.meta.versionId,
verifier,
});

expect(historical.didDocument).not.toBeNull();
expect(historical.didDocumentMetadata.versionId).toBe(updated1.meta.versionId);
expect(historical.didDocumentMetadata.versionTime).toBe(updated1.meta.versionTime);
expect(historical.didDocumentMetadata.deactivated).toBe(true);
});

test('deactivateDID rejects keys that are not in the prior nextKeyHashes', async () => {
const nextKeyHash = await deriveNextKeyHash(authKey2.publicKeyMultibase!);
const { log: log1 } = await createDID({
Expand Down
2 changes: 2 additions & 0 deletions test/resolver-result.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {

const baseMeta: DIDResolutionMeta = {
versionId: '1-abc',
versionTime: '2023-01-01T00:00:00Z',
created: '2023-01-01T00:00:00Z',
updated: '2023-01-01T00:00:00Z',
deactivated: false,
Expand Down Expand Up @@ -87,6 +88,7 @@ describe('toResolutionResult', () => {
expect(result.didDocument).toEqual(doc);
expect(result.didDocumentMetadata.versionId).toBe('1-abc');
expect((result.didDocumentMetadata as { scid?: string }).scid).toBe('SCID');
expect(result.didDocumentMetadata.versionTime).toBe(baseMeta.versionTime);
expect((result.didDocumentMetadata as { updateKeys?: string[] }).updateKeys).toEqual(['z6Mk...']);
expect(result.didDocumentMetadata.deactivated).toBe(false);
});
Expand Down
Loading