Skip to content

Commit

Permalink
feat: Curve25519, and Curve448 support for WebCryptoAPI runtimes
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Sep 27, 2022
1 parent 47d0d77 commit fea359a
Show file tree
Hide file tree
Showing 14 changed files with 172 additions and 142 deletions.
10 changes: 2 additions & 8 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,20 +66,14 @@ jobs:
test:
needs:
- build
continue-on-error: ${{ matrix.experimental || false }}
strategy:
fail-fast: false
matrix:
node-version:
- 12.20.0
- 12
- 14.15.0
- 14
- 16.13.0
- 16
include:
- experimental: true
node-version: '>=17'
- 18

runs-on: ubuntu-latest
steps:
Expand Down Expand Up @@ -116,7 +110,7 @@ jobs:
if: ${{ !startsWith(matrix.node-version, '14') && !startsWith(matrix.node-version, '12') }}
- name: Test with Node.js Web API
run: npm run test-webapi
if: ${{ matrix.experimental }}
if: ${{ !startsWith(matrix.node-version, '14') && !startsWith(matrix.node-version, '12') }}

test-deno:
needs:
Expand Down
18 changes: 16 additions & 2 deletions src/lib/crypto_key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ export function checkSigCryptoKey(key: CryptoKey, alg: string, ...usages: KeyUsa
if (!isAlgorithm(key.algorithm, 'NODE-ED25519')) throw unusable('NODE-ED25519')
break
}
case 'EdDSA': {
if (key.algorithm.name !== 'Ed25519' && key.algorithm.name !== 'Ed448') {
throw unusable('Ed25519 or Ed448')
}
break
}
case 'ES256':
case 'ES384':
case 'ES512': {
Expand Down Expand Up @@ -111,9 +117,17 @@ export function checkEncCryptoKey(key: CryptoKey, alg: string, ...usages: KeyUsa
if (actual !== expected) throw unusable(expected, 'algorithm.length')
break
}
case 'ECDH':
if (!isAlgorithm(key.algorithm, 'ECDH')) throw unusable('ECDH')
case 'ECDH': {
switch (key.algorithm.name) {
case 'ECDH':
case 'X25519':
case 'X448':
break
default:
throw unusable('ECDH, X25519, or X448')
}
break
}
case 'PBES2-HS256+A128KW':
case 'PBES2-HS384+A192KW':
case 'PBES2-HS512+A256KW':
Expand Down
21 changes: 17 additions & 4 deletions src/runtime/browser/asn1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,14 @@ const getNamedCurve = (keyData: Uint8Array): string => {
return 'P-384'
case findOid(keyData, [0x2b, 0x81, 0x04, 0x00, 0x23]):
return 'P-521'
case isCloudflareWorkers() && findOid(keyData, [0x2b, 0x65, 0x70]):
case findOid(keyData, [0x2b, 0x65, 0x6e]):
return 'X25519'
case findOid(keyData, [0x2b, 0x65, 0x6f]):
return 'X448'
case findOid(keyData, [0x2b, 0x65, 0x70]):
return 'Ed25519'
case findOid(keyData, [0x2b, 0x65, 0x71]):
return 'Ed448'
default:
throw new JOSENotSupported('Invalid or unsupported EC Key Curve or OKP Key Sub Type')
}
Expand Down Expand Up @@ -123,15 +129,22 @@ const genericImport = async (
case 'ECDH-ES':
case 'ECDH-ES+A128KW':
case 'ECDH-ES+A192KW':
case 'ECDH-ES+A256KW':
algorithm = { name: 'ECDH', namedCurve: getNamedCurve(keyData) }
case 'ECDH-ES+A256KW': {
const namedCurve = getNamedCurve(keyData)
algorithm = namedCurve.startsWith('P-') ? { name: 'ECDH', namedCurve } : { name: namedCurve }
keyUsages = isPublic ? [] : ['deriveBits']
break
case isCloudflareWorkers() && 'EdDSA':
}
case isCloudflareWorkers() && 'EdDSA': {
const namedCurve = getNamedCurve(keyData).toUpperCase()
algorithm = { name: `NODE-${namedCurve}`, namedCurve: `NODE-${namedCurve}` }
keyUsages = isPublic ? ['verify'] : ['sign']
break
}
case 'EdDSA':
algorithm = { name: getNamedCurve(keyData) }
keyUsages = isPublic ? ['verify'] : ['sign']
break
default:
throw new JOSENotSupported('Invalid or unsupported "alg" (Algorithm) value')
}
Expand Down
20 changes: 17 additions & 3 deletions src/runtime/browser/ecdhes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,24 @@ export async function deriveKey(
uint32be(keyLength),
)

let length: number
if (publicKey.algorithm.name === 'X25519') {
length = 256
} else if (publicKey.algorithm.name === 'X448') {
length = 448
} else {
length =
Math.ceil(parseInt((<EcKeyAlgorithm>publicKey.algorithm).namedCurve.substr(-3), 10) / 8) << 3
}

const sharedSecret = new Uint8Array(
await crypto.subtle.deriveBits(
{
name: 'ECDH',
name: publicKey.algorithm.name,
public: publicKey,
},
privateKey,
Math.ceil(parseInt((<EcKeyAlgorithm>privateKey.algorithm).namedCurve.slice(-3), 10) / 8) << 3,
length,
),
)

Expand All @@ -54,5 +64,9 @@ export function ecdhAllowed(key: unknown) {
if (!isCryptoKey(key)) {
throw new TypeError(invalidKeyInput(key, ...types))
}
return ['P-256', 'P-384', 'P-521'].includes((<EcKeyAlgorithm>key.algorithm).namedCurve)
return (
['P-256', 'P-384', 'P-521'].includes((<EcKeyAlgorithm>key.algorithm).namedCurve) ||
key.algorithm.name === 'X25519' ||
key.algorithm.name === 'X448'
)
}
12 changes: 9 additions & 3 deletions src/runtime/browser/env.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
export function isCloudflareWorkers(): boolean {
// @ts-expect-error
return typeof WebSocketPair === 'function'
export function isCloudflareWorkers() {
return (
// @ts-ignore
typeof WebSocketPair !== 'undefined' ||
// @ts-ignore
(typeof navigator !== 'undefined' && navigator.userAgent === 'Cloudflare-Workers') ||
// @ts-ignore
(typeof EdgeRuntime !== 'undefined' && EdgeRuntime === 'vercel')
)
}
35 changes: 32 additions & 3 deletions src/runtime/browser/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ function getModulusLengthOption(options?: GenerateKeyPairOptions) {
}

export async function generateKeyPair(alg: string, options?: GenerateKeyPairOptions) {
let algorithm: RsaHashedKeyGenParams | EcKeyGenParams
let algorithm: RsaHashedKeyGenParams | EcKeyGenParams | KeyAlgorithm
let keyUsages: KeyUsage[]

switch (alg) {
Expand Down Expand Up @@ -120,13 +120,42 @@ export async function generateKeyPair(alg: string, options?: GenerateKeyPairOpti
throw new JOSENotSupported('Invalid or unsupported crv option provided')
}
break
case 'EdDSA':
keyUsages = ['sign', 'verify']
const crv = options?.crv ?? 'Ed25519'
switch (crv) {
case 'Ed25519':
case 'Ed448':
algorithm = { name: crv }
break
default:
throw new JOSENotSupported('Invalid or unsupported crv option provided')
}
break
case 'ECDH-ES':
case 'ECDH-ES+A128KW':
case 'ECDH-ES+A192KW':
case 'ECDH-ES+A256KW':
algorithm = { name: 'ECDH', namedCurve: options?.crv ?? 'P-256' }
case 'ECDH-ES+A256KW': {
keyUsages = ['deriveKey', 'deriveBits']
const crv = options?.crv ?? 'P-256'
switch (crv) {
case 'P-256':
case 'P-384':
case 'P-521': {
algorithm = { name: 'ECDH', namedCurve: crv }
break
}
case 'X25519':
case 'X448':
algorithm = { name: crv }
break
default:
throw new JOSENotSupported(
'Invalid or unsupported crv option provided, supported values are P-256, P-384, P-521, X25519, and X448',
)
}
break
}
default:
throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value')
}
Expand Down
22 changes: 19 additions & 3 deletions src/runtime/browser/jwk_to_key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,27 @@ function subtleMapping(jwk: JWK): {
keyUsages = jwk.d ? ['sign'] : ['verify']
break
default:
throw new JOSENotSupported(
'Invalid or unsupported JWK "crv" (Subtype of Key Pair) Parameter value',
)
throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value')
}
break
case 'OKP': {
switch (jwk.alg) {
case 'EdDSA':
algorithm = { name: jwk.crv! }
keyUsages = jwk.d ? ['sign'] : ['verify']
break
case 'ECDH-ES':
case 'ECDH-ES+A128KW':
case 'ECDH-ES+A192KW':
case 'ECDH-ES+A256KW':
algorithm = { name: jwk.crv! }
keyUsages = jwk.d ? ['deriveBits'] : []
break
default:
throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value')
}
break
}
default:
throw new JOSENotSupported('Invalid or unsupported JWK "kty" (Key Type) Parameter value')
}
Expand Down
2 changes: 2 additions & 0 deletions src/runtime/browser/subtle_dsa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export default function subtleDsa(alg: string, algorithm: KeyAlgorithm | EcKeyAl
case isCloudflareWorkers() && 'EdDSA':
const { namedCurve } = <EcKeyAlgorithm>algorithm
return <EcKeyAlgorithm>{ name: namedCurve, namedCurve }
case 'EdDSA':
return { name: algorithm.name }
default:
throw new JOSENotSupported(
`alg ${alg} is not supported either by JOSE or your javascript runtime`,
Expand Down
8 changes: 4 additions & 4 deletions test/jwe/smoke.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -299,12 +299,12 @@ test('as keyobject', smoke, 'oct256gcm', ['encrypt'], ['decrypt'], true)
test(smoke, 'oct256c')
test(smoke, 'oct384c')
test(smoke, 'oct512c')
test(smoke, 'x25519dir')

conditional({ webcrypto: 0 })(smoke, 'rsa1_5')
conditional({ webcrypto: 0, electron: 0 })(smoke, 'x25519kw')
conditional({ webcrypto: 0 })(smoke, 'x25519dir')
conditional({ webcrypto: 0, electron: 0 })(smoke, 'x448kw')
conditional({ webcrypto: 0, electron: 0 })(smoke, 'x448dir')
conditional({ electron: 0 })(smoke, 'x25519kw')
conditional({ electron: 0 })(smoke, 'x448kw')
conditional({ electron: 0 })(smoke, 'x448dir')
conditional({ webcrypto: 0 })('as keyobject', smoke, 'oct256c', undefined, undefined, true)
conditional({ webcrypto: 0 })('as keyobject', smoke, 'oct384c', undefined, undefined, true)
conditional({ webcrypto: 0 })('as keyobject', smoke, 'oct512c', undefined, undefined, true)
Expand Down
71 changes: 35 additions & 36 deletions test/jwk/jwk2key.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,43 @@ const rsa = {
qi: 'htPHLViOVG6QrldfuHn9evfdlD-UEuViOWNx8aKR3IBv0qegpJ78vYB4hdAcJZtBslKI97En5rzOAN3Y6Y8MbI4oN77WeiePJl2cMrS64evmlERvjJ6ZTs8jK0iV5q_gIZ9Qg9drmolUgb_CccQOBFbqSL6YkXwCBxlkCrzTlhc',
kty: 'RSA',
}
const secp256k1 = {
crv: 'secp256k1',
x: 'WsY3Cti12AIuzgUEIINSmyhT8O6-o_6sBaUnjxKtJkE',
y: 'yejzoIE2tLzM_av8Pbd3rW7adTxlUqys2Ajk-JCBLp8',
d: '47Iw2GXvj-hpfgGsfF3F2mekHKaDc2qv7WTqtAkU1H0',
kty: 'EC',
}
const ed25519 = {
crv: 'Ed25519',
x: 'GVLslCt7dY6H8p_yatNaGOtpdrCho5qaLvIvNTMd29M',
d: 'FRaWZohbbDyzhYpTCS9m4fv2xoK6HG83bw6jq6zNxEs',
kty: 'OKP',
}
const ed448 = {
crv: 'Ed448',
x: 'KYWcaDwgH77xdAwcbzOgvCVcGMy9I6prRQBhQTTdKXUcr-VquTz7Fd5adJO0wT2VHysF3bk3kBoA',
d: 'UhC3-vN5vp_g9PnTknXZgfXUez7Xvw-OfuJ0pYkuwzpYkcTvacqoFkV_O05WMHpyXkzH9q2wzx5n',
kty: 'OKP',
}
const x25519 = {
crv: 'X25519',
x: 'axR8Q7PEd74nY9nWaAoAYpMe3gp5sWbau6V6X1inPw4',
d: 'aCvvb3jEBnxJJBjCIN2a9ZDTL-HG6LVgBbij4m8-d3Y',
kty: 'OKP',
}
const x448 = {
crv: 'X448',
x: 'z8s0Ej7D4pgIDu233UHoDW48EbiEm5eFv8_LuFwRr0xVREHhCtdxH75x6J8egZbjDGweOSbeHbY',
d: 'xBrCwLlrHa1ov2cbmD4eMw4t6DoN_MWsBT_mxcA_QWsCS_9sKMRyFpphNN9_2iKrGPTC9pWCS5w',
kty: 'OKP',
}
test(testKeyImportExport, { ...rsa, alg: 'RS256' })
test(testKeyImportExport, { ...rsa, alg: 'PS256' })
test(testKeyImportExport, { ...rsa, alg: 'RSA-OAEP' })
test(testKeyImportExport, { ...rsa, alg: 'RSA-OAEP-256' })
test(testKeyImportExport, { ...ed25519, alg: 'EdDSA' })
test(testKeyImportExport, { ...x25519, alg: 'ECDH-ES' })

test('Uin8tArray can be transformed to a JWK', async (t) => {
t.deepEqual(
Expand All @@ -203,43 +236,9 @@ conditional({ webcrypto: 0 })('secret key object can be transformed to a JWK', a
kty: 'oct',
})
})

const secp256k1 = {
crv: 'secp256k1',
x: 'WsY3Cti12AIuzgUEIINSmyhT8O6-o_6sBaUnjxKtJkE',
y: 'yejzoIE2tLzM_av8Pbd3rW7adTxlUqys2Ajk-JCBLp8',
d: '47Iw2GXvj-hpfgGsfF3F2mekHKaDc2qv7WTqtAkU1H0',
kty: 'EC',
}
conditional({ webcrypto: 0, electron: 0 })(testKeyImportExport, {
...secp256k1,
alg: 'ES256K',
})
const ed25519 = {
crv: 'Ed25519',
x: 'GVLslCt7dY6H8p_yatNaGOtpdrCho5qaLvIvNTMd29M',
d: 'FRaWZohbbDyzhYpTCS9m4fv2xoK6HG83bw6jq6zNxEs',
kty: 'OKP',
}
conditional({ webcrypto: 0 })(testKeyImportExport, { ...ed25519, alg: 'EdDSA' })
const ed448 = {
crv: 'Ed448',
x: 'KYWcaDwgH77xdAwcbzOgvCVcGMy9I6prRQBhQTTdKXUcr-VquTz7Fd5adJO0wT2VHysF3bk3kBoA',
d: 'UhC3-vN5vp_g9PnTknXZgfXUez7Xvw-OfuJ0pYkuwzpYkcTvacqoFkV_O05WMHpyXkzH9q2wzx5n',
kty: 'OKP',
}
conditional({ webcrypto: 0, electron: 0 })(testKeyImportExport, { ...ed448, alg: 'EdDSA' })
const x25519 = {
crv: 'X25519',
x: 'axR8Q7PEd74nY9nWaAoAYpMe3gp5sWbau6V6X1inPw4',
d: 'aCvvb3jEBnxJJBjCIN2a9ZDTL-HG6LVgBbij4m8-d3Y',
kty: 'OKP',
}
conditional({ webcrypto: 0 })(testKeyImportExport, { ...x25519, alg: 'ECDH-ES' })
const x448 = {
crv: 'X448',
x: 'z8s0Ej7D4pgIDu233UHoDW48EbiEm5eFv8_LuFwRr0xVREHhCtdxH75x6J8egZbjDGweOSbeHbY',
d: 'xBrCwLlrHa1ov2cbmD4eMw4t6DoN_MWsBT_mxcA_QWsCS_9sKMRyFpphNN9_2iKrGPTC9pWCS5w',
kty: 'OKP',
}
conditional({ webcrypto: 0, electron: 0 })(testKeyImportExport, { ...x448, alg: 'ECDH-ES' })
conditional({ electron: 0 })(testKeyImportExport, { ...ed448, alg: 'EdDSA' })
conditional({ electron: 0 })(testKeyImportExport, { ...x448, alg: 'ECDH-ES' })
2 changes: 1 addition & 1 deletion test/jws/cookbook.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ const vectors = [
},
{
title: 'https://www.rfc-editor.org/rfc/rfc8037#appendix-A.4 - Ed25519 Signing',
webcrypto: false,
webcrypto: true,
reproducible: true,
input: {
payload: 'Example of Ed25519 signing',
Expand Down
4 changes: 2 additions & 2 deletions test/jws/smoke.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -193,10 +193,10 @@ test(smoke, 'p521')
test(smoke, 'oct256')
test(smoke, 'oct384')
test(smoke, 'oct512')
test(smoke, 'ed25519')
test('as keyobject', smoke, 'oct256', true)
test('as keyobject', smoke, 'oct384', true)
test('as keyobject', smoke, 'oct512', true)

conditional({ webcrypto: 0, electron: 0 })(smoke, 'secp256k1')
conditional({ webcrypto: 0 })(smoke, 'ed25519')
conditional({ webcrypto: 0, electron: 0 })(smoke, 'ed448')
conditional({ electron: 0 })(smoke, 'ed448')
Loading

0 comments on commit fea359a

Please sign in to comment.