Skip to content

feat(envelope): add ModelMessageEnvelope with cryptographic signing [OMN-1898] - #478

Merged
jonahgabriel merged 9 commits into
mainfrom
jonah/omn-1898-modelmessageenvelope-signed-envelope-with-realmruntime
Feb 4, 2026
Merged

jonahgabriel merged 9 commits into
mainfrom
jonah/omn-1898-modelmessageenvelope-signed-envelope-with-realmruntime

Conversation

@jonahgabriel

@jonahgabriel jonahgabriel commented Feb 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Implement runtime-gateway-centric message envelope with Ed25519 signatures and Blake3 payload hashing for secure inter-service communication.

Ticket: OMN-1898

Key Components

Crypto Utilities (src/omnibase_core/crypto/)

  • blake3_hasher.py: Blake3 hashing for deterministic payload hashing
  • ed25519_signer.py: Ed25519 signing/verification for envelope authentication
  • file_key_provider.py: File-backed runtime public key management

Envelope Models (src/omnibase_core/models/envelope/)

  • ModelMessageEnvelope[T]: Generic signed envelope with realm-based routing
  • ModelEnvelopeSignature: Cryptographic signature metadata
  • ModelEmitterIdentity: Component identity for observability

Protocol (src/omnibase_core/protocols/crypto/)

  • ProtocolKeyProvider: Interface for key storage backends (supports file, DB, Vault)

Design Decisions

  1. Signature bound to runtime_id (gateway), not emitter_identity

    • Runtime gateway vouches for the message
    • emitter_identity is optional observability metadata
  2. Identity mismatch validation

    • When emitter_identity is present, emitter_identity.env MUST equal realm
    • Enforced at model validation time
  3. Canonical JSON serialization

    • Ensures deterministic hashing regardless of key ordering
    • Critical for signature verification
  4. Fail-closed security model

    • Unknown runtime_id = untrusted envelope = rejection
    • ENVELOPE_KEY_NOT_FOUND error raised for missing keys

New Error Codes (341-345)

  • ENVELOPE_IDENTITY_MISMATCH: emitter_identity.env doesn't match realm
  • ENVELOPE_SIGNATURE_INVALID: Signature verification failed
  • ENVELOPE_KEY_NOT_FOUND: Public key not found for runtime_id
  • ENVELOPE_SIGNING_FAILED: Signing operation failed
  • ENVELOPE_VALIDATION_FAILED: General validation failure

Test Coverage

  • 63 tests covering:
    • Blake3 hashing (12 tests)
    • Ed25519 signing/verification (18 tests)
    • FileKeyProvider (16 tests)
    • ModelMessageEnvelope (17 tests)
  • Full sign → serialize → verify roundtrip
  • Identity mismatch rejection
  • Protocol compliance verification

Test Plan

  • All unit tests pass (63/63)
  • mypy strict mode passes
  • ruff lint passes
  • Pre-commit hooks pass
  • CI pipeline passes

Summary by CodeRabbit

  • New Features

    • Cryptographic envelope support for signed, verifiable messages with identity and timestamp validation.
    • Runtime key provider with persistent, file-backed storage and hot-reload for public keys.
    • Blake3 hashing for deterministic payload integrity checks.
  • Tests

    • Comprehensive unit test suites covering envelope models, signing/verification, hashing, and key provider behavior.
  • Chores

    • Added blake3 dependency (>=1.0.8,<2.0.0).

…OMN-1898]

## Summary

Implement runtime-gateway-centric message envelope with Ed25519 signatures
and Blake3 payload hashing for secure inter-service communication.

## Key Components

### Crypto Utilities
- Blake3 hasher for deterministic payload hashing
- Ed25519 signer for envelope authentication
- FileKeyProvider for runtime public key management

### Envelope Models
- ModelMessageEnvelope[T]: Generic signed envelope
- ModelEnvelopeSignature: Signature metadata
- ModelEmitterIdentity: Component identity for observability

### Protocol
- ProtocolKeyProvider: Interface for key storage backends

## Design Decisions

- Signature bound to runtime_id (gateway), not emitter_identity
- emitter_identity is optional observability metadata
- Identity mismatch validation: emitter_identity.env must match realm
- Canonical JSON serialization for deterministic hashing

## Test Coverage

- 63 tests for crypto utilities and envelope models
- Full sign → serialize → verify roundtrip tests
- Identity mismatch rejection tests
- Protocol compliance tests
@coderabbitai

coderabbitai Bot commented Feb 4, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds Blake3 hashing, Ed25519 signing, a file-backed key provider and ProtocolKeyProvider, envelope models (emitter identity, signature, generic message envelope) with signing/verification, new envelope-related error codes, public re-exports, and comprehensive unit tests. Dependency blake3 added.

Changes

