mirror of
https://github.com/mtrojnar/osslsigncode.git
synced 2025-04-04 17:00:11 -05:00
Enable verification that a catalog file was signed by a valid certificate.
Simplify setting and signing a content blob. Clean up a support library.
This commit is contained in:
parent
33253afb5e
commit
900ffed596
6
appx.c
6
appx.c
@ -587,12 +587,16 @@ static PKCS7 *appx_pkcs7_prepare(FILE_FORMAT_CTX *ctx, BIO *hash, BIO *outdata)
|
||||
printf("Creating a new signature failed\n");
|
||||
return NULL; /* FAILED */
|
||||
}
|
||||
if (!add_indirect_data_object(p7, hashes, ctx)) {
|
||||
if (!add_indirect_data_object(p7)) {
|
||||
printf("Adding SPC_INDIRECT_DATA_OBJID failed\n");
|
||||
BIO_free_all(hashes);
|
||||
PKCS7_free(p7);
|
||||
return NULL; /* FAILED */
|
||||
}
|
||||
if (!sign_spc_indirect_data_content(p7, hashes, ctx)) {
|
||||
printf("Failed to set signed content\n");
|
||||
return NULL; /* FAILED */
|
||||
}
|
||||
BIO_free_all(hashes);
|
||||
}
|
||||
return p7; /* OK */
|
||||
|
6
cab.c
6
cab.c
@ -498,11 +498,15 @@ static PKCS7 *cab_pkcs7_prepare(FILE_FORMAT_CTX *ctx, BIO *hash, BIO *outdata)
|
||||
PKCS7_free(p7);
|
||||
return NULL; /* FAILED */
|
||||
}
|
||||
if (!add_indirect_data_object(p7, hash, ctx)) {
|
||||
if (!add_indirect_data_object(p7)) {
|
||||
printf("Adding SPC_INDIRECT_DATA_OBJID failed\n");
|
||||
PKCS7_free(p7);
|
||||
return NULL; /* FAILED */
|
||||
}
|
||||
if (!sign_spc_indirect_data_content(p7, hash, ctx)) {
|
||||
printf("Failed to set signed content\n");
|
||||
return NULL; /* FAILED */
|
||||
}
|
||||
}
|
||||
if (ctx->options->nest)
|
||||
ctx->options->prevsig = cursig;
|
||||
|
137
cat.c
137
cat.c
@ -19,10 +19,13 @@ struct cat_ctx_st {
|
||||
uint32_t sigpos;
|
||||
uint32_t siglen;
|
||||
uint32_t fileend;
|
||||
PKCS7 *p7;
|
||||
};
|
||||
|
||||
/* FILE_FORMAT method prototypes */
|
||||
static FILE_FORMAT_CTX *cat_ctx_new(GLOBAL_OPTIONS *options, BIO *hash, BIO *outdata);
|
||||
static int cat_check_file(FILE_FORMAT_CTX *ctx, int detached);
|
||||
static int cat_verify_digests(FILE_FORMAT_CTX *ctx, PKCS7 *p7);
|
||||
static PKCS7 *cat_pkcs7_extract(FILE_FORMAT_CTX *ctx);
|
||||
static PKCS7 *cat_pkcs7_prepare(FILE_FORMAT_CTX *ctx, BIO *hash, BIO *outdata);
|
||||
static int cat_append_pkcs7(FILE_FORMAT_CTX *ctx, BIO *outdata, PKCS7 *p7);
|
||||
@ -31,6 +34,8 @@ static void cat_ctx_cleanup(FILE_FORMAT_CTX *ctx, BIO *hash, BIO *outdata);
|
||||
|
||||
FILE_FORMAT file_format_cat = {
|
||||
.ctx_new = cat_ctx_new,
|
||||
.check_file = cat_check_file,
|
||||
.verify_digests = cat_verify_digests,
|
||||
.pkcs7_extract = cat_pkcs7_extract,
|
||||
.pkcs7_prepare = cat_pkcs7_prepare,
|
||||
.append_pkcs7 = cat_append_pkcs7,
|
||||
@ -40,6 +45,8 @@ FILE_FORMAT file_format_cat = {
|
||||
|
||||
/* Prototypes */
|
||||
static CAT_CTX *cat_ctx_get(char *indata, uint32_t filesize);
|
||||
static int cat_add_ms_ctl_object(PKCS7 *p7);
|
||||
static int cat_sign_ms_ctl_content(PKCS7 *p7, PKCS7 *contents);
|
||||
|
||||
/*
|
||||
* FILE_FORMAT method definitions
|
||||
@ -58,17 +65,12 @@ static FILE_FORMAT_CTX *cat_ctx_new(GLOBAL_OPTIONS *options, BIO *hash, BIO *out
|
||||
CAT_CTX *cat_ctx;
|
||||
uint32_t filesize;
|
||||
|
||||
/* squash unused parameter warnings */
|
||||
(void)outdata;
|
||||
(void)hash;
|
||||
|
||||
if (options->cmd == CMD_REMOVE || options->cmd==CMD_ATTACH) {
|
||||
printf("Unsupported command\n");
|
||||
return NULL; /* FAILED */
|
||||
}
|
||||
if (options->cmd == CMD_VERIFY) {
|
||||
printf("Use -catalog option\n");
|
||||
return NULL; /* FAILED */
|
||||
printf("Warning: Use -catalog option to verify that a file, listed in catalog file, is signed\n\n");
|
||||
}
|
||||
filesize = get_file_size(options->infile);
|
||||
if (filesize == 0)
|
||||
@ -109,6 +111,45 @@ static FILE_FORMAT_CTX *cat_ctx_new(GLOBAL_OPTIONS *options, BIO *hash, BIO *out
|
||||
return ctx;
|
||||
}
|
||||
|
||||
static int cat_check_file(FILE_FORMAT_CTX *ctx, int detached)
|
||||
{
|
||||
STACK_OF(PKCS7_SIGNER_INFO) *signer_info;
|
||||
PKCS7_SIGNER_INFO *si;
|
||||
|
||||
if (!ctx) {
|
||||
printf("Init error\n\n");
|
||||
return 0; /* FAILED */
|
||||
}
|
||||
if (detached) {
|
||||
printf("CAT format does not support detached PKCS#7 signature\n\n");
|
||||
return 0; /* FAILED */
|
||||
}
|
||||
signer_info = PKCS7_get_signer_info(ctx->cat_ctx->p7);
|
||||
if (!signer_info) {
|
||||
printf("Failed catalog file\n\n");
|
||||
return 0; /* FAILED */
|
||||
}
|
||||
si = sk_PKCS7_SIGNER_INFO_value(signer_info, 0);
|
||||
if (!si) {
|
||||
printf("No signature found\n\n");
|
||||
return 0; /* FAILED */
|
||||
}
|
||||
return 1; /* OK */
|
||||
}
|
||||
|
||||
/*
|
||||
* ContentInfo value is the inner content of pkcs7-signedData.
|
||||
* An extra verification is not necessary when a content type data
|
||||
* is the inner content of the signed-data type.
|
||||
*/
|
||||
static int cat_verify_digests(FILE_FORMAT_CTX *ctx, PKCS7 *p7)
|
||||
{
|
||||
/* squash unused parameter warnings */
|
||||
(void)ctx;
|
||||
(void)p7;
|
||||
return 1; /* OK */
|
||||
}
|
||||
|
||||
/*
|
||||
* Extract existing signature in DER format.
|
||||
* [in] ctx: structure holds input and output data
|
||||
@ -116,7 +157,7 @@ static FILE_FORMAT_CTX *cat_ctx_new(GLOBAL_OPTIONS *options, BIO *hash, BIO *out
|
||||
*/
|
||||
static PKCS7 *cat_pkcs7_extract(FILE_FORMAT_CTX *ctx)
|
||||
{
|
||||
return pkcs7_get(ctx->options->indata, ctx->cat_ctx->sigpos, ctx->cat_ctx->siglen);
|
||||
return PKCS7_dup(ctx->cat_ctx->p7);
|
||||
}
|
||||
|
||||
/*
|
||||
@ -128,36 +169,33 @@ static PKCS7 *cat_pkcs7_extract(FILE_FORMAT_CTX *ctx)
|
||||
*/
|
||||
static PKCS7 *cat_pkcs7_prepare(FILE_FORMAT_CTX *ctx, BIO *hash, BIO *outdata)
|
||||
{
|
||||
PKCS7 *cursig = NULL, *p7 = NULL;
|
||||
PKCS7 *p7 = NULL;
|
||||
|
||||
/* squash unused parameter warnings */
|
||||
(void)outdata;
|
||||
(void)hash;
|
||||
|
||||
/* Obtain an existing signature */
|
||||
cursig = pkcs7_get(ctx->options->indata, ctx->cat_ctx->sigpos, ctx->cat_ctx->siglen);
|
||||
if (!cursig) {
|
||||
printf("Unable to extract existing signature\n");
|
||||
return NULL; /* FAILED */
|
||||
}
|
||||
if (ctx->options->cmd == CMD_ADD || ctx->options->cmd == CMD_ATTACH) {
|
||||
p7 = cursig;
|
||||
p7 = PKCS7_dup(ctx->cat_ctx->p7);
|
||||
} else if (ctx->options->cmd == CMD_SIGN) {
|
||||
/* Create a new signature */
|
||||
p7 = pkcs7_create(ctx);
|
||||
if (!p7) {
|
||||
printf("Creating a new signature failed\n");
|
||||
PKCS7_free(cursig);
|
||||
return NULL; /* FAILED */
|
||||
}
|
||||
if (!add_ms_ctl_object(p7, cursig)) {
|
||||
if (!cat_add_ms_ctl_object(p7)) {
|
||||
printf("Adding MS_CTL_OBJID failed\n");
|
||||
PKCS7_free(p7);
|
||||
PKCS7_free(cursig);
|
||||
return NULL; /* FAILED */
|
||||
}
|
||||
PKCS7_free(cursig);
|
||||
}
|
||||
if (!cat_sign_ms_ctl_content(p7, ctx->cat_ctx->p7->d.sign->contents)) {
|
||||
printf("Failed to set signed content\n");
|
||||
PKCS7_free(p7);
|
||||
return 0; /* FAILED */
|
||||
}
|
||||
}
|
||||
return p7; /* OK */
|
||||
}
|
||||
|
||||
@ -224,6 +262,7 @@ static void cat_ctx_cleanup(FILE_FORMAT_CTX *ctx, BIO *hash, BIO *outdata)
|
||||
}
|
||||
}
|
||||
unmap_file(ctx->options->indata, ctx->cat_ctx->fileend);
|
||||
PKCS7_free(ctx->cat_ctx->p7);
|
||||
OPENSSL_free(ctx->cat_ctx);
|
||||
OPENSSL_free(ctx);
|
||||
}
|
||||
@ -233,7 +272,7 @@ static void cat_ctx_cleanup(FILE_FORMAT_CTX *ctx, BIO *hash, BIO *outdata)
|
||||
*/
|
||||
|
||||
/*
|
||||
* Verify mapped CAT file TODO and create CAT format specific structure.
|
||||
* Verify mapped CAT file and create CAT format specific structure.
|
||||
* [in] indata: mapped CAT file (unused)
|
||||
* [in] filesize: size of CAT file
|
||||
* [returns] pointer to CAT format specific structure
|
||||
@ -241,17 +280,69 @@ static void cat_ctx_cleanup(FILE_FORMAT_CTX *ctx, BIO *hash, BIO *outdata)
|
||||
static CAT_CTX *cat_ctx_get(char *indata, uint32_t filesize)
|
||||
{
|
||||
CAT_CTX *cat_ctx;
|
||||
PKCS7 *p7 = pkcs7_get(indata, 0, filesize);
|
||||
|
||||
/* squash the unused parameter warning */
|
||||
(void)indata;
|
||||
|
||||
if (!p7)
|
||||
return NULL; /* FAILED */
|
||||
cat_ctx = OPENSSL_zalloc(sizeof(CAT_CTX));
|
||||
cat_ctx->p7 = p7;
|
||||
cat_ctx->sigpos = 0;
|
||||
cat_ctx->siglen = filesize;
|
||||
cat_ctx->fileend = filesize;
|
||||
return cat_ctx; /* OK */
|
||||
}
|
||||
|
||||
/*
|
||||
* Add "1.3.6.1.4.1.311.10.1" MS_CTL_OBJID signed attribute
|
||||
* [in, out] p7: new PKCS#7 signature
|
||||
* [returns] 0 on error or 1 on success
|
||||
*/
|
||||
static int cat_add_ms_ctl_object(PKCS7 *p7)
|
||||
{
|
||||
STACK_OF(PKCS7_SIGNER_INFO) *signer_info;
|
||||
PKCS7_SIGNER_INFO *si;
|
||||
|
||||
signer_info = PKCS7_get_signer_info(p7);
|
||||
if (!signer_info)
|
||||
return 0; /* FAILED */
|
||||
si = sk_PKCS7_SIGNER_INFO_value(signer_info, 0);
|
||||
if (!si)
|
||||
return 0; /* FAILED */
|
||||
if (!PKCS7_add_signed_attribute(si, NID_pkcs9_contentType,
|
||||
V_ASN1_OBJECT, OBJ_txt2obj(MS_CTL_OBJID, 1)))
|
||||
return 0; /* FAILED */
|
||||
return 1; /* OK */
|
||||
}
|
||||
|
||||
/*
|
||||
* Sign the MS CTL blob.
|
||||
* Certificate Trust List (CTL) is a list of file names or thumbprints.
|
||||
* All the items in this list are authenticated (approved) by the signing entity.
|
||||
* [in, out] p7: new PKCS#7 signature
|
||||
* [in] contents: Certificate Trust List (CTL)
|
||||
* [returns] 0 on error or 1 on success
|
||||
*/
|
||||
static int cat_sign_ms_ctl_content(PKCS7 *p7, PKCS7 *contents)
|
||||
{
|
||||
u_char *content;
|
||||
int seqhdrlen, content_length;
|
||||
|
||||
seqhdrlen = asn1_simple_hdr_len(contents->d.other->value.sequence->data,
|
||||
contents->d.other->value.sequence->length);
|
||||
content = contents->d.other->value.sequence->data + seqhdrlen;
|
||||
content_length = contents->d.other->value.sequence->length - seqhdrlen;
|
||||
|
||||
if (!pkcs7_sign_content(p7, content, content_length)) {
|
||||
printf("Failed to sign content\n");
|
||||
return 0; /* FAILED */
|
||||
}
|
||||
if (!PKCS7_set_content(p7, PKCS7_dup(contents))) {
|
||||
printf("PKCS7_set_content failed\n");
|
||||
return 0; /* FAILED */
|
||||
}
|
||||
return 1; /* OK */
|
||||
}
|
||||
|
||||
/*
|
||||
Local Variables:
|
||||
c-basic-offset: 4
|
||||
|
436
helpers.c
436
helpers.c
@ -9,12 +9,12 @@
|
||||
#include "helpers.h"
|
||||
|
||||
/* Prototypes */
|
||||
static int pkcs7_set_content_blob(PKCS7 *sig, PKCS7 *cursig);
|
||||
static SpcSpOpusInfo *spc_sp_opus_info_create(FILE_FORMAT_CTX *ctx);
|
||||
static int spc_indirect_data_content_get(u_char **blob, int *len, FILE_FORMAT_CTX *ctx);
|
||||
static int pkcs7_set_spc_indirect_data_content(PKCS7 *p7, BIO *hash, u_char *buf, int len, FILE_FORMAT_CTX *ctx);
|
||||
static int pkcs7_signer_info_add_spc_sp_opus_info(PKCS7_SIGNER_INFO *si, FILE_FORMAT_CTX *ctx);
|
||||
static int pkcs7_signer_info_add_purpose(PKCS7_SIGNER_INFO *si, FILE_FORMAT_CTX *ctx);
|
||||
static STACK_OF(X509) *X509_chain_get_sorted(FILE_FORMAT_CTX *ctx, int signer);
|
||||
static int X509_compare(const X509 *const *a, const X509 *const *b);
|
||||
|
||||
/*
|
||||
* Common functions
|
||||
@ -108,60 +108,6 @@ void unmap_file(char *indata, const size_t size)
|
||||
#endif /* WIN32 */
|
||||
}
|
||||
|
||||
/*
|
||||
* [in, out] si: PKCS7_SIGNER_INFO structure
|
||||
* [in] ctx: FILE_FORMAT_CTX structure
|
||||
* [returns] 0 on error or 1 on success
|
||||
*/
|
||||
static int pkcs7_signer_info_add_spc_sp_opus_info(PKCS7_SIGNER_INFO *si, FILE_FORMAT_CTX *ctx)
|
||||
{
|
||||
SpcSpOpusInfo *opus;
|
||||
ASN1_STRING *astr;
|
||||
int len;
|
||||
u_char *p = NULL;
|
||||
|
||||
opus = spc_sp_opus_info_create(ctx);
|
||||
if ((len = i2d_SpcSpOpusInfo(opus, NULL)) <= 0
|
||||
|| (p = OPENSSL_malloc((size_t)len)) == NULL) {
|
||||
SpcSpOpusInfo_free(opus);
|
||||
return 0; /* FAILED */
|
||||
}
|
||||
i2d_SpcSpOpusInfo(opus, &p);
|
||||
p -= len;
|
||||
astr = ASN1_STRING_new();
|
||||
ASN1_STRING_set(astr, p, len);
|
||||
OPENSSL_free(p);
|
||||
SpcSpOpusInfo_free(opus);
|
||||
return PKCS7_add_signed_attribute(si, OBJ_txt2nid(SPC_SP_OPUS_INFO_OBJID),
|
||||
V_ASN1_SEQUENCE, astr);
|
||||
}
|
||||
|
||||
/*
|
||||
* [in, out] si: PKCS7_SIGNER_INFO structure
|
||||
* [in] ctx: structure holds input and output data
|
||||
* [returns] 0 on error or 1 on success
|
||||
*/
|
||||
static int pkcs7_signer_info_add_purpose(PKCS7_SIGNER_INFO *si, FILE_FORMAT_CTX *ctx)
|
||||
{
|
||||
static const u_char purpose_ind[] = {
|
||||
0x30, 0x0c, 0x06, 0x0a, 0x2b, 0x06, 0x01, 0x04,
|
||||
0x01, 0x82, 0x37, 0x02, 0x01, 0x15
|
||||
};
|
||||
static const u_char purpose_comm[] = {
|
||||
0x30, 0x0c, 0x06, 0x0a, 0x2b, 0x06, 0x01, 0x04,
|
||||
0x01, 0x82, 0x37, 0x02, 0x01, 0x16
|
||||
};
|
||||
ASN1_STRING *purpose = ASN1_STRING_new();
|
||||
|
||||
if (ctx->options->comm) {
|
||||
ASN1_STRING_set(purpose, purpose_comm, sizeof purpose_comm);
|
||||
} else {
|
||||
ASN1_STRING_set(purpose, purpose_ind, sizeof purpose_ind);
|
||||
}
|
||||
return PKCS7_add_signed_attribute(si, OBJ_txt2nid(SPC_STATEMENT_TYPE_OBJID),
|
||||
V_ASN1_SEQUENCE, purpose);
|
||||
}
|
||||
|
||||
/*
|
||||
* Add a custom, non-trusted time to the PKCS7 structure to prevent OpenSSL
|
||||
* adding the _current_ time. This allows to create a deterministic signature
|
||||
@ -216,81 +162,6 @@ PKCS7 *pkcs7_get_sigfile(FILE_FORMAT_CTX *ctx)
|
||||
return p7;
|
||||
}
|
||||
|
||||
/*
|
||||
* X.690-compliant certificate comparison function
|
||||
* Windows requires catalog files to use PKCS#7
|
||||
* content ordering specified in X.690 section 11.6
|
||||
* https://support.microsoft.com/en-us/topic/october-13-2020-kb4580358-security-only-update-d3f6eb3c-d7c4-a9cb-0de6-759386bf7113
|
||||
* This algorithm is different from X509_cmp()
|
||||
* [in] a_ptr, b_ptr: pointers to X509 certificates
|
||||
* [returns] certificates order
|
||||
*/
|
||||
static int X509_compare(const X509 *const *a, const X509 *const *b)
|
||||
{
|
||||
u_char *a_data, *b_data, *a_tmp, *b_tmp;
|
||||
size_t a_len, b_len;
|
||||
int ret;
|
||||
|
||||
a_len = (size_t)i2d_X509(*a, NULL);
|
||||
a_tmp = a_data = OPENSSL_malloc(a_len);
|
||||
i2d_X509(*a, &a_tmp);
|
||||
|
||||
b_len = (size_t)i2d_X509(*b, NULL);
|
||||
b_tmp = b_data = OPENSSL_malloc(b_len);
|
||||
i2d_X509(*b, &b_tmp);
|
||||
|
||||
ret = memcmp(a_data, b_data, MIN(a_len, b_len));
|
||||
OPENSSL_free(a_data);
|
||||
OPENSSL_free(b_data);
|
||||
|
||||
if (ret == 0 && a_len != b_len) /* identical up to the length of the shorter DER */
|
||||
ret = a_len < b_len ? -1 : 1; /* shorter is smaller */
|
||||
return ret;
|
||||
}
|
||||
|
||||
/*
|
||||
* Create certificate chain sorted in ascending order by their DER encoding.
|
||||
* [in] ctx: structure holds input and output data
|
||||
* [in] signer: signer's certificate number in the certificate chain
|
||||
* [returns] sorted certificate chain
|
||||
*/
|
||||
static STACK_OF(X509) *X509_chain_get_sorted(FILE_FORMAT_CTX *ctx, int signer)
|
||||
{
|
||||
int i;
|
||||
STACK_OF(X509) *chain = sk_X509_new(X509_compare);
|
||||
|
||||
/* add the signer's certificate */
|
||||
if (ctx->options->cert != NULL && !sk_X509_push(chain, ctx->options->cert)) {
|
||||
sk_X509_free(chain);
|
||||
return NULL;
|
||||
}
|
||||
if (signer != -1 && !sk_X509_push(chain, sk_X509_value(ctx->options->certs, signer))) {
|
||||
sk_X509_free(chain);
|
||||
return NULL;
|
||||
}
|
||||
/* add the certificate chain */
|
||||
for (i=0; i<sk_X509_num(ctx->options->certs); i++) {
|
||||
if (i == signer)
|
||||
continue;
|
||||
if (!sk_X509_push(chain, sk_X509_value(ctx->options->certs, i))) {
|
||||
sk_X509_free(chain);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
/* add all cross certificates */
|
||||
if (ctx->options->xcerts) {
|
||||
for (i=0; i<sk_X509_num(ctx->options->xcerts); i++) {
|
||||
if (!sk_X509_push(chain, sk_X509_value(ctx->options->xcerts, i))) {
|
||||
sk_X509_free(chain);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* sort certificate chain using the supplied comparison function */
|
||||
sk_X509_sort(chain);
|
||||
return chain;
|
||||
}
|
||||
|
||||
/*
|
||||
* Allocate, set type, add content and return a new PKCS#7 signature
|
||||
* [in] ctx: structure holds input and output data
|
||||
@ -305,7 +176,7 @@ PKCS7 *pkcs7_create(FILE_FORMAT_CTX *ctx)
|
||||
|
||||
p7 = PKCS7_new();
|
||||
PKCS7_set_type(p7, NID_pkcs7_signed);
|
||||
|
||||
PKCS7_content_new(p7, NID_pkcs7_data);
|
||||
if (ctx->options->cert != NULL) {
|
||||
/*
|
||||
* the private key and corresponding certificate are parsed from the PKCS12
|
||||
@ -335,17 +206,14 @@ PKCS7 *pkcs7_create(FILE_FORMAT_CTX *ctx)
|
||||
}
|
||||
}
|
||||
pkcs7_signer_info_add_signing_time(si, ctx);
|
||||
|
||||
if (!pkcs7_signer_info_add_purpose(si, ctx))
|
||||
if (!pkcs7_signer_info_add_purpose(si, ctx)) {
|
||||
return NULL; /* FAILED */
|
||||
|
||||
}
|
||||
if ((ctx->options->desc || ctx->options->url) &&
|
||||
!pkcs7_signer_info_add_spc_sp_opus_info(si, ctx)) {
|
||||
printf("Couldn't allocate memory for opus info\n");
|
||||
return NULL; /* FAILED */
|
||||
}
|
||||
PKCS7_content_new(p7, NID_pkcs7_data);
|
||||
|
||||
/* create X509 chain sorted in ascending order by their DER encoding */
|
||||
chain = X509_chain_get_sorted(ctx, signer);
|
||||
if (chain == NULL) {
|
||||
@ -366,12 +234,12 @@ PKCS7 *pkcs7_create(FILE_FORMAT_CTX *ctx)
|
||||
}
|
||||
|
||||
/*
|
||||
* PE, MSI, CAB and APPX file specific
|
||||
* Add "1.3.6.1.4.1.311.2.1.4" SPC_INDIRECT_DATA_OBJID signed attribute
|
||||
* [in, out] p7: new PKCS#7 signature
|
||||
* [in] hash: message digest BIO
|
||||
* [in] ctx: structure holds input and output data
|
||||
* [returns] 0 on error or 1 on success
|
||||
*/
|
||||
int add_indirect_data_object(PKCS7 *p7, BIO *hash, FILE_FORMAT_CTX *ctx)
|
||||
int add_indirect_data_object(PKCS7 *p7)
|
||||
{
|
||||
STACK_OF(PKCS7_SIGNER_INFO) *signer_info;
|
||||
PKCS7_SIGNER_INFO *si;
|
||||
@ -385,67 +253,85 @@ int add_indirect_data_object(PKCS7 *p7, BIO *hash, FILE_FORMAT_CTX *ctx)
|
||||
if (!PKCS7_add_signed_attribute(si, NID_pkcs9_contentType,
|
||||
V_ASN1_OBJECT, OBJ_txt2obj(SPC_INDIRECT_DATA_OBJID, 1)))
|
||||
return 0; /* FAILED */
|
||||
if (!pkcs7_set_data_content(p7, hash, ctx)) {
|
||||
printf("Signing failed\n");
|
||||
return 1; /* OK */
|
||||
}
|
||||
|
||||
/*
|
||||
* PE, MSI, CAB and APPX format specific
|
||||
* Sign the MS Authenticode spcIndirectDataContent blob.
|
||||
* The spcIndirectDataContent structure is used in Authenticode signatures
|
||||
* to store the digest and other attributes of the signed file.
|
||||
* [in, out] p7: new PKCS#7 signature
|
||||
* [in] hash: message digest BIO
|
||||
* [in] ctx: structure holds input and output data
|
||||
* [returns] 0 on error or 1 on success
|
||||
*/
|
||||
int sign_spc_indirect_data_content(PKCS7 *p7, BIO *hash, FILE_FORMAT_CTX *ctx)
|
||||
{
|
||||
u_char mdbuf[5 * EVP_MAX_MD_SIZE + 24];
|
||||
int mdlen, seqhdrlen, hashlen;
|
||||
PKCS7 *td7;
|
||||
u_char *p = NULL;
|
||||
int len = 0;
|
||||
u_char *buf;
|
||||
|
||||
hashlen = ctx->format->hash_length_get(ctx);
|
||||
if (hashlen > EVP_MAX_MD_SIZE) {
|
||||
/* APPX format specific */
|
||||
mdlen = BIO_read(hash, (char*)mdbuf, hashlen);
|
||||
} else {
|
||||
mdlen = BIO_gets(hash, (char*)mdbuf, EVP_MAX_MD_SIZE);
|
||||
}
|
||||
if (!spc_indirect_data_content_get(&p, &len, ctx))
|
||||
return 0; /* FAILED */
|
||||
|
||||
buf = OPENSSL_malloc(SIZE_64K);
|
||||
memcpy(buf, p, (size_t)len);
|
||||
OPENSSL_free(p);
|
||||
memcpy(buf + len, mdbuf, (size_t)mdlen);
|
||||
seqhdrlen = asn1_simple_hdr_len(buf, len);
|
||||
|
||||
if (!pkcs7_sign_content(p7, buf + seqhdrlen, len - seqhdrlen + mdlen)) {
|
||||
printf("Failed to sign content\n");
|
||||
OPENSSL_free(buf);
|
||||
return 0; /* FAILED */
|
||||
}
|
||||
td7 = PKCS7_new();
|
||||
td7->type = OBJ_txt2obj(SPC_INDIRECT_DATA_OBJID, 1);
|
||||
td7->d.other = ASN1_TYPE_new();
|
||||
td7->d.other->type = V_ASN1_SEQUENCE;
|
||||
td7->d.other->value.sequence = ASN1_STRING_new();
|
||||
ASN1_STRING_set(td7->d.other->value.sequence, buf, len + mdlen);
|
||||
OPENSSL_free(buf);
|
||||
if (!PKCS7_set_content(p7, td7)) {
|
||||
printf("PKCS7_set_content failed\n");
|
||||
PKCS7_free(td7);
|
||||
return 0; /* FAILED */
|
||||
}
|
||||
return 1; /* OK */
|
||||
}
|
||||
|
||||
/*
|
||||
* Signs the data and place the signature in p7
|
||||
* [in, out] p7: new PKCS#7 signature
|
||||
* [in] cursig: current PKCS#7 signature
|
||||
* [returns] 0 on error or 1 on success
|
||||
* [in] data: content data
|
||||
* [in] len: content length
|
||||
*/
|
||||
int add_ms_ctl_object(PKCS7 *p7, PKCS7 *cursig)
|
||||
int pkcs7_sign_content(PKCS7 *p7, u_char *data, int len)
|
||||
{
|
||||
STACK_OF(PKCS7_SIGNER_INFO) *signer_info;
|
||||
PKCS7_SIGNER_INFO *si;
|
||||
BIO *p7bio;
|
||||
|
||||
signer_info = PKCS7_get_signer_info(p7);
|
||||
if (!signer_info)
|
||||
return 0; /* FAILED */
|
||||
si = sk_PKCS7_SIGNER_INFO_value(signer_info, 0);
|
||||
if (!si)
|
||||
return 0; /* FAILED */
|
||||
if (!PKCS7_add_signed_attribute(si, NID_pkcs9_contentType,
|
||||
V_ASN1_OBJECT, OBJ_txt2obj(MS_CTL_OBJID, 1)))
|
||||
return 0; /* FAILED */
|
||||
if (!pkcs7_set_content_blob(p7, cursig)) {
|
||||
printf("Signing failed\n");
|
||||
return 0; /* FAILED */
|
||||
}
|
||||
return 1; /* OK */
|
||||
}
|
||||
|
||||
static int pkcs7_set_content_blob(PKCS7 *sig, PKCS7 *cursig)
|
||||
{
|
||||
PKCS7 *contents;
|
||||
u_char *content;
|
||||
int seqhdrlen, content_length;
|
||||
BIO *sigbio;
|
||||
|
||||
contents = cursig->d.sign->contents;
|
||||
seqhdrlen = asn1_simple_hdr_len(contents->d.other->value.sequence->data,
|
||||
contents->d.other->value.sequence->length);
|
||||
content = contents->d.other->value.sequence->data + seqhdrlen;
|
||||
content_length = contents->d.other->value.sequence->length - seqhdrlen;
|
||||
|
||||
if ((sigbio = PKCS7_dataInit(sig, NULL)) == NULL) {
|
||||
if ((p7bio = PKCS7_dataInit(p7, NULL)) == NULL) {
|
||||
printf("PKCS7_dataInit failed\n");
|
||||
return 0; /* FAILED */
|
||||
}
|
||||
BIO_write(sigbio, content, content_length);
|
||||
(void)BIO_flush(sigbio);
|
||||
if (!PKCS7_dataFinal(sig, sigbio)) {
|
||||
BIO_write(p7bio, data, len);
|
||||
(void)BIO_flush(p7bio);
|
||||
if (!PKCS7_dataFinal(p7, p7bio)) {
|
||||
printf("PKCS7_dataFinal failed\n");
|
||||
return 0; /* FAILED */
|
||||
}
|
||||
BIO_free_all(sigbio);
|
||||
if (!PKCS7_set_content(sig, PKCS7_dup(contents))) {
|
||||
printf("PKCS7_set_content failed\n");
|
||||
return 0; /* FAILED */
|
||||
}
|
||||
BIO_free_all(p7bio);
|
||||
return 1; /* OK */
|
||||
}
|
||||
|
||||
@ -525,32 +411,6 @@ int is_content_type(PKCS7 *p7, const char *objid)
|
||||
return ret;
|
||||
}
|
||||
|
||||
/*
|
||||
* [out] p7: new PKCS#7 signature
|
||||
* [in] hash: message digest BIO
|
||||
* [in] ctx: structure holds input and output data
|
||||
* [returns] 0 on error or 1 on success
|
||||
*/
|
||||
int pkcs7_set_data_content(PKCS7 *p7, BIO *hash, FILE_FORMAT_CTX *ctx)
|
||||
{
|
||||
u_char *p = NULL;
|
||||
int len = 0;
|
||||
u_char *buf;
|
||||
|
||||
if (!spc_indirect_data_content_get(&p, &len, ctx))
|
||||
return 0; /* FAILED */
|
||||
buf = OPENSSL_malloc(SIZE_64K);
|
||||
memcpy(buf, p, (size_t)len);
|
||||
OPENSSL_free(p);
|
||||
if (!pkcs7_set_spc_indirect_data_content(p7, hash, buf, len, ctx)) {
|
||||
OPENSSL_free(buf);
|
||||
return 0; /* FAILED */
|
||||
}
|
||||
OPENSSL_free(buf);
|
||||
|
||||
return 1; /* OK */
|
||||
}
|
||||
|
||||
/*
|
||||
* PE and CAB format specific
|
||||
* [in] none
|
||||
@ -673,56 +533,132 @@ static int spc_indirect_data_content_get(u_char **blob, int *len, FILE_FORMAT_CT
|
||||
}
|
||||
|
||||
/*
|
||||
* Replace the data part with the MS Authenticode spcIndirectDataContent blob
|
||||
* [out] p7: new PKCS#7 signature
|
||||
* [in] hash: message digest BIO
|
||||
* [in] blob: SpcIndirectDataContent data
|
||||
* [in] len: SpcIndirectDataContent data length
|
||||
* [in, out] si: PKCS7_SIGNER_INFO structure
|
||||
* [in] ctx: FILE_FORMAT_CTX structure
|
||||
* [returns] 0 on error or 1 on success
|
||||
*/
|
||||
static int pkcs7_set_spc_indirect_data_content(PKCS7 *p7, BIO *hash, u_char *buf, int len, FILE_FORMAT_CTX *ctx)
|
||||
static int pkcs7_signer_info_add_spc_sp_opus_info(PKCS7_SIGNER_INFO *si, FILE_FORMAT_CTX *ctx)
|
||||
{
|
||||
u_char mdbuf[5 * EVP_MAX_MD_SIZE + 24];
|
||||
int mdlen, seqhdrlen, hashlen;
|
||||
BIO *bio;
|
||||
PKCS7 *td7;
|
||||
SpcSpOpusInfo *opus;
|
||||
ASN1_STRING *astr;
|
||||
int len;
|
||||
u_char *p = NULL;
|
||||
|
||||
hashlen = ctx->format->hash_length_get(ctx);
|
||||
if (hashlen > EVP_MAX_MD_SIZE) {
|
||||
/* APPX format specific */
|
||||
mdlen = BIO_read(hash, (char*)mdbuf, hashlen);
|
||||
opus = spc_sp_opus_info_create(ctx);
|
||||
if ((len = i2d_SpcSpOpusInfo(opus, NULL)) <= 0
|
||||
|| (p = OPENSSL_malloc((size_t)len)) == NULL) {
|
||||
SpcSpOpusInfo_free(opus);
|
||||
return 0; /* FAILED */
|
||||
}
|
||||
i2d_SpcSpOpusInfo(opus, &p);
|
||||
p -= len;
|
||||
astr = ASN1_STRING_new();
|
||||
ASN1_STRING_set(astr, p, len);
|
||||
OPENSSL_free(p);
|
||||
SpcSpOpusInfo_free(opus);
|
||||
return PKCS7_add_signed_attribute(si, OBJ_txt2nid(SPC_SP_OPUS_INFO_OBJID),
|
||||
V_ASN1_SEQUENCE, astr);
|
||||
}
|
||||
|
||||
/*
|
||||
* [in, out] si: PKCS7_SIGNER_INFO structure
|
||||
* [in] ctx: structure holds input and output data
|
||||
* [returns] 0 on error or 1 on success
|
||||
*/
|
||||
static int pkcs7_signer_info_add_purpose(PKCS7_SIGNER_INFO *si, FILE_FORMAT_CTX *ctx)
|
||||
{
|
||||
static const u_char purpose_ind[] = {
|
||||
0x30, 0x0c, 0x06, 0x0a, 0x2b, 0x06, 0x01, 0x04,
|
||||
0x01, 0x82, 0x37, 0x02, 0x01, 0x15
|
||||
};
|
||||
static const u_char purpose_comm[] = {
|
||||
0x30, 0x0c, 0x06, 0x0a, 0x2b, 0x06, 0x01, 0x04,
|
||||
0x01, 0x82, 0x37, 0x02, 0x01, 0x16
|
||||
};
|
||||
ASN1_STRING *purpose = ASN1_STRING_new();
|
||||
|
||||
if (ctx->options->comm) {
|
||||
ASN1_STRING_set(purpose, purpose_comm, sizeof purpose_comm);
|
||||
} else {
|
||||
mdlen = BIO_gets(hash, (char*)mdbuf, EVP_MAX_MD_SIZE);
|
||||
ASN1_STRING_set(purpose, purpose_ind, sizeof purpose_ind);
|
||||
}
|
||||
memcpy(buf + len, mdbuf, (size_t)mdlen);
|
||||
seqhdrlen = asn1_simple_hdr_len(buf, len);
|
||||
return PKCS7_add_signed_attribute(si, OBJ_txt2nid(SPC_STATEMENT_TYPE_OBJID),
|
||||
V_ASN1_SEQUENCE, purpose);
|
||||
}
|
||||
|
||||
if ((bio = PKCS7_dataInit(p7, NULL)) == NULL) {
|
||||
printf("PKCS7_dataInit failed\n");
|
||||
return 0; /* FAILED */
|
||||
}
|
||||
BIO_write(bio, buf + seqhdrlen, len - seqhdrlen + mdlen);
|
||||
(void)BIO_flush(bio);
|
||||
/*
|
||||
* Create certificate chain sorted in ascending order by their DER encoding.
|
||||
* [in] ctx: structure holds input and output data
|
||||
* [in] signer: signer's certificate number in the certificate chain
|
||||
* [returns] sorted certificate chain
|
||||
*/
|
||||
static STACK_OF(X509) *X509_chain_get_sorted(FILE_FORMAT_CTX *ctx, int signer)
|
||||
{
|
||||
int i;
|
||||
STACK_OF(X509) *chain = sk_X509_new(X509_compare);
|
||||
|
||||
if (!PKCS7_dataFinal(p7, bio)) {
|
||||
printf("PKCS7_dataFinal failed\n");
|
||||
return 0; /* FAILED */
|
||||
/* add the signer's certificate */
|
||||
if (ctx->options->cert != NULL && !sk_X509_push(chain, ctx->options->cert)) {
|
||||
sk_X509_free(chain);
|
||||
return NULL;
|
||||
}
|
||||
BIO_free_all(bio);
|
||||
if (signer != -1 && !sk_X509_push(chain, sk_X509_value(ctx->options->certs, signer))) {
|
||||
sk_X509_free(chain);
|
||||
return NULL;
|
||||
}
|
||||
/* add the certificate chain */
|
||||
for (i=0; i<sk_X509_num(ctx->options->certs); i++) {
|
||||
if (i == signer)
|
||||
continue;
|
||||
if (!sk_X509_push(chain, sk_X509_value(ctx->options->certs, i))) {
|
||||
sk_X509_free(chain);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
/* add all cross certificates */
|
||||
if (ctx->options->xcerts) {
|
||||
for (i=0; i<sk_X509_num(ctx->options->xcerts); i++) {
|
||||
if (!sk_X509_push(chain, sk_X509_value(ctx->options->xcerts, i))) {
|
||||
sk_X509_free(chain);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* sort certificate chain using the supplied comparison function */
|
||||
sk_X509_sort(chain);
|
||||
return chain;
|
||||
}
|
||||
|
||||
td7 = PKCS7_new();
|
||||
td7->type = OBJ_txt2obj(SPC_INDIRECT_DATA_OBJID, 1);
|
||||
td7->d.other = ASN1_TYPE_new();
|
||||
td7->d.other->type = V_ASN1_SEQUENCE;
|
||||
td7->d.other->value.sequence = ASN1_STRING_new();
|
||||
ASN1_STRING_set(td7->d.other->value.sequence, buf, len + mdlen);
|
||||
if (!PKCS7_set_content(p7, td7)) {
|
||||
PKCS7_free(td7);
|
||||
printf("PKCS7_set_content failed\n");
|
||||
return 0; /* FAILED */
|
||||
}
|
||||
return 1; /* OK */
|
||||
/*
|
||||
* X.690-compliant certificate comparison function
|
||||
* Windows requires catalog files to use PKCS#7
|
||||
* content ordering specified in X.690 section 11.6
|
||||
* https://support.microsoft.com/en-us/topic/october-13-2020-kb4580358-security-only-update-d3f6eb3c-d7c4-a9cb-0de6-759386bf7113
|
||||
* This algorithm is different from X509_cmp()
|
||||
* [in] a_ptr, b_ptr: pointers to X509 certificates
|
||||
* [returns] certificates order
|
||||
*/
|
||||
static int X509_compare(const X509 *const *a, const X509 *const *b)
|
||||
{
|
||||
u_char *a_data, *b_data, *a_tmp, *b_tmp;
|
||||
size_t a_len, b_len;
|
||||
int ret;
|
||||
|
||||
a_len = (size_t)i2d_X509(*a, NULL);
|
||||
a_tmp = a_data = OPENSSL_malloc(a_len);
|
||||
i2d_X509(*a, &a_tmp);
|
||||
|
||||
b_len = (size_t)i2d_X509(*b, NULL);
|
||||
b_tmp = b_data = OPENSSL_malloc(b_len);
|
||||
i2d_X509(*b, &b_tmp);
|
||||
|
||||
ret = memcmp(a_data, b_data, MIN(a_len, b_len));
|
||||
OPENSSL_free(a_data);
|
||||
OPENSSL_free(b_data);
|
||||
|
||||
if (ret == 0 && a_len != b_len) /* identical up to the length of the shorter DER */
|
||||
ret = a_len < b_len ? -1 : 1; /* shorter is smaller */
|
||||
return ret;
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -12,14 +12,13 @@ void unmap_file(char *indata, const size_t size);
|
||||
int pkcs7_signer_info_add_signing_time(PKCS7_SIGNER_INFO *si, FILE_FORMAT_CTX *ctx);
|
||||
PKCS7 *pkcs7_get_sigfile(FILE_FORMAT_CTX *ctx);
|
||||
PKCS7 *pkcs7_create(FILE_FORMAT_CTX *ctx);
|
||||
void add_content_type(PKCS7 *p7);
|
||||
int add_indirect_data_object(PKCS7 *p7, BIO *hash, FILE_FORMAT_CTX *ctx);
|
||||
int add_ms_ctl_object(PKCS7 *p7, PKCS7 *cursig);
|
||||
int add_indirect_data_object(PKCS7 *p7);
|
||||
int sign_spc_indirect_data_content(PKCS7 *p7, BIO *hash, FILE_FORMAT_CTX *ctx);
|
||||
int pkcs7_sign_content(PKCS7 *p7, u_char *data, int len);
|
||||
int asn1_simple_hdr_len(const u_char *p, int len);
|
||||
int bio_hash_data(BIO *hash, char *indata, size_t idx, size_t fileend);
|
||||
void print_hash(const char *descript1, const char *descript2, const u_char *hashbuf, int length);
|
||||
int is_content_type(PKCS7 *p7, const char *objid);
|
||||
int pkcs7_set_data_content(PKCS7 *sig, BIO *hash, FILE_FORMAT_CTX *ctx);
|
||||
SpcLink *spc_link_obsolete_get(void);
|
||||
PKCS7 *pkcs7_get(char *indata, uint32_t sigpos, uint32_t siglen);
|
||||
int compare_digests(u_char *mdbuf, u_char *cmdbuf, int mdtype);
|
||||
|
8
msi.c
8
msi.c
@ -668,12 +668,16 @@ static PKCS7 *msi_pkcs7_prepare(FILE_FORMAT_CTX *ctx, BIO *hash, BIO *outdata)
|
||||
printf("Creating a new signature failed\n");
|
||||
return NULL; /* FAILED */
|
||||
}
|
||||
if (!add_indirect_data_object(p7, hash, ctx)) {
|
||||
if (!add_indirect_data_object(p7)) {
|
||||
printf("Adding SPC_INDIRECT_DATA_OBJID failed\n");
|
||||
PKCS7_free(p7);
|
||||
return NULL; /* FAILED */
|
||||
}
|
||||
}
|
||||
if (!sign_spc_indirect_data_content(p7, hash, ctx)) {
|
||||
printf("Failed to set signed content\n");
|
||||
return NULL; /* FAILED */
|
||||
}
|
||||
}
|
||||
if (ctx->options->nest)
|
||||
ctx->options->prevsig = cursig;
|
||||
return p7;
|
||||
|
@ -1236,7 +1236,7 @@ static int trusted_cert(X509 *cert, int error) {
|
||||
BIO_gets(bhash, (char *)mdbuf, EVP_MD_size(md));
|
||||
BIO_free_all(bhash);
|
||||
|
||||
hex = OPENSSL_buf2hexstr(mdbuf, (size_t)EVP_MD_size(md));
|
||||
hex = OPENSSL_buf2hexstr(mdbuf, (long)EVP_MD_size(md));
|
||||
if (!hex) {
|
||||
return 0; /* FAILED */
|
||||
}
|
||||
@ -1746,6 +1746,13 @@ static int verify_authenticode(FILE_FORMAT_CTX *ctx, PKCS7 *p7, time_t time, X50
|
||||
p7->d.sign->contents->d.other->value.sequence->length);
|
||||
}
|
||||
printf("\nSigning Certificate Chain:\n");
|
||||
/*
|
||||
* In the PKCS7_verify() function, the BIO *indata parameter refers to
|
||||
* the signed data if the content is detached from p7.
|
||||
* Otherwise, indata should be NULL, and then the signed data must be in p7.
|
||||
* The OpenSSL error workaround is to put the inner content into BIO *indata parameter
|
||||
* https://github.com/openssl/openssl/pull/22575
|
||||
*/
|
||||
if (!PKCS7_verify(p7, NULL, store, bio, NULL, 0)) {
|
||||
printf("\nPKCS7_verify error\n");
|
||||
X509_STORE_free(store);
|
||||
@ -2304,21 +2311,25 @@ static int verify_member(FILE_FORMAT_CTX *ctx, CatalogAuthAttr *attribute)
|
||||
ASN1_TYPE_free(content);
|
||||
if (mdtype == -1) {
|
||||
printf("Failed to extract current message digest\n\n");
|
||||
SpcIndirectDataContent_free(idc);
|
||||
return 1; /* FAILED */
|
||||
}
|
||||
if (!ctx->format->digest_calc) {
|
||||
printf("Unsupported method: digest_calc\n");
|
||||
SpcIndirectDataContent_free(idc);
|
||||
return 1; /* FAILED */
|
||||
}
|
||||
md = EVP_get_digestbynid(mdtype);
|
||||
cmdbuf = ctx->format->digest_calc(ctx, md);
|
||||
if (!cmdbuf) {
|
||||
printf("Failed to compute a message digest value\n\n");
|
||||
return 1; /* Failed */
|
||||
SpcIndirectDataContent_free(idc);
|
||||
return 1; /* FAILED */
|
||||
}
|
||||
mdlen = EVP_MD_size(EVP_get_digestbynid(mdtype));
|
||||
if (memcmp(mdbuf, cmdbuf, (size_t)mdlen)) {
|
||||
OPENSSL_free(cmdbuf);
|
||||
SpcIndirectDataContent_free(idc);
|
||||
return 1; /* FAILED */
|
||||
} else {
|
||||
printf("Message digest algorithm : %s\n", OBJ_nid2sn(mdtype));
|
||||
|
6
pe.c
6
pe.c
@ -462,11 +462,15 @@ static PKCS7 *pe_pkcs7_prepare(FILE_FORMAT_CTX *ctx, BIO *hash, BIO *outdata)
|
||||
printf("Creating a new signature failed\n");
|
||||
return NULL; /* FAILED */
|
||||
}
|
||||
if (!add_indirect_data_object(p7, hash, ctx)) {
|
||||
if (!add_indirect_data_object(p7)) {
|
||||
printf("Adding SPC_INDIRECT_DATA_OBJID failed\n");
|
||||
PKCS7_free(p7);
|
||||
return NULL; /* FAILED */
|
||||
}
|
||||
if (!sign_spc_indirect_data_content(p7, hash, ctx)) {
|
||||
printf("Failed to set signed content\n");
|
||||
return NULL; /* FAILED */
|
||||
}
|
||||
}
|
||||
if (ctx->options->nest)
|
||||
ctx->options->prevsig = cursig;
|
||||
|
Loading…
x
Reference in New Issue
Block a user