Add intermediate certificates for S/MIME signing

This commit is contained in:
Felix Eckhofer 2025-01-07 21:26:18 +01:00
parent 8bf4727555
commit dddf360db6
No known key found for this signature in database
GPG key ID: B4543DCDE458BF73
8 changed files with 39 additions and 4 deletions

View file

@ -24,6 +24,7 @@ export class IdentityModel extends EmailModel /*AbstractModel*/ {
smimeKey: '', smimeKey: '',
smimeCertificate: '', smimeCertificate: '',
smimeCertificateChain: '',
askDelete: false, askDelete: false,
@ -33,7 +34,9 @@ export class IdentityModel extends EmailModel /*AbstractModel*/ {
addComputablesTo(this, { addComputablesTo(this, {
smimeKeyEncrypted: () => this.smimeKey().includes('-----BEGIN ENCRYPTED PRIVATE KEY-----'), smimeKeyEncrypted: () => this.smimeKey().includes('-----BEGIN ENCRYPTED PRIVATE KEY-----'),
smimeKeyValid: () => /^-----BEGIN (ENCRYPTED |RSA )?PRIVATE KEY-----/.test(this.smimeKey()), smimeKeyValid: () => /^-----BEGIN (ENCRYPTED |RSA )?PRIVATE KEY-----/.test(this.smimeKey()),
smimeCertificateValid: () => /^-----BEGIN CERTIFICATE-----/.test(this.smimeCertificate()) smimeCertificateValid: () => /^-----BEGIN CERTIFICATE-----/.test(this.smimeCertificate()),
smimeCertificateChainValid: () => !this.smimeCertificateChain()
|| /^-----BEGIN CERTIFICATE-----/.test(this.smimeCertificateChain())
}); });
} }

View file

