Skip to content

fix(blueprint): reject empty digest instead of silently passing verification - #319

Closed
pjt222 wants to merge 2 commits into
NVIDIA:mainfrom
pjt222:fix/empty-digest-bypass
Closed

fix(blueprint): reject empty digest instead of silently passing verification#319
pjt222 wants to merge 2 commits into
NVIDIA:mainfrom
pjt222:fix/empty-digest-bypass

Conversation

@pjt222

@pjt222 pjt222 commented Mar 18, 2026

Copy link
Copy Markdown

Summary

  • Fix silent verification bypass when blueprint manifest has an empty or missing digest
  • Add comprehensive test suite for verify.ts (20 tests covering digest verification and version compatibility)

Problem

verifyBlueprintDigest() uses a falsy guard on manifest.digest:

if (manifest.digest && manifest.digest !== actualDigest) {

When parseManifestHeader returns "" for a missing digest: field (the regex match?.[1]?.trim() ?? "" produces empty string), the condition short-circuits to false. The function returns { valid: true } despite performing no integrity check.

Both cliLaunch (line 59) and cliMigrate (line 92) trust verification.valid and proceed with deployment. A blueprint without a digest field silently bypasses integrity verification.

Fix

Replace the falsy guard with an explicit empty-check:

if (!manifest.digest) {
    errors.push("Blueprint manifest is missing a digest — cannot verify integrity");
} else if (manifest.digest !== actualDigest) {
    errors.push(`Digest mismatch: expected ${manifest.digest}, got ${actualDigest}`);
}

Note: The same falsy-guard pattern in checkCompatibility for minOpenShellVersion / minOpenClawVersion is semantically correct — a missing minimum version means "no requirement." The digest field has opposite semantics: missing means "cannot verify," not "verification unnecessary."

Tests added

Group Tests Coverage
verifyBlueprintDigest — happy path Digest matches
verifyBlueprintDigest — mismatch Wrong digest detected
verifyBlueprintDigest — empty digest (bug fix) Empty string and undefined both fail
verifyBlueprintDigest — result fields actualDigest and expectedDigest populated correctly
verifyBlueprintDigest — multi-file Digest computed across sorted files
checkCompatibility — satisfied Equal and newer versions pass
checkCompatibility — too old OpenShell, OpenClaw, and both-too-old detected
checkCompatibility — missing minimum Empty min version skips check (correct behavior)
checkCompatibility — edge cases Two-segment, four-segment, year-based versions

Test plan

  • All 20 new tests pass (npx vitest run — 42 total, 0 failures)
  • Existing status.test.ts tests unaffected
  • Fix is minimal (4-line change in verify.ts)
  • Test file follows existing patterns from status.test.ts

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Digest verification now skips hashing when a manifest digest is absent and reports a clear "cannot verify integrity" error; mismatches continue to report expected vs actual digests.
  • Tests

    • Added comprehensive tests for blueprint verification and compatibility: digest validation (including sorting, nested directories, missing digests, and mismatch reporting) and version-compatibility edge cases.

…ication

When a blueprint manifest has an empty or missing digest field,
verifyBlueprintDigest() returned valid: true because "" is falsy in
JavaScript, causing the digest comparison to be skipped entirely.

Both cliLaunch and cliMigrate trust the valid flag and proceed with
deployment, so a blueprint without a digest silently bypasses integrity
verification.

The fix explicitly checks for empty/missing digest and returns an error.
Comprehensive tests are added for verifyBlueprintDigest (digest match,
mismatch, empty, undefined, multi-file) and checkCompatibility (version
satisfied, too old, missing minimum, edge cases).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Mar 18, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6c6c16db-27c5-45cb-a3ca-96751b20e24a

📥 Commits

Reviewing files that changed from the base of the PR and between 621b62a and d927740.

📒 Files selected for processing (2)
  • nemoclaw/src/blueprint/verify.test.ts
  • nemoclaw/src/blueprint/verify.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • nemoclaw/src/blueprint/verify.ts
  • nemoclaw/src/blueprint/verify.test.ts

📝 Walkthrough

Walkthrough

Refactors verification logic to treat a missing manifest digest as an explicit error and only compute the directory digest when a manifest digest is present. Adds a comprehensive Vitest test suite validating digest computation, mismatch reporting, nested files, and version compatibility edge cases.

Changes

Cohort / File(s) Summary
Implementation Refactor
nemoclaw/src/blueprint/verify.ts
Changed verifyBlueprintDigest to record an error when manifest.digest is missing and to compute actualDigest only if a manifest digest exists; preserved existing mismatch reporting when digests differ.
Test Coverage
nemoclaw/src/blueprint/verify.test.ts
Added tests that mock filesystem calls to validate digest computation for flat and nested directories, digest mismatch messaging and fields (actualDigest/expectedDigest), handling of missing/empty manifest digests, and extensive checkCompatibility version-parsing and comparison cases.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐇 I nibble at lines of code so neat,
I hop through digests, hashes, and feat,
When manifests forget their sign,
I thump a warning, clear and fine,
Hooray — tests make every path complete!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main fix: changing the verification logic to explicitly reject empty digests instead of silently passing validation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
nemoclaw/src/blueprint/verify.ts (1)

21-25: Skip hashing when the manifest is already invalid.

This branch still walks and hashes the blueprint before reporting the missing-digest error. Moving the hash behind the manifest.digest guard avoids unnecessary I/O and keeps malformed manifests on the intended failure path.

♻️ Proposed adjustment
 export function verifyBlueprintDigest(
   blueprintPath: string,
   manifest: BlueprintManifest,
 ): VerificationResult {
   const errors: string[] = [];
-  const actualDigest = computeDirectoryDigest(blueprintPath);
+  let actualDigest = "";
 
   if (!manifest.digest) {
     errors.push("Blueprint manifest is missing a digest — cannot verify integrity");
-  } else if (manifest.digest !== actualDigest) {
-    errors.push(`Digest mismatch: expected ${manifest.digest}, got ${actualDigest}`);
+  } else {
+    actualDigest = computeDirectoryDigest(blueprintPath);
+    if (manifest.digest !== actualDigest) {
+      errors.push(`Digest mismatch: expected ${manifest.digest}, got ${actualDigest}`);
+    }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nemoclaw/src/blueprint/verify.ts` around lines 21 - 25,
computeDirectoryDigest(blueprintPath) is called before checking manifest.digest,
causing unnecessary hashing even when the manifest is invalid; change the flow
so you first check if manifest.digest exists and push the error
(errors.push("Blueprint manifest is missing a digest — cannot verify
integrity")) and only call computeDirectoryDigest(blueprintPath) when
manifest.digest is present, then compare manifest.digest to the computed
actualDigest and push the integrity error if they differ.
nemoclaw/src/blueprint/verify.test.ts (2)

41-54: Add one nested-directory digest fixture.

mockDirectory() always reports files, so the "multi-file directory" case still only proves flat-file ordering. A nested fixture would exercise collectFiles() recursion and prefix hashing, which are part of the digest contract.

Also applies to: 160-177

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nemoclaw/src/blueprint/verify.test.ts` around lines 41 - 54, The test helper
mockDirectory() only fakes a flat listing so it never exercises collectFiles()
recursion or prefix hashing; update the tests by adding a nested-directory
fixture and enhancing mockDirectory() behavior to simulate directories: make
vi.mocked(readdirSync) return directory entries for nested folders (not just
file paths), make vi.mocked(statSync) return isDirectory() true for directory
paths and false for files, and keep readFileSync returning Buffer content for
leaf file paths; then add a test case that supplies a nested file (e.g.,
"dir/sub.txt") and the expected digest to validate recursive collection and
prefix hashing.

115-136: Assert the exact missing-digest error here.

Both tests only look for "missing", so an unrelated failure reason could still satisfy them. Since this PR is pinning a specific guard, it's worth locking the full message down.

♻️ Tighten the expectation
       expect(result.valid).toBe(false);
-      expect(result.errors).toHaveLength(1);
-      expect(result.errors[0]).toContain("missing");
+      expect(result.errors).toEqual([
+        "Blueprint manifest is missing a digest — cannot verify integrity",
+      ]);

Apply the same assertion shape to the undefined case as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nemoclaw/src/blueprint/verify.test.ts` around lines 115 - 136, The tests
currently only assert that the error string contains "missing", which is too
loose; update both cases in verify.test.ts so they assert the exact
missing-digest message returned by verifyBlueprintDigest (replace
expect(result.errors[0]).toContain("missing") with an exact equality check, e.g.
expect(result.errors[0]).toBe("manifest digest is missing") for both the
empty-string and undefined manifest cases), and keep the other expectations the
same so the test fails if any other error reason is returned.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@nemoclaw/src/blueprint/verify.test.ts`:
- Around line 41-54: The test helper mockDirectory() only fakes a flat listing
so it never exercises collectFiles() recursion or prefix hashing; update the
tests by adding a nested-directory fixture and enhancing mockDirectory()
behavior to simulate directories: make vi.mocked(readdirSync) return directory
entries for nested folders (not just file paths), make vi.mocked(statSync)
return isDirectory() true for directory paths and false for files, and keep
readFileSync returning Buffer content for leaf file paths; then add a test case
that supplies a nested file (e.g., "dir/sub.txt") and the expected digest to
validate recursive collection and prefix hashing.
- Around line 115-136: The tests currently only assert that the error string
contains "missing", which is too loose; update both cases in verify.test.ts so
they assert the exact missing-digest message returned by verifyBlueprintDigest
(replace expect(result.errors[0]).toContain("missing") with an exact equality
check, e.g. expect(result.errors[0]).toBe("manifest digest is missing") for both
the empty-string and undefined manifest cases), and keep the other expectations
the same so the test fails if any other error reason is returned.

In `@nemoclaw/src/blueprint/verify.ts`:
- Around line 21-25: computeDirectoryDigest(blueprintPath) is called before
checking manifest.digest, causing unnecessary hashing even when the manifest is
invalid; change the flow so you first check if manifest.digest exists and push
the error (errors.push("Blueprint manifest is missing a digest — cannot verify
integrity")) and only call computeDirectoryDigest(blueprintPath) when
manifest.digest is present, then compare manifest.digest to the computed
actualDigest and push the integrity error if they differ.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c5d904c9-06cb-4d9f-9f5b-c06a6c87abdf

📥 Commits

Reviewing files that changed from the base of the PR and between 1e23347 and 621b62a.

📒 Files selected for processing (2)
  • nemoclaw/src/blueprint/verify.test.ts
  • nemoclaw/src/blueprint/verify.ts

Address CodeRabbit review feedback:
- Move computeDirectoryDigest() inside the else branch so malformed
  manifests (missing digest) skip unnecessary filesystem I/O
- Replace loose toContain("missing") assertions with exact toEqual()
  matching the full error message
- Add test verifying no I/O occurs when digest is missing
- Add nested directory test exercising collectFiles() recursion and
  prefix path hashing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@north-echo north-echo left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security Review, PR #319

The core fix is correct and well-tested. Replacing the falsy guard with an explicit empty-check closes the bypass. The decision to skip hashing when the digest is missing (avoiding unnecessary I/O) is also the right call. The test coverage is thorough for the cases it covers. A few observations on remaining attack surface:

  1. Whitespace-only digest strings

The guard !manifest.digest is falsy for empty string and undefined, but a whitespace-only string like " " is truthy in JavaScript. parseManifestHeader uses match?.[1]?.trim() ?? "", so a YAML line like digest: (with trailing spaces but no value) would still produce "". But a crafted manifest with digest: " " (quoted whitespace) would pass the guard, trigger a hash computation, and produce a generic "Digest mismatch" error instead of the more informative "missing a digest" message.

Not a bypass (the mismatch would still reject it), but the error message is misleading. A trim() check would catch this:

if (!manifest.digest || !manifest.digest.trim()) {

  1. No digest format validation

Any non-empty string is accepted as a valid expected digest. A malformed value like "sha256:" (prefix only), "abc", or a truncated hex string would pass the empty-check and produce a generic mismatch error. Validating the format (e.g., /^[a-f0-9]{64}$/ for a bare SHA-256 hex string) would catch malformed manifests earlier with a specific error.

  1. Timing-safe comparison

The comparison uses !== which is subject to timing side-channel analysis. For digest verification this is lower risk than for auth tokens (the attacker would need to observe many verification attempts with controlled digests), but the standard practice is crypto.timingSafeEqual. Worth considering, especially since the fix is a one-line change:

const match = Buffer.from(manifest.digest).length === Buffer.from(actualDigest).length
&& crypto.timingSafeEqual(Buffer.from(manifest.digest), Buffer.from(actualDigest));

  1. Failure mode in callers

Both cliLaunch (line 60-62) and cliMigrate (line 93-95) handle verification failure with logger.error() + return. This means the process may exit with code 0 even when verification fails, depending on how the CLI runner handles the return. A process.exit(1) or thrown error would make the failure more visible in CI/CD pipelines and scripts.

  1. The PR note about checkCompatibility semantics is valuable

The observation that the falsy-guard pattern is correct for minOpenShellVersion/minOpenClawVersion (missing means "no requirement") but incorrect for digest (missing means "cannot verify") is a good architectural note. Worth preserving in a code comment for future contributors.

Overall this is a clean, well-scoped fix. The test suite is the strongest part. I would be comfortable seeing this merged with or without the above suggestions.

Christopher Lusk (christopherdlusk@gmail.com)

north-echo added a commit to north-echo/NemoClaw that referenced this pull request Mar 19, 2026
…n and timing-safe comparison

The existing falsy guard on manifest.digest silently passes verification
when the digest field is empty, allowing unverified blueprint artifacts
to execute through both cliLaunch and cliMigrate.

- Reject empty, undefined, and whitespace-only digests with clear error
- Validate digest format against /^[a-f0-9]{64}$/ (SHA-256 hex)
- Skip directory hashing when digest is missing or malformed (no wasted I/O)
- Use crypto.timingSafeEqual for digest comparison
- Add test suite covering all rejection and acceptance cases

Related: NVIDIA#319

Signed-off-by: Christopher Lusk <christopherdlusk@gmail.com>
Assisted-by: Claude (Anthropic)
Signed-off-by: Christopher Lusk <122107484+north-echo@users.noreply.github.com>
@pjt222

pjt222 commented Mar 22, 2026

Copy link
Copy Markdown
Author

Closing this PR — the file it patches (nemoclaw/src/blueprint/verify.ts) was removed upstream in #492 as dead code. The empty-digest bypass vulnerability no longer exists in the codebase since the entire verification module was deleted.

@pjt222 pjt222 closed this Mar 22, 2026
mafueee pushed a commit to mafueee/NemoClaw that referenced this pull request Mar 28, 2026
@wscurran wscurran added bug-fix PR fixes a bug or regression and removed Migration labels Jun 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug-fix PR fixes a bug or regression

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants