Skip to content

Commit ba19ebd

Browse files
authored
Merge branch 'main' into penpal-dos-bounds
2 parents ad6a98e + c025a91 commit ba19ebd

20 files changed

Lines changed: 437 additions & 25 deletions

File tree

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
name: security-review
2+
3+
on:
4+
pull_request_target:
5+
branches: ["*"]
6+
concurrency:
7+
group: ${{ github.workflow }}-${{ github.ref_name }}
8+
cancel-in-progress: true
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
authorize:
14+
runs-on: ubuntu-latest
15+
outputs:
16+
approval-env: ${{ steps.authorization.outputs.approval-env }}
17+
steps:
18+
- uses: actions/checkout@v4
19+
- name: Check authorization
20+
id: authorization
21+
uses: ./.github/actions/check-authorization
22+
23+
execute:
24+
needs: authorize
25+
runs-on: ubuntu-latest
26+
environment: ${{ needs.authorize.outputs.approval-env }}
27+
permissions:
28+
id-token: write
29+
contents: read
30+
statuses: write
31+
steps:
32+
- uses: actions/checkout@v4
33+
with:
34+
ref: ${{ github.event.pull_request.head.sha }}
35+
36+
- name: Get AWS credentials
37+
uses: aws-actions/configure-aws-credentials@v4
38+
with:
39+
role-to-assume: arn:aws:iam::547182295936:role/SecurityReview-GitHubOIDCRole #TODO: migrate to production account
40+
role-session-name: ${{ github.run_id }}-${{ github.run_attempt }}
41+
aws-region: us-west-2
42+
43+
- name: Start CodeBuild and wait for completion
44+
id: codebuild
45+
shell: bash
46+
env:
47+
PROJECT_NAME: SecurityReview-${{ github.event.repository.name }}
48+
SOURCE_VERSION: "pr/${{ github.event.pull_request.number }}"
49+
PR_NUMBER: ${{ github.event.pull_request.number }}
50+
run: |
51+
# Start the build
52+
BUILD_ID=$(aws codebuild start-build \
53+
--project-name "${PROJECT_NAME}" \
54+
--source-version "${SOURCE_VERSION}" \
55+
--environment-variables-override "name=PR_NUMBER,value=${PR_NUMBER},type=PLAINTEXT" \
56+
--query 'build.id' --output text)
57+
58+
# Wait for completion
59+
while STATUS=$(aws codebuild batch-get-builds --ids "${BUILD_ID}" --query 'builds[0].buildStatus' --output text); [[ "$STATUS" == "IN_PROGRESS" ]]; do
60+
sleep 30
61+
done
62+
63+
if [[ "$STATUS" != "SUCCEEDED" ]]; then
64+
echo "blocking=skip" >> "$GITHUB_OUTPUT"
65+
exit 1
66+
fi
67+
68+
REVIEW_STATUS=$(aws codebuild batch-get-builds --ids "${BUILD_ID}" --query 'builds[0].exportedEnvironmentVariables[?name==`REVIEW_STATUS`].value' --output text)
69+
echo "blocking=$([[ "$REVIEW_STATUS" == "FAIL" ]] && echo true || echo false)" >> "$GITHUB_OUTPUT"
70+
71+
- name: Update commit status
72+
if: always() && steps.codebuild.outputs.blocking != 'skip'
73+
shell: bash
74+
env:
75+
GH_TOKEN: ${{ github.token }}
76+
STATUS_URL: ${{ github.api_url }}/repos/${{ github.repository }}/statuses/${{ github.event.pull_request.head.sha }}
77+
REPORT_URL: https://d28bfvmis1skm5.cloudfront.net/${{ github.event.repository.name }}/pr-${{ github.event.pull_request.number }}/${{ github.event.pull_request.head.sha }}.html
78+
BLOCKING: ${{ steps.codebuild.outputs.blocking }}
79+
run: |
80+
STATE=$([[ "$BLOCKING" == "true" ]] && echo "failure" || echo "success")
81+
curl -sS -X POST \
82+
-H "Authorization: token ${GH_TOKEN}" \
83+
"${STATUS_URL}" \
84+
-d "{\"state\":\"${STATE}\",\"context\":\"security-review / report\",\"target_url\":\"${REPORT_URL}\"}"

.github/workflows/zig.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ on:
77
concurrency:
88
group: ${{ github.workflow }}-${{ github.ref_name }}
99
cancel-in-progress: true
10+
11+
permissions:
12+
contents: read
13+
1014
jobs:
1115
zig:
1216
if: github.repository_owner == 'aws'

crypto/asn1/a_mbstr.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ OPENSSL_DECLARE_ERROR_REASON(ASN1, INVALID_UTF8STRING)
3333
int ASN1_mbstring_ncopy(ASN1_STRING **out, const unsigned char *in,
3434
ossl_ssize_t len, int inform, unsigned long mask,
3535
ossl_ssize_t minsize, ossl_ssize_t maxsize) {
36+
if (len < -1) {
37+
OPENSSL_PUT_ERROR(ASN1, ASN1_R_ILLEGAL_FORMAT);
38+
return -1;
39+
}
3640
if (len == -1) {
3741
len = strlen((const char *)in);
3842
}

crypto/asn1/asn1_test.cc

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1711,6 +1711,25 @@ TEST(ASN1Test, MBString) {
17111711
ERR_clear_error();
17121712
EXPECT_EQ(nullptr, str);
17131713
}
1714+
1715+
// |len| values below -1 must be rejected; only -1 is special-cased to mean
1716+
// "call strlen on |in|". Any other negative value would otherwise be cast
1717+
// to a huge |size_t| by |CBS_init|.
1718+
static const uint8_t kDummy[] = {'a'};
1719+
ASN1_STRING *str = nullptr;
1720+
EXPECT_EQ(-1, ASN1_mbstring_ncopy(&str, kDummy, -2, MBSTRING_UTF8,
1721+
B_ASN1_UTF8STRING, /*minsize=*/0,
1722+
/*maxsize=*/0));
1723+
EXPECT_EQ(nullptr, str);
1724+
ERR_clear_error();
1725+
1726+
// |len == -1| treats |in| as NUL-terminated and should succeed.
1727+
ASN1_STRING *str_from_strlen = nullptr;
1728+
EXPECT_GE(ASN1_mbstring_ncopy(&str_from_strlen, (const uint8_t *)"a", -1,
1729+
MBSTRING_UTF8, B_ASN1_UTF8STRING,
1730+
/*minsize=*/0, /*maxsize=*/0),
1731+
0);
1732+
ASN1_STRING_free(str_from_strlen);
17141733
}
17151734

