Skip to content

Commit 227ff6e

Browse files
davidbenBoringssl LUCI CQ
authored and
Boringssl LUCI CQ
committed
Remove unions in EC_SCALAR and EC_FELEM.
When introducing EC_SCALAR and EC_FELEM, I used unions as convenience for converting to and from the byte representation. However, type-punning with unions is not allowed in C++ and hard to use correctly in C. As I understand the rules, they are: - The abstract machine knows what member of union was last written to. - In C, reading from an inactive member is defined to type-pun. In C++, it is UB though some compilers promise the C behavior anyway. - However, if you read or write from a *pointer* to a union member, the strict aliasing rule applies. (A function passed two pointers of different types otherwise needs to pessimally assume they came from the same union.) That last rule means the type-punning allowance doesn't apply if you take a pointer to an inactive member, and it's common to abstract otherwise direct accesses of members via pointers. openssl/openssl#18225 is an example where similar union tricks have caused problems for OpenSSL. While we don't have that code, EC_SCALAR and EC_FELEM play similar tricks. We do get a second lifeline because our alternate view is a uint8_t, which we require to be unsigned char. Strict aliasing always allows the pointer type to be a character type, so pointer-indirected accesses of EC_SCALAR.bytes aren't necessarily UB. But if we ever write to EC_SCALAR.bytes directly (and we do), we'll switch the active arm and then pointers to EC_SCALAR.words become strict aliasing violations! This is all far too complicated to deal with. Ideally everyone would build with -fno-strict-aliasing because no real C code actually follows these rules. But we don't always control our downstream consumers' CFLAGS, so let's just avoid the union. This also avoids a pitfall if we ever move libcrypto to C++. For p224-64.c, I just converted the representations directly, which avoids worrying about the top 32 bits in p224_felem_to_generic. Most of the rest was words vs. bytes conversions and boils down to a cast (we're still dealing with a character type, at the end of the day). But I took the opportunity to extract some more "words"-based helper functions out of BIGNUM, so the casts would only be in one place. That too saves us from the top bits problem in the bytes-to-words direction. Bug: 301 Change-Id: I3285a86441daaf824a4f6862e825d463a669efdb Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/52505 Commit-Queue: Bob Beck <[email protected]> Reviewed-by: Bob Beck <[email protected]>
1 parent 3f180b8 commit 227ff6e

13 files changed

+219
-201
lines changed

crypto/fipsmodule/bn/bytes.c

+76-60
Original file line numberDiff line numberDiff line change
@@ -61,32 +61,48 @@
6161

6262
#include "internal.h"
6363

