diff --git a/package-lock.json b/package-lock.json index ec1ed62..564aed6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.2.0", "license": "Apache-2.0", "dependencies": { + "@noble/post-quantum": "^0.1.0", "@peculiar/x509": "^1.9.7", "@transmute/cose": "^0.1.0", "@transmute/rfc9162": "^0.0.5", @@ -1194,6 +1195,37 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@noble/post-quantum": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@noble/post-quantum/-/post-quantum-0.1.0.tgz", + "integrity": "sha512-JG1K5NaeYr7hVzLdbtm0OYaNDbr95k2kxHFOyELuwQveRnfcoRNdHcHnG67XdxJuRVgsfs3ZWzjme4LIWaxVuw==", + "dependencies": { + "@noble/ciphers": "0.5.2", + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/post-quantum/node_modules/@noble/ciphers": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.2.tgz", + "integrity": "sha512-GADtQmZCdgbnNp+daPLc3OY3ibEtGGDV/+CzeM3MFnhiQ7ELQKlsHWYq0YbYUXx4jU3/Y1erAxU6r+hwpewqmQ==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/post-quantum/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -6273,6 +6305,27 @@ "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", "dev": true }, + "@noble/post-quantum": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@noble/post-quantum/-/post-quantum-0.1.0.tgz", + "integrity": "sha512-JG1K5NaeYr7hVzLdbtm0OYaNDbr95k2kxHFOyELuwQveRnfcoRNdHcHnG67XdxJuRVgsfs3ZWzjme4LIWaxVuw==", + "requires": { + "@noble/ciphers": "0.5.2", + "@noble/hashes": "1.4.0" + }, + "dependencies": { + "@noble/ciphers": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.2.tgz", + "integrity": "sha512-GADtQmZCdgbnNp+daPLc3OY3ibEtGGDV/+CzeM3MFnhiQ7ELQKlsHWYq0YbYUXx4jU3/Y1erAxU6r+hwpewqmQ==" + }, + "@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==" + } + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/package.json b/package.json index 18baa93..6776227 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/cose/key/convertCoseKeyToJsonWebKey.ts b/src/cose/key/convertCoseKeyToJsonWebKey.ts index f7fd85a..5432344 100644 --- a/src/cose/key/convertCoseKeyToJsonWebKey.ts +++ b/src/cose/key/convertCoseKeyToJsonWebKey.ts @@ -15,6 +15,16 @@ export const convertCoseKeyToJsonWebKey = async (coseKey: CoseKey): Promise(alg: CoseSignatureAlgorithms, contentType: PrivateKeyContentType = 'application/jwk+json'): Promise => { 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([ + [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.') } diff --git a/src/cose/key/index.ts b/src/cose/key/index.ts index df2b9af..1102407 100644 --- a/src/cose/key/index.ts +++ b/src/cose/key/index.ts @@ -16,4 +16,5 @@ export * from './generate' export * from './convertJsonWebKeyToCoseKey' export * from './convertCoseKeyToJsonWebKey' export * from './publicFromPrivate' -export * from './serialize' \ No newline at end of file +export * from './serialize' +export * from './signer' \ No newline at end of file diff --git a/src/cose/key/publicFromPrivate.ts b/src/cose/key/publicFromPrivate.ts index 3d397a8..9738609 100644 --- a/src/cose/key/publicFromPrivate.ts +++ b/src/cose/key/publicFromPrivate.ts @@ -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') } diff --git a/src/cose/key/signer.ts b/src/cose/key/signer.ts new file mode 100644 index 0000000..1ba93a6 --- /dev/null +++ b/src/cose/key/signer.ts @@ -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') +} \ No newline at end of file diff --git a/src/cose/key/verifier.ts b/src/cose/key/verifier.ts new file mode 100644 index 0000000..5221ffd --- /dev/null +++ b/src/cose/key/verifier.ts @@ -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') +} \ No newline at end of file diff --git a/src/cose/sign1/getAlgFromVerificationKey.ts b/src/cose/sign1/getAlgFromVerificationKey.ts index 552ce1e..280beee 100644 --- a/src/cose/sign1/getAlgFromVerificationKey.ts +++ b/src/cose/sign1/getAlgFromVerificationKey.ts @@ -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') } diff --git a/src/cose/sign1/verifier.ts b/src/cose/sign1/verifier.ts index e330cba..609a884 100644 --- a/src/cose/sign1/verifier.ts +++ b/src/cose/sign1/verifier.ts @@ -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 => { 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)) { @@ -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; } } diff --git a/src/x509/certificate.ts b/src/x509/certificate.ts index 60b9d63..c93d948 100644 --- a/src/x509/certificate.ts +++ b/src/x509/certificate.ts @@ -43,7 +43,8 @@ const algTowebCryptoParams: Record { + const secretKey = await cose.key.generate('ML-DSA-65') + const publicKey = await cose.key.publicFromPrivate(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) +}) \ No newline at end of file diff --git a/test/ml-dsa/sanity.test.ts b/test/ml-dsa/sanity.test.ts new file mode 100644 index 0000000..c7f8fb6 --- /dev/null +++ b/test/ml-dsa/sanity.test.ts @@ -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) +}) \ No newline at end of file