Improve display of S/MIME certificates

pull/240/head
Francis Lachapelle 2018-01-23 13:30:56 -05:00
parent 511aa63a34
commit 7ebdac5525
17 changed files with 291 additions and 60 deletions

View File

@ -1,6 +1,6 @@
/* NSData+SMIME.h - this file is part of SOGo
*
* Copyright (C) 2017 Inverse inc.
* Copyright (C) 2017-2018 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
@ -33,6 +33,7 @@
- (NGMimeMessage *) messageFromEncryptedDataAndCertificate: (NSData *) theCertificate;
- (NSData *) convertPKCS12ToPEMUsingPassword: (NSString *) thePassword;
- (NSData *) convertPKCS7ToPEM;
- (NSDictionary *) certificateDescription;
@end

View File

@ -1,6 +1,6 @@
/* NSData+SMIME.m - this file is part of SOGo
*
* Copyright (C) 2017 Inverse inc.
* Copyright (C) 2017-2018 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
@ -19,6 +19,7 @@
*/
#import <Foundation/NSArray.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSString.h>
#import <NGExtensions/NGBase64Coding.h>
@ -36,6 +37,7 @@
#include <openssl/pem.h>
#endif
#import <SOGo/NSString+Utilities.h>
#import "NSData+SMIME.h"
@implementation NSData (SOGoMailSMIME)
@ -457,4 +459,60 @@
return output;
}
/**
* Extract usefull information from PEM certificate
*/
- (NSDictionary *) certificateDescription
{
BIO *pemBio;
NSDictionary *data;
X509 *x;
data = nil;
OpenSSL_add_all_algorithms();
pemBio = BIO_new_mem_buf((void *) [self bytes], [self length]);
x = PEM_read_bio_X509(pemBio, NULL, 0, NULL);
if (x)
{
BIO *buf;
char p[1024];
NSString *subject, *issuer;
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);
data = [NSDictionary dictionaryWithObjectsAndKeys:
[subject componentsFromMultilineDN], @"subject",
[issuer componentsFromMultilineDN], @"issuer",
nil];
}
else
{
int err = ERR_get_error();
const char* sslError;
NSString *error;
ERR_load_crypto_strings();
sslError = ERR_reason_error_string(err);
error = [NSString stringWithUTF8String: sslError];
NSLog(@"FATAL: failed to read certificate: %@", error);
}
return data;
}
@end

View File

@ -115,6 +115,7 @@
"You cannot (un)subscribe to a folder that you own!" = "You cannot (un)subscribe to a folder that you own!";
/* SMIME Certificate field */
"S/MIME Certificate" = "S/MIME Certificate";
"Subject Name" = "Subject Name";
"Issuer" = "Issuer";
"countryName" = "Country";

View File

@ -239,6 +239,12 @@
"Unable to subscribe to that folder!"
= "Unable to subscribe to that folder.";
/* security */
"Security" = "Security";
"Uninstall" = "Uninstall";
"Error reading the card certificate." = "Error reading the card certificate.";
"No certificate associated to card." = "No certificate associated to card.";
/* acls */
"Access rights to" = "Access rights to";
"For user" = "For user";

View File

