diff --git a/apps/ca.c b/apps/ca.c index 8de58288ba..454c218d98 100644 --- a/apps/ca.c +++ b/apps/ca.c @@ -1,5 +1,5 @@ /* - * Copyright 1995-2021 The OpenSSL Project Authors. All Rights Reserved. + * Copyright 1995-2022 The OpenSSL Project Authors. All Rights Reserved. * * Licensed under the Apache License 2.0 (the "License"). You may not use * this file except in compliance with the License. You can obtain a copy @@ -129,7 +129,6 @@ static int do_body(X509 **xret, EVP_PKEY *pkey, X509 *x509, CONF *conf, unsigned long certopt, unsigned long nameopt, int default_op, int ext_copy, int selfsign, unsigned long dateopt); static int get_certificate_status(const char *ser_status, CA_DB *db); -static int do_updatedb(CA_DB *db); static int check_time_format(const char *str); static int do_revoke(X509 *x509, CA_DB *db, REVINFO_TYPE rev_type, const char *extval); @@ -755,7 +754,7 @@ end_of_options: if (verbose) BIO_printf(bio_err, "Updating %s ...\n", dbfile); - i = do_updatedb(db); + i = do_updatedb(db, NULL); if (i == -1) { BIO_printf(bio_err, "Malloc failure\n"); goto end; @@ -2290,7 +2289,7 @@ static int get_certificate_status(const char *serial, CA_DB *db) return ok; } -static int do_updatedb(CA_DB *db) +int do_updatedb(CA_DB *db, time_t *now) { ASN1_TIME *a_tm = NULL; int i, cnt = 0; @@ -2301,7 +2300,7 @@ static int do_updatedb(CA_DB *db) return -1; /* get actual time */ - if (X509_gmtime_adj(a_tm, 0) == NULL) { + if (X509_time_adj(a_tm, 0, now) == NULL) { ASN1_TIME_free(a_tm); return -1; } diff --git a/apps/include/apps.h b/apps/include/apps.h index 28c2bbdad2..c567ed5664 100644 --- a/apps/include/apps.h +++ b/apps/include/apps.h @@ -1,5 +1,5 @@ /* - * Copyright 1995-2021 The OpenSSL Project Authors. All Rights Reserved. + * Copyright 1995-2022 The OpenSSL Project Authors. All Rights Reserved. * * Licensed under the Apache License 2.0 (the "License"). You may not use * this file except in compliance with the License. You can obtain a copy @@ -221,6 +221,8 @@ typedef struct ca_db_st { # endif } CA_DB; +extern int do_updatedb(CA_DB *db, time_t *now); + void app_bail_out(char *fmt, ...); void *app_malloc(size_t sz, const char *what); BIGNUM *load_serial(const char *serialfile, int create, ASN1_INTEGER **retai); diff --git a/apps/lib/apps.c b/apps/lib/apps.c index 77edc1d936..021371201b 100644 --- a/apps/lib/apps.c +++ b/apps/lib/apps.c @@ -1,5 +1,5 @@ /* - * Copyright 1995-2021 The OpenSSL Project Authors. All Rights Reserved. + * Copyright 1995-2022 The OpenSSL Project Authors. All Rights Reserved. * * Licensed under the Apache License 2.0 (the "License"). You may not use * this file except in compliance with the License. You can obtain a copy @@ -3247,18 +3247,6 @@ void make_uppercase(char *string) string[i] = toupper((unsigned char)string[i]); } -/* This function is defined here due to visibility of bio_err */ -int opt_printf_stderr(const char *fmt, ...) -{ - va_list ap; - int ret; - - va_start(ap, fmt); - ret = BIO_vprintf(bio_err, fmt, ap); - va_end(ap); - return ret; -} - OSSL_PARAM *app_params_new_from_opts(STACK_OF(OPENSSL_STRING) *opts, const OSSL_PARAM *paramdefs) { diff --git a/apps/lib/apps_opt_printf.c b/apps/lib/apps_opt_printf.c new file mode 100644 index 0000000000..e15f4b795e --- /dev/null +++ b/apps/lib/apps_opt_printf.c @@ -0,0 +1,25 @@ +/* + * Copyright 2022 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the Apache License 2.0 (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#include "opt.h" +#include +#include "apps_ui.h" + +/* This function is defined here due to visibility of bio_err */ +int opt_printf_stderr(const char *fmt, ...) +{ + va_list ap; + int ret; + + va_start(ap, fmt); + ret = BIO_vprintf(bio_err, fmt, ap); + va_end(ap); + return ret; +} + diff --git a/apps/lib/build.info b/apps/lib/build.info index 923ef5d92b..727d924745 100644 --- a/apps/lib/build.info +++ b/apps/lib/build.info @@ -10,7 +10,7 @@ ENDIF # Source for libapps $LIBAPPSSRC=apps.c apps_ui.c opt.c fmt.c s_cb.c s_socket.c app_rand.c \ columns.c app_params.c names.c app_provider.c app_x509.c http_server.c \ - engine.c engine_loader.c app_libctx.c + engine.c engine_loader.c app_libctx.c apps_opt_printf.c IF[{- !$disabled{apps} -}] LIBS{noinst}=../libapps.a diff --git a/crypto/asn1/a_time.c b/crypto/asn1/a_time.c index 9b3074e47e..e9df23af92 100644 --- a/crypto/asn1/a_time.c +++ b/crypto/asn1/a_time.c @@ -1,5 +1,5 @@ /* - * Copyright 1999-2021 The OpenSSL Project Authors. All Rights Reserved. + * Copyright 1999-2022 The OpenSSL Project Authors. All Rights Reserved. * * Licensed under the Apache License 2.0 (the "License"). You may not use * this file except in compliance with the License. You can obtain a copy @@ -589,3 +589,41 @@ int ASN1_TIME_compare(const ASN1_TIME *a, const ASN1_TIME *b) return -1; return 0; } + +/* + * tweak for Windows + */ +#ifdef WIN32 +# define timezone _timezone +#endif + +time_t asn1_string_to_time_t(const char *asn1_string) +{ + ASN1_TIME *timestamp_asn1 = NULL; + struct tm *timestamp_tm = NULL; + time_t timestamp_local; + time_t timestamp_utc; + + timestamp_asn1 = ASN1_TIME_new(); + if (!ASN1_TIME_set_string(timestamp_asn1, asn1_string)) + { + ASN1_TIME_free(timestamp_asn1); + return -1; + } + + timestamp_tm = OPENSSL_malloc(sizeof(*timestamp_tm)); + + if (!(ASN1_TIME_to_tm(timestamp_asn1, timestamp_tm))) { + OPENSSL_free(timestamp_tm); + ASN1_TIME_free(timestamp_asn1); + return -1; + } + + timestamp_local = mktime(timestamp_tm); + OPENSSL_free(timestamp_tm); + + timestamp_utc = timestamp_local - timezone; + + ASN1_TIME_free(timestamp_asn1); + return timestamp_utc; +} diff --git a/include/crypto/asn1.h b/include/crypto/asn1.h index ff02cac573..26e48ef717 100644 --- a/include/crypto/asn1.h +++ b/include/crypto/asn1.h @@ -1,5 +1,5 @@ /* - * Copyright 2015-2021 The OpenSSL Project Authors. All Rights Reserved. + * Copyright 2015-2022 The OpenSSL Project Authors. All Rights Reserved. * * Licensed under the Apache License 2.0 (the "License"). You may not use * this file except in compliance with the License. You can obtain a copy @@ -147,4 +147,6 @@ EVP_PKEY * ossl_d2i_PrivateKey_legacy(int keytype, EVP_PKEY **a, OSSL_LIB_CTX *libctx, const char *propq); X509_ALGOR *ossl_X509_ALGOR_from_nid(int nid, int ptype, void *pval); +time_t asn1_string_to_time_t(const char *asn1_string); + #endif /* ndef OSSL_CRYPTO_ASN1_H */ diff --git a/test/asn1_time_test.c b/test/asn1_time_test.c index 9dbad22a2d..2383ec25c9 100644 --- a/test/asn1_time_test.c +++ b/test/asn1_time_test.c @@ -1,5 +1,5 @@ /* - * Copyright 1999-2020 The OpenSSL Project Authors. All Rights Reserved. + * Copyright 1999-2022 The OpenSSL Project Authors. All Rights Reserved. * * Licensed under the Apache License 2.0 (the "License"). You may not use * this file except in compliance with the License. You can obtain a copy @@ -12,6 +12,7 @@ #include #include +#include #include #include #include @@ -28,6 +29,53 @@ struct testdata { int convert_result; /* conversion result */ }; +struct TESTDATA_asn1_to_utc { + char *input; + time_t expected; +}; + +static const struct TESTDATA_asn1_to_utc asn1_to_utc[] = { + { + /* + * last second of standard time in central Europe in 2021 + * specified in GMT + */ + "210328005959Z", + 1616893199, + }, + { + /* + * first second of daylight saving time in central Europe in 2021 + * specified in GMT + */ + "210328010000Z", + 1616893200, + }, + { + /* + * last second of standard time in central Europe in 2021 + * specified in offset to GMT + */ + "20210328015959+0100", + 1616893199, + }, + { + /* + * first second of daylight saving time in central Europe in 2021 + * specified in offset to GMT + */ + "20210328030000+0200", + 1616893200, + }, + { + /* + * Invalid strings should get -1 as a result + */ + "INVALID", + -1, + }, +}; + static struct testdata tbl_testdata_pos[] = { { "0", V_ASN1_GENERALIZEDTIME, V_ASN1_GENERALIZEDTIME, 0, 0, 0, 0, }, /* Bad time */ { "ABCD", V_ASN1_GENERALIZEDTIME, V_ASN1_GENERALIZEDTIME, 0, 0, 0, 0, }, @@ -379,6 +427,20 @@ static int test_time_dup(void) return ret; } +static int convert_asn1_to_time_t(int idx) +{ + time_t testdateutc; + + testdateutc = asn1_string_to_time_t(asn1_to_utc[idx].input); + + if (!TEST_time_t_eq(testdateutc, asn1_to_utc[idx].expected)) { + TEST_info("asn1_string_to_time_t (%s) failed: expected %li, got %li\n", + asn1_to_utc[idx].input, asn1_to_utc[idx].expected, (signed long) testdateutc); + return 0; + } + return 1; +} + int setup_tests(void) { /* @@ -414,5 +476,6 @@ int setup_tests(void) } ADD_ALL_TESTS(test_table_compare, OSSL_NELEM(tbl_compare_testdata)); ADD_TEST(test_time_dup); + ADD_ALL_TESTS(convert_asn1_to_time_t, OSSL_NELEM(asn1_to_utc)); return 1; } diff --git a/test/build.info b/test/build.info index 188b850beb..f304c4cef2 100644 --- a/test/build.info +++ b/test/build.info @@ -62,7 +62,7 @@ IF[{- !$disabled{tests} -}] context_internal_test aesgcmtest params_test evp_pkey_dparams_test \ keymgmt_internal_test hexstr_test provider_status_test defltfips_test \ bio_readbuffer_test user_property_test pkcs7_test upcallstest \ - provfetchtest prov_config_test rand_test + provfetchtest prov_config_test rand_test ca_internals_test IF[{- !$disabled{'deprecated-3.0'} -}] PROGRAMS{noinst}=enginetest @@ -575,6 +575,13 @@ IF[{- !$disabled{tests} -}] INCLUDE[cmp_client_test]=.. ../include ../apps/include DEPEND[cmp_client_test]=../libcrypto.a libtestutil.a + SOURCE[ca_internals_test]=ca_internals_test.c ../apps/ca.c ../apps/lib/apps.c \ + ../apps/lib/app_rand.c ../apps/lib/engine.c ../apps/lib/app_provider.c \ + ../apps/lib/app_libctx.c ../apps/lib/fmt.c ../apps/lib/apps_ui.c \ + ../apps/lib/app_x509.c ../crypto/asn1/a_time.c ../crypto/ctype.c + INCLUDE[ca_internals_test]=.. ../include ../apps/include + DEPEND[ca_internals_test]=libtestutil.a ../libssl + # Internal test programs. These are essentially a collection of internal # test routines. Some of them need to reach internal symbols that aren't # available through the shared library (at least on Linux, Solaris, Windows @@ -780,7 +787,8 @@ IF[{- !$disabled{tests} -}] ENDIF PROGRAMS{noinst}=asn1_time_test - SOURCE[asn1_time_test]=asn1_time_test.c + SOURCE[asn1_time_test]=asn1_time_test.c ../crypto/ctype.c \ + ../crypto/asn1/a_time.c INCLUDE[asn1_time_test]=../include ../apps/include DEPEND[asn1_time_test]=../libcrypto libtestutil.a diff --git a/test/ca_internals_test.c b/test/ca_internals_test.c new file mode 100644 index 0000000000..2928e5b0b3 --- /dev/null +++ b/test/ca_internals_test.c @@ -0,0 +1,93 @@ +/* + * Copyright 2021-2022 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the Apache License 2.0 (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#include +#include "apps.h" +#include "testutil.h" +#include "crypto/asn1.h" + +#define binname "ca_internals_test" + +char *default_config_file = NULL; + +static int test_do_updatedb(void) +{ + CA_DB *db = NULL; + time_t testdateutc; + int rv; + size_t argc = test_get_argument_count(); + BIO *bio_tmp; + char *testdate; + char *indexfile; + int need64bit; + int have64bit; + + if (argc != 4) { + TEST_error("Usage: %s: do_updatedb dbfile testdate need64bit\n", binname); + TEST_error(" testdate format: ASN1-String\n"); + return 0; + } + + /* + * if the test will only work with 64bit time_t and + * the build only supports 32, assume the test as success + */ + need64bit = (int)strtol(test_get_argument(3), NULL, 0); + have64bit = sizeof(time_t) > sizeof(uint32_t); + if (need64bit && !have64bit) { + BIO_printf(bio_out, "skipping test (need64bit: %i, have64bit: %i)", + need64bit, have64bit); + return 1; + } + + testdate = test_get_argument(2); + testdateutc = asn1_string_to_time_t(testdate); + if (TEST_time_t_lt(testdateutc, 0)) { + return 0; + } + + indexfile = test_get_argument(1); + db = load_index(indexfile, NULL); + if (TEST_ptr_null(db)) { + return 0; + } + + bio_tmp = bio_err; + bio_err = bio_out; + rv = do_updatedb(db, &testdateutc); + bio_err = bio_tmp; + + if (rv > 0) { + if (!TEST_true(save_index(indexfile, "new", db))) + goto end; + + if (!TEST_true(rotate_index(indexfile, "new", "old"))) + goto end; + } +end: + free_index(db); + return 1; +} + +int setup_tests(void) +{ + char *command = test_get_argument(0); + + if (test_get_argument_count() < 1) { + TEST_error("%s: no command specified for testing\n", binname); + return 0; + } + + if (strcmp(command, "do_updatedb") == 0) + return test_do_updatedb(); + + TEST_error("%s: command '%s' is not supported for testing\n", binname, command); + return 0; +} + diff --git a/test/recipes/80-test_ca_internals.t b/test/recipes/80-test_ca_internals.t new file mode 100644 index 0000000000..b84abdfa66 --- /dev/null +++ b/test/recipes/80-test_ca_internals.t @@ -0,0 +1,165 @@ +#! /usr/bin/env perl +# Copyright 2021-2022 The OpenSSL Project Authors. All Rights Reserved. +# +# Licensed under the Apache License 2.0 (the "License"). You may not use +# this file except in compliance with the License. You can obtain a copy +# in the file LICENSE in the source distribution or at +# https://www.openssl.org/source/license.html + +use strict; +use warnings; + +use POSIX; +use OpenSSL::Test qw/:DEFAULT data_file/; +use File::Copy; + +setup('test_ca_internals'); + +my @updatedb_tests = ( + { + description => 'updatedb called before the first certificate expires', + filename => 'index.txt', + copydb => 1, + testdate => '990101000000Z', + need64bit => 0, + expirelist => [ ] + }, + { + description => 'updatedb called before Y2k', + filename => 'index.txt', + copydb => 0, + testdate => '991201000000Z', + need64bit => 0, + expirelist => [ '1000' ] + }, + { + description => 'updatedb called after year 2020', + filename => 'index.txt', + copydb => 0, + testdate => '211201000000Z', + need64bit => 0, + expirelist => [ '1001' ] + }, + { + description => 'updatedb called in year 2049 (last year with 2 digits)', + filename => 'index.txt', + copydb => 0, + testdate => '491201000000Z', + need64bit => 1, + expirelist => [ '1002' ] + }, + { + description => 'updatedb called in year 2050 (first year with 4 digits) before the last certificate expires', + filename => 'index.txt', + copydb => 0, + testdate => '20500101000000Z', + need64bit => 1, + expirelist => [ ] + }, + { + description => 'updatedb called after the last certificate expired', + filename => 'index.txt', + copydb => 0, + testdate => '20501201000000Z', + need64bit => 1, + expirelist => [ '1003' ] + }, + { + description => 'updatedb called for the first time after the last certificate expired', + filename => 'index.txt', + copydb => 1, + testdate => '20501201000000Z', + need64bit => 1, + expirelist => [ '1000', + '1001', + '1002', + '1003' ] + } +); + +my @unsupported_commands = ( + { + command => 'unsupported' + } +); + +# every "test_updatedb" makes 3 checks +plan tests => 3 * scalar(@updatedb_tests) + + 1 * scalar(@unsupported_commands); + + +foreach my $test (@updatedb_tests) { + test_updatedb($test); +} +foreach my $test (@unsupported_commands) { + test_unsupported_commands($test); +} + + +################### subs to do tests per supported command ################ + +sub test_unsupported_commands { + my ($opts) = @_; + + run( + test(['ca_internals_test', + $opts->{command} + ]), + capture => 0, + statusvar => \my $exit + ); + + is($exit, 0, "command '".$opts->{command}."' completed without an error"); +} + +sub test_updatedb { + my ($opts) = @_; + my $amtexpectedexpired = scalar(@{$opts->{expirelist}}); + my @output; + my $expirelistcorrect = 1; + my $cert; + my $amtexpired = 0; + my $skipped = 0; + + if ($opts->{copydb}) { + copy(data_file('index.txt'), 'index.txt'); + } + + @output = run( + test(['ca_internals_test', + "do_updatedb", + $opts->{filename}, + $opts->{testdate}, + $opts->{need64bit} + ]), + capture => 1, + statusvar => \my $exit + ); + + foreach my $tmp (@output) { + ($cert) = $tmp =~ /^[\x20\x23]*[^0-9A-Fa-f]*([0-9A-Fa-f]+)=Expired/; + if ($tmp =~ /^[\x20\x23]*skipping test/) { + $skipped = 1; + } + if (defined($cert) && (length($cert) > 0)) { + $amtexpired++; + my $expirefound = 0; + foreach my $expire (@{$opts->{expirelist}}) { + if ($expire eq $cert) { + $expirefound = 1; + } + } + if ($expirefound != 1) { + $expirelistcorrect = 0; + } + } + } + + if ($skipped) { + $amtexpired = $amtexpectedexpired; + $expirelistcorrect = 1; + } + is($exit, 1, "ca_internals_test: returned EXIT_FAILURE (".$opts->{description}.")"); + is($amtexpired, $amtexpectedexpired, "ca_internals_test: amount of expired certificates differs from expected amount (".$opts->{description}.")"); + is($expirelistcorrect, 1, "ca_internals_test: list of expired certificates differs from expected list (".$opts->{description}.")"); +} diff --git a/test/recipes/80-test_ca_internals_data/index.txt b/test/recipes/80-test_ca_internals_data/index.txt new file mode 100644 index 0000000000..e528e8cd43 --- /dev/null +++ b/test/recipes/80-test_ca_internals_data/index.txt @@ -0,0 +1,4 @@ +V 990501230004Z 1000 unknown /C=AT/ST=Austria/L=OpenSSL/O=OpenSSL/OU=OpenSSL/CN=1999 +V 200430230003Z 1001 unknown /C=AT/ST=Austria/L=OpenSSL/O=OpenSSL/OU=OpenSSL/CN=2020 +V 490501230003Z 1002 unknown /C=AT/ST=Austria/L=OpenSSL/O=OpenSSL/OU=OpenSSL/CN=2049 +V 20500501230003Z 1003 unknown /C=AT/ST=Austria/L=OpenSSL/O=OpenSSL/OU=OpenSSL/CN=2050