Cohort / File(s) Summary
Dependencies
pyproject.toml
Added blake3 dependency entries.
Crypto package exports
src/omnibase_core/crypto/__init__.py
Re-exported hashing, signing, key provider types and functions to public API.
Hashing & Signing
src/omnibase_core/crypto/crypto_blake3_hasher.py, src/omnibase_core/crypto/crypto_ed25519_signer.py
New Blake3 hashing utilities and Ed25519 keypair, sign/verify helpers (including base64 helpers).
File-backed Key Provider
src/omnibase_core/crypto/crypto_file_key_provider.py
New thread-safe FileKeyProvider with JSON persistence, atomic writes, permission checks, validation, reload, and removal.
Key Provider Protocols
src/omnibase_core/protocols/crypto/protocol_key_provider.py, src/omnibase_core/protocols/__init__.py, src/omnibase_core/protocols/crypto/__init__.py
Introduced ProtocolKeyProvider protocol, runtime_checkable, and re-exported it at package level.
Envelope models package
src/omnibase_core/models/envelope/__init__.py
New package initializer exporting envelope model classes.
Emitter Identity model
src/omnibase_core/models/envelope/model_emitter_identity.py
Added immutable Pydantic ModelEmitterIdentity with strict validation fields.
Envelope Signature model
src/omnibase_core/models/envelope/model_envelope_signature.py
Added immutable ModelEnvelopeSignature enforcing 64-char hex payload_hash and signature metadata.
Message Envelope model
src/omnibase_core/models/envelope/model_message_envelope.py
Added generic ModelMessageEnvelope[T] with signing factory, signature verification, identity/realm and timezone validations, and related helpers.
Error codes
src/omnibase_core/enums/enum_core_error_code.py
Added multiple envelope- and registry-related error codes, descriptions, exit-code mappings, and module coverage validation.
Tests — crypto
tests/unit/crypto/*, tests/unit/crypto/__init__.py
Added unit tests for Blake3 hasher, Ed25519 signer, and FileKeyProvider (persistence, thread-safety, protocol compliance).
Tests — envelope models
tests/unit/models/envelope/*
Added extensive tests for ModelEmitterIdentity, ModelEnvelopeSignature, and ModelMessageEnvelope (creation, signing, verification, validation, serialization lifecycle).

Sequence Diagram

sequenceDiagram
    actor App
    participant Envelope as ModelMessageEnvelope
    participant Hasher as Blake3Hasher
    participant Signer as Ed25519Signer
    participant Provider as FileKeyProvider
    participant Verifier as SignatureVerifier

    App->>Envelope: create_signed(payload, realm, runtime_id, keypair, ...)
    Envelope->>Hasher: hash_canonical_json(payload)
    Hasher-->>Envelope: payload_hash
    Envelope->>Signer: sign(private_key, signed_content)
    Signer-->>Envelope: signature
    Envelope-->>App: signed ModelMessageEnvelope

    App->>Envelope: verify_signature(key_provider)
    Envelope->>Provider: get_public_key(runtime_id)
    Provider-->>Envelope: public_key / None
    Envelope->>Verifier: verify(public_key, signed_content, signature)
    Verifier-->>Envelope: True / False
    Envelope-->>App: verification result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A hop, a nibble, hashes spun so bright,

Ed25519 sings through day and night,
Keys kept safe in a JSON burrow,
Envelopes signed — no room for sorrow,
Verification hops back with delight.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main feature being added: ModelMessageEnvelope with cryptographic signing capabilities in the envelope module.
Docstring Coverage ✅ Passed Docstring coverage is 98.72% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch jonah/omn-1898-modelmessageenvelope-signed-envelope-with-realmruntime

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

- Narrow exception handling in verify() and verify_base64() to specific
  crypto exceptions (ValueError, TypeError, InvalidSignature) instead of
  bare Exception catch to avoid masking system errors
- Set explicit file permissions (0o600) when saving keys to disk in
  FileKeyProvider to prevent unauthorized access
- Add prominent Security Warning in ModelMessageEnvelope docstring about
  emitter_identity being untrusted metadata not covered by signature

Review iteration: 1/10

@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.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@pyproject.toml`:
- Line 44: The blake3 dependency line uses the PEP 508 parentheses style
("blake3 (>=1.0.8,<2.0.0)"), which is inconsistent with the other dependencies;
update that entry to the normalized, modern format without parentheses (e.g.,
"blake3>=1.0.8,<2.0.0") so it matches the rest of the dependency syntax in
pyproject.toml and retains the same version constraints.
🧹 Nitpick comments (12)
src/omnibase_core/crypto/file_key_provider.py (2)

1-17: Consider renaming file to follow naming conventions.

The file is named file_key_provider.py, but the coding guidelines require files in src/omnibase_core/**/*.py to follow specific naming prefixes (e.g., service_*, util_*, protocol_*). Consider renaming to service_file_key_provider.py or util_file_key_provider.py.

As per coding guidelines: "File names must follow the naming convention based on directory: cli_*, constants_*, container_*, context_*, contract_*, decorator_*, enum_*, error_* or exception_*, factory_*, node_*, mixin_*, model_*, protocol_*, runtime_*, service_*, util_*, validator_* or checker_*"


71-73: Consider logging when key file is invalid.

When the key file has invalid JSON or format, the code silently resets to an empty state. While this is reasonable for development/testing, consider adding a warning log to help diagnose configuration issues.

             except (json.JSONDecodeError, KeyError, ValueError):
-                # Invalid file format - start fresh
+                # Invalid file format - start fresh
+                # Consider: logging.warning("Invalid key file format at %s, starting fresh", self._key_file)
                 self._keys = {}
src/omnibase_core/crypto/ed25519_signer.py (1)

1-12: Consider renaming file to follow naming conventions.

The file is named ed25519_signer.py, but the coding guidelines require specific prefixes. Consider renaming to util_ed25519_signer.py.

As per coding guidelines: "File names must follow the naming convention based on directory."

tests/unit/crypto/test_blake3_hasher.py (1)

10-11: Missing @pytest.mark.unit marker on test classes.

Per coding guidelines, test functions should use pytest markers like @pytest.mark.unit. Consider adding the marker to each test class or method.

♻️ Proposed fix to add pytest markers
+@pytest.mark.unit
 class TestHashBytes:
     """Tests for hash_bytes function."""
+@pytest.mark.unit
 class TestHashCanonicalJson:
     """Tests for hash_canonical_json function."""

As per coding guidelines: tests/**/*.py: Use @pytest.mark.unit, @pytest.mark.integration, @pytest.mark.slow, @pytest.mark.performance, @pytest.mark.memory_intensive, @pytest.mark.isolated markers on test functions.

src/omnibase_core/models/envelope/model_envelope_signature.py (1)

54-59: Consider adding hex character validation for payload_hash.

The payload_hash field validates length but not that the content is valid hexadecimal. While invalid hex will fail during signature verification, early validation provides clearer error messages.