@ -1,6 +1,6 @@
/* UIxContactActions.m - this file is part of SOGo
*
* Copyright (C) 2010-2016 Inverse inc.
* Copyright (C) 2010-2018 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
@ -19,6 +19,7 @@
*/
#import <Foundation/NSArray.h>
#import <Foundation/NSDictionary.h>
#import <NGExtensions/NSString+misc.h>
@ -30,6 +31,8 @@
#import <Contacts/SOGoContactGCSEntry.h>
#import <Mailer/NSData+SMIME.h>
#import <SOGoUI/SOGoDirectAction.h>
@interface NGVCard (SOGoActionCategory)
@ -147,4 +150,46 @@
return response;
}
- (WOResponse *) certificateAction
{
NSData *pkcs7;
NSDictionary *data;
WOResponse *response;
pkcs7 = [[[self clientObject] vCard] certificate];
if (pkcs7)
{
data = [[pkcs7 convertPKCS7ToPEM] certificateDescription];
if (data)
{
response = [self responseWithStatus: 200 andJSONRepresentation: data];
}
else
{
data = [NSDictionary
dictionaryWithObject: [self labelForKey: @"Error reading the card certificate."]
forKey: @"message"];
response = [self responseWithStatus: 500 andJSONRepresentation: data];
}
}
else
{
data = [NSDictionary
dictionaryWithObject: [self labelForKey: @"No certificate associated to card."]
forKey: @"message"];
response = [self responseWithStatus: 404 andJSONRepresentation: data];
}
return response;
}
- (WOResponse *) removeCertificateAction
{
[[[self clientObject] vCard] setCertificate: nil];
return [self responseWith204];
}
@end

View File

@ -252,6 +252,7 @@
* @apiSuccess (Success 200) {String[]} allCategories All available categories
* @apiSuccess (Success 200) {Object[]} [categories] Categories assigned to the card
* @apiSuccess (Success 200) {String} categories.value Category name
* @apiSuccess (Success 200) {Number} hasCertificate 1 if contact has a mail certificate
* @apiSuccess (Success 200) {Object[]} [addresses] Postal addresses
* @apiSuccess (Success 200) {String} addresses.type Type (e.g., home or work)
* @apiSuccess (Success 200) {String} addresses.postoffice Post office box
@ -330,6 +331,10 @@
[data setObject: [values subarrayWithRange: NSMakeRange(1, [values count] - 1)] forKey: @"orgs"];
}
o = [card certificate];
if ([o length])
[data setObject: [NSNumber numberWithBool: YES] forKey: @"hasCertificate"];
o = [card birthday];
if (o)
[data setObject: [o descriptionWithCalendarFormat: @"%Y-%m-%d"]
@ -361,7 +366,7 @@
if ((o = [[card uniqueChildWithTag: @"custom1"] flattenedValuesForKey: @""]) && [o length])
[customFields setObject: o forKey: @"1"];
if ((o = [[card uniqueChildWithTag: @"custom2"] flattenedValuesForKey: @""]) && [o length])
if ((o = [[card uniqueChildWithTag: @"custom2"] flattenedValuesForKey: @""]) && [o length])
[customFields setObject: o forKey: @"2"];
if ((o = [[card uniqueChildWithTag: @"custom3"] flattenedValuesForKey: @""]) && [o length])

View File

@ -234,6 +234,16 @@
actionClass = "UIxContactActions";
actionName = "raw";
};
certificate = {
protectedBy = "View";
actionClass = "UIxContactActions";
actionName = "certificate";
};
removeCertificate = {
protectedBy = "Change Images And Files";
actionClass = "UIxContactActions";
actionName = "removeCertificate";
};
};
};

View File

@ -249,6 +249,9 @@
/* Encrypted message notification */
"This message is encrypted" = "This message is encrypted";
/* Encrypted message but no certificate */
"This message can't be decrypted. Please make sure you have uploaded your S/MIME certificate from the mail preferences module." = "This message can't be decrypted. Please make sure you have uploaded your S/MIME certificate from the mail preferences module.";
/* OpenSSL certificate error - unknown issuer */
"certificate verify error" = "Unable to verify message signature";

View File