17161735
TEST(ASN1Test, StringByNID) {

crypto/bio/bio_addr.c

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,12 @@ int BIO_ADDR_rawmake(BIO_ADDR *ap, int family, const void *where,
110110
GUARD_PTR(where);
111111
// wherelen is expected to be the length of the path string
112112
// not including the terminating NUL.
113-
if (wherelen + 1 > sizeof(ap->s_un.sun_path)) {
113+
if (wherelen >= sizeof(ap->s_un.sun_path)) {
114114
return 0;
115115
}
116116
OPENSSL_cleanse(&ap->s_un, sizeof(ap->s_un));
117117
ap->s_un.sun_family = family;
118-
OPENSSL_strlcpy(ap->s_un.sun_path, where, wherelen);
118+
OPENSSL_memcpy(ap->s_un.sun_path, where, wherelen);
119119
return 1;
120120
}
121121
#endif
@@ -151,7 +151,10 @@ int BIO_ADDR_rawaddress(const BIO_ADDR *ap, void *p, size_t *l) {
151151
return 0;
152152
}
153153
if (l != NULL) {
154-
*l = len;
154+
// AF_UNIX includes the trailing NUL written below so |*l| matches the
155+
// bytes written to |p| and reflects sockaddr_un's NUL-terminated path.
156+
// OpenSSL does not write this NUL; aws-lc intentionally does.
157+
*l = (ap->sa.sa_family == AF_UNIX) ? len + 1 : len;
155158
}
156159

157160
if (p != NULL) {

crypto/bio/bio_socket_test.cc

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ struct SockaddrStorage {
130130
if (1 !=
131131
BIO_ADDR_rawmake(
132132
bap, AF_UNIX, &sock->sun_path,
133-
OPENSSL_strnlen(sock->sun_path, sizeof(sock->sun_path)) + 1,
133+
OPENSSL_strnlen(sock->sun_path, sizeof(sock->sun_path)),
134134
0)) {
135135
BIO_ADDR_free(bap);
136136
bap = nullptr;
@@ -963,6 +963,24 @@ TEST_F(BIOAddrTest, CopySpecifiedAddressUnix) {
963963
EXPECT_EQ(BIO_ADDR_copy(addr1, addr2), 1);
964964
EXPECT_EQ(BIO_ADDR_cmp(addr1, addr2), 0);
965965
}
966+
967+
// |BIO_ADDR_rawaddress| writes a NUL terminator after the AF_UNIX path. The
968+
// reported |*l| must include that NUL so that callers using the standard
969+
// probe-then-allocate-exactly-|*l| pattern allocate a buffer large enough to
970+
// hold both the path and the terminator.
971+
TEST_F(BIOAddrTest, RawAddressUnixLengthIncludesNul) {
972+
const char *path = "/tmp/test.sock";
973+
const size_t path_len = strlen(path);
974+
ASSERT_EQ(BIO_ADDR_rawmake(addr1, AF_UNIX, path, path_len, 0), 1);
975+
976+
size_t len = 0;
977+
ASSERT_EQ(BIO_ADDR_rawaddress(addr1, nullptr, &len), 1);
978+
EXPECT_EQ(len, path_len + 1);
979+
980+
char buf[sizeof(sockaddr_un::sun_path)] = {};
981+
ASSERT_EQ(BIO_ADDR_rawaddress(addr1, buf, &len), 1);
982+
EXPECT_STREQ(buf, path);
983+
}
966984
#endif
967985

968986
// Tests for BIO_ADDR_dup

crypto/cipher_extra/cipher_test.cc

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include <openssl/sha.h>
2121
#include <openssl/span.h>
2222

23+
#include "../fipsmodule/cipher/internal.h"
2324
#include "../internal.h"
2425
#include "../test/file_test.h"
2526
#include "../test/test_util.h"
@@ -1406,6 +1407,106 @@ TEST(CipherTest, Empty_EVP_CIPHER_CTX_V1187459157) {
14061407
CHECK_ERROR(EVP_DecryptFinal(ctx.get(), out_vec.data(), &out_len), ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED);
14071408
}
14081409

1410+
// Mock cipher whose |EVP_CTRL_INIT| handler always fails. It's needed to
1411+
// exercise the error paths in |EVP_CipherInit_ex| and |EVP_CIPHER_CTX_copy|.
1412+
// In addition, the mock needs |ctx_size| != 0, otherwise |cipher_data| is
1413+
// not allocated before the ctrl callback runs. This is needed below for
1414+
// InitErrorPathReleasesCipherData, to test |cipher_data| is free'd.
1415+
static int mock_failing_init(EVP_CIPHER_CTX *, const uint8_t *, const uint8_t *,
1416+
int) {
1417+
return 1;
1418+
}
1419+
1420+
static int mock_failing_cipher(EVP_CIPHER_CTX *, uint8_t *, const uint8_t *,
1421+
size_t) {
1422+
return 1;
1423+
}
1424+
1425+
static int mock_failing_ctrl(EVP_CIPHER_CTX *, int type, int, void *) {
1426+
if (type == EVP_CTRL_INIT) {
1427+
return 0;
1428+
}
1429+
return -1;
1430+
}
1431+
1432+
static int mock_failing_copy_ctrl(EVP_CIPHER_CTX *, int type, int, void *) {
1433+
if (type == EVP_CTRL_INIT) {
1434+
return 1;
1435+
}
1436+
if (type == EVP_CTRL_COPY) {
1437+
return 0;
1438+
}
1439+
return -1;
1440+
}
1441+
1442+
static const EVP_CIPHER kFailingInitCipher = {
1443+
NID_undef,
1444+
1,
1445+
16,
1446+
0,
1447+
128,
1448+
EVP_CIPH_STREAM_CIPHER | EVP_CIPH_CTRL_INIT,
1449+
mock_failing_init,
1450+
mock_failing_cipher,
1451+
nullptr,
1452+
mock_failing_ctrl,
1453+
};
1454+
1455+
static const EVP_CIPHER kFailingCopyCipher = {
1456+
NID_undef,
1457+
1,
1458+
16,
1459+
0,
1460+
128,
1461+
EVP_CIPH_STREAM_CIPHER | EVP_CIPH_CTRL_INIT | EVP_CIPH_CUSTOM_COPY,
1462+
mock_failing_init,
1463+
mock_failing_cipher,
1464+
nullptr,
1465+
mock_failing_copy_ctrl,
1466+
};
1467+
1468+
// On |EVP_CipherInit_ex| error path at |EVP_CTRL_INIT|, the context must not be
1469+
// left with an orphaned |cipher_data|. Otherwise a subsequent
1470+
// |EVP_CipherInit_ex| call on the same context would overwrite and leak it. Not
1471+
// exactly how one would probably use this API, but who knows.
1472+
TEST(CipherTest, InitErrorPathReleasesCipherData) {
1473+
bssl::UniquePtr<EVP_CIPHER_CTX> ctx(EVP_CIPHER_CTX_new());
1474+
ASSERT_TRUE(ctx);
1475+
1476+
std::vector<uint8_t> key(16, 0);
1477+
EXPECT_FALSE(EVP_CipherInit_ex(ctx.get(), &kFailingInitCipher, nullptr,
1478+
key.data(), nullptr, 1));
1479+
EXPECT_EQ(ctx->cipher, nullptr);
1480+
EXPECT_EQ(ctx->cipher_data, nullptr);
1481+
1482+
// A follow-up init with a real cipher must still work and must not depend on
1483+
// leaked state from the failed attempt.
1484+
EXPECT_TRUE(EVP_CipherInit_ex(ctx.get(), EVP_aes_128_ecb(), nullptr,
1485+
key.data(), nullptr, 1));
1486+
}
1487+
1488+
// |EVP_CIPHER_CTX_copy| shares the same shape of error path for
1489+
// |EVP_CTRL_COPY| failures as |EVP_CTRL_INIT| above. The destination's
1490+
// |cipher_data| must not be orphaned when the custom copy callback fails.
1491+
TEST(CipherTest, CopyErrorPathReleasesCipherData) {
1492+
bssl::UniquePtr<EVP_CIPHER_CTX> src(EVP_CIPHER_CTX_new());
1493+
ASSERT_TRUE(src);
1494+
std::vector<uint8_t> key(16, 0);
1495+
ASSERT_TRUE(EVP_CipherInit_ex(src.get(), &kFailingCopyCipher, nullptr,
1496+
key.data(), nullptr, 1));
1497+
1498+
bssl::UniquePtr<EVP_CIPHER_CTX> dst(EVP_CIPHER_CTX_new());
1499+
ASSERT_TRUE(dst);
1500+
EXPECT_FALSE(EVP_CIPHER_CTX_copy(dst.get(), src.get()));
1501+
EXPECT_EQ(dst->cipher, nullptr);
1502+
EXPECT_EQ(dst->cipher_data, nullptr);
1503+
1504+
// A follow-up init with a real cipher on |dst| must still work and must not
1505+
// depend on leaked state from the failed copy.
1506+
EXPECT_TRUE(EVP_CipherInit_ex(dst.get(), EVP_aes_128_ecb(), nullptr,
1507+
key.data(), nullptr, 1));
1508+
}
1509+
14091510
struct CipherInfo {
14101511
const char *name;
14111512
const EVP_CIPHER *(*func)(void);

crypto/evp_extra/evp_asn1.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,12 @@ EVP_PKEY *EVP_parse_private_key(CBS *cbs) {
172172
has_pub = 1;
173173
}
174174

175+
// Reject trailing data within the SEQUENCE.
176+
if (CBS_len(&pkcs8) != 0) {
177+
OPENSSL_PUT_ERROR(EVP, EVP_R_DECODE_ERROR);
178+
return NULL;
179+
}
180+
175181
// Set up an |EVP_PKEY| of the appropriate type.
176182
EVP_PKEY *ret = EVP_PKEY_new();
177183
if (ret == NULL) {

crypto/evp_extra/evp_extra_test.cc

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1683,6 +1683,30 @@ TEST(EVPExtraTest, Ed25519) {
16831683
ERR_clear_error();
16841684
}
16851685

1686+
// EVP_parse_private_key must reject trailing data inside the PrivateKeyInfo
1687+
// SEQUENCE.
1688+
TEST(EVPExtraTest, ParsePrivateKeyRejectsTrailingData) {
1689+
// Ed25519 PKCS#8 (the kPrivateKeyPKCS8 vector from EVPExtraTest.Ed25519)
1690+
// with two extra bytes (ASN.1 NULL: 05 00) appended inside the outer
1691+
// SEQUENCE. The SEQUENCE length byte is bumped 0x2e -> 0x30 to cover
1692+
// the extra bytes, so the outer TLV is itself well-formed.
1693+
static const uint8_t kDERWithTrailing[] = {
1694+
0x30, 0x30, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70,
1695+
0x04, 0x22, 0x04, 0x20, 0x9d, 0x61, 0xb1, 0x9d, 0xef, 0xfd, 0x5a, 0x60,
1696+
0xba, 0x84, 0x4a, 0xf4, 0x92, 0xec, 0x2c, 0xc4, 0x44, 0x49, 0xc5, 0x69,
1697+
0x7b, 0x32, 0x69, 0x19, 0x70, 0x3b, 0xac, 0x03, 0x1c, 0xae, 0x7f, 0x60,
1698+
0x05, 0x00,
1699+
};
1700+
1701+
CBS cbs;
1702+
CBS_init(&cbs, kDERWithTrailing, sizeof(kDERWithTrailing));
1703+
ERR_clear_error();
1704+
bssl::UniquePtr<EVP_PKEY> parsed(EVP_parse_private_key(&cbs));
1705+
EXPECT_FALSE(parsed);
1706+
EXPECT_TRUE(ErrorEquals(ERR_get_error(), ERR_LIB_EVP, EVP_R_DECODE_ERROR));
1707+
ERR_clear_error();
1708+
}
1709+
16861710
// EVP_PKEY_get_private_seed returns an error for key types that don't
16871711
// provide a get_priv_seed method. It must also reject NULL |key| regardless of
16881712
// key type.

crypto/evp_extra/p_kem_asn1.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,14 @@ static int kem_get_pub_raw(const EVP_PKEY *pkey, uint8_t *out,
8686
return 1;
8787
}
8888

89+
// kem_cmp_parameters returns 1 if |a| and |b| hold populated KEM keys with
90+
// the same KEM NID, 0 if their NIDs differ, or -2 if either operand is
91+
// missing its key or parameters. The tri-state return aligns with the
92+
// |EVP_PKEY_cmp| convention (1 = equal, 0 = not equal, negative = error).
8993
static int kem_cmp_parameters(const EVP_PKEY *a, const EVP_PKEY *b) {
94+
if (a == NULL || b == NULL) {
95+
return -2;
96+
}
9097
const KEM_KEY *a_key = a->pkey.kem_key;
9198
const KEM_KEY *b_key = b->pkey.kem_key;
9299
if (a_key == NULL || b_key == NULL) {

0 commit comments

Comments
 (0)