Skip to content

Commit

Permalink
Add ML-DSA
Browse files Browse the repository at this point in the history
  • Loading branch information
OR13 committed May 26, 2024
1 parent 277bce1 commit e56cd85
Show file tree
Hide file tree
Showing 13 changed files with 181 additions and 5 deletions.
53 changes: 53 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"typescript": "^4.9.4"
},
"dependencies": {
"@noble/post-quantum": "^0.1.0",
"@peculiar/x509": "^1.9.7",
"@transmute/cose": "^0.1.0",
"@transmute/rfc9162": "^0.0.5",
Expand Down
10 changes: 10 additions & 0 deletions src/cose/key/convertCoseKeyToJsonWebKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ export const convertCoseKeyToJsonWebKey = async <T>(coseKey: CoseKey): Promise<T
const kid = coseKey.get(2)
const alg = coseKey.get(3)
const crv = coseKey.get(-1)

if (alg === -49) {
return formatJwk({
kid: '',
kty: 'ML-DSA',
alg: 'ML-DSA-65',
x: base64url.encode(coseKey.get(-1) as any),
d: coseKey.get(-2) ? base64url.encode(coseKey.get(-2) as any) : undefined
}) as T
}
// kty EC, kty: EK
if (![2, 5].includes(kty)) {
throw new Error('This library requires does not support the given key type')
Expand Down
17 changes: 16 additions & 1 deletion src/cose/key/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { IANACOSEAlgorithms } from "../algorithms"

import { CoseKey } from '.'
export type CoseKeyAgreementAlgorithms = 'ECDH-ES+A128KW'
export type CoseSignatureAlgorithms = 'ES256' | 'ES384' | 'ES512'
export type CoseSignatureAlgorithms = 'ES256' | 'ES384' | 'ES512' | 'ML-DSA-65'
export type ContentTypeOfJsonWebKey = 'application/jwk+json'
export type ContentTypeOfCoseKey = 'application/cose-key'
export type PrivateKeyContentType = ContentTypeOfCoseKey | ContentTypeOfJsonWebKey
Expand All @@ -19,12 +19,27 @@ import { thumbprint } from "./thumbprint"
import { formatJwk } from './formatJwk'


import { ml_dsa65 } from '@noble/post-quantum/ml-dsa';
import { randomBytes } from "@noble/post-quantum/utils"
import { toArrayBuffer } from "../../cbor"


export const generate = async <T>(alg: CoseSignatureAlgorithms, contentType: PrivateKeyContentType = 'application/jwk+json'): Promise<T> => {
const knownAlgorithm = Object.values(IANACOSEAlgorithms).find((
entry
) => {
return entry.Name === alg
})
if (alg === 'ML-DSA-65') {
const seed = randomBytes(32)
const keys = ml_dsa65.keygen(seed);
return new Map<any, any>([
[1, 7], // kty : ML-DSA
[3, -49], // alg : ML-DSA-65
[-1, toArrayBuffer(keys.publicKey)], // public key
[-2, toArrayBuffer(keys.secretKey)], // secret key
]) as T
}
if (!knownAlgorithm) {
throw new Error('Algorithm is not supported.')
}
Expand Down
3 changes: 2 additions & 1 deletion src/cose/key/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ export * from './generate'
export * from './convertJsonWebKeyToCoseKey'
export * from './convertCoseKeyToJsonWebKey'
export * from './publicFromPrivate'
export * from './serialize'
export * from './serialize'
export * from './signer'
6 changes: 6 additions & 0 deletions src/cose/key/publicFromPrivate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ export const extracePublicKeyJwk = (secretKeyJwk: SecretKeyJwk) => {

export const extractPublicCoseKey = (secretKey: CoseKey) => {
const publicCoseKeyMap = new Map(secretKey)

if (publicCoseKeyMap.get(1) === 7) {
publicCoseKeyMap.delete(-2);
return publicCoseKeyMap
}

if (publicCoseKeyMap.get(1) !== 2) {
throw new Error('Only EC2 keys are supported')
}
Expand Down
16 changes: 16 additions & 0 deletions src/cose/key/signer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { CoseKey } from ".";

import { ml_dsa65 } from '@noble/post-quantum/ml-dsa';
import { toArrayBuffer } from "../../cbor";

export const signer = (secretKey: CoseKey) => {
const alg = secretKey.get(3);
if (alg === -49) {
return {
sign: async (bytes: ArrayBuffer) => {
return toArrayBuffer(ml_dsa65.sign(new Uint8Array(secretKey.get(-2) as ArrayBuffer), new Uint8Array(bytes)))
}
}
}
throw new Error('Unsupported algorithm')
}
16 changes: 16 additions & 0 deletions src/cose/key/verifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { CoseKey } from ".";

import { ml_dsa65 } from '@noble/post-quantum/ml-dsa';
import { toArrayBuffer } from "../../cbor";

export const signer = (publicKey: CoseKey) => {
const alg = publicKey.get(3);
if (alg === -49) {
return {
verify: async (bytes: ArrayBuffer) => {
return toArrayBuffer(ml_dsa65.sign(new Uint8Array(secretKey.get(-2) as ArrayBuffer), new Uint8Array(bytes)))
}
}
}
throw new Error('Unsupported algorithm')
}
3 changes: 3 additions & 0 deletions src/cose/sign1/getAlgFromVerificationKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ const getAlgFromVerificationKey = (alg: string): number => {
const foundAlg = algorithms.find((entry) => {
return entry.Name === alg
})
if (alg === 'ML-DSA-65') {
return -49
}
if (!foundAlg) {
throw new Error('This library requires keys to contain fully specified algorithms')
}
Expand Down
19 changes: 17 additions & 2 deletions src/cose/sign1/verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,24 @@ import { RequestCoseSign1Verifier, RequestCoseSign1Verify } from './types'
import getAlgFromVerificationKey from './getAlgFromVerificationKey'
import { DecodedToBeSigned, ProtectedHeaderMap } from './types'
import rawVerifier from '../../crypto/verifier'
import { base64url } from 'jose'
import { ml_dsa65 } from '@noble/post-quantum/ml-dsa';

const ecdsaPublicKeyJwkVerify = async (publicKeyJwk: any, encodedToBeSigned: any, signature: any) => {
const ecdsa = rawVerifier({ publicKeyJwk })
return ecdsa.verify(encodedToBeSigned, signature)
}

const mldsaPublicKeyJwkVerifier = async (publicKeyJwk: any, encodedToBeSigned: any, signature: any) => {
const publicKey = base64url.decode(publicKeyJwk.x)
return ml_dsa65.verify(publicKey, encodedToBeSigned, signature)
}

const verifier = ({ resolver }: RequestCoseSign1Verifier) => {
return {
verify: async ({ coseSign1, externalAAD }: RequestCoseSign1Verify): Promise<ArrayBuffer> => {
const publicKeyJwk = await resolver.resolve(coseSign1)
const algInPublicKey = getAlgFromVerificationKey(`${publicKeyJwk.alg}`)
const ecdsa = rawVerifier({ publicKeyJwk })
const obj = await decodeFirst(coseSign1);
const signatureStructure = obj.value;
if (!Array.isArray(signatureStructure)) {
Expand All @@ -35,7 +46,11 @@ const verifier = ({ resolver }: RequestCoseSign1Verifier) => {
payload
] as DecodedToBeSigned
const encodedToBeSigned = encode(decodedToBeSigned);
await ecdsa.verify(encodedToBeSigned, signature)
if (algInPublicKey === -49) {
mldsaPublicKeyJwkVerifier(publicKeyJwk, encodedToBeSigned, signature)
} else {
await ecdsaPublicKeyJwkVerify(publicKeyJwk, encodedToBeSigned, signature)
}
return payload;
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/x509/certificate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ const algTowebCryptoParams: Record<CoseSignatureAlgorithms, { name: string, hash
name: "ECDSA",
hash: "SHA-512",
namedCurve: "P-521",
}
},
'ML-DSA-65': null as any
}

export type RequestRootCertificate = {
Expand Down
28 changes: 28 additions & 0 deletions test/ml-dsa/cose.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as cose from '../../src'

it('ML-DSA-65', async () => {
const secretKey = await cose.key.generate<cose.key.CoseKey>('ML-DSA-65')
const publicKey = await cose.key.publicFromPrivate<cose.key.CoseKey>(secretKey)
const signer = cose.detached.signer({
remote: cose.key.signer(secretKey)
})
const message = '💣 test ✨ mesage 🔥'
const payload = new TextEncoder().encode(message)
const coseSign1 = await signer.sign({
protectedHeader: new Map([
[1, -49] // alg : ML-DSA-65
]),
unprotectedHeader: new Map(),
payload
})
const verifier = cose.detached.verifier({
resolver: {
resolve: async () => {
return cose.key.convertCoseKeyToJsonWebKey(publicKey)
}
}
})
// console.log(await cose.cbor.diagnose(coseSign1))
const verified = await verifier.verify({ coseSign1, payload: payload })
expect(new TextDecoder().decode(verified)).toBe(message)
})
11 changes: 11 additions & 0 deletions test/ml-dsa/sanity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

import { ml_dsa65 } from '@noble/post-quantum/ml-dsa';

it('ml_dsa65', () => {
const seed = new TextEncoder().encode('not a safe seed')
const aliceKeys = ml_dsa65.keygen(seed);
const msg = new Uint8Array(1);
const sig = ml_dsa65.sign(aliceKeys.secretKey, msg);
const isValid = ml_dsa65.verify(aliceKeys.publicKey, msg, sig);
expect(isValid).toBe(true)
})

0 comments on commit e56cd85

Please sign in to comment.