From f01162a5c536d0927f5fb76f97ce710370f56a6a Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Tue, 23 Jun 2026 11:05:22 +0200 Subject: [PATCH 1/2] feat(CSAF2.1): add recommendedTest_6_2_52.js --- README.md | 2 +- csaf_2_1/recommendedTests.js | 1 + .../recommendedTest_6_2_52.js | 226 ++++++++++++++++++ tests/csaf_2_1/oasis.js | 1 - tests/csaf_2_1/recommendedTest_6_2_52.js | 179 ++++++++++++++ tests/csaf_2_1/shared/csafDocHelper.js | 17 ++ 6 files changed, 424 insertions(+), 2 deletions(-) create mode 100644 csaf_2_1/recommendedTests/recommendedTest_6_2_52.js create mode 100644 tests/csaf_2_1/recommendedTest_6_2_52.js diff --git a/README.md b/README.md index afdaa0c6..7d844b15 100644 --- a/README.md +++ b/README.md @@ -352,7 +352,6 @@ The following tests are not yet implemented and therefore missing: - Recommended Test 6.2.50.2 - Recommended Test 6.2.50.3 - Recommended Test 6.2.51 -- Recommended Test 6.2.52 - Recommended Test 6.2.53 - Recommended Test 6.2.54.1 - Recommended Test 6.2.54.2 @@ -503,6 +502,7 @@ export const recommendedTest_6_2_41: DocumentTest export const recommendedTest_6_2_43: DocumentTest export const recommendedTest_6_2_47: DocumentTest export const recommendedTest_6_2_48: DocumentTest +export const recommendedTest_6_2_52: DocumentTest ``` [(back to top)](#bsi-csaf-validator-lib) diff --git a/csaf_2_1/recommendedTests.js b/csaf_2_1/recommendedTests.js index 46213ccb..8b2f31ac 100644 --- a/csaf_2_1/recommendedTests.js +++ b/csaf_2_1/recommendedTests.js @@ -42,3 +42,4 @@ export { recommendedTest_6_2_41 } from './recommendedTests/recommendedTest_6_2_4 export { recommendedTest_6_2_43 } from './recommendedTests/recommendedTest_6_2_43.js' export { recommendedTest_6_2_47 } from './recommendedTests/recommendedTest_6_2_47.js' export { recommendedTest_6_2_48 } from './recommendedTests/recommendedTest_6_2_48.js' +export { recommendedTest_6_2_52 } from './recommendedTests/recommendedTest_6_2_52.js' diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_52.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_52.js new file mode 100644 index 00000000..9f43b82e --- /dev/null +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_52.js @@ -0,0 +1,226 @@ +import { Ajv } from 'ajv/dist/jtd.js' + +const ajv = new Ajv() + +const productIdentificationHelperSchema = /** @type {const} */ ({ + additionalProperties: true, + optionalProperties: { + hashes: { + elements: { + additionalProperties: true, + optionalProperties: { + filename: { type: 'string' }, + file_hashes: { + elements: { + additionalProperties: true, + optionalProperties: { + algorithm: { type: 'string' }, + }, + }, + }, + }, + }, + }, + }, +}) + +const branchSchema = /** @type {const} */ ({ + additionalProperties: true, + optionalProperties: { + branches: { + elements: { + additionalProperties: true, + properties: {}, + }, + }, + product: { + additionalProperties: true, + optionalProperties: { + product_identification_helper: productIdentificationHelperSchema, + }, + }, + }, +}) + +const validateBranch = ajv.compile(branchSchema) + +const fullProductNameSchema = /** @type {const} */ ({ + additionalProperties: true, + optionalProperties: { + product_identification_helper: productIdentificationHelperSchema, + }, +}) + +/* + This is the jtd schema that needs to match the input document so that the + test is activated. If this schema doesn't match, it normally means that the input + document does not validate against the csaf JSON schema or optional fields that + the test checks are not present. + */ +const inputSchema = /** @type {const} */ ({ + additionalProperties: true, + optionalProperties: { + product_tree: { + additionalProperties: true, + optionalProperties: { + branches: { + elements: branchSchema, + }, + full_product_names: { + elements: fullProductNameSchema, + }, + product_paths: { + elements: { + additionalProperties: true, + optionalProperties: { + full_product_name: fullProductNameSchema, + }, + }, + }, + }, + }, + }, +}) + +const validate = ajv.compile(inputSchema) + +/** + * @typedef {import('ajv/dist/core.js').JTDDataType} Branch + * @typedef {import('ajv/dist/core.js').JTDDataType} FullProductName + */ + +/** + * All hash algorithm names mentioned in section 3.1.4.3.2 of the CSAF 2.1 standard. + * These are derived from the OpenSSL dgst -list output (version 3.4.0, 2024-10-22) + * with leading dashes removed. + */ +const ALGORITHMS_IN_SPEC = new Set([ + 'blake2b512', + 'blake2s256', + 'md4', + 'md5', + 'md5-sha1', + 'mdc2', + 'ripemd', + 'ripemd160', + 'rmd160', + 'sha1', + 'sha224', + 'sha256', + 'sha3-224', + 'sha3-256', + 'sha3-384', + 'sha3-512', + 'sha384', + 'sha512', + 'sha512-224', + 'sha512-256', + 'shake128', + 'shake256', + 'sm3', + 'ssl3-md5', + 'ssl3-sha1', + 'whirlpool', +]) + +/** + * This implements the recommended test 6.2.52 of the CSAF 2.1 standard. + * + * @param {unknown} doc + */ +export function recommendedTest_6_2_52(doc) { + const ctx = { + warnings: + /** @type {Array<{ instancePath: string; message: string }>} */ ([]), + } + + if (!validate(doc)) { + return ctx + } + + doc.product_tree?.branches?.forEach((branch, index) => { + checkBranch(`/product_tree/branches/${index}`, branch) + }) + + doc.product_tree?.full_product_names?.forEach((fullProductName, index) => { + checkFullProductName( + `/product_tree/full_product_names/${index}`, + fullProductName + ) + }) + + doc.product_tree?.product_paths?.forEach((productPath, index) => { + const fullProductName = productPath.full_product_name + if (fullProductName) { + checkFullProductName( + `/product_tree/product_paths/${index}/full_product_name`, + fullProductName + ) + } + }) + + return ctx + + /** + * Check all file_hashes algorithm values of a full product name. + * + * @param {string} prefix + * @param {FullProductName} fullProductName + */ + function checkFullProductName(prefix, fullProductName) { + fullProductName.product_identification_helper?.hashes?.forEach( + (hash, hashIndex) => { + checkHashAlgorithms( + hash, + `${prefix}/product_identification_helper/hashes/${hashIndex}` + ) + } + ) + } + + /** + * Check all file_hashes algorithm values of a branch and its children. + * + * @param {string} prefix + * @param {Branch} branch + */ + function checkBranch(prefix, branch) { + branch.product?.product_identification_helper?.hashes?.forEach( + (hash, hashIndex) => { + checkHashAlgorithms( + hash, + `${prefix}/product/product_identification_helper/hashes/${hashIndex}` + ) + } + ) + branch.branches?.forEach((childBranch, index) => { + if (validateBranch(childBranch)) { + checkBranch(`${prefix}/branches/${index}`, childBranch) + } + }) + } + + /** + * Iterate over file_hashes and warn for each unsupported algorithm value. + * Differentiates between algorithms listed in section 3.1.4.3.2 (known to + * the standard) and those not mentioned there at all. + * + * @param {{ file_hashes?: Array<{ algorithm?: string }> }} hash + * @param {string} hashPrefix e.g. ".../hashes/0" + */ + function checkHashAlgorithms(hash, hashPrefix) { + if (!Array.isArray(hash.file_hashes)) return + hash.file_hashes.forEach((fileHash, fileHashIndex) => { + if (fileHash.algorithm == null) return + const algorithm = fileHash.algorithm + const instancePath = `${hashPrefix}/file_hashes/${fileHashIndex}/algorithm` + + if (!ALGORITHMS_IN_SPEC.has(algorithm)) { + ctx.warnings.push({ + instancePath, + message: `the hash algorithm '${algorithm}' is not listed in section 3.1.4.3.2`, + }) + } + }) + } +} diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index 13b54e0b..5a3334e7 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -48,7 +48,6 @@ const excluded = [ '6.2.50.2', '6.2.50.3', '6.2.51', - '6.2.52', '6.2.53', '6.2.54.1', '6.2.54.2', diff --git a/tests/csaf_2_1/recommendedTest_6_2_52.js b/tests/csaf_2_1/recommendedTest_6_2_52.js new file mode 100644 index 00000000..9e7a9ffb --- /dev/null +++ b/tests/csaf_2_1/recommendedTest_6_2_52.js @@ -0,0 +1,179 @@ +import assert from 'node:assert/strict' +import { recommendedTest_6_2_52 } from '../../csaf_2_1/recommendedTests/recommendedTest_6_2_52.js' +import { productWithFileHashes } from './shared/csafDocHelper.js' + +describe('recommendedTest_6_2_52', function () { + it('returns early with no warnings when product_tree is not an object', function () { + assert.equal( + recommendedTest_6_2_52({ product_tree: 'string' }).warnings.length, + 0 + ) + }) + + it('warns for unsupported algorithm in product_tree.branches product hashes', function () { + const result = recommendedTest_6_2_52({ + product_tree: { + branches: [ + { + product: productWithFileHashes('product.exe', [ + { algorithm: 'md9000' }, + ]), + }, + ], + }, + }) + assert.equal(result.warnings.length, 1) + assert.equal( + result.warnings[0].instancePath, + '/product_tree/branches/0/product/product_identification_helper/hashes/0/file_hashes/0/algorithm' + ) + assert.match(result.warnings[0].message, /md9000/) + }) + + it('warns for unsupported algorithm in product_tree.product_paths full_product_name', function () { + const result = recommendedTest_6_2_52({ + product_tree: { + product_paths: [ + { + full_product_name: productWithFileHashes('product.exe', [ + { algorithm: 'unknown-algo' }, + ]), + }, + ], + }, + }) + assert.equal(result.warnings.length, 1) + assert.equal( + result.warnings[0].instancePath, + '/product_tree/product_paths/0/full_product_name/product_identification_helper/hashes/0/file_hashes/0/algorithm' + ) + assert.match(result.warnings[0].message, /unknown-algo/) + }) + + it('does not warn for product_paths entry without full_product_name', function () { + const result = recommendedTest_6_2_52({ + product_tree: { + product_paths: [{}], + }, + }) + assert.equal(result.warnings.length, 0) + }) + + it('warns for unsupported algorithm in branch product hashes', function () { + const result = recommendedTest_6_2_52({ + product_tree: { + branches: [ + { + product: productWithFileHashes('lib.so', [ + { algorithm: 'fakehash' }, + ]), + }, + ], + }, + }) + assert.equal(result.warnings.length, 1) + assert.equal( + result.warnings[0].instancePath, + '/product_tree/branches/0/product/product_identification_helper/hashes/0/file_hashes/0/algorithm' + ) + }) + + it('warns for unsupported algorithm in nested child branch', function () { + const result = recommendedTest_6_2_52({ + product_tree: { + branches: [ + { + branches: [ + { + product: productWithFileHashes('nested.dll', [ + { algorithm: 'not-in-spec' }, + ]), + }, + ], + }, + ], + }, + }) + assert.equal(result.warnings.length, 1) + assert.equal( + result.warnings[0].instancePath, + '/product_tree/branches/0/branches/0/product/product_identification_helper/hashes/0/file_hashes/0/algorithm' + ) + assert.match(result.warnings[0].message, /not-in-spec/) + }) + + it('does not warn when file_hashes is absent (line 212 guard)', function () { + const result = recommendedTest_6_2_52({ + product_tree: { + full_product_names: [ + { + product_identification_helper: { + hashes: [{ filename: 'product.exe' }], + }, + }, + ], + }, + }) + assert.equal(result.warnings.length, 0) + }) + + it('does not warn when algorithm is undefined', function () { + const result = recommendedTest_6_2_52({ + product_tree: { + full_product_names: [productWithFileHashes('product.exe', [{}])], + }, + }) + assert.equal(result.warnings.length, 0) + }) + + it('does not warn for any of the 26 spec-listed algorithms', function () { + const specAlgorithms = [ + 'blake2b512', + 'blake2s256', + 'md4', + 'md5', + 'md5-sha1', + 'mdc2', + 'ripemd', + 'ripemd160', + 'rmd160', + 'sha1', + 'sha224', + 'sha256', + 'sha3-224', + 'sha3-256', + 'sha3-384', + 'sha3-512', + 'sha384', + 'sha512', + 'sha512-224', + 'sha512-256', + 'shake128', + 'shake256', + 'sm3', + 'ssl3-md5', + 'ssl3-sha1', + 'whirlpool', + ] + for (const algorithm of specAlgorithms) { + const result = recommendedTest_6_2_52({ + product_tree: { + full_product_names: [ + productWithFileHashes('product.exe', [ + { + algorithm, + value: + '026a37919b182ef7c63791e82c9645e2f897a3f0b73c7a6028c7febf62e93838', + }, + ]), + ], + }, + }) + assert.equal( + result.warnings.length, + 0, + `expected no warning for spec algorithm '${algorithm}'` + ) + } + }) +}) diff --git a/tests/csaf_2_1/shared/csafDocHelper.js b/tests/csaf_2_1/shared/csafDocHelper.js index 359e4e10..bed13565 100644 --- a/tests/csaf_2_1/shared/csafDocHelper.js +++ b/tests/csaf_2_1/shared/csafDocHelper.js @@ -13,6 +13,23 @@ export function productTreeWithFullProductName(productId, name) { } } +/** + * @param {string} filename + * @param {Array<{algorithm?: string | null, value?: string}>} fileHashes + */ +export function productWithFileHashes(filename, fileHashes) { + return { + product_identification_helper: { + hashes: [ + { + filename: filename, + file_hashes: fileHashes, + }, + ], + }, + } +} + /** * @param {number} baseSCore * @param {string} vectorString From 8eb524e5b25c0b215a7724b5e58bf4ca44c47acf Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Tue, 23 Jun 2026 11:48:16 +0200 Subject: [PATCH 2/2] feat(CSAF2.1): add SECURE_ALGORITHMS --- .../recommendedTest_6_2_52.js | 36 +++++++++++- tests/csaf_2_1/recommendedTest_6_2_52.js | 56 ++++++++++++++----- 2 files changed, 78 insertions(+), 14 deletions(-) diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_52.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_52.js index 9f43b82e..9414531a 100644 --- a/csaf_2_1/recommendedTests/recommendedTest_6_2_52.js +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_52.js @@ -123,6 +123,33 @@ const ALGORITHMS_IN_SPEC = new Set([ 'whirlpool', ]) +/** + * Subset of ALGORITHMS_IN_SPEC considered cryptographically secure, + * based on the OpenSSL 3.x provider classification: + * - Algorithms requiring the legacy provider (md4, mdc2, ripemd*, whirlpool) + * are excluded. + * - Algorithms in the default provider but cryptographically broken or + * deprecated (md5, md5-sha1, sha1, ssl3-md5, ssl3-sha1) are excluded. + * - All remaining default-provider algorithms are considered secure. + */ +const SECURE_ALGORITHMS = new Set([ + 'blake2b512', + 'blake2s256', + 'sha224', + 'sha256', + 'sha3-224', + 'sha3-256', + 'sha3-384', + 'sha3-512', + 'sha384', + 'sha512', + 'sha512-224', + 'sha512-256', + 'shake128', + 'shake256', + 'sm3', +]) + /** * This implements the recommended test 6.2.52 of the CSAF 2.1 standard. * @@ -215,7 +242,14 @@ export function recommendedTest_6_2_52(doc) { const algorithm = fileHash.algorithm const instancePath = `${hashPrefix}/file_hashes/${fileHashIndex}/algorithm` - if (!ALGORITHMS_IN_SPEC.has(algorithm)) { + if (ALGORITHMS_IN_SPEC.has(algorithm)) { + if (!SECURE_ALGORITHMS.has(algorithm)) { + ctx.warnings.push({ + instancePath, + message: `the hash algorithm '${algorithm}' is listed in section 3.1.4.3.2 but is not considered a secure cryptographic hash algorithm; a secure algorithm SHOULD be preferred`, + }) + } + } else { ctx.warnings.push({ instancePath, message: `the hash algorithm '${algorithm}' is not listed in section 3.1.4.3.2`, diff --git a/tests/csaf_2_1/recommendedTest_6_2_52.js b/tests/csaf_2_1/recommendedTest_6_2_52.js index 9e7a9ffb..8bc8602a 100644 --- a/tests/csaf_2_1/recommendedTest_6_2_52.js +++ b/tests/csaf_2_1/recommendedTest_6_2_52.js @@ -126,18 +126,10 @@ describe('recommendedTest_6_2_52', function () { assert.equal(result.warnings.length, 0) }) - it('does not warn for any of the 26 spec-listed algorithms', function () { - const specAlgorithms = [ + it('does not warn for secure spec-listed algorithms', function () { + const secureAlgorithms = [ 'blake2b512', 'blake2s256', - 'md4', - 'md5', - 'md5-sha1', - 'mdc2', - 'ripemd', - 'ripemd160', - 'rmd160', - 'sha1', 'sha224', 'sha256', 'sha3-224', @@ -151,11 +143,44 @@ describe('recommendedTest_6_2_52', function () { 'shake128', 'shake256', 'sm3', + ] + for (const algorithm of secureAlgorithms) { + const result = recommendedTest_6_2_52({ + product_tree: { + full_product_names: [ + productWithFileHashes('product.exe', [ + { + algorithm, + value: + '026a37919b182ef7c63791e82c9645e2f897a3f0b73c7a6028c7febf62e93838', + }, + ]), + ], + }, + }) + assert.equal( + result.warnings.length, + 0, + `expected no warning for secure algorithm '${algorithm}'` + ) + } + }) + + it('warns for insecure but spec-listed algorithms with distinct message', function () { + const insecureAlgorithms = [ + 'md4', + 'md5', + 'md5-sha1', + 'mdc2', + 'ripemd', + 'ripemd160', + 'rmd160', + 'sha1', 'ssl3-md5', 'ssl3-sha1', 'whirlpool', ] - for (const algorithm of specAlgorithms) { + for (const algorithm of insecureAlgorithms) { const result = recommendedTest_6_2_52({ product_tree: { full_product_names: [ @@ -171,8 +196,13 @@ describe('recommendedTest_6_2_52', function () { }) assert.equal( result.warnings.length, - 0, - `expected no warning for spec algorithm '${algorithm}'` + 1, + `expected 1 warning for insecure algorithm '${algorithm}'` + ) + assert.match( + result.warnings[0].message, + /secure/, + `expected 'secure' in warning message for '${algorithm}'` ) } })