curl: support embedding a CA bundle

Add the ability to embed a CA bundle into the curl binary. It is used
when no other runtime or build-time option set one.

This helps curl-for-win macOS and Linux builds to run standalone, and
also helps Windows builds to avoid picking up the CA bundle from an
arbitrary (possibly world-writable) location (though this behaviour is
not currently disablable).

Usage:
- cmake: `-DCURL_CA_EMBED=/path/to/curl-ca-bundle.crt`
- autotools: `--with-ca-embed=/path/to/curl-ca-bundle.crt`
- Makefile.mk: `CURL_CA_EMBED=/path/to/curl-ca-bundle.crt`

Also add new command-line option `--dump-ca-embed` to dump the embedded
CA bundle to standard output.

Closes #14059
This commit is contained in:
Viktor Szakats 2024-06-29 03:30:14 +02:00
parent 87aa4ebd82
commit 8a3740bc8e
No known key found for this signature in database
GPG Key ID: B5ABD165E2AEF201
26 changed files with 268 additions and 14 deletions

View File

@ -1133,6 +1133,8 @@ if(curl_ca_bundle_supported)
"Set ON to use built-in CA store of TLS backend. Defaults to OFF")
set(CURL_CA_PATH "auto" CACHE STRING
"Location of default CA path. Set 'none' to disable or 'auto' for auto-detection. Defaults to 'auto'.")
set(CURL_CA_EMBED "" CACHE STRING
"Path to the CA bundle to embed into the curl tool.")
if(CURL_CA_BUNDLE STREQUAL "")
message(FATAL_ERROR "Invalid value of CURL_CA_BUNDLE. Use 'none', 'auto' or file path.")
@ -1196,6 +1198,15 @@ if(curl_ca_bundle_supported)
endif()
endif()
endif()
set(CURL_CA_EMBED_SET FALSE)
if(BUILD_CURL_EXE AND NOT CURL_CA_EMBED STREQUAL "")
if(EXISTS "${CURL_CA_EMBED}")
set(CURL_CA_EMBED_SET TRUE)
else()
message(FATAL_ERROR "CA bundle to embed is missing: '${CURL_CA_EMBED}'")
endif()
endif()
endif()
# Check for header files
@ -1798,6 +1809,7 @@ if(NOT CURL_DISABLE_INSTALL)
_add_if("TrackMemory" ENABLE_CURLDEBUG)
_add_if("ECH" SSL_ENABLED AND HAVE_ECH)
_add_if("PSL" USE_LIBPSL)
_add_if("CAcert" CURL_CA_EMBED_SET)
if(_items)
if(NOT CMAKE_VERSION VERSION_LESS 3.13)
list(SORT _items CASE INSENSITIVE)

View File