♻️ Optional: Add hex pattern validation
     payload_hash: str = Field(
         ...,
         min_length=64,
         max_length=64,
+        pattern=r"^[0-9a-fA-F]{64}$",
         description="Blake3 hash of the payload (64 hex characters).",
     )
tests/unit/crypto/test_ed25519_signer.py (1)

17-18: Missing @pytest.mark.unit markers on test classes.

Per coding guidelines, test functions should use pytest markers. Consider adding @pytest.mark.unit to each test class.

♻️ Proposed fix to add pytest markers
+@pytest.mark.unit
 class TestGenerateKeypair:
     """Tests for generate_keypair function."""

Apply similarly to TestEd25519KeyPair, TestSignAndVerify, and TestBase64Functions.

As per coding guidelines: tests/**/*.py: Use @pytest.mark.unit, @pytest.mark.integration, @pytest.mark.slow, @pytest.mark.performance, @pytest.mark.memory_intensive, @pytest.mark.isolated markers on test functions.

tests/unit/crypto/test_file_key_provider.py (1)

26-73: Add @pytest.mark.unit marker to test classes.

Per coding guidelines, test functions should use appropriate markers (@pytest.mark.unit, @pytest.mark.integration, etc.).

♻️ Proposed fix
+@pytest.mark.unit
 class TestFileKeyProviderBasics:
     """Basic functionality tests."""

Apply similarly to TestFileKeyProviderPersistence, TestFileKeyProviderRemoval, and TestFileKeyProviderProtocolCompliance.

As per coding guidelines: tests/**/*.py: Use @pytest.mark.unit, @pytest.mark.integration, @pytest.mark.slow, @pytest.mark.performance, @pytest.mark.memory_intensive, @pytest.mark.isolated markers on test functions.

tests/unit/models/envelope/test_model_message_envelope.py (2)

31-42: Add type annotations to fixtures and @pytest.mark.unit markers to test classes.

The fixtures are missing return type annotations, and test classes are missing the required markers.

♻️ Proposed fix for fixtures
 `@pytest.fixture`
-def keypair():
+def keypair() -> Ed25519KeyPair:
     """Generate a keypair for testing."""
     return generate_keypair()


 `@pytest.fixture`
-def key_provider(tmp_path, keypair):
+def key_provider(tmp_path: Path, keypair: Ed25519KeyPair) -> FileKeyProvider:
     """Create a key provider with a registered key."""
     provider = FileKeyProvider(tmp_path / "keys.json")
     provider.register_key("runtime-dev-001", keypair.public_key_bytes)
     return provider

Add required imports:

from pathlib import Path
from omnibase_core.crypto.ed25519_signer import Ed25519KeyPair

As per coding guidelines: Use @pytest.mark.unit markers on test functions.


69-75: Consider using specific exception types in pytest.raises.

Using pytest.raises(Exception) is broad. Pydantic frozen models raise ValidationError on attribute mutation attempts.

♻️ Proposed fix
+from pydantic import ValidationError
+
 ...
 
     def test_identity_is_frozen(self) -> None:
         """Identity model is immutable."""
         identity = ModelEmitterIdentity(...)
-        with pytest.raises(Exception):  # ValidationError for frozen model
+        with pytest.raises(ValidationError):
             identity.env = "prod"  # type: ignore[misc]

     def test_identity_requires_all_fields(self) -> None:
         """All fields are required."""
-        with pytest.raises(Exception):
+        with pytest.raises(ValidationError):
             ModelEmitterIdentity(env="dev")  # type: ignore[call-arg]
src/omnibase_core/models/envelope/model_message_envelope.py (3)

87-87: Consider adding frozen=True to make the envelope immutable.

Per coding guidelines, immutable value models should use ConfigDict(frozen=True, extra="forbid", from_attributes=True). Since signed envelopes should not be modified after creation (cryptographic integrity), immutability is appropriate here.

♻️ Proposed fix
-    model_config = ConfigDict(extra="forbid", from_attributes=True)
+    model_config = ConfigDict(frozen=True, extra="forbid", from_attributes=True)

As per coding guidelines: src/omnibase_core/models/**/*.py: Pydantic immutable value models must use ConfigDict(frozen=True, extra="forbid", from_attributes=True).


172-194: Consider extracting shared signing content logic to reduce duplication.

The signing content dictionary construction is duplicated between _get_signing_content (Lines 179-194) and create_signed (Lines 287-302). Both build the same structure with identical keys.

One approach: extract a static helper method that both can call:

♻️ Suggested refactor
`@staticmethod`
def _build_signing_dict(
    realm: str,
    runtime_id: str,
    bus_id: str,
    trace_id: UUID,
    emitted_at: datetime,
    payload_hash: str,
    causality_id: UUID | None = None,
    tenant_id: str | None = None,
) -> dict[str, str]:
    signing_dict: dict[str, str] = {
        "realm": realm,
        "runtime_id": runtime_id,
        "bus_id": bus_id,
        "trace_id": str(trace_id),
        "emitted_at": emitted_at.isoformat(),
        "payload_hash": payload_hash,
    }
    if causality_id:
        signing_dict["causality_id"] = str(causality_id)
    if tenant_id:
        signing_dict["tenant_id"] = tenant_id
    return signing_dict

Then both methods can use this helper.

Also applies to: 286-302


276-284: Document or reconsider the fallback payload handling.

When payload is neither a BaseModel nor dict, it's wrapped as {"value": payload} (Line 283). This implicit transformation affects the payload_hash and could be surprising. For example, a string payload "hello" becomes {"value": "hello"} for hashing purposes.

Consider either:

  1. Documenting this behavior explicitly in the docstring
  2. Raising an error for unsupported payload types to make behavior explicit
♻️ Option: Make unsupported types explicit
         else:
-            # For other types, attempt JSON serialization
-            payload_dict = {"value": payload}
+            # Reject unsupported payload types to ensure deterministic hashing
+            raise TypeError(
+                f"Payload must be a BaseModel or dict, got {type(payload).__name__}"
+            )