@ -1413,7 +1413,8 @@ export class ComposePopupView extends AbstractViewPopup {
key && options.push(['OpenPGP', key]); key && options.push(['OpenPGP', key]);
key = GnuPGUserStore.getPrivateKeyFor(email, 1); key = GnuPGUserStore.getPrivateKeyFor(email, 1);
key && options.push(['GnuPG', key]); key && options.push(['GnuPG', key]);
identity.smimeKeyValid() && identity.smimeCertificateValid() && identity.email === email identity.smimeKeyValid() && identity.smimeCertificateValid()
&& identity.smimeCertificateChainValid() && identity.email === email
&& options.push(['S/MIME']); && options.push(['S/MIME']);
console.dir({signOptions: options}); console.dir({signOptions: options});
this.signOptions(options); this.signOptions(options);
@ -1610,6 +1611,7 @@ export class ComposePopupView extends AbstractViewPopup {
// TODO: sign in PHP fails // TODO: sign in PHP fails
params.sign = 'S/MIME'; params.sign = 'S/MIME';
// params.signCertificate = identity.smimeCertificate(); // params.signCertificate = identity.smimeCertificate();
// params.signCertificateChain = identity.smimeCertificateChain();
// params.signPrivateKey = identity.smimeKey(); // params.signPrivateKey = identity.smimeKey();
// params.attachCertificate = false; // params.attachCertificate = false;
if (identity.smimeKeyEncrypted()) { if (identity.smimeKeyEncrypted()) {

View file

@ -1255,6 +1255,7 @@ trait Messages
$oPart->SubParts->append($oSignaturePart); $oPart->SubParts->append($oSignaturePart);
} else { } else {
$sCertificate = $this->GetActionParam('signCertificate', ''); $sCertificate = $this->GetActionParam('signCertificate', '');
$sCertificateChain = $this->GetActionParam('signCertificateChain', '');
$sPrivateKey = $this->GetActionParam('signPrivateKey', ''); $sPrivateKey = $this->GetActionParam('signPrivateKey', '');
if ('S/MIME' === $this->GetActionParam('sign', '')) { if ('S/MIME' === $this->GetActionParam('sign', '')) {
$sID = $this->GetActionParam('identityID', ''); $sID = $this->GetActionParam('identityID', '');
@ -1263,6 +1264,7 @@ trait Messages
&& ($oIdentity->Id() === $sID || $oIdentity->Email() === $oFrom->GetEmail()) && ($oIdentity->Id() === $sID || $oIdentity->Email() === $oFrom->GetEmail())
) { ) {
$sCertificate = $oIdentity->smimeCertificate; $sCertificate = $oIdentity->smimeCertificate;
$sCertificateChain = $oIdentity->smimeCertificateChain;
$sPrivateKey = $oIdentity->smimeKey; $sPrivateKey = $oIdentity->smimeKey;
break; break;
} }
@ -1303,6 +1305,7 @@ trait Messages
$SMIME = $this->SMIME(); $SMIME = $this->SMIME();
$SMIME->setCertificate($sCertificate); $SMIME->setCertificate($sCertificate);
$SMIME->setCertificateChain($sCertificateChain);
$SMIME->setPrivateKey($sPrivateKey, $oPassphrase); $SMIME->setPrivateKey($sPrivateKey, $oPassphrase);
$sSignature = $SMIME->sign($tmp, $detached); $sSignature = $SMIME->sign($tmp, $detached);

View file

@ -31,6 +31,7 @@ class Identity implements \JsonSerializable
private ?SensitiveString $smimeKey = null; private ?SensitiveString $smimeKey = null;
private string $smimeCertificate = ''; private string $smimeCertificate = '';
private string $smimeCertificateChain = '';
function __construct(string $sId = '', string $sEmail = '') function __construct(string $sId = '', string $sEmail = '')
{ {
@ -114,6 +115,7 @@ class Identity implements \JsonSerializable
$this->pgpSign = !empty($aData['pgpSign']); $this->pgpSign = !empty($aData['pgpSign']);
$this->smimeKey = new SensitiveString(isset($aData['smimeKey']) ? $aData['smimeKey'] : ''); $this->smimeKey = new SensitiveString(isset($aData['smimeKey']) ? $aData['smimeKey'] : '');
$this->smimeCertificate = isset($aData['smimeCertificate']) ? $aData['smimeCertificate'] : ''; $this->smimeCertificate = isset($aData['smimeCertificate']) ? $aData['smimeCertificate'] : '';
$this->smimeCertificateChain = isset($aData['smimeCertificateChain']) ? $aData['smimeCertificateChain'] : '';
return true; return true;
} }
@ -136,7 +138,8 @@ class Identity implements \JsonSerializable
'pgpEncrypt' => $this->pgpEncrypt, 'pgpEncrypt' => $this->pgpEncrypt,
'pgpSign' => $this->pgpSign, 'pgpSign' => $this->pgpSign,
'smimeKey' => (string) $this->smimeKey, 'smimeKey' => (string) $this->smimeKey,
'smimeCertificate' => $this->smimeCertificate 'smimeCertificate' => $this->smimeCertificate,
'smimeCertificateChain' => $this->smimeCertificateChain
); );
} }
@ -158,6 +161,7 @@ class Identity implements \JsonSerializable
'pgpSign' => $this->pgpSign, 'pgpSign' => $this->pgpSign,
'smimeKey' => (string) $this->smimeKey, 'smimeKey' => (string) $this->smimeKey,
'smimeCertificate' => $this->smimeCertificate, 'smimeCertificate' => $this->smimeCertificate,
'smimeCertificateChain' => $this->smimeCertificateChain,
'exists' => $this->exists 'exists' => $this->exists
); );
} }

View file