@ -1357,6 +1357,37 @@ AS_HELP_STRING([--without-ca-fallback], [Don't use the built in CA store of the
fi
])
dnl CURL_CHECK_CA_EMBED
dnl -------------------------------------------------
dnl Check if a ca-bundle should be embedded
AC_DEFUN([CURL_CHECK_CA_EMBED], [
AC_MSG_CHECKING([CA cert bundle path to embed])
AC_ARG_WITH(ca-embed,
AS_HELP_STRING([--with-ca-embed=FILE],
[Path to a file containing CA certificates (example: /etc/ca-bundle.crt)])
AS_HELP_STRING([--without-ca-embed], [Don't embed a default CA bundle]),
[
want_ca_embed="$withval"
if test "x$want_ca_embed" = "xyes"; then
AC_MSG_ERROR([--with-ca-embed=FILE requires a path to the CA bundle])
fi
],
[ want_ca_embed="unset" ])
CURL_CA_EMBED=''
if test "x$want_ca_embed" != "xno" -a "x$want_ca_embed" != "xunset" -a -f "$want_ca_embed"; then
CURL_CA_EMBED='"'$want_ca_embed'"'
AC_SUBST(CURL_CA_EMBED)
AC_MSG_RESULT([$want_ca_embed])
else
AC_MSG_RESULT([no])
fi
])
dnl CURL_CHECK_WIN32_LARGEFILE
dnl -------------------------------------------------
dnl Check if curl's WIN32 large file will be used

View File

@ -2090,8 +2090,11 @@ dnl **********************************************************************
if test -n "$check_for_ca_bundle"; then
CURL_CHECK_CA_BUNDLE
CURL_CHECK_CA_EMBED
fi
AM_CONDITIONAL(CURL_CA_EMBED_SET, test "x$CURL_CA_EMBED" != "x")
dnl **********************************************************************
dnl Check for libpsl
dnl **********************************************************************
@ -3844,13 +3847,13 @@ AC_CHECK_DECL([fseeko],
CURL_CHECK_NONBLOCKING_SOCKET
if test "x$BUILD_DOCS" != "x0" -o "x$USE_MANUAL" != "x0"; then
if test "x$BUILD_DOCS" != "x0" -o "x$USE_MANUAL" != "x0" -o "x$CURL_CA_EMBED" != "x"; then
AC_PATH_PROG( PERL, perl, ,
$PATH:/usr/local/bin/perl:/usr/bin/:/usr/local/bin )
AC_SUBST(PERL)
if test -z "$PERL"; then
AC_MSG_ERROR([perl was not found, needed for docs and manual])
AC_MSG_ERROR([perl was not found, needed for docs, manual and CA embed])
fi
fi
@ -4866,6 +4869,9 @@ fi
if test "x$want_curldebug" = "xyes"; then
SUPPORT_FEATURES="$SUPPORT_FEATURES TrackMemory"
fi
if test "x$CURL_CA_EMBED" != "x"; then
SUPPORT_FEATURES="$SUPPORT_FEATURES CAcert"
fi
dnl replace spaces with newlines
dnl sort the lines

View File

@ -89,6 +89,7 @@ DPAGES = \
doh-cert-status.md \
doh-insecure.md \
doh-url.md \
dump-ca-embed.md \
dump-header.md \
ech.md \
egd-file.md \

View File

@ -10,6 +10,7 @@ Multi: boolean
See-also:
- cacert
- capath
- dump-ca-embed
- insecure
Example:
- --ca-native $URL

View File

@ -10,6 +10,7 @@ Added: 7.5
Multi: single
See-also:
- capath
- dump-ca-embed
- insecure
Example:
- --cacert CA-file.txt $URL

View File

@ -10,6 +10,7 @@ Added: 7.9.8
Multi: single
See-also:
- cacert
- dump-ca-embed
- insecure
Example:
- --capath /local/directory $URL

View File

@ -0,0 +1,25 @@
---
c: Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
SPDX-License-Identifier: curl
Long: dump-ca-embed
Help: Write the embedded CA bundle to standard output
Protocols: TLS
Category: http proxy tls
Added: 8.10.0
Multi: single
See-also:
- ca-native
- cacert
- capath
- proxy-ca-native
- proxy-cacert
- proxy-capath
Example:
- --dump-ca-embed
---
# `--dump-ca-embed`
Write the CA bundle embedded in curl to standard output, then quit.
If curl was not built with a default CA bundle embedded, the output is empty.

View File

@ -10,6 +10,7 @@ Multi: boolean
See-also:
- cacert
- capath
- dump-ca-embed
- insecure
Example:
- --proxy-ca-native $URL

View File

@ -11,6 +11,7 @@ See-also:
- proxy-capath
- cacert
- capath
- dump-ca-embed
- proxy
Example:
- --proxy-cacert CA-file.txt -x https://proxy $URL

View File

@ -11,6 +11,7 @@ See-also:
- proxy-cacert
- proxy
- capath
- dump-ca-embed
Example:
- --proxy-capath /local/directory -x https://proxy $URL
---

View File

@ -54,6 +54,7 @@
--doh-cert-status 7.76.0
--doh-insecure 7.76.0
--doh-url 7.62.0
--dump-ca-embed 8.10.0
--dump-header (-D) 5.7
--ech 8.8.0
--egd-file 7.7

View File

@ -115,7 +115,7 @@ rem ***************************************************************************
if "%CHECK_SRC%" == "TRUE" (
rem Check the src directory
if exist %SRC_DIR%\src (
for /f "delims=" %%i in ('dir "%SRC_DIR%\src\*.c.*" /b 2^>NUL') do @perl "%SRC_DIR%\scripts\checksrc.pl" "-D%SRC_DIR%\src" -Wtool_hugehelp.c "%%i"
for /f "delims=" %%i in ('dir "%SRC_DIR%\src\*.c.*" /b 2^>NUL') do @perl "%SRC_DIR%\scripts\checksrc.pl" "-D%SRC_DIR%\src" -Wtool_ca_embed.c -Wtool_hugehelp.c "%%i"
for /f "delims=" %%i in ('dir "%SRC_DIR%\src\*.h.*" /b 2^>NUL') do @perl "%SRC_DIR%\scripts\checksrc.pl" "-D%SRC_DIR%\src" "%%i"
)
)

1
src/.gitignore vendored
View File

@ -10,5 +10,6 @@ curl
curl_config.h
curl_config.h.in
stamp-h2
tool_ca_embed.c
tool_hugehelp.c
tool_version.h.dist

View File

@ -54,6 +54,22 @@ endif()
transform_makefile_inc("Makefile.inc" "${CMAKE_CURRENT_BINARY_DIR}/Makefile.inc.cmake")
include(${CMAKE_CURRENT_BINARY_DIR}/Makefile.inc.cmake)
if(CURL_CA_EMBED_SET)
if(PERL_FOUND)
add_definitions("-DCURL_CA_EMBED")
add_custom_command(
OUTPUT tool_ca_embed.c
COMMAND "${PERL_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/mk-file-embed.pl" --var curl_ca_embed < "${CURL_CA_EMBED}" > tool_ca_embed.c
DEPENDS
"${CURL_CA_EMBED}"
"${CMAKE_CURRENT_SOURCE_DIR}/mk-file-embed.pl"
VERBATIM)
list(APPEND CURL_CFILES tool_ca_embed.c)
else()
message(WARNING "Perl not found. Will not embed the CA bundle.")
endif()
endif()
if(WIN32)
list(APPEND CURL_CFILES curl.rc)
endif()

View File

@ -88,7 +88,7 @@ CLEANFILES = tool_hugehelp.c
# embedded text.
NROFF=env LC_ALL=C @NROFF@ @MANOPT@ 2>/dev/null # figured out by the configure script
EXTRA_DIST = mkhelp.pl \
EXTRA_DIST = mk-file-embed.pl mkhelp.pl \
Makefile.mk curl.rc Makefile.inc CMakeLists.txt .checksrc
# Use absolute directory to disable VPATH
@ -135,11 +135,25 @@ $(HUGE):
echo '#include "tool_hugehelp.h"' >> $(HUGE)
endif
# ignore tool_hugehelp.c since it is generated source code and it plays
# by slightly different rules!
CA_EMBED_CSOURCE = tool_ca_embed.c
CURL_CFILES += $(CA_EMBED_CSOURCE)
CLEANFILES += $(CA_EMBED_CSOURCE)
if CURL_CA_EMBED_SET
AM_CPPFLAGS += -DCURL_CA_EMBED
MK_FILE_EMBED = $(top_srcdir)/src/mk-file-embed.pl
$(CA_EMBED_CSOURCE): $(MK_FILE_EMBED)
$(PERL) $(MK_FILE_EMBED) --var curl_ca_embed < $(CURL_CA_EMBED) > $(CA_EMBED_CSOURCE)
else
$(CA_EMBED_CSOURCE):
echo 'extern const void *curl_ca_embed; const void *curl_ca_embed;' > $(CA_EMBED_CSOURCE)
endif
# ignore generated C files since they play by slightly different rules!
checksrc:
$(CHECKSRC)(@PERL@ $(top_srcdir)/scripts/checksrc.pl -D$(srcdir) \
-W$(srcdir)/tool_hugehelp.c $(srcdir)/*.[ch])
-W$(srcdir)/$(HUGE) \
-W$(srcdir)/$(CA_EMBED_CSOURCE) \
$(srcdir)/*.[ch])
if DEBUGBUILD
# for debug builds, we scan the sources on all regular make invokes

View File

@ -45,6 +45,11 @@ TARGETS := curl$(BIN_EXT)
CURL_CFILES += $(notdir $(CURLX_CFILES))
ifneq ($(CURL_CA_EMBED),)
CPPFLAGS += -DCURL_CA_EMBED
CURL_CFILES += tool_ca_embed.c
endif
curl_OBJECTS := $(patsubst %.c,$(OBJ_DIR)/%.o,$(strip $(CURL_CFILES)))
ifdef MAP
CURL_MAP := curl.map
@ -57,8 +62,9 @@ TOCLEAN := $(curl_OBJECTS)
### Rules
ifneq ($(wildcard tool_hugehelp.c.cvs),)
PERL ?= perl
ifneq ($(wildcard tool_hugehelp.c.cvs),)
NROFF ?= groff
TOCLEAN += tool_hugehelp.c
@ -84,6 +90,12 @@ tool_hugehelp.c:
endif
endif
ifneq ($(CURL_CA_EMBED),)
TOCLEAN += tool_ca_embed.c
tool_ca_embed.c: mk-file-embed.pl
$(PERL) mk-file-embed.pl --var curl_ca_embed < $(CURL_CA_EMBED) > $@
endif
$(TARGETS): $(curl_OBJECTS) $(PROOT)/lib/libcurl.a
$(CC) $(LDFLAGS) -o $@ $(curl_OBJECTS) $(LIBS)

56
src/mk-file-embed.pl Executable file
View File

@ -0,0 +1,56 @@
#!/usr/bin/env perl
#***************************************************************************
# _ _ ____ _
# Project ___| | | | _ \| |
# / __| | | | |_) | |
# | (__| |_| | _ <| |___
# \___|\___/|_| \_\_____|
#
# Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution. The terms
# are also available at https://curl.se/docs/copyright.html.
#
# You may opt to use, copy, modify, merge, publish, distribute and/or sell
# copies of the Software, and permit persons to whom the Software is
# furnished to do so, under the terms of the COPYING file.
#
# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
# KIND, either express or implied.
#
# SPDX-License-Identifier: curl
#
###########################################################################
my $varname = "var";
if($ARGV[0] eq "--var") {
shift;
$varname = shift @ARGV;
}
print <<HEAD
/*
* NEVER EVER edit this manually, fix the mk-file-embed.pl script instead!
*/
extern const unsigned char ${varname}[];
const unsigned char ${varname}[] = {
HEAD
;
while (<STDIN>) {
my $line = $_;
foreach my $n (split //, $line) {
my $ord = ord($n);
printf("%s,", $ord);
if($ord == 10) {
printf("\n");
}
}
}
print <<ENDLINE
0
};
ENDLINE
;

View File

@ -122,6 +122,7 @@ typedef enum {
C_DOH_CERT_STATUS,
C_DOH_INSECURE,
C_DOH_URL,
C_DUMP_CA_EMBED,
C_DUMP_HEADER,
C_ECH,
C_EGD_FILE,
@ -408,6 +409,7 @@ static const struct LongShort aliases[]= {
{"doh-cert-status", ARG_BOOL, ' ', C_DOH_CERT_STATUS},
{"doh-insecure", ARG_BOOL, ' ', C_DOH_INSECURE},
{"doh-url" , ARG_STRG, ' ', C_DOH_URL},
{"dump-ca-embed", ARG_NONE, ' ', C_DUMP_CA_EMBED},
{"dump-header", ARG_FILE, 'D', C_DUMP_HEADER},
{"ech", ARG_STRG, ' ', C_ECH},
{"egd-file", ARG_STRG, ' ', C_EGD_FILE},
@ -2113,6 +2115,9 @@ ParameterError getparameter(const char *flag, /* f or -long-flag */
case C_URL_QUERY: /* --url-query */
err = url_query(nextarg, global, config);
break;
case C_DUMP_CA_EMBED: /* --dump-ca-embed */
err = PARAM_CA_EMBED_REQUESTED;
break;
case C_DUMP_HEADER: /* --dump-header */
err = getstr(&config->headerfile, nextarg, DENY_BLANK);
break;
@ -2984,7 +2989,8 @@ ParameterError parse_args(struct GlobalConfig *global, int argc,
if(result && result != PARAM_HELP_REQUESTED &&
result != PARAM_MANUAL_REQUESTED &&
result != PARAM_VERSION_INFO_REQUESTED &&
result != PARAM_ENGINES_REQUESTED) {
result != PARAM_ENGINES_REQUESTED &&
result != PARAM_CA_EMBED_REQUESTED) {
const char *reason = param2text(result);
if(orig_opt && strcmp(":", orig_opt))

View File

@ -35,6 +35,7 @@ typedef enum {
PARAM_MANUAL_REQUESTED,
PARAM_VERSION_INFO_REQUESTED,
PARAM_ENGINES_REQUESTED,
PARAM_CA_EMBED_REQUESTED,
PARAM_GOT_EXTRA_PARAMETER,
PARAM_BAD_NUMERIC,
PARAM_NEGATIVE_NUMERIC,

View File

@ -244,10 +244,28 @@ void tool_version_info(void)
puts(""); /* newline */
}
if(feature_names[0]) {
printf("Features:");
for(builtin = feature_names; *builtin; ++builtin)
printf(" %s", *builtin);
puts(""); /* newline */
const char **feat_ext;
size_t feat_ext_count = feature_count;
#ifdef CURL_CA_EMBED
++feat_ext_count;
#endif
feat_ext = malloc(sizeof(*feature_names) * (feat_ext_count + 1));
if(feat_ext) {
memcpy((void *)feat_ext, feature_names,
sizeof(*feature_names) * feature_count);
feat_ext_count = feature_count;
#ifdef CURL_CA_EMBED
feat_ext[feat_ext_count++] = "CAcert";
#endif
feat_ext[feat_ext_count] = NULL;
qsort((void *)feat_ext, feat_ext_count, sizeof(*feat_ext),
struplocompare4sort);
printf("Features:");
for(builtin = feat_ext; *builtin; ++builtin)
printf(" %s", *builtin);
puts(""); /* newline */
free((void *)feat_ext);
}
}
if(strcmp(CURL_VERSION, curlinfo->version)) {
printf("WARNING: curl and libcurl versions do not match. "

View File

@ -124,6 +124,7 @@ static struct feature_name_presentp {
static const char *fnames[sizeof(maybe_feature) / sizeof(maybe_feature[0])];
const char * const *feature_names = fnames;
size_t feature_count;
/*
* libcurl_info_init: retrieves runtime information about libcurl,
@ -182,6 +183,7 @@ CURLcode get_libcurl_info(void)
*p->feature_presentp = TRUE;
break;
}
++feature_count;
}
return CURLE_OK;

View File

@ -34,6 +34,7 @@ extern const char * const *built_in_protos;
extern size_t proto_count;
extern const char * const *feature_names;
extern size_t feature_count;
extern const char *proto_file;
extern const char *proto_ftp;

View File

@ -165,6 +165,9 @@ const struct helptxt helptext[] = {
{" --doh-url <URL>",
"Resolve hostnames over DoH",
CURLHELP_DNS},
{" --dump-ca-embed",
"Write the embedded CA bundle to standard output",
CURLHELP_HTTP | CURLHELP_PROXY | CURLHELP_TLS},
{"-D, --dump-header <filename>",
"Write the received headers to <filename>",
CURLHELP_HTTP | CURLHELP_FTP},

View File

@ -94,6 +94,10 @@
#include "memdebug.h" /* keep this as LAST include */
#ifdef CURL_CA_EMBED
extern const unsigned char curl_ca_embed[];
#endif
#ifndef O_BINARY
/* since O_BINARY as used in bitmasks, setting it to zero makes it usable in
source code but yet it does not ruin anything */
@ -1657,6 +1661,37 @@ static CURLcode single_transfer(struct GlobalConfig *global,
break;
}
#ifdef CURL_CA_EMBED
if(!config->cacert && !config->capath) {
struct curl_blob blob;
blob.data = (void *)curl_ca_embed;
blob.len = strlen((const char *)curl_ca_embed);
blob.flags = CURL_BLOB_NOCOPY;
notef(config->global,
"Using embedded CA bundle (%zu bytes)",
blob.len);
result = curl_easy_setopt(curl, CURLOPT_CAINFO_BLOB, &blob);
if(result == CURLE_NOT_BUILT_IN) {
warnf(global,
"ignoring embedded CA bundle, not supported by libcurl");
}
}
if(!config->proxy_cacert && !config->proxy_capath) {
struct curl_blob blob;
blob.data = (void *)curl_ca_embed;
blob.len = strlen((const char *)curl_ca_embed);
blob.flags = CURL_BLOB_NOCOPY;
notef(config->global,
"Using embedded CA bundle, for proxies (%zu bytes)",
blob.len);
result = curl_easy_setopt(curl, CURLOPT_PROXY_CAINFO_BLOB, &blob);
if(result == CURLE_NOT_BUILT_IN) {
warnf(global,
"ignoring embedded CA bundle, not supported by libcurl");
}
}
#endif
if(config->crlfile)
my_setopt_str(curl, CURLOPT_CRLFILE, config->crlfile);
if(config->proxy_crlfile)
@ -2842,6 +2877,12 @@ CURLcode operate(struct GlobalConfig *global, int argc, argv_item_t argv[])
/* Check if we were asked to list the SSL engines */
else if(res == PARAM_ENGINES_REQUESTED)
tool_list_engines();
/* Check if we were asked to dump the embedded CA bundle */
else if(res == PARAM_CA_EMBED_REQUESTED) {
#ifdef CURL_CA_EMBED
printf("%s", curl_ca_embed);
#endif
}
else if(res == PARAM_LIBCURL_UNSUPPORTED_PROTOCOL)
result = CURLE_UNSUPPORTED_PROTOCOL;
else if(res == PARAM_READ_ERROR)

View File

@ -262,7 +262,8 @@ int parseconfig(const char *filename, struct GlobalConfig *global)
if(res != PARAM_HELP_REQUESTED &&
res != PARAM_MANUAL_REQUESTED &&
res != PARAM_VERSION_INFO_REQUESTED &&
res != PARAM_ENGINES_REQUESTED) {
res != PARAM_ENGINES_REQUESTED &&
res != PARAM_CA_EMBED_REQUESTED) {
const char *reason = param2text(res);
errorf(operation->global, "%s:%d: '%s' %s",
filename, lineno, option, reason);