@ -208,60 +208,25 @@
if (pem)
{
BIO *pemBio;
X509 *x;
OpenSSL_add_all_algorithms();
ERR_load_crypto_strings();
pemBio = BIO_new_mem_buf((void *) [pem bytes], [pem length]);
x = PEM_read_bio_X509(pemBio, NULL, 0, NULL);
if (x)
data = [pem certificateDescription];
if (data)
{
BIO *buf;
char p[1024];
NSString *subject, *issuer;
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);
data = [NSDictionary dictionaryWithObjectsAndKeys:
[subject componentsFromMultilineDN], @"subject",
[issuer componentsFromMultilineDN], @"issuer",
nil];
response = [self responseWithStatus: 200
andJSONRepresentation: data];
response = [self responseWithStatus: 200 andJSONRepresentation: data];
}
else
{
NSLog(@"FATAL: failed to read certificate.");
data = [NSDictionary
dictionaryWithObject: [self labelForKey: @"Error reading the certificate. Please install a new certificate."]
forKey: @"message"];
response = [self responseWithStatus: 500
andJSONRepresentation: data];
response = [self responseWithStatus: 500 andJSONRepresentation: data];
}
BIO_free(pemBio);
X509_free(x);
}
else
{
response = [self responseWithStatus: 404
andJSONRepresentation: [NSDictionary dictionaryWithObject: [self labelForKey: @"No certificate associated to account."]
forKey: @"message"]];
data = [NSDictionary
dictionaryWithObject: [self labelForKey: @"No certificate associated to account."]
forKey: @"message"];
response = [self responseWithStatus: 404 andJSONRepresentation: data];
}
return response;

View File

@ -206,7 +206,6 @@
"Please specify a valid reply-to address." = "Please specify a valid reply-to address.";
"Specify a hostname other than the local host" = "Specify a hostname other than the local host";
"S/MIME Certificate" = "S/MIME Certificate";
"No certificate installed" = "No certificate installed";
"The SSL certificate must use the PKCS#12 (PFX) format." = "The SSL certificate must use the PKCS#12 (PFX) format.";
"Uninstall" = "Uninstall";

View File

@ -3,7 +3,8 @@
xmlns="http://www.w3.org/1999/xhtml"
xmlns:var="http://www.skyrix.com/od/binding"
xmlns:const="http://www.skyrix.com/od/constant"
xmlns:label="OGo:label">
xmlns:label="OGo:label"
xmlns:rsrc="OGo:url">
<div layout="column" class="layout-fill sg-reversible">
<md-card style="overflow: hidden">
@ -131,6 +132,48 @@
</md-autocomplete>
</md-chips>
<!-- S/MIME certificate -->
<div class="section" ng-if="editor.card.hasCertificate">
<div class="pseudo-input-container">
<label class="pseudo-input-label"><var:string label:value="Security"/></label>
<sg-block-toggle class="sg-no-print" layout="column">
<md-list-item class="sg-button-toggle">
<p class="md-flex">
<md-icon rsrc:md-svg-src="img/certificate.svg"><!-- certificate --></md-icon>
{{::'S/MIME Certificate' | loc}}
</p>
<md-button class="md-warn"
ng-click="editor.card.$removeCertificate()">
<var:string label:value="Uninstall"/>
</md-button>
<md-icon class="sg-icon-toggle">expand_more</md-icon>
</md-list-item>
<div class="sg-block-toggle">
<div class="md-margin" md-whiteframe="3">
<div class="md-padding" layout="row" layout-wrap="layout-wrap">
<div flex="50" flex-xs="100">
<div class="md-subhead md-default-theme md-fg md-primary"
ng-bind="::'Subject Name' | loc"><!-- Subject Name --></div>
<div ng-repeat="field in editor.certificate.subject">
<div class="pseudo-input-label" ng-bind="field[0] | loc"><!-- label --></div>
<div class="pseudo-input-field md-body-1" ng-bind="field[1]"><!-- value --></div>
</div>
</div>
<div flex="50" flex-xs="100">
<div class="md-subhead md-default-theme md-fg md-primary"
ng-bind="::'Issuer' | loc"><!-- Issuer --></div>
<div ng-repeat="field in editor.certificate.issuer">
<div class="pseudo-input-label" ng-bind="field[0] | loc"><!-- label --></div>
<div class="pseudo-input-field md-body-1" ng-bind="field[1]"><!-- value --></div>
</div>
</div>
</div>
</div>
</div>
</sg-block-toggle>
</div>
</div>
<!-- emails -->
<div class="section">
<div class="attr" ng-repeat="email in editor.card.emails">