64+
void bn_big_endian_to_words(BN_ULONG *out, size_t out_len, const uint8_t *in,
65+
size_t in_len) {
66+
for (size_t i = 0; i < out_len; i++) {
67+
if (in_len < sizeof(BN_ULONG)) {
68+
// Load the last partial word.
69+
BN_ULONG word = 0;
70+
for (size_t j = 0; j < in_len; j++) {
71+
word = (word << 8) | in[j];
72+
}
73+
in_len = 0;
74+
out[i] = word;
75+
// Fill the remainder with zeros.
76+
OPENSSL_memset(out + i + 1, 0, (out_len - i - 1) * sizeof(BN_ULONG));
77+
break;
78+
}
6479

65-
BIGNUM *BN_bin2bn(const uint8_t *in, size_t len, BIGNUM *ret) {
66-
size_t num_words;
67-
unsigned m;
68-
BN_ULONG word = 0;
69-
BIGNUM *bn = NULL;
70-
71-
if (ret == NULL) {
72-
ret = bn = BN_new();
80+
in_len -= sizeof(BN_ULONG);
81+
out[i] = CRYPTO_load_word_be(in + in_len);
7382
}
7483

84+
// The caller should have sized the output to avoid truncation.
85+
assert(in_len == 0);
86+
}
87+
88+
BIGNUM *BN_bin2bn(const uint8_t *in, size_t len, BIGNUM *ret) {
89+
BIGNUM *bn = NULL;
7590
if (ret == NULL) {
76-
return NULL;
91+
bn = BN_new();
92+
if (bn == NULL) {
93+
return NULL;
94+
}
95+
ret = bn;
7796
}
7897

7998
if (len == 0) {
8099
ret->width = 0;
81100
return ret;
82101
}
83102

84-
num_words = ((len - 1) / BN_BYTES) + 1;
85-
m = (len - 1) % BN_BYTES;
103+
size_t num_words = ((len - 1) / BN_BYTES) + 1;
86104
if (!bn_wexpand(ret, num_words)) {
87-
if (bn) {
88-
BN_free(bn);
89-
}
105+
BN_free(bn);
90106
return NULL;
91107
}
92108

@@ -96,29 +112,20 @@ BIGNUM *BN_bin2bn(const uint8_t *in, size_t len, BIGNUM *ret) {
96112
ret->width = (int)num_words;
97113
ret->neg = 0;
98114

99-
while (len--) {
100-
word = (word << 8) | *(in++);
101-
if (m-- == 0) {
102-
ret->d[--num_words] = word;
103-
word = 0;
104-
m = BN_BYTES - 1;
105-
}
106-
}
107-
115+
bn_big_endian_to_words(ret->d, ret->width, in, len);
108116
return ret;
109117
}
110118

111119
BIGNUM *BN_le2bn(const uint8_t *in, size_t len, BIGNUM *ret) {
112120
BIGNUM *bn = NULL;
113121
if (ret == NULL) {
114122
bn = BN_new();
123+
if (bn == NULL) {
124+
return NULL;
125+
}
115126
ret = bn;
116127
}
117128

118-
if (ret == NULL) {
119-
return NULL;
120-
}
121-
122129
if (len == 0) {
123130
ret->width = 0;
124131
ret->neg = 0;
@@ -142,61 +149,70 @@ BIGNUM *BN_le2bn(const uint8_t *in, size_t len, BIGNUM *ret) {
142149
return ret;
143150
}
144151

145-
size_t BN_bn2bin(const BIGNUM *in, uint8_t *out) {
146-
size_t n, i;
147-
BN_ULONG l;
148-
149-
n = i = BN_num_bytes(in);
150-
while (i--) {
151-
l = in->d[i / BN_BYTES];
152-
*(out++) = (unsigned char)(l >> (8 * (i % BN_BYTES))) & 0xff;
153-
}
154-
return n;
155-
}
156-
157-
static int fits_in_bytes(const uint8_t *bytes, size_t num_bytes, size_t len) {
152+
// fits_in_bytes returns one if the |num_words| words in |words| can be
153+
// represented in |num_bytes| bytes.
154+
static int fits_in_bytes(const BN_ULONG *words, size_t num_words,
155+
size_t num_bytes) {
156+
const uint8_t *bytes = (const uint8_t *)words;
157+
size_t tot_bytes = num_words * sizeof(BN_ULONG);
158158
uint8_t mask = 0;
159-
for (size_t i = len; i < num_bytes; i++) {
159+
for (size_t i = num_bytes; i < tot_bytes; i++) {
160160
mask |= bytes[i];
161161
}
162162
return mask == 0;
163163
}
164164

165+
void bn_words_to_big_endian(uint8_t *out, size_t out_len, const BN_ULONG *in,
166+
size_t in_len) {
167+
// The caller should have selected an output length without truncation.
168+
assert(fits_in_bytes(in, in_len, out_len));
169+
170+
// We only support little-endian platforms, so the internal representation is
171+
// also little-endian as bytes. We can simply copy it in reverse.
172+
const uint8_t *bytes = (const uint8_t *)in;
173+
size_t num_bytes = in_len * sizeof(BN_ULONG);
174+
if (out_len < num_bytes) {
175+
num_bytes = out_len;
176+
}
177+
178+
for (size_t i = 0; i < num_bytes; i++) {
179+
out[out_len - i - 1] = bytes[i];
180+
}
181+
// Pad out the rest of the buffer with zeroes.
182+
OPENSSL_memset(out, 0, out_len - num_bytes);
183+
}
184+
185+
size_t BN_bn2bin(const BIGNUM *in, uint8_t *out) {
186+
size_t n = BN_num_bytes(in);
187+
bn_words_to_big_endian(out, n, in->d, in->width);
188+
return n;
189+
}
190+
165191
int BN_bn2le_padded(uint8_t *out, size_t len, const BIGNUM *in) {
192+
if (!fits_in_bytes(in->d, in->width, len)) {
193+
return 0;
194+
}
195+
196+
// We only support little-endian platforms, so we can simply memcpy into the
197+
// internal representation.
166198
const uint8_t *bytes = (const uint8_t *)in->d;
167199
size_t num_bytes = in->width * BN_BYTES;
168200
if (len < num_bytes) {
169-
if (!fits_in_bytes(bytes, num_bytes, len)) {
170-
return 0;
171-
}
172201
num_bytes = len;
173202
}
174203

175-
// We only support little-endian platforms, so we can simply memcpy into the
176-
// internal representation.
177204
OPENSSL_memcpy(out, bytes, num_bytes);
178205
// Pad out the rest of the buffer with zeroes.
179206
OPENSSL_memset(out + num_bytes, 0, len - num_bytes);
180207
return 1;
181208
}
182209

183210
int BN_bn2bin_padded(uint8_t *out, size_t len, const BIGNUM *in) {
184-
const uint8_t *bytes = (const uint8_t *)in->d;
185-
size_t num_bytes = in->width * BN_BYTES;
186-
if (len < num_bytes) {
187-
if (!fits_in_bytes(bytes, num_bytes, len)) {
188-
return 0;
189-
}
190-
num_bytes = len;
211+
if (!fits_in_bytes(in->d, in->width, len)) {
212+
return 0;
191213
}
192214

193-
// We only support little-endian platforms, so we can simply write the buffer
194-
// in reverse.
195-
for (size_t i = 0; i < num_bytes; i++) {
196-
out[len - i - 1] = bytes[i];
197-
}
198-
// Pad out the rest of the buffer with zeroes.
199-
OPENSSL_memset(out, 0, len - num_bytes);
215+
bn_words_to_big_endian(out, len, in->d, in->width);
200216
return 1;
201217
}
202218

crypto/fipsmodule/bn/internal.h

+19
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,25 @@ void bn_mod_inverse0_prime_mont_small(BN_ULONG *r, const BN_ULONG *a,
708708
size_t num, const BN_MONT_CTX *mont);
709709

710710

711+
// Word-based byte conversion functions.
712+
713+
// bn_big_endian_to_words interprets |in_len| bytes from |in| as a big-endian,
714+
// unsigned integer and writes the result to |out_len| words in |out|. |out_len|
715+
// must be large enough to represent any |in_len|-byte value. That is, |out_len|
716+
// must be at least |BN_BYTES * in_len|.
717+
void bn_big_endian_to_words(BN_ULONG *out, size_t out_len, const uint8_t *in,
718+
size_t in_len);
719+
720+
// bn_words_to_big_endian represents |in_len| words from |in| as a big-endian,
721+
// unsigned integer in |out_len| bytes. It writes the result to |out|. |out_len|
722+
// must be large enough to represent |in| without truncation.
723+
//
724+
// Note |out_len| may be less than |BN_BYTES * in_len| if |in| is known to have
725+
// leading zeros.
726+
void bn_words_to_big_endian(uint8_t *out, size_t out_len, const BN_ULONG *in,
727+
size_t in_len);
728+
729+
711730
#if defined(__cplusplus)
712731
} // extern C
713732
#endif

crypto/fipsmodule/ec/ec.c

+11-17
Original file line numberDiff line numberDiff line change
@@ -1175,15 +1175,12 @@ int ec_get_x_coordinate_as_scalar(const EC_GROUP *group, EC_SCALAR *out,
11751175
return 0;
11761176
}
11771177

1178-
// For simplicity, in case of width mismatches between |group->field| and
1179-
// |group->order|, zero any untouched words in |out|.
1180-
OPENSSL_memset(out, 0, sizeof(EC_SCALAR));
1181-
for (size_t i = 0; i < len; i++) {
1182-
out->bytes[len - i - 1] = bytes[i];
1183-
}
1184-
1185-
// We must have p < 2×order, assuming p is not tiny (p >= 17). Thus rather we
1186-
// can reduce by performing at most one subtraction.
1178+
// The x-coordinate is bounded by p, but we need a scalar, bounded by the
1179+
// order. These may not have the same size. However, we must have p < 2×order,
1180+
// assuming p is not tiny (p >= 17).
1181+
//
1182+
// Thus |bytes| will fit in |order.width + 1| words, and we can reduce by
1183+
// performing at most one subtraction.
11871184
//
11881185
// Proof: We only work with prime order curves, so the number of points on
11891186
// the curve is the order. Thus Hasse's theorem gives:
@@ -1197,14 +1194,11 @@ int ec_get_x_coordinate_as_scalar(const EC_GROUP *group, EC_SCALAR *out,
11971194
//
11981195
// Additionally, one can manually check this property for built-in curves. It
11991196
// is enforced for legacy custom curves in |EC_GROUP_set_generator|.
1200-
1201-
// The above does not guarantee |group->field| is not one word larger than
1202-
// |group->order|, so read one extra carry word.
1203-
BN_ULONG tmp[EC_MAX_WORDS];
1204-
BN_ULONG carry =
1205-
group->order.width < EC_MAX_WORDS ? out->words[group->order.width] : 0;
1206-
bn_reduce_once_in_place(out->words, carry, group->order.d, tmp,
1207-
group->order.width);
1197+
const BIGNUM *order = &group->order;
1198+
BN_ULONG words[EC_MAX_WORDS + 1];
1199+
bn_big_endian_to_words(words, order->width + 1, bytes, len);
1200+
bn_reduce_once(out->words, words, /*carry=*/words[order->width], order->d,
1201+
order->width);
12081202
return 1;
12091203
}
12101204

crypto/fipsmodule/ec/internal.h

+2-6
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,7 @@ OPENSSL_STATIC_ASSERT(EC_MAX_WORDS <= BN_SMALL_MAX_WORDS,
100100
// An EC_SCALAR is an integer fully reduced modulo the order. Only the first
101101
// |order->width| words are used. An |EC_SCALAR| is specific to an |EC_GROUP|
102102
// and must not be mixed between groups.
103-
typedef union {
104-
// bytes is the representation of the scalar in little-endian order.
105-
uint8_t bytes[EC_MAX_BYTES];
103+
typedef struct {
106104
BN_ULONG words[EC_MAX_WORDS];
107105
} EC_SCALAR;
108106

@@ -192,9 +190,7 @@ void ec_scalar_select(const EC_GROUP *group, EC_SCALAR *out, BN_ULONG mask,
192190
// are used. An |EC_FELEM| is specific to an |EC_GROUP| and must not be mixed
193191
// between groups. Additionally, the representation (whether or not elements are
194192
// represented in Montgomery-form) may vary between |EC_METHOD|s.
195-
typedef union {
196-
// bytes is the representation of the field element in little-endian order.
197-
uint8_t bytes[EC_MAX_BYTES];
193+
typedef struct {
198194
BN_ULONG words[EC_MAX_WORDS];
199195
} EC_FELEM;
200196

0 commit comments

Comments
 (0)