Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/content/docs/api/pqc.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ const recoveredKey = await subtle.decapsulateKey(
| Algorithm | `spki` | `pkcs8` | `jwk` | `raw-public` | `raw-seed` |
|:----------|:------:|:-------:|:-----:|:------------:|:----------:|
| ML-DSA-44/65/87 | ✅ | ✅ | ✅ | ✅ | ✅ |
| ML-KEM-512/768/1024 | ✅ | ✅ | | ✅ | ✅ |
| ML-KEM-512/768/1024 | ✅ | ✅ | | ✅ | ✅ |

```ts
// Export ML-DSA public key as JWK
Expand Down
2 changes: 1 addition & 1 deletion example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2815,7 +2815,7 @@ SPEC CHECKSUMS:
NitroMmkv: afbc5b2fbf963be567c6c545aa1efcf6a9cec68e
NitroModules: 11bba9d065af151eae51e38a6425e04c3b223ff3
OpenSSL-Universal: 9110d21982bb7e8b22a962b6db56a8aa805afde7
QuickCrypto: 962a3395bab0cacced1815b6cc0fa177515aff9f
QuickCrypto: 30f0d4eceadf1dbb9df5c17605e9f4279a0041ea
RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669
RCTDeprecation: c4b9e2fd0ab200e3af72b013ed6113187c607077
RCTRequired: e97dd5dafc1db8094e63bc5031e0371f092ae92a
Expand Down
2 changes: 1 addition & 1 deletion example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"react-native-mmkv": "4.0.1",
"react-native-nitro-modules": "0.33.2",
"react-native-quick-base64": "2.2.2",
"react-native-quick-crypto": "1.1.1",
"react-native-quick-crypto": "workspace:*",
"react-native-safe-area-context": "5.6.2",
"react-native-screens": "4.18.0",
"react-native-vector-icons": "10.3.0",
Expand Down
244 changes: 240 additions & 4 deletions example/src/tests/subtle/import_export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2246,18 +2246,40 @@ for (const variant of MLDSA_VARIANTS) {
});
}

// ML-DSA error handling tests
test(SUITE, 'ML-DSA-44 importKey rejects jwk format', async () => {
// ML-DSA JWK error handling tests
test(SUITE, 'ML-DSA-44 importKey rejects wrong jwk kty', async () => {
await assertThrowsAsync(
async () =>
await subtle.importKey(
'jwk',
{ kty: 'OKP', alg: 'ML-DSA-44' },
{ name: 'ML-DSA-44' },
true,
['sign'],
['verify'],
),
'DataError',
);
});

test(SUITE, 'ML-DSA-44 importKey rejects jwk alg mismatch', async () => {
const keyPair = (await subtle.generateKey({ name: 'ML-DSA-44' }, true, [
'sign',
'verify',
])) as CryptoKeyPair;
const jwk = (await subtle.exportKey(
'jwk',
keyPair.publicKey as CryptoKey,
)) as JWK;
await assertThrowsAsync(
async () =>
await subtle.importKey(
'jwk',
{ ...jwk, alg: 'ML-DSA-65' },
{ name: 'ML-DSA-44' },
true,
['verify'],
),
'NotSupportedError',
'DataError',
);
});

Expand Down Expand Up @@ -2386,6 +2408,116 @@ for (const variant of MLDSA_VARIANTS) {
);
}

// --- ML-DSA JWK import/export round-trip tests ---

for (const variant of MLDSA_VARIANTS) {
test(SUITE, `${variant} jwk export public key`, async () => {
const keyPair = (await subtle.generateKey({ name: variant }, true, [
'sign',
'verify',
])) as CryptoKeyPair;

const jwk = (await subtle.exportKey(
'jwk',
keyPair.publicKey as CryptoKey,
)) as JWK;

expect(jwk.kty).to.equal('AKP');
expect(jwk.alg).to.equal(variant);
expect(jwk.pub).to.be.a('string');
expect(jwk.priv).to.equal(undefined);
expect(jwk.key_ops).to.deep.equal(['verify']);
expect(jwk.ext).to.equal(true);
});

test(SUITE, `${variant} jwk export private key`, async () => {
const keyPair = (await subtle.generateKey({ name: variant }, true, [
'sign',
'verify',
])) as CryptoKeyPair;

const jwk = (await subtle.exportKey(
'jwk',
keyPair.privateKey as CryptoKey,
)) as JWK;

expect(jwk.kty).to.equal('AKP');
expect(jwk.alg).to.equal(variant);
expect(jwk.pub).to.be.a('string');
expect(jwk.priv).to.be.a('string');
expect(jwk.key_ops).to.deep.equal(['sign']);
expect(jwk.ext).to.equal(true);
});

test(SUITE, `${variant} jwk public key round-trip + verify`, async () => {
const keyPair = (await subtle.generateKey({ name: variant }, true, [
'sign',
'verify',
])) as CryptoKeyPair;

const jwk = (await subtle.exportKey(
'jwk',
keyPair.publicKey as CryptoKey,
)) as JWK;

const imported = await subtle.importKey(
'jwk',
jwk,
{ name: variant },
true,
['verify'],
);
expect(imported.type).to.equal('public');
expect(imported.algorithm.name).to.equal(variant);

const testData = new TextEncoder().encode('ML-DSA JWK round-trip');
const signature = await subtle.sign(
{ name: variant },
keyPair.privateKey as CryptoKey,
testData,
);
const isValid = await subtle.verify(
{ name: variant },
imported,
signature,
testData,
);
expect(isValid).to.equal(true);
});

test(SUITE, `${variant} jwk private key round-trip + sign`, async () => {
const keyPair = (await subtle.generateKey({ name: variant }, true, [
'sign',
'verify',
])) as CryptoKeyPair;

const jwk = (await subtle.exportKey(
'jwk',
keyPair.privateKey as CryptoKey,
)) as JWK;

const imported = await subtle.importKey(
'jwk',
jwk,
{ name: variant },
true,
['sign'],
);
expect(imported.type).to.equal('private');
expect(imported.algorithm.name).to.equal(variant);

const testData = new TextEncoder().encode('ML-DSA JWK private round-trip');
const signature = await subtle.sign({ name: variant }, imported, testData);
const isValid = await subtle.verify(
{ name: variant },
keyPair.publicKey as CryptoKey,
signature,
testData,
);
expect(isValid).to.equal(true);
});
}

// --- ML-KEM Import/Export Tests ---

for (const variant of MLKEM_VARIANTS) {
Expand Down Expand Up @@ -2434,6 +2566,110 @@ for (const variant of MLKEM_VARIANTS) {
});
}

// --- ML-KEM JWK import/export round-trip tests ---

for (const variant of MLKEM_VARIANTS) {
test(SUITE, `${variant} jwk export public key`, async () => {
const keyPair = (await subtle.generateKey({ name: variant }, true, [
'encapsulateBits',
'decapsulateBits',
])) as CryptoKeyPair;

const jwk = (await subtle.exportKey(
'jwk',
keyPair.publicKey as CryptoKey,
)) as JWK;

expect(jwk.kty).to.equal('AKP');
expect(jwk.alg).to.equal(variant);
expect(jwk.pub).to.be.a('string');
expect(jwk.priv).to.equal(undefined);
expect(jwk.key_ops).to.deep.equal(['encapsulateBits']);
expect(jwk.ext).to.equal(true);
});

test(SUITE, `${variant} jwk export private key`, async () => {
const keyPair = (await subtle.generateKey({ name: variant }, true, [
'encapsulateBits',
'decapsulateBits',
])) as CryptoKeyPair;

const jwk = (await subtle.exportKey(
'jwk',
keyPair.privateKey as CryptoKey,
)) as JWK;

expect(jwk.kty).to.equal('AKP');
expect(jwk.alg).to.equal(variant);
expect(jwk.pub).to.be.a('string');
expect(jwk.priv).to.be.a('string');
expect(jwk.key_ops).to.deep.equal(['decapsulateBits']);
expect(jwk.ext).to.equal(true);
});

test(SUITE, `${variant} jwk public key round-trip`, async () => {
const keyPair = (await subtle.generateKey({ name: variant }, true, [
'encapsulateBits',
'decapsulateBits',
])) as CryptoKeyPair;

const jwk = (await subtle.exportKey(
'jwk',
keyPair.publicKey as CryptoKey,
)) as JWK;

const imported = await subtle.importKey(
'jwk',
jwk,
{ name: variant },
true,
['encapsulateBits'],
);
expect(imported.type).to.equal('public');
expect(imported.algorithm.name).to.equal(variant);
});

test(
SUITE,
`${variant} jwk private key round-trip + decapsulate`,
async () => {
const keyPair = (await subtle.generateKey({ name: variant }, true, [
'encapsulateBits',
'decapsulateBits',
])) as CryptoKeyPair;

const jwk = (await subtle.exportKey(
'jwk',
keyPair.privateKey as CryptoKey,
)) as JWK;

const imported = await subtle.importKey(
'jwk',
jwk,
{ name: variant },
true,
['decapsulateBits'],
);
expect(imported.type).to.equal('private');
expect(imported.algorithm.name).to.equal(variant);

// Encapsulate with original public key, decapsulate with imported private key
const { sharedKey, ciphertext } = await subtle.encapsulateBits(
{ name: variant },
keyPair.publicKey as CryptoKey,
);
const recovered = await subtle.decapsulateBits(
{ name: variant },
imported,
ciphertext,
);
expect(new Uint8Array(recovered)).to.deep.equal(
new Uint8Array(sharedKey),
);
},
);
}

test(SUITE, 'ML-KEM-768 importKey raw rejects bad usages', async () => {
await assertThrowsAsync(
async () =>
Expand Down
Loading
Loading