View File

@ -3,7 +3,8 @@
xmlns="http://www.w3.org/1999/xhtml"
xmlns:var="http://www.skyrix.com/od/binding"
xmlns:const="http://www.skyrix.com/od/constant"
xmlns:label="OGo:label">
xmlns:label="OGo:label"
xmlns:rsrc="OGo:url">
<div class="sg-reversible" ng-class="{ 'sg-flip': editor.showRawSource }">
<div class="sg-face" layout="column" layout-fill="layout-fill">
@ -102,6 +103,44 @@
</md-list>
</div>
<!-- S/MIME certificate -->
<div class="section" ng-if="editor.card.hasCertificate">
<div class="pseudo-input-container">
<label class="pseudo-input-label"><var:string label:value="Security"/></label>
<sg-block-toggle class="sg-no-print" layout="column">
<md-list-item class="sg-button-toggle">
<p class="md-flex">
<md-icon rsrc:md-svg-src="img/certificate.svg"><!-- certificate --></md-icon>
{{::'S/MIME Certificate' | loc}}
</p>
<md-icon class="sg-icon-toggle">expand_more</md-icon>
</md-list-item>
<div class="sg-block-toggle">
<div class="md-margin" md-whiteframe="3">
<div class="md-padding" layout="row" layout-wrap="layout-wrap">
<div flex="50" flex-xs="100">
<div class="md-subhead md-default-theme md-fg md-primary"
ng-bind="::'Subject Name' | loc"><!-- Subject Name --></div>
<div ng-repeat="field in editor.certificate.subject">
<div class="pseudo-input-label" ng-bind="field[0] | loc"><!-- label --></div>
<div class="pseudo-input-field md-body-1" ng-bind="field[1]"><!-- value --></div>
</div>
</div>
<div flex="50" flex-xs="100">
<div class="md-subhead md-default-theme md-fg md-primary"
ng-bind="::'Issuer' | loc"><!-- Issuer --></div>
<div ng-repeat="field in editor.certificate.issuer">
<div class="pseudo-input-label" ng-bind="field[0] | loc"><!-- label --></div>
<div class="pseudo-input-field md-body-1" ng-bind="field[1]"><!-- value --></div>
</div>
</div>
</div>
</div>
</div>
</sg-block-toggle>
</div>
</div>
<div class="section" ng-show="editor.card.emails.length > 0">
<div class="pseudo-input-container" ng-repeat="email in editor.card.emails">
<label class="pseudo-input-label"><var:entity const:name="nbsp"/>{{email.type.capitalize() | loc}}</label>

View File

@ -211,10 +211,10 @@
<md-divider><!-- divider --></md-divider>
<md-list-item class="sg-button-toggle">
<div>
<md-icon ng-hide="::viewer.message.$smime.validSignature"
<md-icon ng-hide="::viewer.message.$smime.valid"
class="md-warn"
rsrc:md-svg-src="img/certificate-off.svg"><!-- certificate --></md-icon>
<md-icon ng-show="::viewer.message.$smime.validSignature"
<md-icon ng-show="::viewer.message.$smime.valid"
class="md-accent"
rsrc:md-svg-src="img/certificate.svg"><!-- certificate --></md-icon>
</div>
@ -249,10 +249,13 @@
<div class="sg-no-print" layout="column"
ng-show="viewer.message.$smime.isEncrypted">
<md-divider><!-- divider --></md-divider>
<div layout="row" layout-align="start center">
<md-icon>lock_outline</md-icon>
<md-list-item>
<div>
<md-icon ng-show="::viewer.message.$smime.valid">lock_outline</md-icon>
<md-icon ng-hide="::viewer.message.$smime.valid" class="md-warn">lock_outline</md-icon>
</div>
<p class="md-padding md-flex" ng-bind-html="::viewer.message.$smime.message"><!-- message --></p>
</div>
</md-list-item>
</div>
<!-- Load external images -->

