Skip to content

Commit d968f99

Browse files
committed
net.http.signature: pad ECDSA private scalar to curve width when parsing PEM
1 parent b79a162 commit d968f99

2 files changed

Lines changed: 60 additions & 2 deletions

File tree

vlib/net/http/signature/key.v

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,17 +261,21 @@ fn ecdsa_key_from_xy_d(xy []u8, d []u8, is_priv bool) !Key {
261261
coord := (xy.len - 1) / 2
262262
x := xy[1..1 + coord]
263263
y := xy[1 + coord..1 + coord * 2]
264+
// `ecdsa.PrivateKey.bytes()` returns the scalar in minimal-length form
265+
// (no leading zeros), so we pad it to the curve byte size; the wire
266+
// layout that `ecdsa_sign` expects is fixed-width.
267+
priv_d := if is_priv { pad_left(d, coord)! } else { d }
264268
return match coord {
265269
32 {
266270
if is_priv {
267-
Key.ecdsa_p256_private(x, y, d)
271+
Key.ecdsa_p256_private(x, y, priv_d)
268272
} else {
269273
Key.ecdsa_p256_public(x, y)
270274
}
271275
}
272276
48 {
273277
if is_priv {
274-
Key.ecdsa_p384_private(x, y, d)
278+
Key.ecdsa_p384_private(x, y, priv_d)
275279
} else {
276280
Key.ecdsa_p384_public(x, y)
277281
}
@@ -283,3 +287,19 @@ fn ecdsa_key_from_xy_d(xy []u8, d []u8, is_priv bool) !Key {
283287
}
284288
}
285289
}
290+
291+
fn pad_left(b []u8, width int) ![]u8 {
292+
if b.len == width {
293+
return b
294+
}
295+
if b.len > width {
296+
return MalformedMessage{
297+
reason: 'ECDSA scalar wider (${b.len}) than curve coordinate size (${width})'
298+
}
299+
}
300+
mut out := []u8{len: width}
301+
for i, v in b {
302+
out[width - b.len + i] = v
303+
}
304+
return out
305+
}

vlib/net/http/signature/key_test.v

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@ AwEHoUQDQgAEqIVYZVLCrPZHGHjP17CTW0/+D9Lfw0EkjqF7xB4FivAxzic30tMM
2323
4GF+hR6Dxh71Z50VGGdldkkDXZCnTNnoXQ==
2424
-----END EC PRIVATE KEY-----'
2525

26+
// P-256 key whose private scalar starts with 0x00, so OpenSSL's
27+
// `BN_bn2binpad(num_bytes)` returns 31 bytes instead of 32. This
28+
// exercises the leading-zero padding in `ecdsa_key_from_xy_d`.
29+
const short_d_p256_private_pem = '-----BEGIN EC PRIVATE KEY-----
30+
MHcCAQEEIACZmEw0q8iipb0amaNiobX/wwn6PoIKUatErMY2Dd4+oAoGCCqGSM49
31+
AwEHoUQDQgAE/z/OBheMT6mCKDapfETr56tkYLOrnQh+ZL293+IqXsJ+iMZgYe0/
32+
WHaZhZfCu1OKUWayaVEkvb7j0o3uUfw+OQ==
33+
-----END EC PRIVATE KEY-----'
34+
2635
fn key_test_b26_components() Components {
2736
return Components{
2837
method: 'POST'
@@ -103,6 +112,35 @@ fn test_from_pem_ecdsa_p256_public_verifies_rfc_b24_reference() {
103112
'sig-b24', pub_key)!
104113
}
105114

115+
fn test_from_pem_ecdsa_p256_pads_short_private_scalar() {
116+
// Regression: a P-256 PEM whose `d` has a leading zero byte must
117+
// still produce a 96-byte (x||y||d) key and sign successfully.
118+
priv := Key.from_pem(short_d_p256_private_pem)!
119+
assert priv.algorithm == .ecdsa_p256_sha256
120+
assert priv.is_private
121+
assert priv.bytes.len == 96
122+
c := Components{
123+
method: 'POST'
124+
target_uri: 'https://example.com/'
125+
}
126+
p := SignatureParams{
127+
components: ['@method', '@target-uri']
128+
created: 1
129+
}
130+
out := sign(c, p, priv, 'sig1')!
131+
verify(c, out.signature_input, out.signature, 'sig1', priv)!
132+
}
133+
134+
fn test_pad_left_pads_to_width_and_rejects_overflow() {
135+
assert pad_left([u8(0x01)], 4)! == [u8(0x00), 0x00, 0x00, 0x01]
136+
assert pad_left([u8(0x01), 0x02, 0x03, 0x04], 4)! == [u8(0x01), 0x02, 0x03, 0x04]
137+
if _ := pad_left([u8(0x01), 0x02, 0x03, 0x04, 0x05], 4) {
138+
assert false, 'must reject scalars wider than the curve'
139+
} else {
140+
assert err is MalformedMessage
141+
}
142+
}
143+
106144
fn test_from_pem_rejects_garbage() {
107145
if _ := Key.from_pem('not a PEM block') {
108146
assert false, 'must reject non-PEM input'

0 commit comments

Comments
 (0)