Skip to content

Commit 0325702

Browse files
committed
Merge remote-tracking branch 'upstream/sigp-audit-fixes' into ssz-update-v2
2 parents 210905e + d834242 commit 0325702

9 files changed

Lines changed: 369 additions & 68 deletions

File tree

api/signer-api.yml

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ paths:
8686
application/json:
8787
schema:
8888
type: object
89-
required: [pubkey, object_root]
89+
required: [pubkey, object_root, nonce]
9090
properties:
9191
pubkey:
9292
description: The 48-byte BLS public key, with optional `0x` prefix, of the proposer key that you want to request a signature from.
@@ -234,7 +234,7 @@ paths:
234234
application/json:
235235
schema:
236236
type: object
237-
required: [proxy, object_root]
237+
required: [proxy, object_root, nonce]
238238
properties:
239239
proxy:
240240
description: The 48-byte BLS public key (for `proxy_bls` mode) or the 20-byte Ethereum address (for `proxy_ecdsa` mode), with optional `0x` prefix, of the proxy key that you want to request a signature from.
@@ -382,7 +382,7 @@ paths:
382382
application/json:
383383
schema:
384384
type: object
385-
required: [proxy, object_root]
385+
required: [proxy, object_root, nonce]
386386
properties:
387387
proxy:
388388
description: The 20-byte Ethereum address, with optional `0x` prefix, of the proxy key that you want to request a signature from.
@@ -695,7 +695,12 @@ components:
695695
$ref: "#/components/schemas/EcdsaSignature"
696696
Nonce:
697697
type: integer
698-
description: If your module tracks nonces per signature (e.g., to prevent replay attacks), this is the unique nonce to use for the signature. It should be an unsigned 64-bit integer in big-endian format. It must be between 0 and 2^64-2, inclusive. If your module doesn't use nonces, we suggest setting this to 2^64-1 instead of 0 because 0 is a legal nonce and will cause complications with your module if you ever want to use a nonce in the future.
698+
description: |
699+
Replay-protection nonce, always mixed into the signing root via `PropCommitSigningInfo`. It
700+
must be an unsigned 64-bit integer between 0 and 2^64-2 (18446744073709551614), inclusive.
701+
702+
Modules that track nonces for replay protection should use a monotonically increasing value
703+
per key. Modules that do not use replay protection should always send `0`.
699704
minimum: 0
700-
maximum: 18446744073709551614 // 2^64-2
705+
maximum: 18446744073709551614
701706
example: 1