Comment thread pyproject.toml Outdated
- Use umask to set restrictive permissions (0o077) atomically before
  creating the key file, preventing a race condition window where
  keys could be read between write_text() and chmod()
- Restore original umask after file creation in finally block

Review iteration: 2/10
@jonahgabriel
jonahgabriel force-pushed the jonah/omn-1898-modelmessageenvelope-signed-envelope-with-realmruntime branch from 5d4c3cd to e9bc7ab Compare February 4, 2026 16:03

@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.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/omnibase_core/enums/enum_core_error_code.py`:
- Line 129: Add the missing ENVELOPE_IDENTITY_MISMATCH enum entry into both the
CORE_ERROR_CODE_TO_EXIT_CODE mapping and the descriptions dict returned by
get_core_error_description: update CORE_ERROR_CODE_TO_EXIT_CODE to explicitly
map ENVELOPE_IDENTITY_MISMATCH (from enum ENVELOPE_IDENTITY_MISMATCH) to the
appropriate ExitCode (use ExitCode.ERROR if that is the intended default) and
add a human-readable message for "ENVELOPE_IDENTITY_MISMATCH" into the
descriptions dict inside get_core_error_description (so calls to
get_core_error_description(ENVELOPE_IDENTITY_MISMATCH) return a meaningful
string instead of "Unknown error").

In `@src/omnibase_core/models/envelope/model_message_envelope.py`:
- Around line 204-219: derive_consumer_group currently returns a fallback string
f"{self.realm}.{self.runtime_id}.{purpose}" which is structurally different from
ModelEmitterIdentity.derive_consumer_group() (which yields
"{env}.{service}.{node_name}.{purpose}.{version}"); update derive_consumer_group
to produce a consistent five-segment format or explicitly document the
asymmetry. Specifically, in derive_consumer_group (and its fallback branch when
emitter_identity is None) either add missing components (e.g.,
env/service/node_name/version placeholders derived from available attributes or
a default version) so the fallback matches
"{env}.{service}.{node_name}.{purpose}.{version}", or add a clear docstring
comment noting the format difference; reference derive_consumer_group,
emitter_identity, and ModelEmitterIdentity.derive_consumer_group when making the
change.
🧹 Nitpick comments (6)
tests/unit/models/envelope/test_model_envelope_signature.py (1)

12-19: Consider adding pytest markers to test classes/functions.

The coding guidelines specify using markers like @pytest.mark.unit on test functions. Consider adding markers to enable selective test execution:

`@pytest.mark.unit`
class TestEnumSignatureAlgorithm:

As per coding guidelines: "Use @pytest.mark.unit, @pytest.mark.integration, @pytest.mark.slow, @pytest.mark.performance, @pytest.mark.memory_intensive, @pytest.mark.isolated markers on test functions".

src/omnibase_core/models/envelope/model_emitter_identity.py (1)

95-103: Consider using proper Pydantic type for validator info parameter.

The info parameter is typed as object, but Pydantic provides FieldValidationInfo for field validators. Using the proper type improves IDE support and type safety:

♻️ Suggested improvement
+from pydantic import BaseModel, ConfigDict, Field, field_validator, FieldValidationInfo
-from pydantic import BaseModel, ConfigDict, Field, field_validator

     `@field_validator`("env", "service", "node_name", "version")
     `@classmethod`
-    def validate_no_dots(cls, v: str, info: object) -> str:
+    def validate_no_dots(cls, v: str, info: FieldValidationInfo) -> str:
         """Validate that identity components don't contain dots (used as separator)."""
         if "." in v:
-            field_name = getattr(info, "field_name", "field")
+            field_name = info.field_name
             msg = f"{field_name} cannot contain dots (used as consumer group separator)"
             raise ValueError(msg)
         return v
tests/unit/crypto/test_ed25519_signer.py (1)

97-100: Consider hoisting repeated imports to module level.

The Encoding and PublicFormat imports are repeated in multiple test methods (lines 97-100, 125-128, 146-149, 170-173, 229-232). Moving them to module level improves readability:

♻️ Suggested improvement
 from cryptography.hazmat.primitives.asymmetric.ed25519 import (
     Ed25519PrivateKey,
     Ed25519PublicKey,
 )
+from cryptography.hazmat.primitives.serialization import (
+    Encoding,
+    PublicFormat,
+)

Then remove the inline imports from each test method.

src/omnibase_core/crypto/ed25519_signer.py (1)

1-16: File naming convention.

The file is named ed25519_signer.py, but coding guidelines require files in src/omnibase_core/ to follow naming conventions like util_*, service_*, etc. Consider renaming to util_ed25519_signer.py for consistency.

As per coding guidelines: "File names must follow the naming convention based on directory: cli_*, constants_*, container_*, context_*, contract_*, decorator_*, enum_*, error_* or exception_*, factory_*, node_*, mixin_*, model_*, protocol_*, runtime_*, service_*, util_*, validator_* or checker_*"

src/omnibase_core/models/envelope/model_message_envelope.py (2)

191-200: Consider consistency in error handling for signer mismatch.

The validate_signature_signer_matches_runtime validator raises ValueError, while validate_identity_realm_match raises ModelOnexError. Both are security-related envelope validations. Consider whether the signer mismatch should also use ModelOnexError with a dedicated error code (e.g., ENVELOPE_SIGNATURE_SIGNER_MISMATCH) for consistency and richer error context.

That said, using ValueError here is acceptable per coding guidelines for boundary validation, so this is optional.


254-254: Misleading parameter name: signature_bytes is actually hex string.

The parameter signature_bytes: str is passed directly to ModelEnvelopeSignature.signature, which per the relevant snippet expects "Ed25519 signature as hex string". The name signature_bytes suggests raw bytes, but it's actually a hex-encoded string.

Consider renaming to signature_hex for clarity and consistency with payload_hash (which is also hex but named appropriately).

♻️ Proposed fix
     def create(
         cls,
         *,
         realm: str,
         runtime_id: str,  # string-id-ok: gateway identifier
         bus_id: str,  # string-id-ok: external system identifier
         trace_id: UUID,
         payload: T,
         payload_hash: str,
-        signature_bytes: str,
+        signature_hex: str,
         tenant_id: str | None = None,  # string-id-ok: external tenant identifier
         causality_id: UUID | None = None,
         emitter_identity: ModelEmitterIdentity | None = None,
         emitted_at: datetime | None = None,
     ) -> "ModelMessageEnvelope[T]":

And update the usage on line 293:

         signature = ModelEnvelopeSignature(
             algorithm=EnumSignatureAlgorithm.ED25519,
             signer=runtime_id,
             payload_hash=payload_hash,
-            signature=signature_bytes,
+            signature=signature_hex,
         )

Comment thread src/omnibase_core/enums/enum_core_error_code.py Outdated
Comment on lines +204 to +219
def derive_consumer_group(self, purpose: str) -> str:
"""
Derive a consumer group ID for this envelope.

Uses emitter_identity if present, otherwise falls back to runtime_id.

Args:
purpose: The purpose suffix (e.g., 'consume', 'handle')

Returns:
Consumer group ID string
"""
if self.emitter_identity is not None:
return self.emitter_identity.derive_consumer_group(purpose)
# Fallback to runtime_id-based group
return f"{self.realm}.{self.runtime_id}.{purpose}"

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.

⚠️ Potential issue | 🟡 Minor

Consumer group format asymmetry.

The fallback format {realm}.{runtime_id}.{purpose} differs structurally from ModelEmitterIdentity.derive_consumer_group() which produces {env}.{service}.{node_name}.{purpose}.{version}. This inconsistency (3 vs 5 dot-separated segments) could cause issues if downstream consumers expect a consistent format.

Consider documenting this asymmetry or adding a version component to the fallback format for consistency.

🤖 Prompt for AI Agents
In `@src/omnibase_core/models/envelope/model_message_envelope.py` around lines 204
- 219, derive_consumer_group currently returns a fallback string
f"{self.realm}.{self.runtime_id}.{purpose}" which is structurally different from
ModelEmitterIdentity.derive_consumer_group() (which yields
"{env}.{service}.{node_name}.{purpose}.{version}"); update derive_consumer_group
to produce a consistent five-segment format or explicitly document the
asymmetry. Specifically, in derive_consumer_group (and its fallback branch when
emitter_identity is None) either add missing components (e.g.,
env/service/node_name/version placeholders derived from available attributes or
a default version) so the fallback matches
"{env}.{service}.{node_name}.{purpose}.{version}", or add a clear docstring
comment noting the format difference; reference derive_consumer_group,
emitter_identity, and ModelEmitterIdentity.derive_consumer_group when making the
change.

- Fix directory permissions bug: set umask before mkdir, not just file write
- Add key length validation on load to reject corrupt keys early
- Add payload integrity verification to verify_signature() method
- Add thread safety tests for FileKeyProvider concurrent access
- Add tampered envelope detection tests to verify integrity protection

Review iteration: 1/10
- Add field_validator for emitted_at to reject naive datetimes
- Add tests for timezone-aware datetime validation

Review iteration: 2/10
@jonahgabriel
jonahgabriel force-pushed the jonah/omn-1898-modelmessageenvelope-signed-envelope-with-realmruntime branch from e9bc7ab to 4dbc4d5 Compare February 4, 2026 16:39

@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.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@src/omnibase_core/crypto/blake3_hasher.py`:
- Around line 1-18: Rename the module file from blake3_hasher.py to
util_blake3_hasher.py to follow the util_* prefix convention, then update all
imports that reference the old module name (e.g., any "from
omnibase_core.crypto.blake3_hasher import ..." or "import
omnibase_core.crypto.blake3_hasher" usages) to point to
omnibase_core.crypto.util_blake3_hasher; also update any package __init__.py
exports, tests, and CI references that import or reference blake3_hasher so they
import util_blake3_hasher instead and ensure the module-level contents
(docstring and imports like blake3 and json) remain unchanged.

In `@src/omnibase_core/crypto/ed25519_signer.py`:
- Around line 1-12: Rename the module file from ed25519_signer.py to
util_ed25519_signer.py and update all imports that reference ed25519_signer to
the new module name (e.g., change "from omnibase_core.crypto.ed25519_signer
import ..." to "from omnibase_core.crypto.util_ed25519_signer import ...");
search the repo for occurrences of the old module name and update them, and if
the package __init__.py in omnibase_core.crypto exports or imports
ed25519_signer, update those export/import lines to util_ed25519_signer as well
so the module prefix convention and all references (including tests and docs)
remain consistent.

In `@src/omnibase_core/models/envelope/model_message_envelope.py`:
- Around line 194-216: The signing-dict construction is duplicated between
_get_signing_content and create_signed causing risk of divergence; add a single
shared helper (e.g., a `@staticmethod` named _build_signing_dict) that accepts
realm, runtime_id, bus_id, trace_id, emitted_at, payload_hash, optional
causality_id and tenant_id and returns the canonical dict (with trace_id and
causality_id stringified and emitted_at.isoformat()); then update
_get_signing_content to call _build_signing_dict and json.dumps that result, and
update create_signed to call _build_signing_dict when constructing the bytes to
sign so both places use the same logic.
- Around line 218-226: Extract the duplicate payload-to-dict logic from
_compute_payload_hash() and create_signed() into a new private helper
_serialize_payload(self) that returns a canonical dict (use
payload.model_dump(mode="json") for BaseModel, passthrough for dict, else
{"value": payload}); update _compute_payload_hash() to call _serialize_payload()
and pass its result to hash_canonical_json(), and update create_signed() to
reuse _serialize_payload() instead of repeating the branching so both methods
share the exact serialization path.
🧹 Nitpick comments (5)
tests/unit/crypto/test_ed25519_signer.py (1)

17-41: Missing @pytest.mark.unit markers on test classes.

Per coding guidelines, test functions/classes should use appropriate markers (@pytest.mark.unit, @pytest.mark.integration, etc.). Add the @pytest.mark.unit marker to each test class.

♻️ Proposed fix
+@pytest.mark.unit
 class TestGenerateKeypair:
     """Tests for generate_keypair function."""

Apply similarly to TestEd25519KeyPair, TestSignAndVerify, and TestBase64Functions.

tests/unit/crypto/test_file_key_provider.py (1)

26-73: Missing @pytest.mark.unit markers on test classes.

Per coding guidelines, test classes should use the @pytest.mark.unit marker. Apply to all test classes in this file.

♻️ Proposed fix
+@pytest.mark.unit
 class TestFileKeyProviderBasics:
     """Basic functionality tests."""

Apply similarly to all other test classes.

src/omnibase_core/crypto/file_key_provider.py (1)

28-42: Consider renaming to follow naming convention.

Per coding guidelines, file names must follow the naming convention with prefixes like service_*, util_*, etc. Consider renaming to service_file_key_provider.py since this is a concrete service implementation of ProtocolKeyProvider.

src/omnibase_core/models/envelope/model_message_envelope.py (2)

94-95: Consider adding frozen=True for immutability.

Per coding guidelines, Pydantic immutable value models should use ConfigDict(frozen=True, extra="forbid", from_attributes=True). Since signed envelopes should not be modified after creation (which would invalidate the signature), consider making this model frozen.

If mutability is intentional for pre-signing construction, document this design decision.


348-355: Broad exception catch is acceptable here.

Catching generic Exception and wrapping in ModelOnexError provides useful context for callers. Consider narrowing to ValueError if you want to be more specific about expected failure modes from sign_base64.

Comment on lines +1 to +18
"""
Blake3 hashing utilities for envelope payload verification.

Blake3 is chosen over SHA-256 for:
- Faster performance (especially on modern CPUs with SIMD)
- Tree-hashing support for parallel processing
- Designed for cryptographic security from the ground up

The canonical JSON serialization ensures deterministic hashing
regardless of key ordering in dictionaries.
"""

from __future__ import annotations

import json

import blake3

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.

🛠️ Refactor suggestion | 🟠 Major

Rename module to match required file prefix.

blake3_hasher.py doesn’t follow the required prefix conventions for src/omnibase_core/**/*.py. Consider renaming to something like util_blake3_hasher.py and updating imports accordingly.
As per coding guidelines, File names must follow the naming convention based on directory: cli_*, constants_*, container_*, context_*, contract_*, decorator_*, enum_*, error_* or exception_*, factory_*, node_*, mixin_*, model_*, protocol_*, runtime_*, service_*, util_*, validator_* or checker_*.

🤖 Prompt for AI Agents
In `@src/omnibase_core/crypto/blake3_hasher.py` around lines 1 - 18, Rename the
module file from blake3_hasher.py to util_blake3_hasher.py to follow the util_*
prefix convention, then update all imports that reference the old module name
(e.g., any "from omnibase_core.crypto.blake3_hasher import ..." or "import
omnibase_core.crypto.blake3_hasher" usages) to point to
omnibase_core.crypto.util_blake3_hasher; also update any package __init__.py
exports, tests, and CI references that import or reference blake3_hasher so they
import util_blake3_hasher instead and ensure the module-level contents
(docstring and imports like blake3 and json) remain unchanged.

Comment on lines +1 to +12
"""
Ed25519 signing utilities for envelope authentication.

Ed25519 is chosen for envelope signing because:
- Fast key generation and signing
- Small keys (32 bytes) and signatures (64 bytes)
- Deterministic signatures (no random nonce needed)
- Strong security guarantees

The cryptography library is used as it's already a dependency
and provides a well-audited Ed25519 implementation.
"""

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.

🛠️ Refactor suggestion | 🟠 Major

Rename module to match required file prefix.

ed25519_signer.py doesn’t follow the required prefix conventions for src/omnibase_core/**/*.py. Consider renaming to something like util_ed25519_signer.py (or equivalent) and updating imports accordingly.
As per coding guidelines, File names must follow the naming convention based on directory: cli_*, constants_*, container_*, context_*, contract_*, decorator_*, enum_*, error_* or exception_*, factory_*, node_*, mixin_*, model_*, protocol_*, runtime_*, service_*, util_*, validator_* or checker_*.

🤖 Prompt for AI Agents
In `@src/omnibase_core/crypto/ed25519_signer.py` around lines 1 - 12, Rename the
module file from ed25519_signer.py to util_ed25519_signer.py and update all
imports that reference ed25519_signer to the new module name (e.g., change "from
omnibase_core.crypto.ed25519_signer import ..." to "from
omnibase_core.crypto.util_ed25519_signer import ..."); search the repo for
occurrences of the old module name and update them, and if the package
__init__.py in omnibase_core.crypto exports or imports ed25519_signer, update
those export/import lines to util_ed25519_signer as well so the module prefix
convention and all references (including tests and docs) remain consistent.

Comment thread src/omnibase_core/models/envelope/model_message_envelope.py Outdated
Comment thread src/omnibase_core/models/envelope/model_message_envelope.py Outdated
…ing consistency

- Fixed: file_key_provider.py - Add permission validation on load and explicit chmod after write
- Fixed: model_message_envelope.py - Wrap payload hash computation in ModelOnexError for consistency
- Added: ENVELOPE_PAYLOAD_SERIALIZATION_FAILED error code for serialization failures

Review iteration: 1/10

@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.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/omnibase_core/crypto/file_key_provider.py`:
- Around line 108-124: The code sets process-wide umask via os.umask(0o077)
around the write, which is unsafe in multithreaded contexts; instead remove the
umask change and explicitly set permissions on both the parent directory and the
file after creation: create the parent directory via
self._key_file.parent.mkdir(...), then call
self._key_file.parent.chmod(self._MAX_ALLOWED_MODE) to ensure directory
permissions, write the file (preferably via an atomic temp-write+rename
pattern), and finally call self._key_file.chmod(self._MAX_ALLOWED_MODE) as
already present; update the block that currently uses os.umask,
self._key_file.parent.mkdir, self._key_file.write_text and self._key_file.chmod
to perform explicit chmods and atomic write without changing global umask.
🧹 Nitpick comments (3)
src/omnibase_core/models/envelope/model_message_envelope.py (1)

95-95: Consider adding frozen=True to prevent post-creation mutation.

Since the envelope contains a cryptographic signature over its contents, mutating any signed field after creation would silently invalidate the signature. Making the model frozen would prevent accidental mutation and align with the "immutable value model" guideline.

♻️ Proposed change
-    model_config = ConfigDict(extra="forbid", from_attributes=True)
+    model_config = ConfigDict(frozen=True, extra="forbid", from_attributes=True)

As per coding guidelines: "Pydantic immutable value models must use ConfigDict(frozen=True, extra="forbid", from_attributes=True)"

src/omnibase_core/crypto/file_key_provider.py (2)

1-25: File naming convention violation.

Per coding guidelines, files must follow the naming convention with prefixes like service_*, util_*, protocol_*, etc. Consider renaming to service_file_key_provider.py or util_file_key_provider.py.

Additionally, logging is imported inside _load_keys (line 74). Move it to module-level imports for clarity and to avoid repeated import overhead on reload.

 import base64
 import json
+import logging
 import os
 import threading
 from pathlib import Path

72-93: Consider more precise permission check and logging invalid keys.

  1. The numeric comparison file_mode > self._MAX_ALLOWED_MODE will flag 0o700 (owner rwx) as insecure even though no group/other access exists. A bitwise check is more semantically correct:

  2. Silently skipping invalid-length keys (line 93) could hide data corruption. A warning log aids operational visibility.

♻️ Suggested improvements
-            file_mode = self._key_file.stat().st_mode & 0o777
-            if file_mode > self._MAX_ALLOWED_MODE:
-                import logging
-
+            file_mode = self._key_file.stat().st_mode & 0o777
+            # Check if group or other permissions are set
+            if file_mode & 0o077:
                 logging.warning(
-                    "Key file %s has insecure permissions %o (should be <= %o). "
+                    "Key file %s has insecure permissions %o (group/other access detected). "
                     "This may allow unauthorized key modification.",
                     self._key_file,
                     file_mode,
-                    self._MAX_ALLOWED_MODE,
                 )
                     if len(key_bytes) == self.ED25519_PUBLIC_KEY_LENGTH:
                         loaded_keys[runtime_id] = key_bytes
-                    # Skip invalid-length keys silently (corrupt data)
+                    else:
+                        logging.warning(
+                            "Skipping key for runtime %s: invalid length %d (expected %d)",
+                            runtime_id,
+                            len(key_bytes),
+                            self.ED25519_PUBLIC_KEY_LENGTH,
+                        )

Comment thread src/omnibase_core/crypto/crypto_file_key_provider.py Outdated
Major fixes:
- Rename crypto modules to follow naming convention (crypto_* prefix)
- Deduplicate signing content and payload hash logic in ModelMessageEnvelope

Minor fixes:
- Normalize blake3 dependency syntax in pyproject.toml
- Add @pytest.mark.unit markers to all test classes
- Add type annotations to test fixtures

Nitpick fixes:
- Add frozen=True to ModelMessageEnvelope for immutability
- Add hex character validation for payload_hash field
- Replace generic pytest.raises(Exception) with specific ValidationError

@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.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@src/omnibase_core/crypto/crypto_file_key_provider.py`:
- Around line 72-82: The current numeric comparison of file_mode against
self._MAX_ALLOWED_MODE misses cases; change the check in the block that computes
file_mode (using self._key_file.stat().st_mode) to a bitwise test for disallowed
group/other bits instead of `file_mode > self._MAX_ALLOWED_MODE`. For example,
replace the `file_mode > self._MAX_ALLOWED_MODE` condition with a test that
fails when any group/other permission bits are set (e.g., `if file_mode &
~self._MAX_ALLOWED_MODE:`) or explicitly test `stat.S_IRWXG | stat.S_IRWXO` so
that secure owner-only modes like 0o700 pass and any group/other bits (like
0o060) trigger the warning; keep the same logging block using self._key_file and
file_mode.

In `@src/omnibase_core/models/envelope/model_message_envelope.py`:
- Around line 304-311: The verify_signature method currently calls
self._compute_payload_hash() which can raise TypeError/ValueError for
non-serializable payloads; wrap the call to _compute_payload_hash() in a
try/except that catches TypeError and ValueError and returns False to keep
verification fail-closed, but do not catch other exceptions (so missing-key
behavior remains unchanged). After the try/except, continue comparing
computed_hash to self.signature.payload_hash and then call
_get_signing_content() and verify_base64(public_key, signing_content,
self.signature.signature) as before.
- Around line 171-192: Add a post-validation that enforces signature.signer
equals the envelope runtime_id: implement a `@model_validator`(mode="after")
method (e.g., validate_signature_signer_matches_runtime_id) on
ModelMessageEnvelope that checks if self.signature is not None and
self.signature.signer != self.runtime_id and, if so, raises ModelOnexError (use
a new or existing error code like
EnumCoreErrorCode.ENVELOPE_SIGNATURE_SIGNER_MISMATCH) with a message and context
including "signature_signer" and "runtime_id"; this prevents tampered signer
metadata from being accepted.

In `@tests/unit/models/envelope/test_model_message_envelope.py`:
- Around line 71-77: The type-ignore comments in this test (the inline ignore on
the assignment to identity.env and the call to ModelEmitterIdentity in
test_identity_requires_all_fields) need concrete ticket references and brief
explanations; update each `# type: ignore[...]` to include the most specific
ignore code (e.g., arg-type, misc) and append a NOTE with a Linear ticket (e.g.,
NOTE(LINEAR-1234):) plus a one-line rationale (what the type checker is
incorrectly flagging and why the ignore is safe). Apply the same change to the
other test ignores referenced in the review (the similar ignores around the
identity manipulation/assertions and any ignores in the ModelEmitterIdentity
tests), ensuring you keep the specific ignore kind and add the `#
NOTE(LINEAR-XXXX): short reason` for each occurrence.
🧹 Nitpick comments (3)
tests/unit/crypto/test_crypto_file_key_provider.py (1)

114-114: Move inline imports to module level.

base64 (lines 114, 230) and threading (line 187) are imported inside test methods. For consistency and readability, consider moving these to the top of the file alongside the other imports.

♻️ Suggested import section
 import json
+import base64
+import threading
 from pathlib import Path

 import pytest

Also applies to: 187-187, 230-230

src/omnibase_core/crypto/crypto_file_key_provider.py (2)

74-74: Move logging import to module level.

The lazy import inside the method is non-standard. Since logging is a standard library module with negligible import cost, it should be imported at the module level for consistency.

♻️ Suggested fix

Add to module-level imports:

import logging

Then remove line 74.


116-118: Non-atomic write could expose partial file content to concurrent readers.

write_text directly overwrites the file, which means external readers (including other FileKeyProvider instances calling reload()) could observe partial/corrupt content during the write window.

For a development/testing provider this is likely acceptable given the docstring recommends production alternatives. If durability matters, consider write-to-temp-then-rename:

♻️ Atomic write pattern
+import tempfile
+
 def _save_keys(self) -> None:
     """Save keys to the file."""
     with self._lock:
         keys_data = {
             runtime_id: base64.urlsafe_b64encode(key).decode("ascii")
             for runtime_id, key in self._keys.items()
         }
         data: dict[str, dict[str, str]] = {"keys": keys_data}

         old_umask = os.umask(0o077)
         try:
             self._key_file.parent.mkdir(parents=True, exist_ok=True)
-            self._key_file.write_text(
-                json.dumps(data, indent=2, sort_keys=True), encoding="utf-8"
-            )
+            # Write to temp file then atomic rename
+            fd, tmp_path = tempfile.mkstemp(
+                dir=self._key_file.parent, suffix=".tmp"
+            )
+            try:
+                os.write(fd, json.dumps(data, indent=2, sort_keys=True).encode("utf-8"))
+                os.close(fd)
+                os.replace(tmp_path, self._key_file)
+            except Exception:
+                os.unlink(tmp_path)
+                raise
             self._key_file.chmod(self._MAX_ALLOWED_MODE)
         finally:
             os.umask(old_umask)

Comment thread src/omnibase_core/crypto/crypto_file_key_provider.py Outdated
Comment thread src/omnibase_core/models/envelope/model_message_envelope.py
Comment on lines +304 to +311
# Step 1: Verify payload integrity - hash must match stored hash
computed_hash = self._compute_payload_hash()
if computed_hash != self.signature.payload_hash:
return False

# Step 2: Verify signature authenticity
signing_content = self._get_signing_content()
return verify_base64(public_key, signing_content, self.signature.signature)

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.

⚠️ Potential issue | 🟠 Major

verify_signature can raise on non-serializable payloads.

_compute_payload_hash() can throw TypeError/ValueError, but the method contract says it returns bool unless the key is missing. Catch and return False to keep verification fail-closed without unexpected exceptions.

🔧 Proposed fix
-        computed_hash = self._compute_payload_hash()
-        if computed_hash != self.signature.payload_hash:
-            return False
+        try:
+            computed_hash = self._compute_payload_hash()
+        except (TypeError, ValueError):
+            return False
+        if computed_hash != self.signature.payload_hash:
+            return False
🤖 Prompt for AI Agents
In `@src/omnibase_core/models/envelope/model_message_envelope.py` around lines 304
- 311, The verify_signature method currently calls self._compute_payload_hash()
which can raise TypeError/ValueError for non-serializable payloads; wrap the
call to _compute_payload_hash() in a try/except that catches TypeError and
ValueError and returns False to keep verification fail-closed, but do not catch
other exceptions (so missing-key behavior remains unchanged). After the
try/except, continue comparing computed_hash to self.signature.payload_hash and
then call _get_signing_content() and verify_base64(public_key, signing_content,
self.signature.signature) as before.

Comment thread tests/unit/models/envelope/test_model_message_envelope.py
…back

Major:
- Add signer validation (signature.signer == runtime_id) with new error code
- Handle non-serializable payloads in verify_signature() with clear errors
- Implement thread-safe atomic file writes in crypto_file_key_provider
- Add 13 missing enum error code mappings with runtime validation

Minor:
- Add blake3 to Poetry dependencies section for consistency
- Add precise permission checks with bitmask validation
- Add logging for invalid key files with remediation advice
- Add type annotations to 14 test fixture parameters
- Add NOTE explanations to 4 type ignore comments

Nitpick:
- Document fallback payload handling strategy in docstrings
- Verify file naming conventions are compliant
- Confirm all imports at module level
- [major] FileKeyProvider now raises ModelOnexError on insecure file
  permissions instead of just logging warnings (fail-closed security)
- [minor] Add 12 new tests for file permission validation scenarios
- [minor] Fix misleading comment about frozen model in test

Security fix ensures keys won't load from files with group/other access
bits or special permission bits (setuid/setgid/sticky).

Review iteration: 1/10
@jonahgabriel
jonahgabriel merged commit fa65a93 into main Feb 4, 2026
30 checks passed
@jonahgabriel
jonahgabriel deleted the jonah/omn-1898-modelmessageenvelope-signed-envelope-with-realmruntime branch February 4, 2026 19:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant