Fix signature validation bug for serviceFqn with colons

The validateServicePublish function was incorrectly parsing the signature
message when serviceFqn contained colons (e.g., 'chat:2.0.0@user').

Old logic: Split by ':' and expected exactly 4 parts
Problem: serviceFqn 'chat:2.0.0@user' contains a colon, so we get 5 parts

Fixed:
- Allow parts.length >= 4
- Extract timestamp from the last part
- Reconstruct serviceFqn from all middle parts (parts[2] to parts[length-2])

This fixes the '403 Invalid signature for username' error that was
preventing service publication.
This commit is contained in:
2025-12-09 22:59:02 +01:00
parent 8111cb9cec
commit 85a3de65e2
3 changed files with 37 additions and 18 deletions

42
package-lock.json generated
View File

@@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"@hono/node-server": "^1.19.6", "@hono/node-server": "^1.19.6",
"@noble/ed25519": "^3.0.0", "@noble/ed25519": "^3.0.0",
"@xtr-dev/rondevu-client": "^0.13.0",
"better-sqlite3": "^12.4.1", "better-sqlite3": "^12.4.1",
"hono": "^4.10.4" "hono": "^4.10.4"
}, },
@@ -23,9 +24,9 @@
} }
}, },
"node_modules/@cloudflare/workers-types": { "node_modules/@cloudflare/workers-types": {
"version": "4.20251115.0", "version": "4.20251209.0",
"resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20251115.0.tgz", "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20251209.0.tgz",
"integrity": "sha512-aM7jp7IfKhqKvfSaK1IhVTbSzxB6KQ4gX8e/W29tOuZk+YHlYXuRd/bMm4hWkfd7B1HWNWdsx1GTaEUoZIuVsw==", "integrity": "sha512-O+cbUVwgb4NgUB39R1cITbRshlAAPy1UQV0l8xEy2xcZ3wTh3fMl9f5oBwLsVmE9JRhIZx6llCLOBVf53eI5xA==",
"dev": true, "dev": true,
"license": "MIT OR Apache-2.0" "license": "MIT OR Apache-2.0"
}, },
@@ -485,9 +486,9 @@
} }
}, },
"node_modules/@hono/node-server": { "node_modules/@hono/node-server": {
"version": "1.19.6", "version": "1.19.7",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.6.tgz", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.7.tgz",
"integrity": "sha512-Shz/KjlIeAhfiuE93NDKVdZ7HdBVLQAfdbaXEaoAVO3ic9ibRSLGIQGkcBbFyuLr+7/1D5ZCINM8B+6IvXeMtw==", "integrity": "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=18.14.1" "node": ">=18.14.1"
@@ -572,15 +573,24 @@
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "24.10.1", "version": "24.10.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.2.tgz",
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "integrity": "sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~7.16.0" "undici-types": "~7.16.0"
} }
}, },
"node_modules/@xtr-dev/rondevu-client": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@xtr-dev/rondevu-client/-/rondevu-client-0.13.0.tgz",
"integrity": "sha512-oauCveLga4lploxpoW8U0Fd9Fyz+SAsNQzIDvAIG1fkAnAJu9eajmLsZ5JfzzDi7h2Ew1ClZ7MOrmlRfG4vaBg==",
"license": "MIT",
"dependencies": {
"@noble/ed25519": "^3.0.0"
}
},
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.15.0", "version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@@ -635,9 +645,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/better-sqlite3": { "node_modules/better-sqlite3": {
"version": "12.4.1", "version": "12.5.0",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.4.1.tgz", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.5.0.tgz",
"integrity": "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==", "integrity": "sha512-WwCZ/5Diz7rsF29o27o0Gcc1Du+l7Zsv7SYtVPG0X3G/uUI1LqdxrQI7c9Hs2FWpqXXERjW9hp6g3/tH7DlVKg==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -645,7 +655,7 @@
"prebuild-install": "^7.1.1" "prebuild-install": "^7.1.1"
}, },
"engines": { "engines": {
"node": "20.x || 22.x || 23.x || 24.x" "node": "20.x || 22.x || 23.x || 24.x || 25.x"
} }
}, },
"node_modules/bindings": { "node_modules/bindings": {
@@ -827,9 +837,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/hono": { "node_modules/hono": {
"version": "4.10.6", "version": "4.10.8",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.10.6.tgz", "resolved": "https://registry.npmjs.org/hono/-/hono-4.10.8.tgz",
"integrity": "sha512-BIdolzGpDO9MQ4nu3AUuDwHZZ+KViNm+EZ75Ae55eMXMqLVhDFqEMXxtUe9Qh8hjL+pIna/frs2j6Y2yD5Ua/g==", "integrity": "sha512-DDT0A0r6wzhe8zCGoYOmMeuGu3dyTAE40HHjwUsWFTEy5WxK1x2WDSsBPlEXgPbRIFY6miDualuUDbasPogIww==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=16.9.0" "node": ">=16.9.0"

View File

@@ -22,6 +22,7 @@
"dependencies": { "dependencies": {
"@hono/node-server": "^1.19.6", "@hono/node-server": "^1.19.6",
"@noble/ed25519": "^3.0.0", "@noble/ed25519": "^3.0.0",
"@xtr-dev/rondevu-client": "^0.13.0",
"better-sqlite3": "^12.4.1", "better-sqlite3": "^12.4.1",
"hono": "^4.10.4" "hono": "^4.10.4"
} }

View File

@@ -425,16 +425,24 @@ export async function validateServicePublish(
} }
// Parse message format: "publish:{username}:{serviceFqn}:{timestamp}" // Parse message format: "publish:{username}:{serviceFqn}:{timestamp}"
// Note: serviceFqn can contain colons (e.g., "chat:2.0.0@user"), so we need careful parsing
const parts = message.split(':'); const parts = message.split(':');
if (parts.length !== 4 || parts[0] !== 'publish' || parts[1] !== username || parts[2] !== serviceFqn) { if (parts.length < 4 || parts[0] !== 'publish' || parts[1] !== username) {
return { valid: false, error: 'Invalid message format (expected: publish:{username}:{serviceFqn}:{timestamp})' }; return { valid: false, error: 'Invalid message format (expected: publish:{username}:{serviceFqn}:{timestamp})' };
} }
const timestamp = parseInt(parts[3], 10); // The timestamp is the last part
const timestamp = parseInt(parts[parts.length - 1], 10);
if (isNaN(timestamp)) { if (isNaN(timestamp)) {
return { valid: false, error: 'Invalid timestamp in message' }; return { valid: false, error: 'Invalid timestamp in message' };
} }
// The serviceFqn is everything between username and timestamp
const extractedServiceFqn = parts.slice(2, parts.length - 1).join(':');
if (extractedServiceFqn !== serviceFqn) {
return { valid: false, error: `Service FQN mismatch (expected: ${serviceFqn}, got: ${extractedServiceFqn})` };
}
// Validate timestamp // Validate timestamp
const timestampCheck = validateTimestamp(timestamp); const timestampCheck = validateTimestamp(timestamp);
if (!timestampCheck.valid) { if (!timestampCheck.valid) {