crates/cli/src/docker_init.rs

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,8 @@ fn create_signer_service_dirk(
497497
let mut envs = IndexMap::from([
498498
get_env_val(CONFIG_ENV, CONFIG_DEFAULT),
499499
get_env_same(JWTS_ENV),
500+
get_env_same(ADMIN_JWT_ENV),
501+
get_env_val(SIGNER_TLS_CERTIFICATES_PATH_ENV, SIGNER_TLS_CERTIFICATES_PATH_DEFAULT),
500502
get_env_val(DIRK_CERT_ENV, DIRK_CERT_DEFAULT),
501503
get_env_val(DIRK_KEY_ENV, DIRK_KEY_DEFAULT),
502504
get_env_val(DIRK_DIR_SECRETS_ENV, DIRK_DIR_SECRETS_DEFAULT),
@@ -548,6 +550,7 @@ fn create_signer_service_dirk(
548550

549551
// write jwts to env
550552
service_config.envs.insert(JWTS_ENV.into(), format_comma_separated(&service_config.jwts));
553+
service_config.envs.insert(ADMIN_JWT_ENV.into(), random_jwt_secret());
551554

552555
// CA cert volume and env
553556
if let Some(ca_cert_path) = ca_cert_path {
@@ -589,8 +592,8 @@ fn create_signer_service_dirk(
589592
environment: Environment::KvPair(envs),
590593
healthcheck: Some(Healthcheck {
591594
test: Some(HealthcheckTest::Single(format!(
592-
"curl -f http://localhost:{}/status",
593-
signer_config.port,
595+
"curl -k -f {}/status",
596+
cb_config.signer_server_url(SIGNER_PORT_DEFAULT),
594597
))),
595598
interval: Some("30s".into()),
596599
timeout: Some("5s".into()),
@@ -932,6 +935,13 @@ mod tests {
932935
service.volumes.iter().any(|v| matches!(v, Volumes::Simple(s) if s.contains(substr)))
933936
}
934937

938+
fn get_healthcheck_cmd(service: &Service) -> Option<String> {
939+
service.healthcheck.as_ref().and_then(|hc| match &hc.test {
940+
Some(HealthcheckTest::Single(cmd)) => Some(cmd.clone()),
941+
_ => None,
942+
})
943+
}
944+
935945
fn has_port(service: &Service, substr: &str) -> bool {
936946
match &service.ports {
937947
Ports::Short(ports) => ports.iter().any(|p| p.contains(substr)),
@@ -1309,12 +1319,33 @@ mod tests {
13091319
assert!(env_str(&service, DIRK_CERT_ENV).is_some());
13101320
assert!(env_str(&service, DIRK_KEY_ENV).is_some());
13111321
assert!(env_str(&service, DIRK_DIR_SECRETS_ENV).is_some());
1322+
assert!(has_env_key(&service, ADMIN_JWT_ENV));
1323+
assert!(has_env_key(&service, SIGNER_TLS_CERTIFICATES_PATH_ENV));
13121324
assert!(has_volume(&service, "client.crt"));
13131325
assert!(has_volume(&service, "client.key"));
13141326
assert!(has_volume(&service, "dirk_secrets"));
13151327
Ok(())
13161328
}
13171329

1330+
#[test]
1331+
fn test_create_signer_service_dirk_generates_admin_jwt() -> eyre::Result<()> {
1332+
let mut sc = minimal_service_config();
1333+
let signer_config = dirk_signer_config();
1334+
create_signer_service_dirk(
1335+
&mut sc,
1336+
&signer_config,
1337+
Path::new("/certs/client.crt"),
1338+
Path::new("/certs/client.key"),
1339+
Path::new("/dirk_secrets"),
1340+
&None,
1341+
&None,
1342+
)?;
1343+
1344+
let admin_jwt = sc.envs.get(ADMIN_JWT_ENV).expect("ADMIN_JWT_ENV must be set");
1345+
assert!(!admin_jwt.is_empty(), "admin JWT secret must not be empty");
1346+
Ok(())
1347+
}
1348+
13181349
#[test]
13191350
fn test_create_signer_service_dirk_with_ca_cert() -> eyre::Result<()> {
13201351
let mut sc = minimal_service_config();
@@ -1690,6 +1721,50 @@ mod tests {
16901721
Ok(())
16911722
}
16921723

1724+
#[test]
1725+
fn test_create_signer_service_dirk_healthcheck_uses_https_with_tls() -> eyre::Result<()> {
1726+
let dir = tempfile::tempdir()?;
1727+
let certs_path = dir.path().to_path_buf();
1728+
std::fs::write(certs_path.join(SIGNER_TLS_CERTIFICATE_NAME), b"cert")?;
1729+
std::fs::write(certs_path.join(SIGNER_TLS_KEY_NAME), b"key")?;
1730+
1731+
let mut sc = service_config_with_tls(certs_path);
1732+
let signer_config = dirk_signer_config();
1733+
let service = create_signer_service_dirk(
1734+
&mut sc,
1735+
&signer_config,
1736+
Path::new("/certs/client.crt"),
1737+
Path::new("/certs/client.key"),
1738+
Path::new("/dirk_secrets"),
1739+
&None,
1740+
&None,
1741+
)?;
1742+
1743+
let cmd = get_healthcheck_cmd(&service).expect("healthcheck must be set");
1744+
assert!(cmd.contains("https://"), "healthcheck must use https with TLS: {cmd}");
1745+
assert!(cmd.contains("-k"), "healthcheck must use -k flag for self-signed certs: {cmd}");
1746+
Ok(())
1747+
}
1748+
1749+
#[test]
1750+
fn test_create_signer_service_dirk_healthcheck_uses_http_without_tls() -> eyre::Result<()> {
1751+
let mut sc = minimal_service_config();
1752+
let signer_config = dirk_signer_config();
1753+
let service = create_signer_service_dirk(
1754+
&mut sc,
1755+
&signer_config,
1756+
Path::new("/certs/client.crt"),
1757+
Path::new("/certs/client.key"),
1758+
Path::new("/dirk_secrets"),
1759+
&None,
1760+
&None,
1761+
)?;
1762+
1763+
let cmd = get_healthcheck_cmd(&service).expect("healthcheck must be set");
1764+
assert!(cmd.contains("http://"), "healthcheck must use http without TLS: {cmd}");
1765+
Ok(())
1766+
}
1767+
16931768
// -------------------------------------------------------------------------
16941769
// create_module_service – TLS cert env/volume
16951770
// -------------------------------------------------------------------------

crates/common/src/commit/request.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ impl<T: ProxyId> fmt::Display for SignedProxyDelegation<T> {
8484
pub struct SignConsensusRequest {
8585
pub pubkey: BlsPublicKey,
8686
pub object_root: B256,
87+
/// Replay-protection nonce mixed into the signing root via
88+
/// `PropCommitSigningInfo`. Modules that do not track nonces should
89+
/// send `0`. Modules that do track nonces should use a monotonically
90+
/// increasing value per key to prevent signature reuse.
8791
pub nonce: u64,
8892
}
8993

@@ -93,7 +97,7 @@ impl SignConsensusRequest {
9397
}
9498

9599
pub fn builder(pubkey: BlsPublicKey) -> Self {
96-
Self::new(pubkey, B256::ZERO, u64::MAX - 1)
100+
Self::new(pubkey, B256::ZERO, 0)
97101
}
98102

99103
pub fn with_root<R: Into<B256>>(self, object_root: R) -> Self {
@@ -125,6 +129,10 @@ impl Display for SignConsensusRequest {
125129
pub struct SignProxyRequest<T: ProxyId> {
126130
pub proxy: T,
127131
pub object_root: B256,
132+
/// Replay-protection nonce mixed into the signing root via
133+
/// `PropCommitSigningInfo`. Modules that do not track nonces should
134+
/// send `0`. Modules that do track nonces should use a monotonically
135+
/// increasing value per key to prevent signature reuse.
128136
pub nonce: u64,
129137
}
130138

@@ -134,7 +142,7 @@ impl<T: ProxyId> SignProxyRequest<T> {
134142
}
135143

136144
pub fn builder(proxy: T) -> Self {
137-
Self::new(proxy, B256::ZERO, u64::MAX - 1)
145+
Self::new(proxy, B256::ZERO, 0)
138146
}
139147

140148
pub fn with_root<R: Into<B256>>(self, object_root: R) -> Self {

crates/common/src/config/module.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ pub fn load_commit_module_config<T: DeserializeOwned>() -> Result<StartCommitMod
8383
struct StubConfig<U> {
8484
chain: Chain,
8585
modules: Vec<ThisModule<U>>,
86-
signer: SignerConfig,
86+
signer: Option<SignerConfig>,
8787
}
8888

8989
// load module config including the extra data (if any)
@@ -106,7 +106,7 @@ pub fn load_commit_module_config<T: DeserializeOwned>() -> Result<StartCommitMod
106106
.find(|m| m.static_config.id == module_id)
107107
.wrap_err(format!("failed to find module for {module_id}"))?;
108108

109-
let certs_path = match cb_config.signer.tls_mode {
109+
let certs_path = match cb_config.signer.context("No [signer] section in config")?.tls_mode {
110110
TlsMode::Insecure => None,
111111
TlsMode::Certificate(path) => Some(
112112
load_env_var(SIGNER_TLS_CERTIFICATES_PATH_ENV)

crates/common/src/config/pbs.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ pub async fn load_pbs_custom_config<T: DeserializeOwned>() -> Result<(PbsModuleC
378378
chain: Chain,
379379
relays: Vec<RelayConfig>,
380380
pbs: CustomPbsConfig<U>,
381-
signer: SignerConfig,
381+
signer: Option<SignerConfig>,
382382
muxes: Option<PbsMuxes>,
383383
}
384384

@@ -435,7 +435,11 @@ pub async fn load_pbs_custom_config<T: DeserializeOwned>() -> Result<(PbsModuleC
435435
// if custom pbs requires a signer client, load jwt
436436
let module_jwt = Jwt(load_env_var(MODULE_JWT_ENV)?);
437437
let signer_server_url = load_env_var(SIGNER_URL_ENV)?.parse()?;
438-
let certs_path = match cb_config.signer.tls_mode {
438+
let certs_path = match cb_config
439+
.signer
440+
.ok_or_else(|| eyre::eyre!("with_signer = true but no [signer] section in config"))?
441+
.tls_mode
442+
{
439443
TlsMode::Insecure => None,
440444
TlsMode::Certificate(path) => Some(
441445
load_env_var(SIGNER_TLS_CERTIFICATES_PATH_ENV)

crates/common/src/config/signer.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,12 @@ impl Display for ReverseProxyHeaderSetup {
9292
write!(f, "\"{header} (unique)\"")
9393
}
9494
ReverseProxyHeaderSetup::Rightmost { header, trusted_count } => {
95-
let suffix = match trusted_count.get() % 10 {
96-
1 => "st",
97-
2 => "nd",
98-
3 => "rd",
95+
let n = trusted_count.get();
96+
let suffix = match (n % 100, n % 10) {
97+
(11..=13, _) => "th",
98+
(_, 1) => "st",
99+
(_, 2) => "nd",
100+
(_, 3) => "rd",
99101
_ => "th",
100102
};
101103
write!(f, "\"{header} ({trusted_count}{suffix} from the right)\"")

crates/common/src/signature.rs

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,18 @@ pub fn verify_proposer_commitment_signature_ecdsa(
165165
#[cfg(test)]
166166
mod tests {
167167

168-
use alloy::primitives::aliases::B32;
169-
170-
use super::compute_domain;
171-
use crate::{constants::APPLICATION_BUILDER_DOMAIN, types::Chain};
168+
use alloy::primitives::{U256, aliases::B32};
169+
170+
use super::{compute_domain, sign_builder_message, verify_signed_message};
171+
use crate::{
172+
constants::APPLICATION_BUILDER_DOMAIN,
173+
pbs::{
174+
BlindedBeaconBlockElectra, BuilderBid, BuilderBidElectra,
175+
ExecutionPayloadHeaderElectra, ExecutionRequests,
176+
},
177+
types::{BlsSecretKey, Chain},
178+
utils::TestRandomSeed,
179+
};
172180

173181
#[test]
174182
fn test_builder_domains() {
@@ -178,4 +186,48 @@ mod tests {
178186
assert_eq!(compute_domain(Chain::Sepolia, domain), Chain::Sepolia.builder_domain());
179187
assert_eq!(compute_domain(Chain::Hoodi, domain), Chain::Hoodi.builder_domain());
180188
}
189+
190+
#[test]
191+
fn test_builder_bid_sign_and_verify() {
192+
let secret_key = BlsSecretKey::test_random();
193+
let pubkey = secret_key.public_key();
194+
195+
let message = BuilderBid::Electra(BuilderBidElectra {
196+
header: ExecutionPayloadHeaderElectra::test_random(),
197+
blob_kzg_commitments: Default::default(),
198+
execution_requests: ExecutionRequests::default(),
199+
value: U256::from(10),
200+
pubkey: pubkey.clone().into(),
201+
});
202+
203+
let sig = sign_builder_message(Chain::Mainnet, &secret_key, &message);
204+
205+
assert!(verify_signed_message(
206+
Chain::Mainnet,
207+
&pubkey,
208+
&message,
209+
&sig,
210+
None,
211+
&B32::from(APPLICATION_BUILDER_DOMAIN),
212+
));
213+
}
214+
215+
#[test]
216+
fn test_blinded_block_sign_and_verify() {
217+
let secret_key = BlsSecretKey::test_random();
218+
let pubkey = secret_key.public_key();
219+
220+
let block = BlindedBeaconBlockElectra::test_random();
221+
222+
let sig = sign_builder_message(Chain::Mainnet, &secret_key, &block);
223+
224+
assert!(verify_signed_message(
225+
Chain::Mainnet,
226+
&pubkey,
227+
&block,
228+
&sig,
229+
None,
230+
&B32::from(APPLICATION_BUILDER_DOMAIN),
231+
));
232+
}
181233
}

0 commit comments

Comments
 (0)