diff --git a/.github/workflows/pages.yaml b/.github/workflows/pages.yaml index 8ff435a..7862641 100644 --- a/.github/workflows/pages.yaml +++ b/.github/workflows/pages.yaml @@ -1,6 +1,6 @@ on: push: - branches: [main] + branches: ["main"] jobs: pages: permissions: @@ -12,6 +12,8 @@ jobs: - uses: actions/setup-go@v4 - name: run for all SDKs run: cd test-harness && go run ./cmd/web5-test-harness many sdks/* && mv _site ../_site + - name: Copy test-vectors + run: cp -r ./web5-test-vectors _site/ - uses: actions/upload-pages-artifact@v2 - name: deploy GitHub Pages uses: actions/deploy-pages@v2 diff --git a/.github/workflows/validate-test-vectors.yaml b/.github/workflows/validate-test-vectors.yaml new file mode 100644 index 0000000..4f2f5c3 --- /dev/null +++ b/.github/workflows/validate-test-vectors.yaml @@ -0,0 +1,30 @@ +name: Validate Test Vectors + +on: + push: + paths: + - 'web5-test-vectors/**' + - 'scripts/test-vector-validation/**' + +jobs: + validate: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Set up Node.js + uses: actions/setup-node@v2 + with: + node-version: '20' + + - name: Install dependencies for test vector validation script + run: | + cd scripts/test-vector-validation + npm install + + - name: Validate test vectors + run: | + cd scripts/test-vector-validation + node main.js diff --git a/README.md b/README.md index b7e5f1d..1c95f74 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,11 @@ - [Publishing Artifacts](#publishing-artifacts) - [Publishing API Reference Documentation](#publishing-api-reference-documentation) - [Example Feature Usage](#example-feature-usage) +- [Test Vectors](#test-vectors) + - [Usage](#usage) + - [Local Dev](#local-dev) + - [Adding/Updating Vectors](#addingupdating-vectors) + - [Feature Completeness By SDK](#feature-completeness-by-sdk) - [Web5 SDK Features](#web5-sdk-features) - [Cryptographic Digital Signature Algorithms (DSA)](#cryptographic-digital-signature-algorithms-dsa) - [Key Management](#key-management) @@ -233,6 +238,46 @@ Each SDK will auto generate API reference documentation using the respective lan Each SDK will **publish** example usage for _each_ implemented feature. This can either be included as a part of API reference documentation _or_ published separately +## Test Vectors + +Test vectors ensure interoporability of features across SDKs and language implementations by providing common test cases with an input and expected output pair. They include both success and failure cases that can be vectorized. + +This repo serves as the home for all web5 feature related vectors. They are available in the [web5-test-vectors](./web5-test-vectors/) directory and hosted on [Github Pages](https://tbd54566975.github.io/sdk-development/web5-test-vectors). + +The `tbdex` repo houses tbdex feature related vectors. They are available in the [test-vectors](https://github.com/TBD54566975/tbdex/test-vectors) directory and hosted on [Github Pages](https://tbdex.dev/). + +### Usage + +#### Local Dev + +SDK implementers should import vectors in order to test their implementation. The recommended pathway to consume them is as follows: + +Fetch the vector and read it into a data model representing the vector structure or a JSON object like so: + +```kt +// for web5 vectors +val stream = URL("https://tbd54566975.github.io/sdk-development/web5-test-vectors/did-jwk/resolve.json").openStream() +val vectorsJson = BufferedReader(InputStreamReader(stream)).readText() +return Json.jsonMapper.readTree(vectorsJson) + +// for tbdex vectors +val stream = URL("https://tbdex.dev/test-vectors/resources/marshal.json").openStream() +val vectorsJson = BufferedReader(InputStreamReader(stream)).readText() +return Json.jsonMapper.readTree(vectorsJson) +``` + +The data model or JSON object can then be used in the implementer's unit testing framework of choice. + +#### Adding/Updating Vectors + +New test vectors should follow the standard [vector structure](./web5-test-vectors/vectors.schema.json). Vectors are automatically validated against the JSON schema via CI. + +Create a PR in this repo for web5 vectors, or in [`tbdex`](https://github.com/TBD54566975/tbdex) for tbdex vectors with the proposed changes or additions. + +#### Feature Completeness By SDK + +Test vectors are also used to determine feature completeness via our [test harness](./test-harness/README.md). Results of test harness runs can be found [here](https://tbd54566975.github.io/sdk-development/). + ## Web5 SDK Features ### Cryptographic Digital Signature Algorithms (DSA) diff --git a/scripts/test-vector-validation/.gitignore b/scripts/test-vector-validation/.gitignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/scripts/test-vector-validation/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/scripts/test-vector-validation/README.md b/scripts/test-vector-validation/README.md new file mode 100644 index 0000000..6d2cda0 --- /dev/null +++ b/scripts/test-vector-validation/README.md @@ -0,0 +1,35 @@ +# Test Vector Validation + +## Description + +Validates test vectors in [`web5-test-vectors`](../../web5-test-vectors/) directory. Uses [vectors.schema.json](../../web5-test-vectors/vectors.schema.json) to validate. + +> [!NOTE] +> Runs automatically anytime a change is made in [`web5-test-vectors`](../../web5-test-vectors/) or to anything in this directory + +## Setup + +### `node` and `npm` + +This project is using a minimum of `node v20.3.0` and `npm v9.6.7`. You can verify your `node` and `npm` installation via the terminal: + +```bash +$ node --version +v20.3.0 +$ npm --version +9.6.7 +``` + +If you don't have `node` installed. Feel free to choose whichever approach you feel the most comfortable with. If you don't have a preferred installation method, i'd recommend using `nvm` (aka node version manager). `nvm` allows you to install and use different versions of node. It can be installed by running `brew install nvm` (assuming that you have homebrew) + +Once you have installed `nvm`, install the desired node version with `nvm install vX.Y.Z`. + +```bash +npm install +``` + +## Run + +```bash +node main.js +``` diff --git a/scripts/test-vector-validation/main.js b/scripts/test-vector-validation/main.js new file mode 100644 index 0000000..df48825 --- /dev/null +++ b/scripts/test-vector-validation/main.js @@ -0,0 +1,46 @@ +import fs from 'node:fs' +import path from 'path' +import Ajv from 'ajv' + +import { fileURLToPath } from 'node:url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +const vectorsDir = `${__dirname}/../../web5-test-vectors` + +let vectorsSchema = fs.readFileSync(`${vectorsDir}/vectors.schema.json`, 'utf8') +vectorsSchema = JSON.parse(vectorsSchema) + +const ajv = new Ajv() +const validate = ajv.compile(vectorsSchema) + +function validateTestVectors() { + const entries = fs.readdirSync(vectorsDir, { withFileTypes: true }) + + for (const entry of entries) { + if (!entry.isDirectory()) { + continue + } + + const featureDir = path.join(vectorsDir, entry.name) + const files = fs.readdirSync(featureDir) + + for (const file of files) { + if (path.extname(file) === '.json') { + const filePath = path.join(featureDir, file) + const fileContent = fs.readFileSync(filePath, 'utf8') + const testData = JSON.parse(fileContent) + + if (!validate(testData)) { + console.log(`Validation failed for ${filePath}:`, validate.errors) + process.exit(1) + } else { + console.log(`Validation passed for ${filePath}`) + } + } + } + } +} + +validateTestVectors() diff --git a/scripts/test-vector-validation/package-lock.json b/scripts/test-vector-validation/package-lock.json new file mode 100644 index 0000000..5dfa208 --- /dev/null +++ b/scripts/test-vector-validation/package-lock.json @@ -0,0 +1,61 @@ +{ + "name": "test-vector-validation", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "ajv": "8.12.0" + } + }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + } + } +} diff --git a/scripts/test-vector-validation/package.json b/scripts/test-vector-validation/package.json new file mode 100644 index 0000000..0a88377 --- /dev/null +++ b/scripts/test-vector-validation/package.json @@ -0,0 +1,7 @@ +{ + "name": "test-vector-validation", + "type": "module", + "dependencies": { + "ajv": "8.12.0" + } +} \ No newline at end of file diff --git a/web5-test-vectors/README.md b/web5-test-vectors/README.md new file mode 100644 index 0000000..2b50b16 --- /dev/null +++ b/web5-test-vectors/README.md @@ -0,0 +1,48 @@ +# Web5 Test Vectors + +## Description + +This directory contains test vectors for all features we intend to support accross several languages. Each feature has its own directory which contains a single vectors file per sub-feature e.g. + +```text +web5-test-vectors +├── README.md +├── did-jwk <--- feature +│   └── resolve.json <--- sub-feature +├── index.html +└── vectors.schema.json +``` + +## Test Vector Files + +Test vector files should adhere to [`vectors.schema.json`]('./vectors.schema.json'). This repo contains a [`test-vector-validation`]('../scripts/test-vector-validation') script that validates all vectors files in this directory. It can be run manually by following the instructions [here](../scripts/test-vector-validation/README.md). + +> [!NOTE] +> Test Vector Validation runs automatically anytime a change is made in this directory or to the script itself. + +Each test vector file is a structured collection of test vector objects, where each vector asserts a specific outcome for a given input. Below is a table that outlines the expected fields in a test vector file: + +| Field | Type | Description | Required | +| ----------------------- | ------- | ---------------------------------------------------------------------------------------------------- | :------: | +| `description` | string | A general description of the test vectors collection. | Yes | +| `vectors` | array | An array of test vector objects. | Yes | +| `vectors[].description` | string | A description of what this test vector is testing. | Yes | +| `vectors[].input` | any | The input for the test vector, which can be of any type. | Yes | +| `vectors[].output` | any | The expected output for the test vector, which can be of any type. | No | +| `vectors[].errors` | boolean | Indicates whether the test vector is expected to produce an error. Defaults to false if not present. | No | + +### Rationale for Test Vector Structure + +The structure of a `vector` object is designed to fulfill two conditions: + +* the function works and returns something that should match `output` +* the function throws an error (in whatever way the consuming language represents errors) + * _optionally_, the error's _output_ should match `output` + +`errors: true` is an instruction to anticipate an error in the implementation language. For example: + +* In languages like Kotlin or Javascript, the presence of `errors: true` would imply that `assertThrows` be used. +* In Go, the expectation would be for the err variable to be non-nil. +* In Rust, the error handling would pivot on matching `Result.Err` rather than `Result.Ok`. + +Should `errors` be set to `true`, the `output` field may optionally be used to include expected error messages. diff --git a/web5-test-vectors/did-jwk/resolve.json b/web5-test-vectors/did-jwk/resolve.json new file mode 100644 index 0000000..c2ad1e2 --- /dev/null +++ b/web5-test-vectors/did-jwk/resolve.json @@ -0,0 +1,98 @@ +{ + "description": "did:jwk resolution test vectors", + "vectors": [ + { + "description": "resolves did:jwk 1", + "input": "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9", + "output": { + "didResolutionResult": { + "@context": "https://w3id.org/did-resolution/v1", + "didDocument": { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/jws-2020/v1" + ], + "id": "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9", + "verificationMethod": [ + { + "type": "JsonWebKey2020", + "id": "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0", + "controller": "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9", + "publicKeyJwk": { + "kty": "EC", + "crv": "P-256", + "x": "acbIQiuMs3i8_uszEjJ2tpTtRM4EU3yz91PH6CdH2V0", + "y": "_KcyLj9vWMptnmKtm46GqDz8wf74I5LKgrl2GzH3nSE" + } + } + ], + "authentication": [ + "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0" + ], + "assertionMethod": [ + "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0" + ], + "keyAgreement": [ + "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0" + ], + "capabilityInvocation": [ + "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0" + ], + "capabilityDelegation": [ + "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0" + ] + } + } + }, + "errors": false + }, + { + "description": "resolves did:jwk 2", + "input": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9", + "output": { + "didResolutionResult": { + "@context": "https://w3id.org/did-resolution/v1", + "didDocument": { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/jws-2020/v1" + ], + "id": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9", + "verificationMethod": [ + { + "type": "JsonWebKey2020", + "id": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0", + "controller": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9", + "publicKeyJwk": { + "kty": "OKP", + "use": "enc", + "crv": "X25519", + "x": "3p7bfXt9wbTTW2HC7OQ1Nz-DQ8hbeGdNrfx-FG-IK08" + } + } + ], + "keyAgreement": [ + "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0" + ] + } + } + }, + "errors": false + }, + { + "description": "resolution for invalid did", + "input": "did:jwk:hehe", + "output": { + "didResolutionResult": { + "@context": "https://w3id.org/did-resolution/v1", + "didDocument": null, + "didResolutionMetadata": { + "error": "invalidDid" + }, + "didDocumentMetadata": {} + } + }, + "errors": false + } + ] +} \ No newline at end of file diff --git a/web5-test-vectors/index.html b/web5-test-vectors/index.html new file mode 100644 index 0000000..44cc5e1 --- /dev/null +++ b/web5-test-vectors/index.html @@ -0,0 +1,46 @@ + + + + + + + Web5 Test Vectors + + + +
+
+

Standard Vector Structure

+ +
+ +
+

Test Vectors

+
+
+

Crypto

+
+ +
+

DIDs

+ +
+ +
+

VCs

+
+ +
+ + + \ No newline at end of file diff --git a/web5-test-vectors/vectors.schema.json b/web5-test-vectors/vectors.schema.json new file mode 100644 index 0000000..73a16a9 --- /dev/null +++ b/web5-test-vectors/vectors.schema.json @@ -0,0 +1,39 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Schema for representing test vectors for various features.", + "type": "object", + "required": ["description", "vectors"], + "properties": { + "description": { + "type": "string", + "description": "A general description of the test vectors collection." + }, + "vectors": { + "type": "array", + "description": "An array of test vectors for testing different features.", + "items": { + "type": "object", + "description": "A single test vector, which includes a description and input, and may optionally include an expected output and an errors indicator.", + "required": ["description", "input"], + "properties": { + "description": { + "type": "string", + "description": "A description of what this test vector is validating." + }, + "input": { + "description": "The input for the test vector, which can be of any type." + }, + "output": { + "description": "The expected output for the test vector, which can be of any type." + }, + "errors": { + "type": "boolean", + "default": false, + "description": "Indicates whether the test vector is expected to produce an error. Defaults to false if not present." + } + } + } + } + } + } + \ No newline at end of file