View File

@ -187,9 +187,9 @@
<!-- S/MIME Certificate -->
<sg-block-toggle class="sg-no-print" layout="column">
<md-list-item class="sg-button-toggle">
<md-icon rsrc:md-svg-src="img/certificate.svg"><!-- certificate --></md-icon>
<p class="md-flex">
<var:string label:value="S/MIME Certificate"/>
<md-icon rsrc:md-svg-src="img/certificate.svg"><!-- certificate --></md-icon>
{{::'S/MIME Certificate' | loc}}
</p>
<md-button class="md-warn"
ng-click="$AccountDialogController.removeCertificate()">

View File

@ -38,10 +38,11 @@
* @desc The factory we'll use to register with Angular.
* @returns the Card constructor
*/
Card.$factory = ['$timeout', 'sgSettings', 'sgCard_STATUS', 'Resource', 'Preferences', function($timeout, Settings, Card_STATUS, Resource, Preferences) {
Card.$factory = ['$q', '$timeout', 'sgSettings', 'sgCard_STATUS', 'Resource', 'Preferences', function($q, $timeout, Settings, Card_STATUS, Resource, Preferences) {
angular.extend(Card, {
STATUS: Card_STATUS,
$$resource: new Resource(Settings.activeUser('folderURL') + 'Contacts', Settings.activeUser()),
$q: $q,
$timeout: $timeout,
$Preferences: Preferences
});
@ -485,6 +486,44 @@
return this.refs.length - 1;
};
/**
* @function $certificate
* @memberof Account.prototype
* @desc View the S/MIME certificate details associated to the account.
* @returns a promise of the HTTP operation
*/
Card.prototype.$certificate = function() {
var _this = this;
if (this.hasCertificate) {
if (this.$$certificate)
return Card.$q.when(this.$$certificate);
else {
return Card.$$resource.fetch([this.pid, this.id].join('/'), 'certificate').then(function(data) {
_this.$$certificate = data;
return data;
});
}
}
else {
return Card.$q.reject();
}
};
/**
* @function $removeCertificate
* @memberof Account.prototype
* @desc Remove any S/MIME certificate associated with the account.
* @returns a promise of the HTTP operation
*/
Card.prototype.$removeCertificate = function() {
var _this = this;
return Card.$$resource.fetch([this.pid, this.id].join('/'), 'removeCertificate').then(function() {
_this.hasCertificate = false;
});
};
/**
* @function explode
* @memberof Card.prototype

View File

@ -43,6 +43,7 @@
_registerHotkeys(hotkeys);
_loadCertificate();
$scope.$on('$destroy', function() {
// Deregister hotkeys
@ -71,6 +72,15 @@
});
}
function _loadCertificate() {
if (vm.card.hasCertificate)
vm.card.$certificate().then(function(crt) {
vm.certificate = crt;
}, function() {
delete vm.card.hasCertificate;
});
}
function transformCategory(input) {
if (angular.isString(input))
return { value: input };

View File

@ -298,7 +298,7 @@
var formattedMessage = "<p>" + part.error.replace(/\n/, "</p><p class=\"md-caption\">");
formattedMessage = formattedMessage.replace(/\n/g, "</p><p class=\"md-caption\">") + "</p>";
_this.$smime = {
validSignature: part.valid,
valid: part.valid,
certificate: part.certificates[part.certificates.length - 1],
message: formattedMessage
};
@ -306,8 +306,12 @@
else if (part.type == 'UIxMailPartEncryptedViewer') {
_this.$smime = {
isEncrypted: true,
message: l("This message is encrypted")
valid: part.valid
};
if (part.valid)
_this.$smime.message = l("This message is encrypted");
else
_this.$smime.message = l("This message can't be decrypted. Please make sure you have uploaded your S/MIME certificate from the mail preferences module.");
}
_.forEach(part.content, function(mixedPart) {
_visit(mixedPart);