75 行
2.4 KiB
TypeScript
75 行
2.4 KiB
TypeScript
|
|
import assert from 'node:assert/strict'
|
||
|
|
import { readFileSync } from 'node:fs'
|
||
|
|
import test from 'node:test'
|
||
|
|
import { decodeBase64 } from '../security/base64.js'
|
||
|
|
import {
|
||
|
|
PluginManifestSignatureError,
|
||
|
|
verifySignedPluginManifest,
|
||
|
|
type SignedPluginManifest,
|
||
|
|
} from '../src/internalSecurity'
|
||
|
|
import { canonicalJson } from '../security/canonical-json.js'
|
||
|
|
|
||
|
|
interface ServerSignatureVector {
|
||
|
|
keyId: string
|
||
|
|
publicKeyRawBase64: string
|
||
|
|
canonicalJson: string
|
||
|
|
signatureBase64Url: string
|
||
|
|
}
|
||
|
|
|
||
|
|
const vectorPath = new URL('./fixtures/rn-release-signature-vector.generated.json', import.meta.url)
|
||
|
|
const vector = JSON.parse(readFileSync(vectorPath, 'utf8')) as ServerSignatureVector
|
||
|
|
const manifest = JSON.parse(vector.canonicalJson) as SignedPluginManifest
|
||
|
|
const trustedKeys = { [vector.keyId]: vector.publicKeyRawBase64 }
|
||
|
|
|
||
|
|
test('accepts the Server-owned Ed25519 verification vector byte-for-byte', () => {
|
||
|
|
assert.deepEqual(
|
||
|
|
Buffer.from(canonicalJson(manifest), 'utf8'),
|
||
|
|
Buffer.from(vector.canonicalJson, 'utf8'),
|
||
|
|
)
|
||
|
|
assert.equal(decodeBase64(vector.publicKeyRawBase64).length, 32)
|
||
|
|
assert.equal(decodeBase64(vector.signatureBase64Url).length, 64)
|
||
|
|
assert.doesNotThrow(() =>
|
||
|
|
verifySignedPluginManifest(manifest, vector.keyId, vector.signatureBase64Url, trustedKeys),
|
||
|
|
)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('canonical manifest omits null fields used only by non-common modules', () => {
|
||
|
|
assert.equal(
|
||
|
|
canonicalJson({ moduleId: 'common', commonVersionRange: null }),
|
||
|
|
'{"moduleId":"common"}',
|
||
|
|
)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('rejects an unknown plugin signing key', () => {
|
||
|
|
assert.throws(
|
||
|
|
() => verifySignedPluginManifest(manifest, 'unknown', vector.signatureBase64Url, trustedKeys),
|
||
|
|
(error: unknown) =>
|
||
|
|
error instanceof PluginManifestSignatureError && error.code === 'UNKNOWN_SIGNING_KEY',
|
||
|
|
)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('rejects any immutable manifest field tampering', () => {
|
||
|
|
assert.throws(
|
||
|
|
() =>
|
||
|
|
verifySignedPluginManifest(
|
||
|
|
{ ...manifest, bundleSha256: 'b'.repeat(64) },
|
||
|
|
vector.keyId,
|
||
|
|
vector.signatureBase64Url,
|
||
|
|
trustedKeys,
|
||
|
|
),
|
||
|
|
(error: unknown) =>
|
||
|
|
error instanceof PluginManifestSignatureError && error.code === 'INVALID_SIGNATURE',
|
||
|
|
)
|
||
|
|
assert.throws(
|
||
|
|
() =>
|
||
|
|
verifySignedPluginManifest(
|
||
|
|
{ ...manifest, archiveSha256: 'd'.repeat(64) },
|
||
|
|
vector.keyId,
|
||
|
|
vector.signatureBase64Url,
|
||
|
|
trustedKeys,
|
||
|
|
),
|
||
|
|
(error: unknown) =>
|
||
|
|
error instanceof PluginManifestSignatureError && error.code === 'INVALID_SIGNATURE',
|
||
|
|
)
|
||
|
|
})
|