Fix signing messages with S/MIME #259

This commit is contained in:
the-djmaze 2024-02-19 12:51:02 +01:00
parent 128e2f6254
commit 4df3766231
7 changed files with 112 additions and 39 deletions

View file

@ -253,7 +253,7 @@ export class ComposePopupView extends AbstractViewPopup {
doSign: false,
doEncrypt: false,
canPgpSign: false,
pgpSignKey: false,
canPgpEncrypt: false,
canMailvelope: false,
@ -334,7 +334,10 @@ export class ComposePopupView extends AbstractViewPopup {
attachmentsInProcessCount: () => this.attachmentsInProcess.length,
isDraft: () => this.draftsFolder() && this.draftUid(),
canSign: () => this.canPgpSign() | this.canSMimeSign(),
canSign: () => {
let s = this.canSMimeSign();
return this.pgpSignKey() || s;
},
canEncrypt: () => this.canPgpEncrypt() | this.canSMimeEncrypt(),
identitiesOptions: () =>
@ -365,14 +368,14 @@ export class ComposePopupView extends AbstractViewPopup {
},
from: value => {
this.canPgpSign(false);
this.pgpSignKey(false);
value = getEmail(value);
value && PgpUserStore.getKeyForSigning(value).then(result => {
console.log({
email: value,
canPgpSign:result
pgpSignKey:result
});
this.canPgpSign(result)
this.pgpSignKey(result)
});
this.initPgpEncrypt();
},
@ -1458,7 +1461,7 @@ export class ComposePopupView extends AbstractViewPopup {
linkedData: []
},
recipients = draft ? [identity.email()] : this.allRecipients(),
sign = !draft && this.doSign() && (this.canPgpSign() || this.canSMimeSign()),
sign = !draft && this.doSign() && (this.pgpSignKey() || this.canSMimeSign()),
encrypt = this.doEncrypt() && (this.canPgpEncrypt() || this.canSMimeEncrypt()),
isHtml = this.oEditor.isHtml();

View file

@ -157,7 +157,7 @@ class BodyStructure implements \JsonSerializable
public function isSMimeEncrypted() : bool
{
$type = \strtolower(\trim($this->aContentTypeParams['smime-type'] ?? ''));
return 'application/pkcs7-mime' === $this->sContentType
return ContentType::isPkcs7Mime($this->sContentType)
&& !empty($this->aContentTypeParams['smime-type'])
&& ('enveloped-data' === $type || 'authenveloped-data' === $type);
}
@ -171,7 +171,7 @@ class BodyStructure implements \JsonSerializable
// The multipart/signed body MUST consist of exactly two parts.
&& 2 === \count($this->aSubParts)
&& ContentType::isPkcs7Signature($this->aSubParts[1]->ContentType())
) || ('application/pkcs7-mime' === $this->sContentType
) || (ContentType::isPkcs7Mime($this->sContentType)
&& !empty($this->aContentTypeParams['smime-type'])
&& 'signed-data' === \strtolower(\trim($this->aContentTypeParams['smime-type']))
);
@ -275,6 +275,13 @@ class BodyStructure implements \JsonSerializable
});
}
public function SearchByContentTypes(array $aContentTypes) : iterable
{
return $this->SearchByCallback(function ($oItem) use ($aContentTypes) {
return \in_array($oItem->sContentType, $aContentTypes);
});
}
public function GetPartByMimeIndex(string $sMimeIndex) : self
{
$oPart = null;

View file

@ -320,7 +320,7 @@ class Message implements \JsonSerializable
}
}
$gEncryptedParts = $oBodyStructure->SearchByContentType('application/pkcs7-mime');
$gEncryptedParts = $oBodyStructure->SearchByContentTypes(['application/pkcs7-mime','application/x-pkcs7-mime']);
foreach ($gEncryptedParts as $oPart) {
if ($oPart->isSMimeEncrypted()) {
$oMessage->smimeEncrypted = [

View file

@ -25,6 +25,12 @@ abstract class ContentType
const PKCS7_SIGNATURE = 'application/pkcs7-signature';
const PKCS7_MIME = 'application/pkcs7-mime';
public static function isPkcs7Mime(string $data) : bool
{
return 'application/pkcs7-mime' === $data
|| 'application/x-pkcs7-mime' === $data;
}
public static function isPkcs7Signature(string $data) : bool
{
return 'application/pkcs7-signature' === $data

View file

@ -103,7 +103,7 @@ class Part
&& 2 === \count($this->SubParts)
&& ContentType::isPkcs7Signature($this->SubParts[1]->ContentType())
) || ($header
&& \preg_match('#application/pkcs7-mime.+smime-type=["\']?signed-data#si', $header->FullValue())
&& \preg_match('#application/(x-)?pkcs7-mime.+smime-type=["\']?signed-data#si', $header->FullValue())
);
}

View file

@ -1239,31 +1239,43 @@ trait Messages
$oMessage->SubParts->Clear();
$oMessage->Attachments()->Clear();
$detached = true;
$SMIME = new \SnappyMail\SMime\OpenSSL;
$SMIME->setCertificate($sCertificate);
$SMIME->setPrivateKey($sPrivateKey, $sPassphrase);
$sSignature = $SMIME->sign($tmp, $sCertificate);
$sSignature = $SMIME->sign($tmp, $detached);
if (!$sSignature) {
throw new \Exception('S/MIME sign() failed');
throw new \RuntimeException('S/MIME sign() failed');
}
$oPart = new MimePart;
$oPart->Headers->AddByName(
MimeEnumHeader::CONTENT_TYPE,
'multipart/signed; micalg="sha-512"; protocol="application/pkcs7-signature"'
);
$oMessage->SubParts->append($oPart);
if ($detached) {
$oPart->Headers->AddByName(
MimeEnumHeader::CONTENT_TYPE,
'multipart/signed; micalg="sha-256"; protocol="application/pkcs7-signature"'
);
$fp = $tmp->fopen();
\rewind($fp);
$oBody->Raw = $fp;
$oPart->SubParts->append($oBody);
$fp = $tmp->fopen();
\rewind($fp);
$oBody->Raw = $fp;
$oPart->SubParts->append($oBody);
$oSignaturePart = new MimePart;
$oSignaturePart->Headers->AddByName(MimeEnumHeader::CONTENT_TYPE, 'application/pkcs7-signature; name="signature.p7s"');
$oSignaturePart->Headers->AddByName(MimeEnumHeader::CONTENT_TRANSFER_ENCODING, 'base64');
$oSignaturePart->Body = $sSignature;
$oPart->SubParts->append($oSignaturePart);
$oSignaturePart = new MimePart;
$oSignaturePart->Headers->AddByName(MimeEnumHeader::CONTENT_TYPE, 'application/pkcs7-signature; name="signature.p7s"');
$oSignaturePart->Headers->AddByName(MimeEnumHeader::CONTENT_TRANSFER_ENCODING, 'base64');
$oSignaturePart->Body = $sSignature;
$oPart->SubParts->append($oSignaturePart);
} else {
$oPart->Headers->AddByName(
MimeEnumHeader::CONTENT_TYPE,
'application/pkcs7-mime; smime-type=signed-data; name="smime.p7m"'
);
$oPart->Headers->AddByName(MimeEnumHeader::CONTENT_TRANSFER_ENCODING, 'base64');
$oPart->Body = $sSignature;
}
}
}

View file

@ -10,34 +10,50 @@ class OpenSSL
private int $flags = 0;
private int $cipher_algo = \OPENSSL_CIPHER_AES_128_CBC;
private ?string $untrusted_certificates_filename = null;
// Used for sign and decrypt
private $certificate; // OpenSSLCertificate|array|string
private $private_key; // OpenSSLAsymmetricKey|OpenSSLCertificate|array|string
private $privateKey; // OpenSSLAsymmetricKey|OpenSSLCertificate|array|string
public static function isSupported() : bool
{
return \defined('PKCS7_DETACHED');
}
public function setPrivateKey($private_key = null,
public function setCertificate(/*OpenSSLCertificate|string*/$certificate)
{
$this->certificate = \openssl_x509_read($certificate);
if (!$this->certificate) {
throw new \RuntimeException('OpenSSL x509: ' . \openssl_error_string());
}
if ($this->privateKey && !\openssl_x509_check_private_key($this->certificate, $this->privateKey)) {
throw new \RuntimeException('OpenSSL x509: ' . \openssl_error_string());
}
}
public function setPrivateKey(/*OpenSSLAsymmetricKey|string*/$privateKey,
#[\SensitiveParameter]
?string $passphrase = null
) : void
{
$this->private_key = \openssl_pkey_get_private($private_key, $passphrase);
if (!$this->private_key) {
$this->privateKey = \openssl_pkey_get_private($privateKey, $passphrase);
if (!$this->privateKey) {
throw new \RuntimeException('OpenSSL setPrivateKey: ' . \openssl_error_string());
}
if ($this->certificate && !\openssl_x509_check_private_key($this->certificate, $this->privateKey)) {
throw new \RuntimeException('OpenSSL setPrivateKey: ' . \openssl_error_string());
}
}
public function decrypt(string $data, $certificate = null, $private_key = null) : ?string
public function decrypt(string $data) : ?string
{
$input = new Temporary('smimein-');
$output = new Temporary('smimeout-');
return ($input->putContents($data) && \openssl_pkcs7_decrypt(
$input->filename(),
$output->filename(),
$certificate ?: $this->certificate, // \openssl_pkey_get_public();
$private_key ?: $this->private_key // \openssl_pkey_get_private($private_key, ?string $passphrase = null);
$this->certificate,
$this->privateKey
)) ? $output->getContents() : null;
}
@ -61,7 +77,7 @@ class OpenSSL
) ? $output->getContents() : null;
}
public function sign(/*string|Temporary*/$input, $certificate = null, $private_key = null)
public function sign(/*string|Temporary*/$input, bool $detached = true)
{
if (\is_string($input)) {
$input = new Temporary('smimein-');
@ -73,18 +89,47 @@ class OpenSSL
if (!\openssl_pkcs7_sign(
$input->filename(),
$output->filename(),
$certificate ?: $this->certificate, // \openssl_pkey_get_public();
$private_key ?: $this->private_key, // \openssl_pkey_get_private($private_key, ?string $passphrase = null);
$this->certificate,
$this->privateKey,
$this->headers,
\PKCS7_DETACHED | \PKCS7_BINARY, // | PKCS7_NOCERTS | PKCS7_NOATTR
$detached ? \PKCS7_DETACHED | \PKCS7_BINARY : \PKCS7_BINARY, // | PKCS7_NOCERTS | PKCS7_NOATTR
$this->untrusted_certificates_filename
)) {
throw new \RuntimeException('OpenSSL sign: ' . \openssl_error_string());
}
$body = $output->getContents();
if (\preg_match('/\\.p7s"\R\R(.+?)------/s', $body, $match)) {
return \trim($match[1]);
/**
* Only fetch the signed part
*/
$fp = $output->fopen();
$micalg = '';
while (!\feof($fp)) {
$line = \fgets($fp);
$fp = $output->fopen();
while (!\feof($fp)) {
$line = \fgets($fp);
/*
if (!$micalg && \str_contains($line, 'Content-Type: multipart/signed')) {
\preg_match('/micalg="([^"+])"/', $line, $match);
$micalg = $match[1];
}
*/
if (($detached && \str_contains($line, 'Content-Type: application/x-pkcs7-signature'))
|| (!$detached && \str_contains($line, 'Content-Type: application/x-pkcs7-mime'))
) {
// Skip headers
while (\trim(\fgets($fp)));
// Fetch the body
$data = '';
do {
$line = \fgets($fp);
if (!\trim($line)) {
return $data;
}
$data .= $line;
} while (true);
}
}
}
throw new \RuntimeException('OpenSSL sign: failed to find p7s');