@ -19,6 +19,7 @@ class OpenSSL
// Used for sign and decrypt // Used for sign and decrypt
private $certificate; // OpenSSLCertificate|array|string private $certificate; // OpenSSLCertificate|array|string
private $privateKey; // OpenSSLAsymmetricKey|OpenSSLCertificate|array|string private $privateKey; // OpenSSLAsymmetricKey|OpenSSLCertificate|array|string
private ?string $certificateChain = null;
function __construct(string $homedir) function __construct(string $homedir)
{ {
@ -131,6 +132,15 @@ class OpenSSL
} }
} }
public function setCertificateChain(/*string*/$certificateChain)
{
if ($certificateChain === "") {
$this->certificateChain = null;
} else {
$this->certificateChain = $certificateChain;
}
}
public function setPrivateKey(/*OpenSSLAsymmetricKey|string*/$privateKey, public function setPrivateKey(/*OpenSSLAsymmetricKey|string*/$privateKey,
?\SnappyMail\SensitiveString $passphrase = null ?\SnappyMail\SensitiveString $passphrase = null
) : void ) : void
@ -244,6 +254,13 @@ class OpenSSL
} }
$input = $tmp; $input = $tmp;
} }
if (\is_string($this->certificateChain)) {
$tmp = new Temporary('smimechain-');
if (!$tmp->putContents($this->certificateChain)) {
return null;
}
$certificateChain = $tmp;
}
$output = new Temporary('smimeout-'); $output = new Temporary('smimeout-');
if (!\openssl_pkcs7_sign( if (!\openssl_pkcs7_sign(
$input->filename(), $input->filename(),
@ -252,7 +269,7 @@ class OpenSSL
$this->privateKey, $this->privateKey,
$this->headers, $this->headers,
$detached ? \PKCS7_DETACHED | \PKCS7_BINARY : 0, // | PKCS7_NOCERTS | PKCS7_NOATTR $detached ? \PKCS7_DETACHED | \PKCS7_BINARY : 0, // | PKCS7_NOCERTS | PKCS7_NOATTR
$this->untrusted_certificates_filename $certificateChain ?? null
)) { )) {
throw new \RuntimeException('OpenSSL sign: ' . \openssl_error_string()); throw new \RuntimeException('OpenSSL sign: ' . \openssl_error_string());
} }

View file

@ -331,6 +331,7 @@
"SMIME": { "SMIME": {
"POPUP_IMPORT_TITLE": "S\/MIME-Zertifikat importieren", "POPUP_IMPORT_TITLE": "S\/MIME-Zertifikat importieren",
"CERTIFICATE": "Zertifikat", "CERTIFICATE": "Zertifikat",
"CERTIFICATECHAIN": "Zwischenzertifikat(e)",
"CERTIFICATES": "S\/MIME-Zertifikate", "CERTIFICATES": "S\/MIME-Zertifikate",
"SIGNED_MESSAGE": "S\/MIME-signierte Nachricht", "SIGNED_MESSAGE": "S\/MIME-signierte Nachricht",
"ENCRYPTED_MESSAGE": "S\/MIME-verschlüsselte Nachricht", "ENCRYPTED_MESSAGE": "S\/MIME-verschlüsselte Nachricht",

View file

@ -331,6 +331,7 @@
"SMIME": { "SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate", "POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate", "CERTIFICATE": "Certificate",
"CERTIFICATECHAIN": "Intermediate Certificate(s)",
"CERTIFICATES": "S\/MIME Certificates", "CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME signed message", "SIGNED_MESSAGE": "S\/MIME signed message",
"ENCRYPTED_MESSAGE": "S\/MIME encrypted message", "ENCRYPTED_MESSAGE": "S\/MIME encrypted message",

View file

@ -101,6 +101,10 @@
<label data-i18n="SMIME/CERTIFICATE"></label> <label data-i18n="SMIME/CERTIFICATE"></label>
<textarea name="smimeCertificate" class="input-xxlarge" rows="14" autofocus="" autocomplete="off" data-bind="value: smimeCertificate"></textarea> <textarea name="smimeCertificate" class="input-xxlarge" rows="14" autofocus="" autocomplete="off" data-bind="value: smimeCertificate"></textarea>
</div> </div>
<div class="control-group" data-bind="css: {'error': smimeCertificateChain() && !smimeCertificateChainValid()}">
<label data-i18n="SMIME/CERTIFICATECHAIN"></label>
<textarea name="smimeCertificateChain" class="input-xxlarge" rows="14" autofocus="" autocomplete="off" data-bind="value: smimeCertificateChain"></textarea>
</div>
<div class="control-group" data-bind="hidden:smimeCertificate"> <div class="control-group" data-bind="hidden:smimeCertificate">
<label></label> <label></label>
<button type="button" data-bind="click: $root.createSelfSigned" data-i18n="CRYPTO/CREATE_SELF_SIGNED"></button> <button type="button" data-bind="click: $root.createSelfSigned" data-i18n="CRYPTO/CREATE_SELF_SIGNED"></button>