mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Add unit tests
This commit is contained in:
parent
ec22421bd9
commit
f3b7847697
2 changed files with 207 additions and 3 deletions
|
|
@ -96,17 +96,17 @@ export function getCombinedCaBundlePath() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read and validate user certificate file with comprehensive security checks.
|
* Read and validate user certificate file
|
||||||
* @param {string} certPath - Path to certificate file
|
* @param {string} certPath - Path to certificate file
|
||||||
* @returns {string | null} Certificate PEM content or null if invalid/unreadable
|
* @returns {string | null} Certificate PEM content or null if invalid/unreadable
|
||||||
*/
|
*/
|
||||||
function readUserCertificateFile(certPath) {
|
function readUserCertificateFile(certPath) {
|
||||||
try {
|
try {
|
||||||
|
// Perform security checks before reading
|
||||||
if (typeof certPath !== "string" || certPath.trim().length === 0) {
|
if (typeof certPath !== "string" || certPath.trim().length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path traversal protection - check for .. and multiple slashes
|
|
||||||
if (certPath.includes("..") || certPath.includes("//") || certPath.includes("\\\\")) {
|
if (certPath.includes("..") || certPath.includes("//") || certPath.includes("\\\\")) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -132,7 +132,7 @@ function readUserCertificateFile(certPath) {
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
} catch {
|
} catch {
|
||||||
// Silently fail on any errors (permissions, parsing, etc.)
|
// Silently fail on any errors
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,13 @@ function removeBundleIfExists() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Utility to get a valid PEM certificate for testing
|
||||||
|
function getValidCert() {
|
||||||
|
const cert = typeof tls.rootCertificates?.[0] === "string" ? tls.rootCertificates[0] : "";
|
||||||
|
assert.ok(cert.includes("BEGIN CERTIFICATE"), "Environment lacks Node root certificates for test");
|
||||||
|
return cert;
|
||||||
|
}
|
||||||
|
|
||||||
describe("certBundle.getCombinedCaBundlePath", () => {
|
describe("certBundle.getCombinedCaBundlePath", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mock.restoreAll();
|
mock.restoreAll();
|
||||||
|
|
@ -69,3 +76,200 @@ describe("certBundle.getCombinedCaBundlePath", () => {
|
||||||
assert.ok(!contents.includes(invalidMarker), "Bundle should not include invalid Safe Chain content");
|
assert.ok(!contents.includes(invalidMarker), "Bundle should not include invalid Safe Chain content");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("certBundle.getCombinedCaBundlePathWithUserCerts", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mock.restoreAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns a path with Safe Chain CA when no user cert provided", async () => {
|
||||||
|
// Mock getCaCertPath to return valid cert
|
||||||
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-"));
|
||||||
|
const safeChainPath = path.join(tmpDir, "safechain.pem");
|
||||||
|
fs.writeFileSync(safeChainPath, getValidCert(), "utf8");
|
||||||
|
|
||||||
|
mock.module("./certUtils.js", {
|
||||||
|
namedExports: {
|
||||||
|
getCaCertPath: () => safeChainPath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getCombinedCaBundlePathWithUserCerts } = await import("./certBundle.js");
|
||||||
|
const bundlePath = getCombinedCaBundlePathWithUserCerts(undefined);
|
||||||
|
|
||||||
|
assert.ok(fs.existsSync(bundlePath), "Bundle path should exist");
|
||||||
|
const contents = fs.readFileSync(bundlePath, "utf8");
|
||||||
|
assert.match(contents, /-----BEGIN CERTIFICATE-----/, "Should contain Safe Chain CA");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("merges user cert with Safe Chain CA", async () => {
|
||||||
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-"));
|
||||||
|
|
||||||
|
// Create Safe Chain CA
|
||||||
|
const safeChainPath = path.join(tmpDir, "safechain.pem");
|
||||||
|
const safeChainCert = getValidCert();
|
||||||
|
fs.writeFileSync(safeChainPath, safeChainCert, "utf8");
|
||||||
|
|
||||||
|
// Create user cert file
|
||||||
|
const userCertPath = path.join(tmpDir, "user-cert.pem");
|
||||||
|
const userCert = getValidCert();
|
||||||
|
fs.writeFileSync(userCertPath, userCert, "utf8");
|
||||||
|
|
||||||
|
mock.module("./certUtils.js", {
|
||||||
|
namedExports: {
|
||||||
|
getCaCertPath: () => safeChainPath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getCombinedCaBundlePathWithUserCerts } = await import("./certBundle.js");
|
||||||
|
const bundlePath = getCombinedCaBundlePathWithUserCerts(userCertPath);
|
||||||
|
|
||||||
|
assert.ok(fs.existsSync(bundlePath), "Bundle path should exist");
|
||||||
|
const contents = fs.readFileSync(bundlePath, "utf8");
|
||||||
|
|
||||||
|
// Both certs should be in the bundle
|
||||||
|
const certCount = (contents.match(/-----BEGIN CERTIFICATE-----/g) || []).length;
|
||||||
|
assert.ok(certCount >= 2, "Bundle should contain both Safe Chain and user certificates");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores non-existent user cert path", async () => {
|
||||||
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-"));
|
||||||
|
const safeChainPath = path.join(tmpDir, "safechain.pem");
|
||||||
|
fs.writeFileSync(safeChainPath, getValidCert(), "utf8");
|
||||||
|
|
||||||
|
mock.module("./certUtils.js", {
|
||||||
|
namedExports: {
|
||||||
|
getCaCertPath: () => safeChainPath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getCombinedCaBundlePathWithUserCerts } = await import("./certBundle.js");
|
||||||
|
const bundlePath = getCombinedCaBundlePathWithUserCerts("/nonexistent/path.pem");
|
||||||
|
|
||||||
|
assert.ok(fs.existsSync(bundlePath), "Bundle path should exist");
|
||||||
|
const contents = fs.readFileSync(bundlePath, "utf8");
|
||||||
|
// Should still have Safe Chain CA
|
||||||
|
assert.match(contents, /-----BEGIN CERTIFICATE-----/, "Should contain Safe Chain CA");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores invalid PEM user cert", async () => {
|
||||||
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-"));
|
||||||
|
|
||||||
|
const safeChainPath = path.join(tmpDir, "safechain.pem");
|
||||||
|
fs.writeFileSync(safeChainPath, getValidCert(), "utf8");
|
||||||
|
|
||||||
|
const userCertPath = path.join(tmpDir, "invalid.pem");
|
||||||
|
fs.writeFileSync(userCertPath, "NOT A VALID CERTIFICATE", "utf8");
|
||||||
|
|
||||||
|
mock.module("./certUtils.js", {
|
||||||
|
namedExports: {
|
||||||
|
getCaCertPath: () => safeChainPath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getCombinedCaBundlePathWithUserCerts } = await import("./certBundle.js");
|
||||||
|
const bundlePath = getCombinedCaBundlePathWithUserCerts(userCertPath);
|
||||||
|
|
||||||
|
assert.ok(fs.existsSync(bundlePath), "Bundle path should exist");
|
||||||
|
const contents = fs.readFileSync(bundlePath, "utf8");
|
||||||
|
// Should still have Safe Chain CA only
|
||||||
|
assert.match(contents, /-----BEGIN CERTIFICATE-----/, "Should contain Safe Chain CA");
|
||||||
|
assert.ok(!contents.includes("NOT A VALID"), "Should not include invalid cert");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects user cert with path traversal attempts", async () => {
|
||||||
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-"));
|
||||||
|
const safeChainPath = path.join(tmpDir, "safechain.pem");
|
||||||
|
fs.writeFileSync(safeChainPath, getValidCert(), "utf8");
|
||||||
|
|
||||||
|
mock.module("./certUtils.js", {
|
||||||
|
namedExports: {
|
||||||
|
getCaCertPath: () => safeChainPath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getCombinedCaBundlePathWithUserCerts } = await import("./certBundle.js");
|
||||||
|
const bundlePath = getCombinedCaBundlePathWithUserCerts("../../../etc/passwd");
|
||||||
|
|
||||||
|
assert.ok(fs.existsSync(bundlePath), "Bundle path should exist");
|
||||||
|
const contents = fs.readFileSync(bundlePath, "utf8");
|
||||||
|
// Should only have Safe Chain CA, rejected the traversal path
|
||||||
|
assert.match(contents, /-----BEGIN CERTIFICATE-----/, "Should contain Safe Chain CA");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects user cert with symlink", async () => {
|
||||||
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-"));
|
||||||
|
|
||||||
|
const safeChainPath = path.join(tmpDir, "safechain.pem");
|
||||||
|
fs.writeFileSync(safeChainPath, getValidCert(), "utf8");
|
||||||
|
|
||||||
|
// Create a target file and a symlink to it
|
||||||
|
const targetCert = path.join(tmpDir, "target.pem");
|
||||||
|
fs.writeFileSync(targetCert, getValidCert(), "utf8");
|
||||||
|
|
||||||
|
const symlinkPath = path.join(tmpDir, "symlink.pem");
|
||||||
|
try {
|
||||||
|
fs.symlinkSync(targetCert, symlinkPath);
|
||||||
|
} catch {
|
||||||
|
// Skip test if symlinks are not supported (e.g., on Windows without admin)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mock.module("./certUtils.js", {
|
||||||
|
namedExports: {
|
||||||
|
getCaCertPath: () => safeChainPath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getCombinedCaBundlePathWithUserCerts } = await import("./certBundle.js");
|
||||||
|
const bundlePath = getCombinedCaBundlePathWithUserCerts(symlinkPath);
|
||||||
|
|
||||||
|
assert.ok(fs.existsSync(bundlePath), "Bundle path should exist");
|
||||||
|
const contents = fs.readFileSync(bundlePath, "utf8");
|
||||||
|
// Should only have Safe Chain CA, symlinks are rejected
|
||||||
|
assert.match(contents, /-----BEGIN CERTIFICATE-----/, "Should contain Safe Chain CA");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects user cert that is a directory", async () => {
|
||||||
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-"));
|
||||||
|
|
||||||
|
const safeChainPath = path.join(tmpDir, "safechain.pem");
|
||||||
|
fs.writeFileSync(safeChainPath, getValidCert(), "utf8");
|
||||||
|
|
||||||
|
const certDir = path.join(tmpDir, "certs");
|
||||||
|
fs.mkdirSync(certDir);
|
||||||
|
|
||||||
|
mock.module("./certUtils.js", {
|
||||||
|
namedExports: {
|
||||||
|
getCaCertPath: () => safeChainPath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getCombinedCaBundlePathWithUserCerts } = await import("./certBundle.js");
|
||||||
|
const bundlePath = getCombinedCaBundlePathWithUserCerts(certDir);
|
||||||
|
|
||||||
|
assert.ok(fs.existsSync(bundlePath), "Bundle path should exist");
|
||||||
|
const contents = fs.readFileSync(bundlePath, "utf8");
|
||||||
|
// Should only have Safe Chain CA
|
||||||
|
assert.match(contents, /-----BEGIN CERTIFICATE-----/, "Should contain Safe Chain CA");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles empty string user cert path", async () => {
|
||||||
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-"));
|
||||||
|
const safeChainPath = path.join(tmpDir, "safechain.pem");
|
||||||
|
fs.writeFileSync(safeChainPath, getValidCert(), "utf8");
|
||||||
|
|
||||||
|
mock.module("./certUtils.js", {
|
||||||
|
namedExports: {
|
||||||
|
getCaCertPath: () => safeChainPath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getCombinedCaBundlePathWithUserCerts } = await import("./certBundle.js");
|
||||||
|
const bundlePath = getCombinedCaBundlePathWithUserCerts(" ");
|
||||||
|
|
||||||
|
assert.ok(fs.existsSync(bundlePath), "Bundle path should exist");
|
||||||
|
const contents = fs.readFileSync(bundlePath, "utf8");
|
||||||
|
assert.match(contents, /-----BEGIN CERTIFICATE-----/, "Should contain Safe Chain CA");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue