From c987c3272f2b3615415206eb1b070defc3259b02 Mon Sep 17 00:00:00 2001 From: Xinyu Ma Date: Tue, 30 Jan 2024 02:08:30 -0800 Subject: [PATCH] Add test for CertStorage --- pnpm-lock.yaml | 43 ++-- src/namespace/name-pattern.test.ts | 41 +++ src/namespace/name-pattern.ts | 147 ++++++++++- src/namespace/schema-tree.ts | 17 ++ src/security/cert-storage.test.ts | 389 +++++++++++++++++++++++++++++ src/security/cert-storage.ts | 7 +- src/test-fixture/bridge.ts | 118 --------- src/test-fixture/mod.ts | 1 - src/utils/name-lit.test.ts | 2 +- 9 files changed, 622 insertions(+), 143 deletions(-) create mode 100644 src/namespace/name-pattern.test.ts create mode 100644 src/namespace/schema-tree.ts create mode 100644 src/security/cert-storage.test.ts delete mode 100644 src/test-fixture/bridge.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 24e0680..8fcb72f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -97,6 +97,10 @@ packages: selderee: 0.11.0 dev: true + /@shigen/polyfill-symbol-dispose@1.0.1: + resolution: {integrity: sha512-g8OSf0SiNNRojr+2PJT8j1yj+TPt7mDCYbjYWQMVOeFWcrDC9NEM87tElI/e6REpykSsKckoup8v0tcquQ6K5w==} + dev: true + /@types/imap@0.8.40: resolution: {integrity: sha512-kWFwOc88CGwWZlHqCnZiceS6EralsAHdjpQyk1+fIA875NQdIHvLpdD5NU3Pi1yZ8FKFdOF81UDNAo8/XS6HiQ==} dependencies: @@ -722,7 +726,7 @@ packages: '@ndnts-nightly.ndn.today/endpoint.tgz': resolution: {tarball: https://ndnts-nightly.ndn.today/endpoint.tgz} name: '@ndn/endpoint' - version: 0.0.20240124-nightly-37c6054 + version: 0.0.20240129-nightly-7e86421 dependencies: '@ndn/fw': '@ndnts-nightly.ndn.today/fw.tgz' '@ndn/packet': '@ndnts-nightly.ndn.today/packet.tgz' @@ -735,7 +739,7 @@ packages: '@ndnts-nightly.ndn.today/fw.tgz': resolution: {tarball: https://ndnts-nightly.ndn.today/fw.tgz} name: '@ndn/fw' - version: 0.0.20240124-nightly-37c6054 + version: 0.0.20240129-nightly-7e86421 dependencies: '@ndn/packet': '@ndnts-nightly.ndn.today/packet.tgz' '@ndn/util': '@ndnts-nightly.ndn.today/util.tgz' @@ -752,7 +756,7 @@ packages: '@ndnts-nightly.ndn.today/keychain.tgz': resolution: {tarball: https://ndnts-nightly.ndn.today/keychain.tgz} name: '@ndn/keychain' - version: 0.0.20240124-nightly-37c6054 + version: 0.0.20240129-nightly-7e86421 dependencies: '@ndn/naming-convention2': '@ndnts-nightly.ndn.today/naming-convention2.tgz' '@ndn/packet': '@ndnts-nightly.ndn.today/packet.tgz' @@ -769,7 +773,7 @@ packages: '@ndnts-nightly.ndn.today/l3face.tgz': resolution: {tarball: https://ndnts-nightly.ndn.today/l3face.tgz} name: '@ndn/l3face' - version: 0.0.20240124-nightly-37c6054 + version: 0.0.20240129-nightly-7e86421 dependencies: '@ndn/fw': '@ndnts-nightly.ndn.today/fw.tgz' '@ndn/lp': '@ndnts-nightly.ndn.today/lp.tgz' @@ -789,7 +793,7 @@ packages: '@ndnts-nightly.ndn.today/lp.tgz': resolution: {tarball: https://ndnts-nightly.ndn.today/lp.tgz} name: '@ndn/lp' - version: 0.0.20240124-nightly-37c6054 + version: 0.0.20240129-nightly-7e86421 dependencies: '@ndn/packet': '@ndnts-nightly.ndn.today/packet.tgz' '@ndn/tlv': '@ndnts-nightly.ndn.today/tlv.tgz' @@ -801,7 +805,7 @@ packages: '@ndnts-nightly.ndn.today/naming-convention2.tgz': resolution: {tarball: https://ndnts-nightly.ndn.today/naming-convention2.tgz} name: '@ndn/naming-convention2' - version: 0.0.20240124-nightly-37c6054 + version: 0.0.20240129-nightly-7e86421 dependencies: '@ndn/packet': '@ndnts-nightly.ndn.today/packet.tgz' '@ndn/tlv': '@ndnts-nightly.ndn.today/tlv.tgz' @@ -812,7 +816,7 @@ packages: '@ndnts-nightly.ndn.today/ndncert.tgz': resolution: {tarball: https://ndnts-nightly.ndn.today/ndncert.tgz} name: '@ndn/ndncert' - version: 0.0.20240124-nightly-37c6054 + version: 0.0.20240129-nightly-7e86421 dependencies: '@ndn/endpoint': '@ndnts-nightly.ndn.today/endpoint.tgz' '@ndn/keychain': '@ndnts-nightly.ndn.today/keychain.tgz' @@ -836,7 +840,7 @@ packages: '@ndnts-nightly.ndn.today/ndnsec.tgz': resolution: {tarball: https://ndnts-nightly.ndn.today/ndnsec.tgz} name: '@ndn/ndnsec' - version: 0.0.20240124-nightly-37c6054 + version: 0.0.20240129-nightly-7e86421 dependencies: '@ndn/keychain': '@ndnts-nightly.ndn.today/keychain.tgz' '@ndn/packet': '@ndnts-nightly.ndn.today/packet.tgz' @@ -851,7 +855,7 @@ packages: '@ndnts-nightly.ndn.today/nfdmgmt.tgz': resolution: {tarball: https://ndnts-nightly.ndn.today/nfdmgmt.tgz} name: '@ndn/nfdmgmt' - version: 0.0.20240124-nightly-37c6054 + version: 0.0.20240129-nightly-7e86421 dependencies: '@ndn/endpoint': '@ndnts-nightly.ndn.today/endpoint.tgz' '@ndn/fw': '@ndnts-nightly.ndn.today/fw.tgz' @@ -867,7 +871,7 @@ packages: '@ndnts-nightly.ndn.today/node-transport.tgz': resolution: {tarball: https://ndnts-nightly.ndn.today/node-transport.tgz} name: '@ndn/node-transport' - version: 0.0.20240124-nightly-37c6054 + version: 0.0.20240129-nightly-7e86421 dependencies: '@ndn/l3face': '@ndnts-nightly.ndn.today/l3face.tgz' event-iterator: 2.0.0 @@ -880,7 +884,7 @@ packages: '@ndnts-nightly.ndn.today/packet.tgz': resolution: {tarball: https://ndnts-nightly.ndn.today/packet.tgz} name: '@ndn/packet' - version: 0.0.20240124-nightly-37c6054 + version: 0.0.20240129-nightly-7e86421 dependencies: '@ndn/tlv': '@ndnts-nightly.ndn.today/tlv.tgz' '@ndn/util': '@ndnts-nightly.ndn.today/util.tgz' @@ -893,7 +897,7 @@ packages: '@ndnts-nightly.ndn.today/rdr.tgz': resolution: {tarball: https://ndnts-nightly.ndn.today/rdr.tgz} name: '@ndn/rdr' - version: 0.0.20240124-nightly-37c6054 + version: 0.0.20240129-nightly-7e86421 dependencies: '@ndn/endpoint': '@ndnts-nightly.ndn.today/endpoint.tgz' '@ndn/naming-convention2': '@ndnts-nightly.ndn.today/naming-convention2.tgz' @@ -905,7 +909,7 @@ packages: '@ndnts-nightly.ndn.today/repo-api.tgz': resolution: {tarball: https://ndnts-nightly.ndn.today/repo-api.tgz} name: '@ndn/repo-api' - version: 0.0.20240124-nightly-37c6054 + version: 0.0.20240129-nightly-7e86421 dependencies: '@ndn/l3face': '@ndnts-nightly.ndn.today/l3face.tgz' '@ndn/naming-convention2': '@ndnts-nightly.ndn.today/naming-convention2.tgz' @@ -926,7 +930,7 @@ packages: '@ndnts-nightly.ndn.today/segmented-object.tgz': resolution: {tarball: https://ndnts-nightly.ndn.today/segmented-object.tgz} name: '@ndn/segmented-object' - version: 0.0.20240124-nightly-37c6054 + version: 0.0.20240129-nightly-7e86421 dependencies: '@ndn/endpoint': '@ndnts-nightly.ndn.today/endpoint.tgz' '@ndn/fw': '@ndnts-nightly.ndn.today/fw.tgz' @@ -947,7 +951,7 @@ packages: '@ndnts-nightly.ndn.today/sync.tgz': resolution: {tarball: https://ndnts-nightly.ndn.today/sync.tgz} name: '@ndn/sync' - version: 0.0.20240124-nightly-37c6054 + version: 0.0.20240129-nightly-7e86421 dependencies: '@ndn/endpoint': '@ndnts-nightly.ndn.today/endpoint.tgz' '@ndn/naming-convention2': '@ndnts-nightly.ndn.today/naming-convention2.tgz' @@ -971,7 +975,7 @@ packages: '@ndnts-nightly.ndn.today/tlv.tgz': resolution: {tarball: https://ndnts-nightly.ndn.today/tlv.tgz} name: '@ndn/tlv' - version: 0.0.20240124-nightly-37c6054 + version: 0.0.20240129-nightly-7e86421 dependencies: '@ndn/util': '@ndnts-nightly.ndn.today/util.tgz' mnemonist: 0.39.7 @@ -982,9 +986,10 @@ packages: '@ndnts-nightly.ndn.today/util.tgz': resolution: {tarball: https://ndnts-nightly.ndn.today/util.tgz} name: '@ndn/util' - version: 0.0.20240124-nightly-37c6054 - engines: {node: ^18.18.0 || ^20.0.0 || ^21.0.0} + version: 0.0.20240129-nightly-7e86421 + engines: {node: ^18.18.0 || ^20.4.0 || ^21.0.0} dependencies: + '@shigen/polyfill-symbol-dispose': 1.0.1 '@types/minimalistic-assert': 1.0.3 minimalistic-assert: 1.0.1 streaming-iterables: 8.0.1 @@ -994,7 +999,7 @@ packages: '@ndnts-nightly.ndn.today/ws-transport.tgz': resolution: {tarball: https://ndnts-nightly.ndn.today/ws-transport.tgz} name: '@ndn/ws-transport' - version: 0.0.20240124-nightly-37c6054 + version: 0.0.20240129-nightly-7e86421 dependencies: '@ndn/l3face': '@ndnts-nightly.ndn.today/l3face.tgz' '@types/ws': 8.5.10 diff --git a/src/namespace/name-pattern.test.ts b/src/namespace/name-pattern.test.ts new file mode 100644 index 0000000..135fec3 --- /dev/null +++ b/src/namespace/name-pattern.test.ts @@ -0,0 +1,41 @@ +import { Component } from '@ndn/packet'; +import { assert as assertMod } from '../dep.ts'; +import { name } from '../utils/mod.ts'; +import * as namePattern from './name-pattern.ts'; +import { pattern } from './name-pattern.ts'; + +const assert = assertMod.assert as ((expr: unknown, msg?: string) => void); +const { assertEquals } = assertMod; + +Deno.test('Name Pattern construction', () => { + const pat = pattern`/8=base/<8=peerId:string>/<58=sequence:number>`; + assertEquals(pat.length, 3); + assert((pat[0] as Component).equals(new Component(8, 'base'))); + assertEquals(pat[1] as namePattern.PatternComponent, { + type: 8, + kind: 'string', + tag: 'peerId', + }); + assertEquals(pat[2] as namePattern.PatternComponent, { + type: 58, + kind: 'number', + tag: 'sequence', + }); + + const mapping = {}; + assert(namePattern.match(pat, name`/base/peer-01/seq=${13}`, mapping)); + assertEquals(mapping, { + peerId: 'peer-01', + sequence: 13, + }); +}); + +Deno.test('Make name from patterns', () => { + const pat = pattern`/8=base/<8=peerId:string>/<58=sequence:number>`; + assert( + namePattern.make(pat, { + peerId: 'peer-01', + sequence: 13, + }).equals(name`/base/peer-01/seq=${13}`), + ); +}); diff --git a/src/namespace/name-pattern.ts b/src/namespace/name-pattern.ts index d5c03ce..7bdf715 100644 --- a/src/namespace/name-pattern.ts +++ b/src/namespace/name-pattern.ts @@ -1,3 +1,146 @@ -export const pattern = ([_value]: TemplateStringsArray) => { - throw new Error('Not implemented'); +import { Component, Name } from '@ndn/packet'; +import { Encoder, NNI } from '@ndn/tlv'; + +export type PatternKind = 'bytes' | 'number' | 'string'; +export type MatchValue = Uint8Array | number | string; + +export type PatternComponent = { + /** TLV-TYPE. */ + type: number; + /** Pattern's matching variable name */ + tag: string; + /** Pattern value type */ + kind: PatternKind; +}; + +export type Pattern = Array; + +export const patternComponentToString = (comp: PatternComponent) => `<${comp.type}=${comp.tag}:${comp.kind}>`; + +export const componentToString = (comp: PatternComponent | Component) => + comp instanceof Component ? comp.toString() : patternComponentToString(comp); + +/** + * Convert a name pattern to string + * @param pat The name pattern + * @returns String representation + */ +export const toString = (pat: Pattern) => '/' + pat.map(componentToString).join('/'); + +export const matchStep = ( + pattern: PatternComponent | Component, + subject: Component, + mapping: Record, +) => { + if (pattern.type !== subject.type) { + return false; + } + if (pattern instanceof Component) { + return pattern.equals(subject); + } else if (pattern.kind === 'bytes') { + mapping[pattern.tag] = subject.value; + return true; + } else if (pattern.kind === 'string') { + mapping[pattern.tag] = new TextDecoder().decode(subject.value); + return true; + } else { + try { + mapping[pattern.tag] = NNI.decode(subject.value); + return true; + } catch { + return false; + } + } +}; + +/** + * Match a given name with a given name pattern, and put matched variables into mapping. + * Digest components are removen. + * + * @param pattern The name pattern to match with. + * @param subject The name to be matched. + * @param mapping The mapping holding matched variables. + * @returns `true` if succeeded, `false` if failed. + * @throws when the component type matches but its value cannot be decoded as specified value kind. + */ +export const match = ( + pattern: Pattern, + subject: Name, + mapping: Record, +) => { + // Remove automatically added component + // ImplicitSha256DigestComponent(0x01) and ParametersSha256DigestComponent(0x02) + while (subject.length > 0 && subject.at(subject.length - 1).type <= 2) { + subject = subject.getPrefix(subject.length - 1); + } + // Must be equal length + if (subject.length !== pattern.length) { + return false; + } + for (const [i, p] of pattern.entries()) { + if (!matchStep(p, subject.at(i), mapping)) { + return false; + } + } + return true; +}; + +export const makeStep = ( + pattern: PatternComponent | Component, + mapping: Record, +) => { + if (pattern instanceof Component) { + return pattern; + } else { + const value = mapping[pattern.tag]; + if (!value) { + throw new Error(`The pattern variable ${pattern.tag} does not exist in the mapping.`); + } + const v = typeof value === 'number' ? Encoder.encode(NNI(value)) : value; + return new Component(pattern.type, v); + } +}; + +/** + * Construct a Name from a given name pattern with variable mapping. + * + * @remarks The value kind of pattern components are not checked. + * @param pattern The input name pattern + * @param mapping The variable mapping + * @returns The constructed name + * @throws if a pattern with a given variable is missing + */ +export const make = ( + pattern: Pattern, + mapping: Record, +) => new Name(pattern.map((p) => makeStep(p, mapping))); + +export const componentFromString = (value: string) => { + if (value.length === 0) { + return new Component(); + } + if (value[0] !== '<') { + return Component.from(value); + } else { + const matching = /^<(?[0-9]+)=(?[a-zA-Z0-9$_-]+):(?bytes|number|string)>$/.exec(value); + if (!matching || !matching.groups) { + throw new Error(`Invalid pattern component: ${value}`); + } + return { + type: parseInt(matching.groups.type), + kind: matching.groups.kind as PatternKind, + tag: matching.groups.tag, + } as PatternComponent; + } +}; + +export const fromString = (value: string) => { + if (value[0] === '/') { + value = value.substring(1); + } + return value.split('/').map(componentFromString) as Pattern; +}; + +export const pattern = ([value]: TemplateStringsArray) => { + return fromString(value); }; diff --git a/src/namespace/schema-tree.ts b/src/namespace/schema-tree.ts new file mode 100644 index 0000000..ff76714 --- /dev/null +++ b/src/namespace/schema-tree.ts @@ -0,0 +1,17 @@ +import { Name } from '@ndn/packet'; +import * as namePattern from './name-pattern.ts'; + +export type Node = { + children: Map; + }>; + parent: WeakRef> | undefined; + resource: R; +}; + +export type MatchedObject = { + mapping: Record; + name: Name; + resource: R; +}; diff --git a/src/security/cert-storage.test.ts b/src/security/cert-storage.test.ts new file mode 100644 index 0000000..c2d14cd --- /dev/null +++ b/src/security/cert-storage.test.ts @@ -0,0 +1,389 @@ +import { Endpoint } from '@ndn/endpoint'; +import { Encoder } from '@ndn/tlv'; +import { Forwarder } from '@ndn/fw'; +import { Data, Interest } from '@ndn/packet'; +import { + Certificate, + CertNaming, + createSigner, + createVerifier, + ECDSA, + generateSigningKey, + ValidityPeriod, +} from '@ndn/keychain'; +import { InMemoryStorage } from '../storage/mod.ts'; +import { CertStorage } from './cert-storage.ts'; +import { assert as assertMod } from '../dep.ts'; +import { AsyncDisposableStack, name, Responder } from '../utils/mod.ts'; + +// const assert = assertMod.assert as ((expr: unknown, msg?: string) => void); +const { assertEquals, assertRejects } = assertMod; + +Deno.test('Known certificates', async () => { + await using closers = new AsyncDisposableStack(); + + const appPrefix = name`/test-app`; + // This is the high level API that generates a non-extractable P-256 key + const [caPrvKey, caPubKey] = await generateSigningKey(/*identity*/ appPrefix, ECDSA); + const anchor = await Certificate.selfSign({ + privateKey: caPrvKey, + publicKey: caPubKey, + }); + // These are controllable API functions to generate an extractable P-256 key + const ownKeyPair = await ECDSA.cryptoGenerate({}, true); + const keyName = CertNaming.makeKeyName(name`/${appPrefix}/8=node-0`); + // const ownPrvKey = createSigner(keyName, ECDSA, ownKeyPair); + const ownPubKey = createVerifier(keyName, ECDSA, ownKeyPair); + const ownPrvKeyBits = await crypto.subtle.exportKey('pkcs8', ownKeyPair.privateKey); + const ownCert = await Certificate.issue({ + issuerId: name`CA`.at(0), + issuerPrivateKey: caPrvKey.withKeyLocator(anchor.name), + publicKey: ownPubKey, + validity: new ValidityPeriod(Date.now(), Date.now() + 3600000), + }); + + const otherKeyPair = await ECDSA.cryptoGenerate({}, true); + const otherKeyName = CertNaming.makeKeyName(name`/${appPrefix}/8=node-1`); + // const otherPrvKey = createSigner(otherKeyName, ECDSA, otherKeyPair); + const otherPubKey = createVerifier(otherKeyName, ECDSA, otherKeyPair); + // const otherPrvKeyBits = await crypto.subtle.exportKey('pkcs8', otherKeyPair.privateKey); + const otherCert = await Certificate.issue({ + issuerId: name`CA`.at(0), + issuerPrivateKey: caPrvKey.withKeyLocator(anchor.name), + publicKey: otherPubKey, + validity: new ValidityPeriod(Date.now(), Date.now() + 3600000), + }); + + const storage = new InMemoryStorage(); + closers.use(storage); + const fwAB = Forwarder.create(); + closers.defer(() => fwAB.close()); + const endpoint = new Endpoint({ fw: fwAB }); + // const responder = new Responder(appPrefix, endpoint, storage); + // closers.use(responder); + + storage.set(anchor.name.toString(), Encoder.encode(anchor.data)); + storage.set(ownCert.name.toString(), Encoder.encode(ownCert.data)); + storage.set(otherCert.name.toString(), Encoder.encode(otherCert.data)); + + const cs = await CertStorage.create(anchor, ownCert, storage, endpoint, new Uint8Array(ownPrvKeyBits), 800); + + // await cs.verifier.verify(anchor.data); // Unable to do so, since it is not signed with a Cert name. + await cs.verifier.verify(ownCert.data); + await cs.verifier.verify(otherCert.data); +}); + +Deno.test('Fetch missing certificate once', async () => { + // This test shows the actual behavior, which is acceptable under current situation, but not ideal. + await using closers = new AsyncDisposableStack(); + + const appPrefix = name`/test-app`; + // This is the high level API that generates a non-extractable P-256 key + const [caPrvKey, caPubKey] = await generateSigningKey(/*identity*/ appPrefix, ECDSA); + const anchor = await Certificate.selfSign({ + privateKey: caPrvKey, + publicKey: caPubKey, + }); + // These are controllable API functions to generate an extractable P-256 key + const ownKeyPair = await ECDSA.cryptoGenerate({}, true); + const keyName = CertNaming.makeKeyName(name`/${appPrefix}/8=node-0`); + // const ownPrvKey = createSigner(keyName, ECDSA, ownKeyPair); + const ownPubKey = createVerifier(keyName, ECDSA, ownKeyPair); + const ownPrvKeyBits = await crypto.subtle.exportKey('pkcs8', ownKeyPair.privateKey); + const ownCert = await Certificate.issue({ + issuerId: name`CA`.at(0), + issuerPrivateKey: caPrvKey.withKeyLocator(anchor.name), + publicKey: ownPubKey, + validity: new ValidityPeriod(Date.now(), Date.now() + 3600000), + }); + + const otherKeyPair = await ECDSA.cryptoGenerate({}, true); + const otherKeyName = CertNaming.makeKeyName(name`/${appPrefix}/8=node-1`); + const otherPrvKey = createSigner(otherKeyName, ECDSA, otherKeyPair); + const otherPubKey = createVerifier(otherKeyName, ECDSA, otherKeyPair); + // const otherPrvKeyBits = await crypto.subtle.exportKey('pkcs8', otherKeyPair.privateKey); + const otherCert = await Certificate.issue({ + issuerId: name`CA`.at(0), + issuerPrivateKey: caPrvKey.withKeyLocator(anchor.name), + publicKey: otherPubKey, + validity: new ValidityPeriod(Date.now(), Date.now() + 3600000), + }); + + const dataToFetch = new Data( + name`/${appPrefix}/8=node-1/data`, + Data.FreshnessPeriod(10000), + new TextEncoder().encode('Hello World'), + ); + await otherPrvKey.withKeyLocator(otherCert.name).sign(dataToFetch); + + const storage = new InMemoryStorage(); + closers.use(storage); + const storage2 = new InMemoryStorage(); + closers.use(storage2); + const fwAB = Forwarder.create(); + closers.defer(() => fwAB.close()); + const endpoint = new Endpoint({ fw: fwAB }); + const responder = new Responder(appPrefix, endpoint, storage2); + closers.use(responder); + + storage.set(anchor.name.toString(), Encoder.encode(anchor.data)); + storage.set(ownCert.name.toString(), Encoder.encode(ownCert.data)); + storage2.set(otherCert.name.toString(), Encoder.encode(otherCert.data)); + storage2.set(dataToFetch.name.toString(), Encoder.encode(dataToFetch)); + + const cs = await CertStorage.create(anchor, ownCert, storage, endpoint, new Uint8Array(ownPrvKeyBits), 800); + + const fetchedData = await endpoint.consume( + new Interest( + name`/${appPrefix}/8=node-1/data`, + Interest.MustBeFresh, + Interest.Lifetime(1000), + ), + { + verifier: cs.verifier, + }, + ); + assertEquals(fetchedData.content, new TextEncoder().encode('Hello World')); +}); + +Deno.test('Properly sign packets', async () => { + await using closers = new AsyncDisposableStack(); + + const appPrefix = name`/test-app`; + // This is the high level API that generates a non-extractable P-256 key + const [caPrvKey, caPubKey] = await generateSigningKey(/*identity*/ appPrefix, ECDSA); + const anchor = await Certificate.selfSign({ + privateKey: caPrvKey, + publicKey: caPubKey, + }); + // These are controllable API functions to generate an extractable P-256 key + const ownKeyPair = await ECDSA.cryptoGenerate({}, true); + const keyName = CertNaming.makeKeyName(name`/${appPrefix}/8=node-0`); + // const ownPrvKey = createSigner(keyName, ECDSA, ownKeyPair); + const ownPubKey = createVerifier(keyName, ECDSA, ownKeyPair); + const ownPrvKeyBits = await crypto.subtle.exportKey('pkcs8', ownKeyPair.privateKey); + const ownCert = await Certificate.issue({ + issuerId: name`CA`.at(0), + issuerPrivateKey: caPrvKey.withKeyLocator(anchor.name), + publicKey: ownPubKey, + validity: new ValidityPeriod(Date.now(), Date.now() + 3600000), + }); + + const storage = new InMemoryStorage(); + closers.use(storage); + const fwAB = Forwarder.create(); + closers.defer(() => fwAB.close()); + const endpoint = new Endpoint({ fw: fwAB }); + + storage.set(anchor.name.toString(), Encoder.encode(anchor.data)); + storage.set(ownCert.name.toString(), Encoder.encode(ownCert.data)); + + const cs = await CertStorage.create(anchor, ownCert, storage, endpoint, new Uint8Array(ownPrvKeyBits), 800); + + const dataToSign = new Data( + name`/${appPrefix}/8=node-0/data`, + Data.FreshnessPeriod(10000), + new TextEncoder().encode('Hello World'), + ); + await cs.signer.sign(dataToSign); + await ownPubKey.verify(dataToSign); +}); + +Deno.test('Reject unavailable certificate', async () => { + await using closers = new AsyncDisposableStack(); + + const appPrefix = name`/test-app`; + // This is the high level API that generates a non-extractable P-256 key + const [caPrvKey, caPubKey] = await generateSigningKey(/*identity*/ appPrefix, ECDSA); + const anchor = await Certificate.selfSign({ + privateKey: caPrvKey, + publicKey: caPubKey, + }); + // These are controllable API functions to generate an extractable P-256 key + const ownKeyPair = await ECDSA.cryptoGenerate({}, true); + const keyName = CertNaming.makeKeyName(name`/${appPrefix}/8=node-0`); + // const ownPrvKey = createSigner(keyName, ECDSA, ownKeyPair); + const ownPubKey = createVerifier(keyName, ECDSA, ownKeyPair); + const ownPrvKeyBits = await crypto.subtle.exportKey('pkcs8', ownKeyPair.privateKey); + const ownCert = await Certificate.issue({ + issuerId: name`CA`.at(0), + issuerPrivateKey: caPrvKey.withKeyLocator(anchor.name), + publicKey: ownPubKey, + validity: new ValidityPeriod(Date.now(), Date.now() + 3600000), + }); + + const otherKeyPair = await ECDSA.cryptoGenerate({}, true); + const otherKeyName = CertNaming.makeKeyName(name`/${appPrefix}/8=node-1`); + const otherPrvKey = createSigner(otherKeyName, ECDSA, otherKeyPair); + const otherPubKey = createVerifier(otherKeyName, ECDSA, otherKeyPair); + // const otherPrvKeyBits = await crypto.subtle.exportKey('pkcs8', otherKeyPair.privateKey); + const otherCert = await Certificate.issue({ + issuerId: name`CA`.at(0), + issuerPrivateKey: caPrvKey.withKeyLocator(anchor.name), + publicKey: otherPubKey, + validity: new ValidityPeriod(Date.now(), Date.now() + 3600000), + }); + + const dataToFetch = new Data( + name`/${appPrefix}/8=node-1/data`, + Data.FreshnessPeriod(10000), + new TextEncoder().encode('Hello World'), + ); + await otherPrvKey.withKeyLocator(otherCert.name).sign(dataToFetch); + + const storage = new InMemoryStorage(); + closers.use(storage); + const storage2 = new InMemoryStorage(); + closers.use(storage2); + const fwAB = Forwarder.create(); + closers.defer(() => fwAB.close()); + const endpoint = new Endpoint({ fw: fwAB }); + const responder = new Responder(appPrefix, endpoint, storage2); + closers.use(responder); + + storage.set(anchor.name.toString(), Encoder.encode(anchor.data)); + storage.set(ownCert.name.toString(), Encoder.encode(ownCert.data)); + // storage2.set(otherCert.name.toString(), Encoder.encode(otherCert.data)); + // Certificate is missing + + const cs = await CertStorage.create(anchor, ownCert, storage, endpoint, new Uint8Array(ownPrvKeyBits), 800); + await assertRejects(async () => { + await cs.verifier.verify(dataToFetch); + }, 'Failed to reject not existing certificates'); +}); + +Deno.test('Reject mutual loop', async () => { + await using closers = new AsyncDisposableStack(); + + const appPrefix = name`/test-app`; + // This is the high level API that generates a non-extractable P-256 key + const [caPrvKey, caPubKey] = await generateSigningKey(/*identity*/ appPrefix, ECDSA); + const anchor = await Certificate.selfSign({ + privateKey: caPrvKey, + publicKey: caPubKey, + }); + // These are controllable API functions to generate an extractable P-256 key + const ownKeyPair = await ECDSA.cryptoGenerate({}, true); + const keyName = CertNaming.makeKeyName(name`/${appPrefix}/8=node-0`); + // const ownPrvKey = createSigner(keyName, ECDSA, ownKeyPair); + const ownPubKey = createVerifier(keyName, ECDSA, ownKeyPair); + const ownPrvKeyBits = await crypto.subtle.exportKey('pkcs8', ownKeyPair.privateKey); + const ownCert = await Certificate.issue({ + issuerId: name`CA`.at(0), + issuerPrivateKey: caPrvKey.withKeyLocator(anchor.name), + publicKey: ownPubKey, + validity: new ValidityPeriod(Date.now(), Date.now() + 3600000), + }); + + const otherKeyPair1 = await ECDSA.cryptoGenerate({}, true); + const otherKeyName1 = CertNaming.makeKeyName(name`/${appPrefix}/8=node-1`); + const otherPrvKey1 = createSigner(otherKeyName1, ECDSA, otherKeyPair1); + const otherPubKey1 = createVerifier(otherKeyName1, ECDSA, otherKeyPair1); + const otherKeyPair2 = await ECDSA.cryptoGenerate({}, true); + const otherKeyName2 = CertNaming.makeKeyName(name`/${appPrefix}/8=node-2`); + const otherPrvKey2 = createSigner(otherKeyName2, ECDSA, otherKeyPair2); + const otherPubKey2 = createVerifier(otherKeyName2, ECDSA, otherKeyPair2); + const cert1Name = name`/${otherKeyName1}/node-2/v=${13}`; + const cert2Name = name`/${otherKeyName2}/node-1/v=${13}`; + const otherCert1 = await Certificate.build({ + name: cert1Name, + signer: otherPrvKey2.withKeyLocator(cert2Name), + publicKeySpki: otherPubKey1.spki!, + validity: new ValidityPeriod(Date.now(), Date.now() + 3600000), + }); + const otherCert2 = await Certificate.build({ + name: cert2Name, + signer: otherPrvKey1.withKeyLocator(cert1Name), + publicKeySpki: otherPubKey2.spki!, + validity: new ValidityPeriod(Date.now(), Date.now() + 3600000), + }); + + const dataToFetch = new Data( + name`/${appPrefix}/8=node-1/data`, + Data.FreshnessPeriod(10000), + new TextEncoder().encode('Hello World'), + ); + await otherPrvKey1.withKeyLocator(otherCert1.name).sign(dataToFetch); + + const storage = new InMemoryStorage(); + closers.use(storage); + const storage2 = new InMemoryStorage(); + closers.use(storage2); + const fwAB = Forwarder.create(); + closers.defer(() => fwAB.close()); + const endpoint = new Endpoint({ fw: fwAB }); + const responder = new Responder(appPrefix, endpoint, storage2); + closers.use(responder); + + storage.set(anchor.name.toString(), Encoder.encode(anchor.data)); + storage.set(ownCert.name.toString(), Encoder.encode(ownCert.data)); + storage2.set(otherCert1.name.toString(), Encoder.encode(otherCert1.data)); + storage2.set(otherCert2.name.toString(), Encoder.encode(otherCert2.data)); + storage2.set(dataToFetch.name.toString(), Encoder.encode(dataToFetch)); + + const cs = await CertStorage.create(anchor, ownCert, storage, endpoint, new Uint8Array(ownPrvKeyBits), 800); + await assertRejects(async () => { + await cs.verifier.verify(dataToFetch); + }, 'Failed to reject mutually signed certificates'); +}); + +Deno.test('Reject self-signed certificate', async () => { + await using closers = new AsyncDisposableStack(); + + const appPrefix = name`/test-app`; + // This is the high level API that generates a non-extractable P-256 key + const [caPrvKey, caPubKey] = await generateSigningKey(/*identity*/ appPrefix, ECDSA); + const anchor = await Certificate.selfSign({ + privateKey: caPrvKey, + publicKey: caPubKey, + }); + // These are controllable API functions to generate an extractable P-256 key + const ownKeyPair = await ECDSA.cryptoGenerate({}, true); + const keyName = CertNaming.makeKeyName(name`/${appPrefix}/8=node-0`); + // const ownPrvKey = createSigner(keyName, ECDSA, ownKeyPair); + const ownPubKey = createVerifier(keyName, ECDSA, ownKeyPair); + const ownPrvKeyBits = await crypto.subtle.exportKey('pkcs8', ownKeyPair.privateKey); + const ownCert = await Certificate.issue({ + issuerId: name`CA`.at(0), + issuerPrivateKey: caPrvKey.withKeyLocator(anchor.name), + publicKey: ownPubKey, + validity: new ValidityPeriod(Date.now(), Date.now() + 3600000), + }); + + const otherKeyPair = await ECDSA.cryptoGenerate({}, true); + const otherKeyName = CertNaming.makeKeyName(name`/${appPrefix}/8=node-1`); + const otherPrvKey = createSigner(otherKeyName, ECDSA, otherKeyPair); + const otherPubKey = createVerifier(otherKeyName, ECDSA, otherKeyPair); + // const otherPrvKeyBits = await crypto.subtle.exportKey('pkcs8', otherKeyPair.privateKey); + const otherCert = await Certificate.selfSign({ + privateKey: otherPrvKey, + publicKey: otherPubKey, + validity: new ValidityPeriod(Date.now(), Date.now() + 3600000), + }); + + const dataToFetch = new Data( + name`/${appPrefix}/8=node-1/data`, + Data.FreshnessPeriod(10000), + new TextEncoder().encode('Hello World'), + ); + await otherPrvKey.withKeyLocator(otherCert.name).sign(dataToFetch); + + const storage = new InMemoryStorage(); + closers.use(storage); + const storage2 = new InMemoryStorage(); + closers.use(storage2); + const fwAB = Forwarder.create(); + closers.defer(() => fwAB.close()); + const endpoint = new Endpoint({ fw: fwAB }); + const responder = new Responder(appPrefix, endpoint, storage2); + closers.use(responder); + + storage.set(anchor.name.toString(), Encoder.encode(anchor.data)); + storage.set(ownCert.name.toString(), Encoder.encode(ownCert.data)); + storage2.set(otherCert.name.toString(), Encoder.encode(otherCert.data)); + + const cs = await CertStorage.create(anchor, ownCert, storage, endpoint, new Uint8Array(ownPrvKeyBits), 800); + await assertRejects(async () => { + await cs.verifier.verify(dataToFetch); + }, 'Failed to reject self-signed certificates'); +}); diff --git a/src/security/cert-storage.ts b/src/security/cert-storage.ts index 4839b7f..8c6f372 100644 --- a/src/security/cert-storage.ts +++ b/src/security/cert-storage.ts @@ -20,6 +20,7 @@ export class CertStorage implements SecurityAgent { readonly storage: Storage, readonly endpoint: Endpoint, prvKeyBits: Uint8Array, + protected readonly interestLifetime = 5000, ) { this.readyEvent = (async () => { await this.importCert(trustAnchor); @@ -67,7 +68,7 @@ export class CertStorage implements SecurityAgent { new Interest( keyName, Interest.MustBeFresh, - Interest.Lifetime(5000), + Interest.Lifetime(this.interestLifetime), ), { // Fetched key must be signed by a known key @@ -126,12 +127,13 @@ export class CertStorage implements SecurityAgent { }; } - async create( + public static async create( trustAnchor: Certificate, ownCertificate: Certificate, storage: Storage, endpoint: Endpoint, prvKeyBits: Uint8Array, + interestLifetime = 5000, ) { const result = new CertStorage( trustAnchor, @@ -139,6 +141,7 @@ export class CertStorage implements SecurityAgent { storage, endpoint, prvKeyBits, + interestLifetime, ); await result.readyEvent; return result; diff --git a/src/test-fixture/bridge.ts b/src/test-fixture/bridge.ts deleted file mode 100644 index bb81c5a..0000000 --- a/src/test-fixture/bridge.ts +++ /dev/null @@ -1,118 +0,0 @@ -import type { Forwarder, FwFace } from '@ndn/fw'; -import type { NameLike } from '@ndn/packet'; -import { Decoder } from '@ndn/tlv'; -import { assert, delay } from '@ndn/util'; -import { L3Face, Transport } from '@ndn/l3face'; -import { pushable } from 'it-pushable'; -import pDefer from 'p-defer'; -import { filter, map, pipeline, transform } from 'streaming-iterables'; - -class BridgeTransport extends Transport { - public override readonly rx: Transport.Rx; - public bridgePeer?: BridgeTransport; - private readonly bridgeRx = pushable({ objectMode: true }); - - constructor(bridgeName: string, relay: RelayFunc, private readonly closePromise: Promise) { - super({ describe: `BRIDGE(${bridgeName})` }); - this.rx = map((wire) => new Decoder(wire).read(), relay(this.bridgeRx)); - } - - public override readonly tx = async (iterable: AsyncIterable) => { - const iterator = iterable[Symbol.asyncIterator](); - while (true) { - const result = await Promise.race([ - iterator.next(), - this.closePromise, - ]); - if (!result || result.done) { // normal close - return; - } - const copy = new Uint8Array(result.value); - this.bridgePeer?.bridgeRx.push(copy); - } - }; -} - -/** A bridge that links two forwarders. */ -export interface Bridge { - faceA: FwFace; - faceB: FwFace; - close: () => void; -} - -function makeRelayFunc(relay: Relay): RelayFunc { - if (typeof relay === 'function') { - return relay; - } - const { - minDelay = 1, - maxDelay = 1, - loss = 0, - } = relay; - assert(minDelay <= maxDelay); - const delayRange = maxDelay - minDelay; - return (it) => - pipeline( - () => it, - filter(() => loss === 0 || Math.random() >= loss), - transform(64, async (pkt) => { - await delay(minDelay + delayRange * Math.random()); - return pkt; - }), - ); -} - -export type RelayFunc = (it: AsyncIterable) => AsyncIterable; - -export interface RelayOptions { - minDelay?: number; - maxDelay?: number; - loss?: number; -} - -export type Relay = RelayFunc | RelayOptions; - -export interface CreateOptions { - bridgeName?: string; - fwA: Forwarder; - fwB: Forwarder; - relayAB?: Relay; - relayBA?: Relay; - routesAB?: NameLike[]; - routesBA?: NameLike[]; -} - -/** - * Create a bridge that links two forwarders. - * The relay functions can inject delay, loss, and jitter to the simulated link. - */ -export function create({ - bridgeName = 'bridge', - fwA, - fwB, - relayAB = (x) => x, - relayBA = (x) => x, - routesAB, - routesBA, -}: CreateOptions): Bridge { - const close = pDefer(); - const tA = new BridgeTransport(bridgeName, makeRelayFunc(relayBA), close.promise); - const tB = new BridgeTransport(bridgeName, makeRelayFunc(relayAB), close.promise); - tA.bridgePeer = tB; - tB.bridgePeer = tA; - // deno-lint-ignore no-explicit-any - const faceA = fwA.addFace(new L3Face(tA, { advertiseFrom: false }) as any); - L3Face.processAddRoutes(faceA, routesAB); - // deno-lint-ignore no-explicit-any - const faceB = fwB.addFace(new L3Face(tB, { advertiseFrom: false }) as any); - L3Face.processAddRoutes(faceB, routesBA); - return { - faceA, - faceB, - close() { - faceA.close(); - faceB.close(); - close.resolve(); - }, - }; -} diff --git a/src/test-fixture/mod.ts b/src/test-fixture/mod.ts index ba713d7..97aa2d3 100644 --- a/src/test-fixture/mod.ts +++ b/src/test-fixture/mod.ts @@ -1,2 +1 @@ export * from './mock-transport.ts'; -export * as Bridge from './bridge.ts'; diff --git a/src/utils/name-lit.test.ts b/src/utils/name-lit.test.ts index ad7d8e9..eb46b83 100644 --- a/src/utils/name-lit.test.ts +++ b/src/utils/name-lit.test.ts @@ -2,7 +2,7 @@ import { assert as assertMod } from '../dep.ts'; import { Component, Name } from '@ndn/packet'; import { name } from './name-lit.ts'; -const { assert } = assertMod; +const assert = assertMod.assert as ((expr: unknown, msg?: string) => void); Deno.test('Name tagged template literal test', () => { assert(name``.equals('/'));