From a1c57914569107d4c4ada73a4e5c85d685eb446c Mon Sep 17 00:00:00 2001 From: Daniel N <2color@users.noreply.github.com> Date: Thu, 19 Jun 2025 13:44:19 +0200 Subject: [PATCH 1/5] feat: add validateValidity helper --- src/validator.ts | 42 +++++++++++++++++++++++++++++++++--- test/validator.spec.ts | 48 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 86 insertions(+), 4 deletions(-) diff --git a/src/validator.ts b/src/validator.ts index 9427120..4e420eb 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -2,10 +2,24 @@ import { publicKeyFromMultihash } from '@libp2p/crypto/keys' import { logger } from '@libp2p/logger' import NanoDate from 'timestamp-nano' import { equals as uint8ArrayEquals } from 'uint8arrays/equals' -import { InvalidEmbeddedPublicKeyError, RecordExpiredError, RecordTooLargeError, SignatureVerificationError, UnsupportedValidityError } from './errors.js' +import { + InvalidEmbeddedPublicKeyError, + RecordExpiredError, + RecordTooLargeError, + SignatureVerificationError, + UnsupportedValidityError, +} from './errors.js' import { IpnsEntry } from './pb/ipns.js' -import { extractPublicKeyFromIPNSRecord, ipnsRecordDataForV2Sig, isCodec, multihashFromIPNSRoutingKey, multihashToIPNSRoutingKey, unmarshalIPNSRecord } from './utils.js' +import { + extractPublicKeyFromIPNSRecord, + ipnsRecordDataForV2Sig, + isCodec, + multihashFromIPNSRoutingKey, + multihashToIPNSRoutingKey, + unmarshalIPNSRecord, +} from './utils.js' import type { PublicKey } from '@libp2p/interface' +import type { IPNSRecord } from './index.js' const log = logger('ipns:validator') @@ -18,7 +32,7 @@ const MAX_RECORD_SIZE = 1024 * 10 * Validates the given IPNS Record against the given public key. We need a "raw" * record in order to be able to access to all of its fields. */ -export const validate = async (publicKey: PublicKey, marshalledRecord: Uint8Array): Promise => { +export async function validate (publicKey: PublicKey, marshalledRecord: Uint8Array): Promise { // unmarshal ensures that (1) SignatureV2 and Data are present, (2) that ValidityType // and Validity are of valid types and have a value, (3) that CBOR data matches protobuf // if it's a V1+V2 record. @@ -92,3 +106,25 @@ export async function ipnsValidator (routingKey: Uint8Array, marshalledRecord: U // Record validation await validate(recordPubKey, marshalledRecord) } + +/** + * Validates the EOL validity of the given IPNS record. + * + * @param record - The IPNS record to validate. + * @returns True if the validity is valid, false otherwise. + */ +export async function isValidityValid (record: IPNSRecord): Promise { + if (record.validityType !== IpnsEntry.ValidityType.EOL) { + return false + } + + if (record.validity == null) { + return false + } + + if (NanoDate.fromString(record.validity).toDate().getTime() < Date.now()) { + return false + } + + return true +} diff --git a/test/validator.spec.ts b/test/validator.spec.ts index 673d443..ee5fd42 100644 --- a/test/validator.spec.ts +++ b/test/validator.spec.ts @@ -6,7 +6,7 @@ import { expect } from 'aegir/chai' import { toString as uint8ArrayToString } from 'uint8arrays/to-string' import { InvalidEmbeddedPublicKeyError, RecordTooLargeError, SignatureVerificationError } from '../src/errors.js' import { createIPNSRecord, marshalIPNSRecord, multihashToIPNSRoutingKey } from '../src/index.js' -import { ipnsValidator } from '../src/validator.js' +import { ipnsValidator, isValidityValid } from '../src/validator.js' import type { PrivateKey } from '@libp2p/interface' describe('validator', function () { @@ -91,4 +91,50 @@ describe('validator', function () { await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected() .with.property('name', RecordTooLargeError.name) }) + + describe('isValidityValid', () => { + it('should return true for a valid EOL record with future expiration', async () => { + const sequence = 0 + const validity = 1000000 + const record = await createIPNSRecord(privateKey1, contentPath, sequence, validity, { v1Compatible: false }) + + const result = await isValidityValid(record) + + expect(result).to.be.true + }) + + + it('should return false for a record with null validity', async () => { + const record = await createIPNSRecord(privateKey1, contentPath, 0, 1000000, { v1Compatible: false }) + // Manually override validity to null + record.validity = null as any + + const result = await isValidityValid(record) + + expect(result).to.be.false + }) + + it('should return false for an expired record', async () => { + const sequence = 0 + const validity = 1000000 + const record = await createIPNSRecord(privateKey1, contentPath, sequence, validity, { v1Compatible: false }) + + // Manually set validity to a past date + record.validity = '2020-01-01T00:00:00.000000000Z' + + const result = await isValidityValid(record) + + expect(result).to.be.false + }) + + it('should return true for a V1+V2 record with valid EOL validity', async () => { + const sequence = 0 + const validity = 1000000 + const record = await createIPNSRecord(privateKey1, contentPath, sequence, validity, { v1Compatible: true }) + + const result = await isValidityValid(record) + + expect(result).to.be.true + }) + }) }) From 17c695d2203866a558df6a7aac37274a8e44219b Mon Sep 17 00:00:00 2001 From: Daniel N <2color@users.noreply.github.com> Date: Thu, 19 Jun 2025 13:44:35 +0200 Subject: [PATCH 2/5] feat: export protobuf functions --- package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/package.json b/package.json index 7e41cae..b615121 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,10 @@ "./validator": { "types": "./dist/src/validator.d.ts", "import": "./dist/src/validator.js" + }, + "./pb": { + "types": "./dist/src/pb/ipns.d.ts", + "import": "./dist/src/pb/ipns.js" } }, "eslintConfig": { From c06ebab6276fc9ee37e31f4d4f07dea5352d3c76 Mon Sep 17 00:00:00 2001 From: Daniel N <2color@users.noreply.github.com> Date: Thu, 19 Jun 2025 14:16:43 +0200 Subject: [PATCH 3/5] feat: change validation helper to return interval --- src/validator.ts | 21 +++++++++------ test/validator.spec.ts | 59 +++++++++++++++++++----------------------- 2 files changed, 39 insertions(+), 41 deletions(-) diff --git a/src/validator.ts b/src/validator.ts index 4e420eb..d15d642 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -107,24 +107,29 @@ export async function ipnsValidator (routingKey: Uint8Array, marshalledRecord: U await validate(recordPubKey, marshalledRecord) } + /** - * Validates the EOL validity of the given IPNS record. + * Returns the number of milliseconds until the record expires. + * If the record is already expired, throws an error. * * @param record - The IPNS record to validate. - * @returns True if the validity is valid, false otherwise. + * @returns The number of milliseconds until the record expires. */ -export async function isValidityValid (record: IPNSRecord): Promise { +export function validFor (record: IPNSRecord): number { if (record.validityType !== IpnsEntry.ValidityType.EOL) { - return false + throw new UnsupportedValidityError() } if (record.validity == null) { - return false + throw new UnsupportedValidityError() } - if (NanoDate.fromString(record.validity).toDate().getTime() < Date.now()) { - return false + const validUntil = NanoDate.fromString(record.validity).toDate().getTime() + const now = Date.now() + + if (validUntil < now) { + throw new RecordExpiredError('The record has expired') } - return true + return validUntil - now } diff --git a/test/validator.spec.ts b/test/validator.spec.ts index ee5fd42..697a472 100644 --- a/test/validator.spec.ts +++ b/test/validator.spec.ts @@ -4,9 +4,9 @@ import { randomBytes } from '@libp2p/crypto' import { generateKeyPair, publicKeyToProtobuf } from '@libp2p/crypto/keys' import { expect } from 'aegir/chai' import { toString as uint8ArrayToString } from 'uint8arrays/to-string' -import { InvalidEmbeddedPublicKeyError, RecordTooLargeError, SignatureVerificationError } from '../src/errors.js' +import { InvalidEmbeddedPublicKeyError, RecordTooLargeError, SignatureVerificationError, RecordExpiredError, UnsupportedValidityError } from '../src/errors.js' import { createIPNSRecord, marshalIPNSRecord, multihashToIPNSRoutingKey } from '../src/index.js' -import { ipnsValidator, isValidityValid } from '../src/validator.js' +import { ipnsValidator, validFor } from '../src/validator.js' import type { PrivateKey } from '@libp2p/interface' describe('validator', function () { @@ -92,49 +92,42 @@ describe('validator', function () { .with.property('name', RecordTooLargeError.name) }) - describe('isValidityValid', () => { - it('should return true for a valid EOL record with future expiration', async () => { - const sequence = 0 - const validity = 1000000 - const record = await createIPNSRecord(privateKey1, contentPath, sequence, validity, { v1Compatible: false }) - - const result = await isValidityValid(record) - - expect(result).to.be.true + describe('validFor', () => { + it('should return the number of milliseconds until the record expires', async () => { + const record = await createIPNSRecord(privateKey1, contentPath, 0, 1000000) + const result = validFor(record) + expect(result).to.be.greaterThan(0) }) + it.only('should throw RecordExpiredError for expired records', async () => { + const record = await createIPNSRecord(privateKey1, contentPath, 0, 0) - it('should return false for a record with null validity', async () => { - const record = await createIPNSRecord(privateKey1, contentPath, 0, 1000000, { v1Compatible: false }) - // Manually override validity to null - record.validity = null as any - - const result = await isValidityValid(record) - - expect(result).to.be.false + expect(() => validFor(record)).to.throw(RecordExpiredError) }) - it('should return false for an expired record', async () => { - const sequence = 0 - const validity = 1000000 - const record = await createIPNSRecord(privateKey1, contentPath, sequence, validity, { v1Compatible: false }) + it('should throw UnsupportedValidityError for non-EOL validity types', async () => { + const record = await createIPNSRecord(privateKey1, contentPath, 0, 1000000) + record.validityType = 5 as any - // Manually set validity to a past date - record.validity = '2020-01-01T00:00:00.000000000Z' + expect(() => validFor(record)).to.throw(UnsupportedValidityError) + }) - const result = await isValidityValid(record) + it('should throw UnsupportedValidityError for null validity', async () => { + const record = await createIPNSRecord(privateKey1, contentPath, 0, 1000000) + record.validityType = null as any - expect(result).to.be.false + expect(() => validFor(record)).to.throw(UnsupportedValidityError) }) - it('should return true for a V1+V2 record with valid EOL validity', async () => { - const sequence = 0 - const validity = 1000000 - const record = await createIPNSRecord(privateKey1, contentPath, sequence, validity, { v1Compatible: true }) + it('should return correct milliseconds until expiration', async () => { + const futureTime = Date.now() + 5000 // 5 seconds from now + const record = await createIPNSRecord(privateKey1, contentPath, 0, futureTime) - const result = await isValidityValid(record) + const result = validFor(record) - expect(result).to.be.true + // Should be approximately 5000ms (within 100ms tolerance for test execution time) + expect(result).to.be.within(5000, 5100) + expect(result).to.be.greaterThan(0) }) }) }) From 80f6677ad03ac7231eedd1879a793ce88c821eae Mon Sep 17 00:00:00 2001 From: Daniel N <2color@users.noreply.github.com> Date: Thu, 19 Jun 2025 14:19:14 +0200 Subject: [PATCH 4/5] test: fix wrong test --- test/validator.spec.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/test/validator.spec.ts b/test/validator.spec.ts index 697a472..68a6c0b 100644 --- a/test/validator.spec.ts +++ b/test/validator.spec.ts @@ -99,7 +99,7 @@ describe('validator', function () { expect(result).to.be.greaterThan(0) }) - it.only('should throw RecordExpiredError for expired records', async () => { + it('should throw RecordExpiredError for expired records', async () => { const record = await createIPNSRecord(privateKey1, contentPath, 0, 0) expect(() => validFor(record)).to.throw(RecordExpiredError) @@ -120,14 +120,11 @@ describe('validator', function () { }) it('should return correct milliseconds until expiration', async () => { - const futureTime = Date.now() + 5000 // 5 seconds from now - const record = await createIPNSRecord(privateKey1, contentPath, 0, futureTime) + const record = await createIPNSRecord(privateKey1, contentPath, 0, 5000) const result = validFor(record) - // Should be approximately 5000ms (within 100ms tolerance for test execution time) - expect(result).to.be.within(5000, 5100) - expect(result).to.be.greaterThan(0) + expect(result).to.be.within(4900, 5000) }) }) }) From ac09937a20b59194f7061fa5316a833f024ea3bc Mon Sep 17 00:00:00 2001 From: Daniel N <2color@users.noreply.github.com> Date: Thu, 19 Jun 2025 14:22:33 +0200 Subject: [PATCH 5/5] chore: eslint fix --- src/validator.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/validator.ts b/src/validator.ts index d15d642..f217da9 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -7,7 +7,7 @@ import { RecordExpiredError, RecordTooLargeError, SignatureVerificationError, - UnsupportedValidityError, + UnsupportedValidityError } from './errors.js' import { IpnsEntry } from './pb/ipns.js' import { @@ -16,10 +16,10 @@ import { isCodec, multihashFromIPNSRoutingKey, multihashToIPNSRoutingKey, - unmarshalIPNSRecord, + unmarshalIPNSRecord } from './utils.js' -import type { PublicKey } from '@libp2p/interface' import type { IPNSRecord } from './index.js' +import type { PublicKey } from '@libp2p/interface' const log = logger('ipns:validator') @@ -107,7 +107,6 @@ export async function ipnsValidator (routingKey: Uint8Array, marshalledRecord: U await validate(recordPubKey, marshalledRecord) } - /** * Returns the number of milliseconds until the record expires. * If the record is already expired, throws an error.