Skip to content

Commit 3018717

Browse files
authored
fix: Separate artifact signature fields (#13059)
## Why Artifact signatures should use a canonical HMAC input so distinct logical fields cannot serialize to the same byte sequence. This closes VULN-11599, where a prefix relationship between team IDs could make one signed artifact validate for another team sharing the same signing key. ## What Updates artifact signature generation and validation to include a versioned domain prefix and length-prefix each signed field before feeding it to HMAC-SHA256. Adds a regression test for the reported prefix-team forgery shape.
1 parent 34514e2 commit 3018717

1 file changed

Lines changed: 40 additions & 25 deletions

File tree

crates/turborepo-cache/src/signature_authentication.rs

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use thiserror::Error;
99
type HmacSha256 = Hmac<Sha256>;
1010

1111
pub const MIN_SIGNATURE_KEY_LENGTH: usize = 32;
12+
const SIGNATURE_MESSAGE_PREFIX: &[u8] = b"artifact-signature:v2";
1213

1314
#[derive(Debug, Error)]
1415
pub enum SignatureError {
@@ -76,18 +77,16 @@ impl ArtifactSignatureAuthenticator {
7677
.into_raw_vec())
7778
}
7879

79-
fn construct_metadata(&self, hash: &[u8]) -> Result<Vec<u8>, SignatureError> {
80-
let mut metadata = hash.to_vec();
81-
metadata.extend_from_slice(&self.team_id);
82-
83-
Ok(metadata)
84-
}
85-
86-
fn get_tag_generator(&self, hash: &[u8]) -> Result<HmacSha256, SignatureError> {
80+
fn get_tag_generator(
81+
&self,
82+
hash: &[u8],
83+
artifact_body: &[u8],
84+
) -> Result<HmacSha256, SignatureError> {
8785
let mut mac = HmacSha256::new_from_slice(&self.secret_key()?)?;
88-
let metadata = self.construct_metadata(hash)?;
89-
90-
mac.update(&metadata);
86+
update_message_field(&mut mac, SIGNATURE_MESSAGE_PREFIX);
87+
update_message_field(&mut mac, hash);
88+
update_message_field(&mut mac, &self.team_id);
89+
update_message_field(&mut mac, artifact_body);
9190

9291
Ok(mac)
9392
}
@@ -98,9 +97,7 @@ impl ArtifactSignatureAuthenticator {
9897
hash: &[u8],
9998
artifact_body: &[u8],
10099
) -> Result<Vec<u8>, SignatureError> {
101-
let mut mac = self.get_tag_generator(hash)?;
102-
103-
mac.update(artifact_body);
100+
let mac = self.get_tag_generator(hash, artifact_body)?;
104101
let hmac_output = mac.finalize();
105102
Ok(hmac_output.into_bytes().to_vec())
106103
}
@@ -111,9 +108,7 @@ impl ArtifactSignatureAuthenticator {
111108
hash: &[u8],
112109
artifact_body: &[u8],
113110
) -> Result<String, SignatureError> {
114-
let mut hmac_ctx = self.get_tag_generator(hash)?;
115-
116-
hmac_ctx.update(artifact_body);
111+
let hmac_ctx = self.get_tag_generator(hash, artifact_body)?;
117112
let hmac_output = hmac_ctx.finalize();
118113
Ok(BASE64_STANDARD.encode(hmac_output.into_bytes()))
119114
}
@@ -125,16 +120,18 @@ impl ArtifactSignatureAuthenticator {
125120
artifact_body: &[u8],
126121
expected_tag: &str,
127122
) -> Result<bool, SignatureError> {
128-
let mut mac = HmacSha256::new_from_slice(&self.secret_key()?)?;
129-
let message = self.construct_metadata(hash)?;
130-
mac.update(&message);
131-
mac.update(artifact_body);
123+
let mac = self.get_tag_generator(hash, artifact_body)?;
132124

133125
let expected_bytes = BASE64_STANDARD.decode(expected_tag)?;
134126
Ok(mac.verify_slice(&expected_bytes).is_ok())
135127
}
136128
}
137129

130+
fn update_message_field(mac: &mut HmacSha256, field: &[u8]) {
131+
mac.update(&(field.len() as u64).to_le_bytes());
132+
mac.update(field);
133+
}
134+
138135
#[cfg(test)]
139136
mod tests {
140137
use anyhow::Result;
@@ -148,10 +145,7 @@ mod tests {
148145
artifact_body: &[u8],
149146
expected_tag: &[u8],
150147
) -> Result<bool, SignatureError> {
151-
let mut mac = HmacSha256::new_from_slice(&self.secret_key()?)?;
152-
let message = self.construct_metadata(hash)?;
153-
mac.update(&message);
154-
mac.update(artifact_body);
148+
let mac = self.get_tag_generator(hash, artifact_body)?;
155149

156150
Ok(mac.verify_slice(expected_tag).is_ok())
157151
}
@@ -354,4 +348,25 @@ mod tests {
354348
let tag = auth.generate_tag(b"hash", b"body").unwrap();
355349
assert!(auth.validate(b"hash", b"body", &tag).unwrap());
356350
}
351+
352+
#[test]
353+
fn test_signature_fields_are_separated() {
354+
let secret_key = b"shared signing key".to_vec();
355+
let attacker = ArtifactSignatureAuthenticator {
356+
team_id: b"team_abc".to_vec(),
357+
secret_key_override: Some(secret_key.clone()),
358+
};
359+
let victim = ArtifactSignatureAuthenticator {
360+
team_id: b"team_ab".to_vec(),
361+
secret_key_override: Some(secret_key),
362+
};
363+
364+
let tag = attacker.generate_tag(b"0123456789abcdef", b"body").unwrap();
365+
366+
assert!(
367+
!victim
368+
.validate(b"0123456789abcdef", b"cbody", &tag)
369+
.unwrap()
370+
);
371+
}
357372
}

0 commit comments

Comments
 (0)