(feat) added support for S/MIME opaque signing (fixes #4582)

pull/259/head
Ludovic Marcotte 2019-08-19 10:37:15 -04:00
parent b52abfcbea
commit 676d2e6790
15 changed files with 569 additions and 33 deletions

1
NEWS
View File

@ -4,6 +4,7 @@
New features
- [core] Debian 10 (Buster) support for x86_64 (#4775)
- [core] now possible to specify which domains you can forward your mails to
- [core] added support for S/MIME opaque signing (#4582)
Enhancements
- [web] avoid saving an empty calendar name

View File

@ -1,7 +1,7 @@
/*
Copyright (C) 2004-2007 SKYRIX Software AG
Copyright (C) 2007 Helge Hess
Copyright (c) 2008-2014 Inverse inc.
Copyright (c) 2008-2019 Inverse inc.
This file is part of SOGo.

View File

@ -31,6 +31,8 @@
- (NSData *) encryptUsingCertificate: (NSData *) theData;
- (NSData *) decryptUsingCertificate: (NSData *) theData;
- (NGMimeMessage *) messageFromEncryptedDataAndCertificate: (NSData *) theCertificate;
- (NSData *) embeddedContent;
- (NGMimeMessage *) messageFromOpaqueSignedData;
- (NSData *) convertPKCS12ToPEMUsingPassword: (NSString *) thePassword;
- (NSData *) signersFromPKCS7;
- (NSDictionary *) certificateDescription;

View File

@ -1,6 +1,6 @@
/* NSData+SMIME.m - this file is part of SOGo
*
* Copyright (C) 2017-2018 Inverse inc.
* Copyright (C) 2017-2019 Inverse inc.
*
* This file is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -296,10 +296,86 @@
NGMimeMessageParser *parser;
NGMimeMessage *message;
NSData *decryptedData;
NGMimeType *contentType;
NSString *type, *subtype, *smimetype;
decryptedData = [self decryptUsingCertificate: theCertificate];
parser = [[NGMimeMessageParser alloc] init];
message = [parser parsePartFromData: decryptedData];
// Extract contents if the encrypted messages contains opaque signed data
contentType = [message contentType];
type = [[contentType type] lowercaseString];
subtype = [[contentType subType] lowercaseString];
if ([type isEqualToString: @"application"])
{
if ([subtype isEqualToString: @"x-pkcs7-mime"] ||
[subtype isEqualToString: @"pkcs7-mime"])
{
smimetype = [[contentType valueOfParameter: @"smime-type"] lowercaseString];
if ([smimetype isEqualToString: @"signed-data"])
{
message = [decryptedData messageFromOpaqueSignedData];
}
}
}
RELEASE(parser);
return message;
}
- (NSData *) embeddedContent
{
NSData *output = NULL;
BIO *sbio, *obio;
BUF_MEM *bptr;
PKCS7 *p7 = NULL;
sbio = BIO_new_mem_buf((void *)[self bytes], [self length]);
p7 = SMIME_read_PKCS7(sbio, NULL);
if (!p7)
{
NSLog(@"FATAL: could not read the signature");
goto cleanup;
}
// We output the S/MIME encrypted message
obio = BIO_new(BIO_s_mem());
if (!PKCS7_verify(p7, NULL, NULL, NULL, obio, PKCS7_NOVERIFY|PKCS7_NOSIGS))
{
NSLog(@"FATAL: could not extract content");
goto cleanup;
}
BIO_get_mem_ptr(obio, &bptr);
output = [NSData dataWithBytes: bptr->data length: bptr->length];
cleanup:
PKCS7_free(p7);
BIO_free(sbio);
BIO_free(obio);
return output;
}
//
//
//
- (NGMimeMessage *) messageFromOpaqueSignedData
{
NGMimeMessageParser *parser;
NGMimeMessage *message;
NSData *extractedData;
extractedData = [self embeddedContent];
parser = [[NGMimeMessageParser alloc] init];
message = [parser parsePartFromData: extractedData];
RELEASE(parser);
return message;

View File

@ -872,6 +872,18 @@ static NSString *userAgent = nil;
}
//
//
//
- (void) _fetchAttachmentsFromOpaqueSignedMail: (SOGoMailObject *) sourceMail
{
NGMimeMessage *m;
m = [[sourceMail content] messageFromOpaqueSignedData];
[self _fileAttachmentsFromPart: [m body]];
}
//
//
//
@ -1007,6 +1019,8 @@ static NSString *userAgent = nil;
[self setText: [sourceMail contentForInlineForward]];
if ([sourceMail isEncrypted])
[self _fetchAttachmentsFromEncryptedMail: sourceMail];
else if ([sourceMail isOpaqueSigned])
[self _fetchAttachmentsFromOpaqueSignedMail: sourceMail];
else
[self _fetchAttachmentsFromMail: sourceMail];
}

View File

@ -197,9 +197,7 @@
- (NGImap4Connection *) imap4Connection
{
NSString *cacheKey, *login;
SOGoCache *sogoCache;
if (!imap4)
{

View File

@ -210,6 +210,28 @@ static BOOL debugOn = NO;
inContext: localContext];
obj = [clazz objectWithName:key inContainer: self];
}
else if ([o isOpaqueSigned])
{
NGMimeMessage *m;
id part;
int i;
m = [[o content] messageFromOpaqueSignedData];
part = [m body];
for (i = 0; i < [[self bodyPartPath] count]; i++)
{
nbr = [[[self bodyPartPath] objectAtIndex: i] intValue]-1;
part = [[part parts] objectAtIndex: nbr];;
}
part = [[part parts] objectAtIndex: ([key intValue]-1)];
mimeType = [[part contentType] stringValue];
clazz = [SOGoMailBodyPart bodyPartClassForMimeType: mimeType
inContext: localContext];
obj = [clazz objectWithName:key inContainer: self];
}
else
{
infos = [self partInfo];
@ -353,6 +375,24 @@ static BOOL debugOn = NO;
m = [[o content] messageFromEncryptedDataAndCertificate: certificate];
part = [m body];
for (i = 0; i < [[self bodyPartPath] count]; i++)
{
nbr = [[[self bodyPartPath] objectAtIndex: i] intValue]-1;
part = [[part parts] objectAtIndex: nbr];;
}
return [part body];
}
else if ([o isOpaqueSigned])
{
NGMimeMessage *m;
id part;
unsigned int i, nbr;
m = [[o content] messageFromOpaqueSignedData];
part = [m body];
for (i = 0; i < [[self bodyPartPath] count]; i++)
{
nbr = [[[self bodyPartPath] objectAtIndex: i] intValue]-1;

View File

@ -238,6 +238,24 @@
return nil;
}
//
//
//
- (NSString *) _contentForEditingFromOpaqueSignedMail
{
SOGoUserDefaults *ud;
NGMimeMessage *m;
m = [[self content] messageFromOpaqueSignedData];
ud = [[context activeUser] userDefaults];
return [self _preferredContentFromPart: [m body]
favorHTML: [[ud mailComposeMessageType] isEqualToString: @"html"]];
return nil;
}
//
//
//
@ -250,6 +268,8 @@
if ([self isEncrypted])
output = [self _contentForEditingFromEncryptedMail];
else if ([self isOpaqueSigned])
output = [self _contentForEditingFromOpaqueSignedMail];
// If not encrypted or if decryption failed, we fallback
// to the normal content fetching code.

View File

@ -124,7 +124,8 @@ NSArray *SOGoMailCoreInfoKeys;
- (BOOL) replied; /* \Answered */
- (BOOL) forwarded; /* $forwarded */
- (BOOL) deleted; /* \Deleted */
- (BOOL) isSigned; /* S/MIME signed message */
- (BOOL) isSigned; /* S/MIME signed message (detached signature) */
- (BOOL) isOpaqueSigned; /* S/MIME signed message (embedded content) */
- (BOOL) isEncrypted; /* S/MIME encrypted message */
/* deletion */

View File

@ -1201,6 +1201,19 @@ static BOOL debugSoParts = NO;
return [clazz objectWithName:_key inContainer: self];
}
}
else if ([self isOpaqueSigned])
{
NGMimeMessage *m;
id part;
m = [[self content] messageFromOpaqueSignedData];
part = [[[m body] parts] objectAtIndex: ([_key intValue]-1)];
mimeType = [[part contentType] stringValue];
clazz = [SOGoMailBodyPart bodyPartClassForMimeType: mimeType
inContext: _ctx];
return [clazz objectWithName:_key inContainer: self];
}
parts = [[self bodyStructure] objectForKey: @"parts"];
@ -1738,18 +1751,47 @@ static BOOL debugSoParts = NO;
[protocol isEqualToString: @"application/pkcs7-signature"]));
}
- (BOOL) isEncrypted
- (BOOL) isOpaqueSigned
{
NSString *type, *subtype;
NSString *type, *subtype, *smimetype;
NGMimeType *contentType;
type = [[[[self mailHeaders] objectForKey: @"content-type"] type] lowercaseString];
subtype = [[[[self mailHeaders] objectForKey: @"content-type"] subType] lowercaseString];
contentType = [[self mailHeaders] objectForKey: @"content-type"];
type = [[contentType type] lowercaseString];
subtype = [[contentType subType] lowercaseString];
if ([type isEqualToString: @"application"])
{
if ([subtype isEqualToString: @"x-pkcs7-mime"] ||
[subtype isEqualToString: @"pkcs7-mime"])
return YES;
{
smimetype = [[contentType valueOfParameter: @"smime-type"] lowercaseString];
if ([smimetype isEqualToString: @"signed-data"])
return YES;
}
}
return NO;
}
- (BOOL) isEncrypted
{
NSString *type, *subtype, *smimetype;
NGMimeType *contentType;
contentType = [[self mailHeaders] objectForKey: @"content-type"];
type = [[contentType type] lowercaseString];
subtype = [[contentType subType] lowercaseString];
if ([type isEqualToString: @"application"])
{
if ([subtype isEqualToString: @"x-pkcs7-mime"] ||
[subtype isEqualToString: @"pkcs7-mime"])
{
smimetype = [[contentType valueOfParameter: @"smime-type"] lowercaseString];
if ([smimetype isEqualToString: @"enveloped-data"])
return YES;
}
}
return NO;

View File

@ -28,8 +28,20 @@
@interface UIxMailPartEncryptedViewer : UIxMailPartViewer
{
BOOL processed;
BOOL encrypted;
BOOL opaqueSigned;
BOOL validSignature;
NSMutableArray *certificates;
NSString *validationMessage;
}
- (BOOL) validSignature;
- (NSString *) validationMessage;
- (NSArray *) smimeCertificates;
- (NSDictionary *) certificateForSubject: (NSString *) subject
andIssuer: (NSString *) issuer;
@end
#endif /* UIXMAILPARTENCRYPTEDVIEWER_H */

View File

@ -1,5 +1,5 @@
/*
Copyright (C) 2017-2018 Inverse inc.
Copyright (C) 2017-2019 Inverse inc.
This file is part of SOGo.
@ -19,6 +19,14 @@
02111-1307, USA.
*/
#if defined(HAVE_OPENSSL) || defined(HAVE_GNUTLS)
#include <openssl/ssl.h>
#include <openssl/bio.h>
#include <openssl/err.h>
#include <openssl/pkcs7.h>
#include <openssl/x509.h>
#endif
#import <Foundation/NSDictionary.h>
#import <Foundation/NSNull.h>
#import <Foundation/NSValue.h>
@ -36,11 +44,222 @@
#import <SoObjects/Mailer/SOGoMailObject.h>
#import <UI/MailerUI/WOContext+UIxMailer.h>
#import <SOGo/NSString+Utilities.h>
#import "UIxMailRenderingContext.h"
#import "UIxMailPartEncryptedViewer.h"
@implementation UIxMailPartEncryptedViewer
#if defined(HAVE_OPENSSL) || defined(HAVE_GNUTLS)
- (X509_STORE *) _setupVerify
{
X509_STORE *store;
X509_LOOKUP *lookup;
BOOL success;
success = NO;
store = X509_STORE_new();
OpenSSL_add_all_algorithms();
if (store)
{
lookup = X509_STORE_add_lookup(store, X509_LOOKUP_file());
if (lookup)
{
X509_LOOKUP_load_file(lookup, NULL, X509_FILETYPE_DEFAULT);
lookup = X509_STORE_add_lookup(store, X509_LOOKUP_hash_dir());
if (lookup)
{
X509_LOOKUP_add_dir(lookup, NULL, X509_FILETYPE_DEFAULT);
ERR_clear_error();
success = YES;
}
}
}
if (!success)
{
if (store)
{
X509_STORE_free(store);
store = NULL;
}
}
return store;
}
- (NSData *) _processMessageWith: (NSData *) signedData
{
NSData *output;
STACK_OF(X509) *certs;
X509_STORE *x509Store;
BIO *msgBio, *obio;
PKCS7 *p7;
int err, i;
ERR_clear_error();
msgBio = BIO_new_mem_buf ((void *) [signedData bytes], [signedData length]);
p7 = SMIME_read_PKCS7(msgBio, NULL);
certs = NULL;
certificates = [NSMutableArray array];
validationMessage = nil;
if (p7)
{
if (OBJ_obj2nid(p7->type) == NID_pkcs7_signed)
{
NSString *subject, *issuer;
X509 *x;
certs = p7->d.sign->cert;
for (i = 0; i < sk_X509_num(certs); i++)
{
BIO *buf;
char p[1024];
x = sk_X509_value(certs, i);
memset(p, 0, 1024);
buf = BIO_new(BIO_s_mem());
X509_NAME_print_ex(buf, X509_get_subject_name(x), 0,
ASN1_STRFLGS_ESC_CTRL | XN_FLAG_SEP_MULTILINE | XN_FLAG_FN_LN);
BIO_read(buf, p, 1024);
subject = [NSString stringWithUTF8String: p];
BIO_free(buf);
memset(p, 0, 1024);
buf = BIO_new(BIO_s_mem());
X509_NAME_print_ex(buf, X509_get_issuer_name(x), 0,
ASN1_STRFLGS_ESC_CTRL | XN_FLAG_SEP_MULTILINE | XN_FLAG_FN_LN);
BIO_read(buf, p, 1024);
issuer = [NSString stringWithUTF8String: p];
BIO_free(buf);
[certificates addObject: [self certificateForSubject: subject
andIssuer: issuer]];
}
}
err = ERR_get_error();
if (err)
{
validSignature = NO;
}
else
{
x509Store = [self _setupVerify];
obio = BIO_new(BIO_s_mem());
validSignature = (PKCS7_verify(p7, NULL, x509Store, NULL,
obio, 0) == 1);
err = ERR_get_error();
if (x509Store)
X509_STORE_free (x509Store);
}
if (err)
{
#ifdef HAVE_GNUTLS
const char* sslError;
ERR_load_crypto_strings();
SSL_load_error_strings();
sslError = ERR_reason_error_string(err);
validationMessage = [[self labelForKey: [NSString stringWithUTF8String: sslError ? sslError : @"No error information available"]] retain];
#elif OPENSSL_VERSION_NUMBER < 0x10100000L
const char* sslError;
ERR_load_crypto_strings();
SSL_load_error_strings();
sslError = ERR_reason_error_string(err);
validationMessage = [[self labelForKey: [NSString stringWithUTF8String: sslError ? sslError : @"No error information available"]] retain];
#else
validationMessage = [[self labelForKey: @"No error information available"] retain];
#endif /* HAVE_GNUTLS */
BUF_MEM *bptr; //DEL
BIO_get_mem_ptr(obio, &bptr); //DEL
// extract contents without validation
output = [ signedData embeddedContent ];
}
else
{
BUF_MEM *bptr;
BIO_get_mem_ptr(obio, &bptr);
output = [NSData dataWithBytes: bptr->data length: bptr->length];
}
}
PKCS7_free(p7);
BIO_free (msgBio);
BIO_free (obio);
if (validSignature)
validationMessage = [NSString stringWithString: [self labelForKey: @"Message is signed"]];
else if (!validationMessage)
validationMessage = [NSString stringWithString: [self labelForKey: @"Digital signature is not valid"]];
processed = YES;
opaqueSigned = YES;
return output;
}
- (BOOL) validSignature
{
if (!processed)
NSLog(@"ERROR: validSignature called but not processed yet");
//[self _processMessage];
return validSignature;
}
- (NSDictionary *) certificateForSubject: (NSString *) subject
andIssuer: (NSString *) issuer
{
return [NSDictionary dictionaryWithObjectsAndKeys:
[subject componentsFromMultilineDN], @"subject",
[issuer componentsFromMultilineDN], @"issuer",
nil];
}
- (NSArray *) smimeCertificates
{
return certificates;
}
- (NSString *) validationMessage
{
if (!processed)
NSLog(@"ERROR: validationMessage called but not processed yet");
//[self _processMessage];
return validationMessage;
}
#else
- (NSArray *) smimeCertificates
{
return nil;
}
- (BOOL) validSignature
{
return NO;
}
- (NSString *) validationMessage
{
return nil;
}
#endif
- (void) _attachmentIdsFromBodyPart: (id) thePart
partPath: (NSString *) thePartPath
{
@ -91,26 +310,112 @@
- (id) renderedPart
{
SOGoMailObject *mailObject;
NSData *certificate, *decryptedData, *encryptedData;
id info, viewer;
certificate = [[[self clientObject] mailAccountFolder] certificate];
encryptedData = [[self clientObject] content];
decryptedData = [encryptedData decryptUsingCertificate: certificate];
mailObject = [[self clientObject] mailObject];
if ([mailObject isEncrypted])
{
encrypted = YES;
certificate = [[[self clientObject] mailAccountFolder] certificate];
encryptedData = [[self clientObject] content];
decryptedData = [encryptedData decryptUsingCertificate: certificate];
if (decryptedData)
if (decryptedData)
{
NGMimeMessageParser *parser;
NGMimeMessage *message;
NGMimeType *contentType;
NSString *type, *subtype, *smimetype;
id part;
parser = [[NGMimeMessageParser alloc] init];
message = [parser parsePartFromData: decryptedData];
// Extract contents if the encrypted messages contains opaque signed data
contentType = [message contentType];
type = [[contentType type] lowercaseString];
subtype = [[contentType subType] lowercaseString];
if ([type isEqualToString: @"application"])
{
if ([subtype isEqualToString: @"x-pkcs7-mime"] ||
[subtype isEqualToString: @"pkcs7-mime"])
{
smimetype = [[contentType valueOfParameter: @"smime-type"] lowercaseString];
if ([smimetype isEqualToString: @"signed-data"])
{
NGMimeMessageParser *parser;
NSData *extractedData;
opaqueSigned = YES;
extractedData = [self _processMessageWith: decryptedData];
if (extractedData)
{
parser = [[NGMimeMessageParser alloc] init];
message = [parser parsePartFromData: extractedData];
decryptedData = extractedData;
RELEASE(parser);
}
}
}
}
processed = YES;
part = [message retain];
info = [NSDictionary dictionaryWithObjectsAndKeys: [[part contentType] type], @"type",
[[part contentType] subType], @"subtype",
[[part contentType] parametersAsDictionary], @"parameterList", nil];
viewer = [[[self context] mailRenderingContext] viewerForBodyInfo: info];
[viewer setBodyInfo: info];
[viewer setFlatContent: decryptedData];
[viewer setDecodedContent: [part body]];
// attachmentIds is empty in an ecrypted email as the IMAP body structure
// is of course not available for file attachments
[self _attachmentIdsFromBodyPart: [part body] partPath: @""];
[viewer setAttachmentIds: attachmentIds];
return [NSDictionary dictionaryWithObjectsAndKeys:
[self className], @"type",
[NSNumber numberWithBool: YES], @"encrypted",
[NSNumber numberWithBool: YES], @"decrypted",
[NSNumber numberWithBool: opaqueSigned], @"opaqueSigned",
[NSNumber numberWithBool: [self validSignature]], @"valid",
[NSArray arrayWithObject: [viewer renderedPart]], @"content",
[self smimeCertificates], @"certificates",
[self validationMessage], @"message",
nil];
}
}
else if ([mailObject isOpaqueSigned])
{
NGMimeMessageParser *parser;
NGMimeMessage *message;
NSData *extractedData;
id part;
parser = [[NGMimeMessageParser alloc] init];
part = [[parser parsePartFromData: decryptedData] retain];
opaqueSigned = YES;
encryptedData = [[self clientObject] content];
extractedData = [self _processMessageWith: encryptedData];
if (extractedData)
{
parser = [[NGMimeMessageParser alloc] init];
message = [parser parsePartFromData: extractedData];
RELEASE(parser);
}
processed = YES;
part = [message retain];
info = [NSDictionary dictionaryWithObjectsAndKeys: [[part contentType] type], @"type",
[[part contentType] subType], @"subtype", nil];
[[part contentType] subType], @"subtype",
[[part contentType] parametersAsDictionary], @"parameterList", nil];
viewer = [[[self context] mailRenderingContext] viewerForBodyInfo: info];
[viewer setBodyInfo: info];
[viewer setFlatContent: decryptedData];
[viewer setFlatContent: extractedData];
[viewer setDecodedContent: [part body]];
// attachmentIds is empty in an ecrypted email as the IMAP body structure
@ -120,8 +425,12 @@
return [NSDictionary dictionaryWithObjectsAndKeys:
[self className], @"type",
[NSNumber numberWithBool: YES], @"valid",
[NSNumber numberWithBool: NO], @"encrypted",
[NSNumber numberWithBool: YES], @"opaqueSigned",
[NSNumber numberWithBool: [self validSignature]], @"valid",
[NSArray arrayWithObject: [viewer renderedPart]], @"content",
[self smimeCertificates], @"certificates",
[self validationMessage], @"message",
nil];
}
@ -129,7 +438,9 @@
// Decryption failed, let's return something else...
return [NSDictionary dictionaryWithObjectsAndKeys:
[self className], @"type",
[NSNumber numberWithBool: NO], @"valid",
[NSNumber numberWithBool: encrypted], @"encrypted",
[NSNumber numberWithBool: NO], @"decrypted",
[NSNumber numberWithBool: NO], @"opaqueSigned",
[NSArray array], @"content",
nil];
}

View File

@ -256,10 +256,20 @@ static BOOL showNamedTextAttachmentsInline = NO;
if ([st isEqualToString: @"x-pkcs7-mime"] ||
[st isEqualToString: @"pkcs7-mime"])
{
// If the mail account has a valid certificate, we try to decode
// the encrypted email. Otherwise, we fallback to a link viewer
if ([[[viewer clientObject] mailAccountFolder] certificate])
return [self encryptedViewer];
NSString *smt;
smt = [[[_info objectForKey:@"parameterList"] valueForKey:@"smime-type"] lowercaseString];
if ([smt isEqualToString:@"signed-data"])
{
return [self encryptedViewer];
}
else if ([smt isEqualToString:@"enveloped-data"])
{
// If the mail account has a valid certificate, we try to decode
// the encrypted email. Otherwise, we fallback to a link viewer
if ([[[viewer clientObject] mailAccountFolder] certificate])
return [self encryptedViewer];
}
}
#if 0 /* the link viewer looks better than plain text ;-) */

View File

@ -1,5 +1,5 @@
/*
Copyright (C) 2005-2015 Inverse inc.
Copyright (C) 2005-2019 Inverse inc.
This file is part of SOGo.

View File

@ -340,13 +340,22 @@
};
}
else if (part.type == 'UIxMailPartEncryptedViewer') {
_this.encrypted = {
valid: part.valid
};
if (part.valid)
_this.encrypted.message = l("This message is encrypted");
else
_this.encrypted.message = l("This message can't be decrypted. Please make sure you have uploaded your S/MIME certificate from the mail preferences module.");
if (part.encrypted) {
_this.encrypted = {
valid: part.decrypted
};
if (part.decrypted)
_this.encrypted.message = l("This message is encrypted");
else
_this.encrypted.message = l("This message can't be decrypted. Please make sure you have uploaded your S/MIME certificate from the mail preferences module.");
}
if (part.opaqueSigned) {
_this.signed = {
valid: part.valid,
certificate: part.certificates[part.certificates.length - 1],
message: part.message
};
}
}
_.forEach(part.content, function(mixedPart) {
_visit(mixedPart);