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
17 changes: 17 additions & 0 deletions patches/@sigstore__core@3.2.1.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
--- a/dist/crypto.js
+++ b/dist/crypto.js
@@ -51,6 +51,14 @@
// The try/catch is to work around an issue in Node 14.x where verify throws
// an error in some scenarios if the signature is invalid.
try {
+ // Match Node/OpenSSL's default digest on Electron/BoringSSL without changing
+ // explicit algorithms or digest-free algorithms such as Ed25519 and Ed448.
+ if (algorithm === undefined) {
+ const publicKey = key instanceof crypto_1.default.KeyObject ? key : crypto_1.default.createPublicKey(key);
+ if (['ec', 'rsa', 'rsa-pss'].includes(publicKey.asymmetricKeyType)) {
+ algorithm = 'sha256';
+ }
+ }
return crypto_1.default.verify(algorithm, data, key, signature);
}
catch (e) {
13 changes: 13 additions & 0 deletions patches/@tufjs__models@4.1.0.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
--- a/dist/utils/verify.js
+++ b/dist/utils/verify.js
@@ -8,6 +8,9 @@
const crypto_1 = __importDefault(require("crypto"));
const verifySignature = (metaDataSignedData, key, signature) => {
const canonicalData = Buffer.from((0, canonical_json_1.canonicalize)(metaDataSignedData));
- return crypto_1.default.verify(undefined, canonicalData, key, Buffer.from(signature, 'hex'));
+ // OpenSSL defaults to SHA-256 for these keys; Electron's BoringSSL requires it explicitly.
+ // Ed25519 signs the message directly and must keep its undefined digest.
+ const algorithm = key.key.asymmetricKeyType === 'ed25519' ? undefined : 'sha256';
+ return crypto_1.default.verify(algorithm, canonicalData, key, Buffer.from(signature, 'hex'));
};
exports.verifySignature = verifySignature;
12 changes: 7 additions & 5 deletions pnpm-lock.yaml

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

2 changes: 2 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ patchedDependencies:
"@react-native-ai/apple@0.12.0": patches/@react-native-ai__apple@0.12.0.patch
"@react-native-menu/menu@2.0.0": patches/@react-native-menu__menu@2.0.0.patch
"@react-navigation/native-stack@7.17.6": patches/@react-navigation%2Fnative-stack@7.17.6.patch
"@sigstore/core@3.2.1": patches/@sigstore__core@3.2.1.patch
"@tufjs/models@4.1.0": patches/@tufjs__models@4.1.0.patch
app-builder-lib@26.15.6: patches/app-builder-lib@26.15.6.patch
dbus-next@0.10.2: patches/dbus-next@0.10.2.patch
effect@4.0.0-rc.112: patches/effect@4.0.0-rc.112.patch
Expand Down
97 changes: 97 additions & 0 deletions scripts/prime-signature-runtime.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import * as NodeCrypto from "node:crypto";
import * as NodeModule from "node:module";
import * as NodePath from "node:path";

import { afterEach, describe, expect, it, vi } from "vite-plus/test";

const serverRequire = NodeModule.createRequire(
new URL("../apps/server/package.json", import.meta.url),
);
const { crypto: sigstoreCrypto } = serverRequire("@sigstore/core");
const tufRequire = NodeModule.createRequire(serverRequire.resolve("@sigstore/tuf"));
const clientRequire = NodeModule.createRequire(tufRequire.resolve("tuf-js"));
const modelsRequire = NodeModule.createRequire(clientRequire.resolve("@tufjs/models"));
const { verifySignature } = modelsRequire(
NodePath.join(NodePath.dirname(clientRequire.resolve("@tufjs/models")), "utils/verify.js"),
);
const { canonicalize } = modelsRequire("@tufjs/canonical-json");
const nativeVerify = NodeCrypto.verify;

// Electron/BoringSSL rejects the implicit SHA-256 digest accepted by Node/OpenSSL.
// Exercise the actual installed dependencies with that runtime behavior on every CI host.
function requireExplicitDigest() {
vi.spyOn(serverRequire("node:crypto"), "verify").mockImplementation(
(algorithm, data, key, signature) => {
const material = typeof key === "object" && "key" in key ? key.key : key;
const publicKey =
material instanceof NodeCrypto.KeyObject ? material : NodeCrypto.createPublicKey(material);
if (algorithm == null && ["ec", "rsa", "rsa-pss"].includes(publicKey.asymmetricKeyType)) {
throw new Error("NO_DEFAULT_DIGEST");
}
return nativeVerify(algorithm, data, key, signature);
},
);
}

afterEach(() => vi.restoreAllMocks());

const keys = [
{
name: "ECDSA",
...NodeCrypto.generateKeyPairSync("ec", { namedCurve: "prime256v1" }),
algorithm: "sha256",
},
{
name: "RSA",
...NodeCrypto.generateKeyPairSync("rsa", { modulusLength: 2048 }),
algorithm: "sha256",
},
{ name: "Ed25519", ...NodeCrypto.generateKeyPairSync("ed25519"), algorithm: null },
];

describe("Prime signed publications on Electron", () => {
for (const key of keys) {
it(`verifies Sigstore ${key.name} and rejects modified data and signatures`, () => {
const data = Buffer.from("signed Prime publication");
const signature = NodeCrypto.sign(key.algorithm, data, key.privateKey);
const modified = Buffer.from(signature);
modified[0] ^= 1;
requireExplicitDigest();
for (const publicKey of [
key.publicKey,
key.publicKey.export({ type: "spki", format: "pem" }),
]) {
expect(sigstoreCrypto.verify(data, publicKey, signature)).toBe(true);
expect(
sigstoreCrypto.verify(Buffer.from("different publication"), publicKey, signature),
).toBe(false);
expect(sigstoreCrypto.verify(data, publicKey, modified)).toBe(false);
}
});

it(`verifies TUF ${key.name} and rejects modified metadata`, () => {
const metadata = { version: 1, role: "root", expires: "2030-01-01T00:00:00Z" };
const options =
key.name === "RSA" ? { padding: NodeCrypto.constants.RSA_PKCS1_PSS_PADDING } : {};
const signature = NodeCrypto.sign(key.algorithm, Buffer.from(canonicalize(metadata)), {
key: key.privateKey,
...options,
}).toString("hex");
requireExplicitDigest();
expect(verifySignature(metadata, { key: key.publicKey, ...options }, signature)).toBe(true);
expect(
verifySignature({ ...metadata, version: 2 }, { key: key.publicKey, ...options }, signature),
).toBe(false);
});
}

it("preserves an explicit non-default digest and rejects malformed keys", () => {
const key = keys[0];
const data = Buffer.from("explicit SHA-384");
const signature = NodeCrypto.sign("sha384", data, key.privateKey);
requireExplicitDigest();
expect(sigstoreCrypto.verify(data, key.publicKey, signature, "sha384")).toBe(true);
expect(sigstoreCrypto.verify(data, key.publicKey, signature, "sha256")).toBe(false);
expect(sigstoreCrypto.verify(data, "invalid key", signature)).toBe(false);
});
});
Loading