-
Notifications
You must be signed in to change notification settings - Fork 1
feat(pq-algorithm-id/ts): phase 1 - package foundation and canonical registry (ENG-1914) #26
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
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import type { MappingTarget } from './types'; | ||
|
|
||
| export type AlgorithmIdentifierErrorCode = | ||
| | 'UNKNOWN_ALGORITHM' | ||
| | 'UNKNOWN_IDENTIFIER' | ||
| | 'UNSUPPORTED_MAPPING'; | ||
|
|
||
| export class AlgorithmIdentifierError extends Error { | ||
| readonly code: AlgorithmIdentifierErrorCode; | ||
|
|
||
| constructor(code: AlgorithmIdentifierErrorCode, message: string) { | ||
| super(message); | ||
| this.name = new.target.name; | ||
| this.code = code; | ||
| } | ||
| } | ||
|
|
||
| export class UnknownAlgorithmError extends AlgorithmIdentifierError { | ||
| readonly algorithm: string; | ||
|
|
||
| constructor(algorithm: string) { | ||
| super('UNKNOWN_ALGORITHM', `Unknown algorithm '${algorithm}'.`); | ||
| this.algorithm = algorithm; | ||
| } | ||
| } | ||
|
|
||
| export class UnknownIdentifierError extends AlgorithmIdentifierError { | ||
| readonly identifierType: MappingTarget; | ||
| readonly identifierValue: string | number; | ||
|
|
||
| constructor(identifierType: MappingTarget, identifierValue: string | number) { | ||
| super( | ||
| 'UNKNOWN_IDENTIFIER', | ||
| `Unknown ${identifierType} identifier '${String(identifierValue)}'.`, | ||
| ); | ||
| this.identifierType = identifierType; | ||
| this.identifierValue = identifierValue; | ||
| } | ||
| } | ||
|
|
||
| export class UnsupportedMappingError extends AlgorithmIdentifierError { | ||
| readonly mapping: MappingTarget; | ||
| readonly algorithm: string; | ||
|
|
||
| constructor(mapping: MappingTarget, algorithm: string) { | ||
| super('UNSUPPORTED_MAPPING', `Algorithm '${algorithm}' does not support ${mapping} mapping.`); | ||
| this.mapping = mapping; | ||
| this.algorithm = algorithm; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,22 @@ | ||
| // pq-algorithm-id - Algorithm identifier mappings (JOSE, COSE, X.509) | ||
| // Implementation coming soon | ||
|
|
||
| export {}; | ||
| export { | ||
| AlgorithmIdentifierError, | ||
| type AlgorithmIdentifierErrorCode, | ||
| UnknownAlgorithmError, | ||
| UnknownIdentifierError, | ||
| UnsupportedMappingError, | ||
| } from './errors'; | ||
| export { | ||
| deriveOidFromName, | ||
| getIdentifierRecord, | ||
| listIdentifierRecords, | ||
| listRegistryAlgorithmNames, | ||
| } from './registry'; | ||
| export type { | ||
| CoseIdentifier, | ||
| IdentifierRecord, | ||
| IdentifierRecordMap, | ||
| JoseIdentifier, | ||
| MappingTarget, | ||
| X509ParametersEncoding, | ||
| X509ParametersPolicy, | ||
| } from './types'; | ||
|
eacet marked this conversation as resolved.
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| import type { AlgorithmName } from 'pq-oid'; | ||
| import { OID } from 'pq-oid'; | ||
| import { UnknownAlgorithmError } from './errors'; | ||
| import type { | ||
| CoseIdentifier, | ||
| IdentifierRecord, | ||
| IdentifierRecordMap, | ||
| JoseIdentifier, | ||
| X509ParametersPolicy, | ||
| } from './types'; | ||
|
|
||
| const DEFAULT_X509_PARAMETERS_POLICY: Readonly<X509ParametersPolicy> = Object.freeze({ | ||
| defaultParametersEncoding: 'absent', | ||
| acceptNull: true, | ||
| acceptAbsent: true, | ||
| }); | ||
|
|
||
| const JOSE_IDENTIFIERS: Readonly<Partial<Record<AlgorithmName, JoseIdentifier>>> = Object.freeze({ | ||
| 'ML-DSA-44': 'ML-DSA-44', | ||
| 'ML-DSA-65': 'ML-DSA-65', | ||
| 'ML-DSA-87': 'ML-DSA-87', | ||
| }); | ||
|
|
||
| const COSE_IDENTIFIERS: Readonly<Partial<Record<AlgorithmName, CoseIdentifier>>> = Object.freeze({ | ||
| 'ML-DSA-44': -48, | ||
| 'ML-DSA-65': -49, | ||
| 'ML-DSA-87': -50, | ||
| }); | ||
|
|
||
| const ALGORITHM_NAMES = [ | ||
| 'ML-KEM-512', | ||
| 'ML-KEM-768', | ||
| 'ML-KEM-1024', | ||
| 'ML-DSA-44', | ||
| 'ML-DSA-65', | ||
| 'ML-DSA-87', | ||
| 'SLH-DSA-SHA2-128s', | ||
| 'SLH-DSA-SHA2-128f', | ||
| 'SLH-DSA-SHA2-192s', | ||
| 'SLH-DSA-SHA2-192f', | ||
| 'SLH-DSA-SHA2-256s', | ||
| 'SLH-DSA-SHA2-256f', | ||
| 'SLH-DSA-SHAKE-128s', | ||
| 'SLH-DSA-SHAKE-128f', | ||
| 'SLH-DSA-SHAKE-192s', | ||
| 'SLH-DSA-SHAKE-192f', | ||
| 'SLH-DSA-SHAKE-256s', | ||
| 'SLH-DSA-SHAKE-256f', | ||
| ] as const satisfies ReadonlyArray<AlgorithmName>; | ||
|
|
||
| const IDENTIFIER_RECORDS = Object.freeze( | ||
| ALGORITHM_NAMES.map((name) => | ||
| Object.freeze({ | ||
| name, | ||
| jose: JOSE_IDENTIFIERS[name], | ||
| cose: COSE_IDENTIFIERS[name], | ||
| x509: DEFAULT_X509_PARAMETERS_POLICY, | ||
| } satisfies IdentifierRecord), | ||
| ), | ||
| ); | ||
|
|
||
| const IDENTIFIER_RECORDS_BY_NAME: IdentifierRecordMap = Object.freeze( | ||
| Object.fromEntries( | ||
| IDENTIFIER_RECORDS.map( | ||
| (record) => [record.name, record] satisfies [AlgorithmName, IdentifierRecord], | ||
| ), | ||
| ) as Record<AlgorithmName, IdentifierRecord>, | ||
| ); | ||
|
eacet marked this conversation as resolved.
|
||
|
|
||
| export function listRegistryAlgorithmNames(): readonly AlgorithmName[] { | ||
| return ALGORITHM_NAMES; | ||
|
eacet marked this conversation as resolved.
|
||
| } | ||
|
|
||
| export function listIdentifierRecords(): readonly IdentifierRecord[] { | ||
| return IDENTIFIER_RECORDS; | ||
| } | ||
|
|
||
| export function getIdentifierRecord(name: AlgorithmName): IdentifierRecord { | ||
| const record = IDENTIFIER_RECORDS_BY_NAME[name]; | ||
| if (record === undefined) { | ||
| throw new UnknownAlgorithmError(name); | ||
| } | ||
| return record; | ||
| } | ||
|
|
||
| export function deriveOidFromName(name: AlgorithmName): string { | ||
| return OID.fromName(name); | ||
| } | ||
|
eacet marked this conversation as resolved.
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import type { AlgorithmName } from 'pq-oid'; | ||
|
|
||
| export type { AlgorithmName }; | ||
|
|
||
| export type JoseIdentifier = 'ML-DSA-44' | 'ML-DSA-65' | 'ML-DSA-87'; | ||
|
|
||
| export type CoseIdentifier = -48 | -49 | -50; | ||
|
|
||
| export type MappingTarget = 'OID' | 'JOSE' | 'COSE' | 'X509'; | ||
|
|
||
| export type X509ParametersEncoding = 'absent' | 'null'; | ||
|
|
||
| export interface X509ParametersPolicy { | ||
| defaultParametersEncoding: X509ParametersEncoding; | ||
| acceptNull: boolean; | ||
| acceptAbsent: boolean; | ||
| } | ||
|
|
||
| export interface IdentifierRecord { | ||
| name: AlgorithmName; | ||
| jose?: JoseIdentifier; | ||
| cose?: CoseIdentifier; | ||
| x509: X509ParametersPolicy; | ||
| } | ||
|
|
||
| export type IdentifierRecordMap = Readonly<Record<AlgorithmName, IdentifierRecord>>; | ||
|
eacet marked this conversation as resolved.
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import { describe, expect, test } from 'bun:test'; | ||
| import { Algorithm, OID } from 'pq-oid'; | ||
| import { listRegistryAlgorithmNames } from '../src/registry'; | ||
|
|
||
| describe('pq-oid contract', () => { | ||
| test('OID.fromName and OID.toName are callable', () => { | ||
| const sampleName = listRegistryAlgorithmNames()[0]; | ||
| const oid = OID.fromName(sampleName); | ||
| expect(typeof oid).toBe('string'); | ||
| expect(OID.toName(oid)).toBe(sampleName); | ||
| }); | ||
|
|
||
| test('Algorithm.list remains callable for compatibility', () => { | ||
| const names = Algorithm.list(); | ||
| expect(Array.isArray(names)).toBe(true); | ||
| expect(names.length).toBeGreaterThan(0); | ||
| }); | ||
| }); |
48 changes: 48 additions & 0 deletions
48
packages/pq-algorithm-id/ts/tests/registry-invariants.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import { describe, expect, test } from 'bun:test'; | ||
| import { listIdentifierRecords, listRegistryAlgorithmNames } from '../src/registry'; | ||
|
|
||
| const VALID_JOSE = new Set(['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']); | ||
| const VALID_COSE = new Set([-48, -49, -50]); | ||
|
|
||
| function hasDirectOidLiteral(value: unknown): boolean { | ||
| return typeof value === 'string' && /^\d+(?:\.\d+)+$/.test(value); | ||
| } | ||
|
|
||
| describe('registry invariants', () => { | ||
| test('every record has required X.509 policy fields', () => { | ||
| for (const record of listIdentifierRecords()) { | ||
| expect(record.x509.defaultParametersEncoding).toBe('absent'); | ||
| expect(record.x509.acceptNull).toBe(true); | ||
| expect(record.x509.acceptAbsent).toBe(true); | ||
| } | ||
| }); | ||
|
|
||
| test('registry records do not store direct OID literals', () => { | ||
| for (const record of listIdentifierRecords()) { | ||
| for (const value of Object.values(record)) { | ||
| expect(hasDirectOidLiteral(value)).toBe(false); | ||
| } | ||
| for (const value of Object.values(record.x509)) { | ||
| expect(hasDirectOidLiteral(value)).toBe(false); | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| test('JOSE and COSE entries are either absent or valid typed values', () => { | ||
| for (const name of listRegistryAlgorithmNames()) { | ||
| const record = listIdentifierRecords().find((candidate) => candidate.name === name); | ||
| expect(record).toBeDefined(); | ||
| if (!record) { | ||
| continue; | ||
| } | ||
|
|
||
| if (record.jose !== undefined) { | ||
| expect(VALID_JOSE.has(record.jose)).toBe(true); | ||
| } | ||
| if (record.cose !== undefined) { | ||
| expect(Number.isInteger(record.cose)).toBe(true); | ||
| expect(VALID_COSE.has(record.cose)).toBe(true); | ||
| } | ||
| } | ||
| }); | ||
|
eacet marked this conversation as resolved.
|
||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import { describe, expect, test } from 'bun:test'; | ||
| import { Algorithm, OID } from 'pq-oid'; | ||
| import { deriveOidFromName, listRegistryAlgorithmNames } from '../src/registry'; | ||
|
|
||
| function asSortedSet(values: readonly string[]): string[] { | ||
| return [...new Set(values)].sort(); | ||
| } | ||
|
|
||
| describe('registry parity', () => { | ||
| test('registry names match pq-oid algorithm names', () => { | ||
| const registryNames = asSortedSet(listRegistryAlgorithmNames()); | ||
| const canonicalNames = asSortedSet(Algorithm.list()); | ||
| expect(registryNames).toEqual(canonicalNames); | ||
| }); | ||
|
|
||
| test('every registry algorithm round-trips through OID.fromName and OID.toName', () => { | ||
| for (const name of listRegistryAlgorithmNames()) { | ||
| const oid = deriveOidFromName(name); | ||
| expect(oid).toBe(OID.fromName(name)); | ||
| expect(OID.toName(oid)).toBe(name); | ||
| } | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.