Skip to content

Conversation

@jclapis
Copy link
Collaborator

@jclapis jclapis commented Jul 1, 2025

This is a large change that modifies the ways proposer commitment signatures are generated. Currently, the requesting module isn't embedded into the data being signed so any authorized module can request the signature of any data - including data that might be used or expected by other modules. This PR precludes that possibility by embedding a new special per-module signing ID into the data being signed.

The signing ID is a 32-byte hex string provided in the Commit Boost configuration's [[modules]] section, per module. Module authors can make it whatever they want, and should publish it within their own documentation so users know which unique value to configure for that module. Since Commit Boost doesn't (and won't) maintain a global registry of such IDs, the onus is on the user to ensure the correct ID is entered for each module. Module authors can then directly bake this into their module code, in their signature validation routines, to confirm that any proposer commitments returned by the signer used the correct ID.

Since different modules will have different signing IDs (a condition enforced by the loader logic), this prevents one module from "forging" a signature destined for another one.

Using a discrete signing ID means module authors can change their module ID at any time (such as with a version number bump or a vendor rename) without invalidating previous signatures. Conversely, if they do want to invalidate previous signatures while retaining the module ID, they can do so by simply changing their signing ID and informing their clients to update with the new configuration accordingly.

This also adds new round-trip integration tests that confirm signing works as expected with the new paradigm and cross-module signatures are no longer possible.

Finally, it starts some documentation on how to request proposer commitments from the signer service, including how to request a signature and verify it. See docs/docs/developing/prop-commit-signing.md.

⚠️ This is a breaking change that will invalidate any existing proposer commitment signatures. Once this is added, clients using the signer module will need to be informed of the breaking change and adapt their signature verification routines with the new logic.

ltitanb and others added 30 commits May 13, 2025 17:17
#[serde(rename = "type")]
pub kind: ModuleKind,
/// Signing ID for the module to use when requesting signatures
pub signing_id: Option<B256>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

why make it optional?

Copy link
Collaborator

Choose a reason for hiding this comment

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

also worth creating a type alias for clarity

Copy link
Collaborator Author

@jclapis jclapis Jul 14, 2025

Choose a reason for hiding this comment

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

why make it optional?

So this was a concession I had to make in order to support the other use cases where the signature generation pipeline is used with COMMIT_BOOST_DOMAIN, but not tied to a specific module. For example, proxy key generation will do this. Proxy keys are already stored by module ID and don't have any sense of signing ID, so coupling them to the signing ID as well would either involve storing that as part of their path or adding a lookup from module ID to signing ID to the signer. Either way, any time the signing ID changes (e.g, when previous signatures need to be invalidated), you'd have to regenerate all of the proxy keys. It seemed easier to make this field optional to support the existing calls instead of refactoring all of that.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Even if we want to still make it optional internally, it should not be optional in the module configuration. After this PR, modules need to have this set

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yep, that makes sense. Fixed in 509dba8.

Copy link
Collaborator

@ManuelBilbao ManuelBilbao Jul 29, 2025

Choose a reason for hiding this comment

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

I agree with making it required for commit modules, but I think for PBS Events modules it's useless as they won't sign anything. With that said, it's not a tedious config to set, so I wouldn't mind to leave it required in favor of a cleaner codebase

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

#338 is going to remove PBS events modules anyway, so assuming that goes in soon-ish then this may end up being OBE?

Comment on lines 41 to 46
pub const SIGNER_JWT_AUTH_FAIL_LIMIT_DEFAULT: u32 = 3;

/// How long to rate limit the client after auth failures
pub const SIGNER_JWT_AUTH_FAIL_TIMEOUT_SECONDS_ENV: &str =
"CB_SIGNER_JWT_AUTH_FAIL_TIMEOUT_SECONDS";
pub const SIGNER_JWT_AUTH_FAIL_TIMEOUT_SECONDS_DEFAULT: u32 = 5 * 60;
Copy link
Collaborator

Choose a reason for hiding this comment

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

is this supposed to be in this PR or does it need to be rebased?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This was some incidental cleanup of #310 that landed in here after it was merged. There are a few little non-functional changes related to it. For cleanliness I can pull all of those out and make a separate PR for them if you'd like, no problem.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yep let's do that to keep the PR clean

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done in PR #346 which has been merged into this now.

Comment on lines 283 to 284
let mut seen_jwt_secrets = HashMap::new();
let mut seen_signing_ids = HashMap::new();
Copy link
Collaborator

Choose a reason for hiding this comment

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

if we go through modules one by one, then these could just be hashsets

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The value (the module_id) is used in the error messages that get thrown during duplication detection so the user knows which module entries are conflicting. We can swap to HashSets, just print the offending duplicated value itself, and let the user find it in the config if that's preferable, but listing the modules directly seems like good UX.

#[derive(Default, Debug, TreeHash)]
pub struct PropCommitSigningInfo {
pub data: [u8; 32],
pub module_signing_id: [u8; 32],
Copy link
Collaborator

Choose a reason for hiding this comment

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

B256 implements TreeHash

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, I left these as [u8; 32] because the original SigningData above it used those. Happy to convert everything to B256 though, in fact I'd prefer it if that's ok.

Copy link
Collaborator

Choose a reason for hiding this comment

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

yep let's do that

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ok, this is a large enough change that I made it a PR for this PR, see #347.

pubkey: &BlsPublicKey,
msg: &T,
signature: &BlsSignature,
module_signing_id: Option<&B256>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Option here and below can be removed, we only do signatures in the signer module and modules will always need to have the id set after this PR

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

See the earlier response - the proxy key generator uses it too, and it (currently) doesn't include signing IDs which is why it's an Option in here. Validating the signature in e.g. SignedProxyDelegationBls won't work without it.

Comment on lines 138 to 149
#### `200 OK`

A successful signing request, with the signature provided as a plaintext quoted hex-encoded string, with a `0x` prefix. For example, the response body would look like:
```
"0xa43e623f009e615faa3987368f64d6286a4103de70e9a81d82562c50c91eae2d5d6fb9db9fe943aa8ee42fd92d8210c1149f25ed6aa72a557d74a0ed5646fdd0e8255ec58e3e2931695fe913863ba0cdf90d29f651bce0a34169a6f6ce5b3115"
```

#### `401 Unauthorized`

Your module did not provide a JWT string in the request's authorization header, or the JWT string was not configured in the signer service's configuration file as belonging to your module.


Copy link
Collaborator

Choose a reason for hiding this comment

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

i wonder if this part is actually needed? we do have an OpenAPI schema after all

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That would be lovely, because maintaining it manually is a chore. I'll ping you about it and add the results to this PR once we have them.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done in ff716a0.

}

let signing_data = SigningData { object_root, signing_domain };
pub fn compute_signing_root<T: TreeHash>(signing_data: &T) -> [u8; 32] {
Copy link
Collaborator

Choose a reason for hiding this comment

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

this function should be either renamed or removed, as technically it's just the tree hash and not a signing root

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Also included in #347.

@jclapis jclapis mentioned this pull request Jul 29, 2025
@jclapis jclapis merged commit 498eed9 into sigp-audit-fixes Aug 19, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

signer Signer module

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants