Added support for S/MIME RFC 5751 de-/encryption and signing/signature-verification.

This commit is contained in:
InfiniteLoopSpace 2018-11-16 14:26:03 +01:00
parent b8968e6c58
commit 45c1d50510
3 changed files with 278 additions and 1 deletions

View File

@ -14,7 +14,7 @@ import (
timestamp "github.com/InfiniteLoopSpace/go_S-MIME/timestamp" timestamp "github.com/InfiniteLoopSpace/go_S-MIME/timestamp"
) )
// CMS is an instance of cms to en/decrypt and sign/verfiy CMS data // CMS is an instance of cms to en-/decrypt and sign/verfiy CMS data
// with the given keyPairs and options. // with the given keyPairs and options.
type CMS struct { type CMS struct {
Intermediate, roots *x509.CertPool Intermediate, roots *x509.CertPool

200
smime/smime.go Normal file
View File

@ -0,0 +1,200 @@
//Package smime implants parts of the S/MIME 4.0 specification rfc5751-bis-12.
//
//See https://www.ietf.org/id/draft-ietf-lamps-rfc5751-bis-12.txt
package smime
import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
"errors"
"strings"
"github.com/InfiniteLoopSpace/go_S-MIME/b64"
cms "github.com/InfiniteLoopSpace/go_S-MIME/cms"
mime "github.com/InfiniteLoopSpace/go_S-MIME/mime"
)
// SMIME is an instance of cms to en-/decrypt and sign/verfiy SMIME messages
// with the given keyPairs and options.
type SMIME struct {
CMS *cms.CMS
}
// New create a new instance of SMIME with given keyPairs.
func New(keyPair ...tls.Certificate) (smime *SMIME, err error) {
CMS, err := cms.New(keyPair...)
if err != nil {
return
}
smime = &SMIME{CMS}
return
}
// Decrypt decrypts SMIME message and returns plaintext.
func (smime *SMIME) Decrypt(msg []byte) (plaintext []byte, err error) {
mail := mime.Parse(msg)
mediaType, params, err := mail.ParseMediaType()
if !strings.HasPrefix(mediaType, "application/pkcs7-mime") {
err = errors.New("Unsupported media type: Can not decrypt this mail")
return
}
if !strings.HasPrefix(params["smime-type"], "enveloped-data") {
err = errors.New("Unsupported smime type: Can not decrypt this mail")
return
}
contentTransferEncoding := mail.GetHeaderField([]byte("Content-Transfer-Encoding"))
if len(contentTransferEncoding) != 1 && !strings.HasPrefix(string(contentTransferEncoding[0]), "base64") {
err = errors.New("Unsupported endoing: Can not decrypt this mail. Only base64 is supported")
return
}
bodyB64 := mail.Body()
body := make([]byte, base64.StdEncoding.DecodedLen(len(bodyB64)))
if _, err = base64.StdEncoding.Decode(body, bodyB64); err != nil {
return
}
plaintext, err = smime.CMS.Decrypt(body)
return
}
// Encrypt encrypts msg for the recipients and returns SMIME message.
func (smime *SMIME) Encrypt(msg []byte, recipients []*x509.Certificate, opts ...Header) (smimemsg []byte, err error) {
mail := mime.Parse(msg)
der, err := smime.CMS.Encrypt(msg, recipients)
if err != nil {
return
}
base64, err := b64.EncodeBase64(der)
if err != nil {
return
}
mail.SetBody(base64)
for _, opt := range opts {
mail.SetHeaderField([]byte(opt.Key), []byte(opt.Value))
}
contentType := []byte("application/pkcs7-mime; smime-type=enveloped-data;\n name=smime.p7m")
contentTransferEncoding := []byte("base64")
contentDisposition := []byte("attachment; filename=smime.p7m")
mail.SetHeaderField([]byte("Content-Type"), contentType)
mail.SetHeaderField([]byte("Content-Transfer-Encoding"), contentTransferEncoding)
mail.SetHeaderField([]byte("Content-Disposition"), contentDisposition)
return mail.Full(), nil
}
// AuthEncrypt authenticated-encrypts msg for the recipients and returns SMIME message.
func (smime *SMIME) AuthEncrypt(msg []byte, recipients []*x509.Certificate, opts ...Header) (smimemsg []byte, err error) {
mail := mime.Parse(msg)
der, err := smime.CMS.AuthEncrypt(msg, recipients)
if err != nil {
return
}
base64, err := b64.EncodeBase64(der)
if err != nil {
return
}
mail.SetBody(base64)
for _, opt := range opts {
mail.SetHeaderField([]byte(opt.Key), []byte(opt.Value))
}
contentType := []byte("application/pkcs7-mime; smime-type=authEnveloped-data;\n name=smime.p7m")
contentTransferEncoding := []byte("base64")
contentDisposition := []byte("attachment; filename=smime.p7m")
mail.SetHeaderField([]byte("Content-Type"), contentType)
mail.SetHeaderField([]byte("Content-Transfer-Encoding"), contentTransferEncoding)
mail.SetHeaderField([]byte("Content-Disposition"), contentDisposition)
return mail.Full(), nil
}
// Header field for creating signed or encrypted messages.
type Header struct {
Key string
Value string
}
// Verify verifies a signed mail and returns certificate chains of the signers if
// the signature is valid.
func (smime *SMIME) Verify(msg []byte) (chains [][][]*x509.Certificate, err error) {
mail := mime.Parse(msg)
mediaType, params, err := mail.ParseMediaType()
if !strings.HasPrefix(mediaType, "multipart/signed") {
err = errors.New("Unsupported media type: can not decrypt this mail")
return
}
if !strings.HasPrefix(params["protocol"], "application/pkcs7-signature") {
err = errors.New("Unsupported smime type: can not decrypt this mail")
return
}
parts, err := mail.MultipartGetParts()
if len(parts) != 2 {
err = errors.New("Multipart/signed Message must have 2 parts")
return
}
signedMsg := parts[0].Bytes(mime.CRLF)
signature := mime.Parse(parts[1].Bytes(nil))
mediaType, params, err = signature.ParseMediaType()
if !strings.HasPrefix(mediaType, "application/pkcs7-signature") {
err = errors.New("Unsupported media type: Can not decrypt this mail")
return
}
contentTransferEncoding := signature.GetHeaderField([]byte("Content-Transfer-Encoding"))
var signatureDer []byte
if len(contentTransferEncoding) == 1 {
switch string(contentTransferEncoding[0]) {
case "base64":
signatureDer = make([]byte, base64.StdEncoding.DecodedLen(len(signature.Body())))
if _, err = base64.StdEncoding.Decode(signatureDer, signature.Body()); err != nil {
return
}
default:
err = errors.New("Unsupported endoing: Can not parse the signature. Only base64 encoding is supported")
return
}
} else {
err = errors.New("Unsupported endoing: Multiple or no Content-Transfer-Encoding field")
return
}
return smime.CMS.VerifyDetached(signatureDer, signedMsg)
}

77
smime/smime_test.go Normal file
View File

@ -0,0 +1,77 @@
package smime
import (
"bytes"
"crypto/tls"
"testing"
)
func TestDecrypt(t *testing.T) {
cert, err := tls.X509KeyPair([]byte(bobCert), []byte(bobRSAkey))
if err != nil {
t.Error(err)
}
SMIME, err := New(cert)
if err != nil {
t.Error(err)
}
plain, err := SMIME.Decrypt([]byte(msg))
if err != nil {
t.Error(err)
}
if !bytes.Equal(plain, []byte("This is some sample content.")) {
t.Fatal("Decrypted plaintext is not correct.")
}
}
var msg = `MIME-Version: 1.0
Message-Id: <00103112005203.00349@amyemily.ig.com>
Date: Tue, 31 Oct 2000 12:00:52 -0600 (Central Standard Time)
From: User1
To: User2
Subject: Example 5.3
Content-Type: application/pkcs7-mime;
name=smime.p7m;
smime-type=enveloped-data
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename=smime.p7m
MIIBHgYJKoZIhvcNAQcDoIIBDzCCAQsCAQAxgcAwgb0CAQAwJjASMRAwDgYDVQQDEwdDYXJ
sUlNBAhBGNGvHgABWvBHTbi7NXXHQMA0GCSqGSIb3DQEBAQUABIGAC3EN5nGIiJi2lsGPcP
2iJ97a4e8kbKQz36zg6Z2i0yx6zYC4mZ7mX7FBs3IWg+f6KgCLx3M1eCbWx8+MDFbbpXadC
DgO8/nUkUNYeNxJtuzubGgzoyEd8Ch4H/dd9gdzTd+taTEgS0ipdSJuNnkVY4/M652jKKHR
LFf02hosdR8wQwYJKoZIhvcNAQcBMBQGCCqGSIb3DQMHBAgtaMXpRwZRNYAgDsiSf8Z9P43
LrY4OxUk660cu1lXeCSFOSOpOJ7FuVyU=`
var bobCert = `-----BEGIN CERTIFICATE-----
MIICJzCCAZCgAwIBAgIQRjRrx4AAVrwR024uzV1x0DANBgkqhkiG9w0BAQUFADASMRAwDg
YDVQQDEwdDYXJsUlNBMB4XDTk5MDkxOTAxMDkwMloXDTM5MTIzMTIzNTk1OVowETEPMA0G
A1UEAxMGQm9iUlNBMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCp4WeYPznVX/Kgk0
FepnmJhcg1XZqRW/sdAdoZcCYXD72lItA1hW16mGYUQVzPt7cIOwnJkbgZaTdt+WUee9mp
MySjfzu7r0YBhjY0MssHA1lS/IWLMQS4zBgIFEjmTxz7XWDE4FwfU9N/U9hpAfEF+Hpw0b
6Dxl84zxwsqmqn6wIDAQABo38wfTAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIFIDAf
BgNVHSMEGDAWgBTp4JAnrHggeprTTPJCN04irp44uzAdBgNVHQ4EFgQU6PS4Z9izlqQq8x
GqKdOVWoYWtCQwHQYDVR0RBBYwFIESQm9iUlNBQGV4YW1wbGUuY29tMA0GCSqGSIb3DQEB
BQUAA4GBAHuOZsXxED8QIEyIcat7QGshM/pKld6dDltrlCEFwPLhfirNnJOIh/uLt359QW
Hh5NZt+eIEVWFFvGQnRMChvVl52R1kPCHWRbBdaDOS6qzxV+WBfZjmNZGjOd539OgcOync
f1EHl/M28FAK3Zvetl44ESv7V+qJba3JiNiPzyvT
-----END CERTIFICATE-----`
var bobRSAkey = `-----BEGIN PRIVATE KEY-----
MIIChQIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKnhZ5g/OdVf8qCTQV6meY
mFyDVdmpFb+x0B2hlwJhcPvaUi0DWFbXqYZhRBXM+3twg7CcmRuBlpN235ZR572akzJKN/
O7uvRgGGNjQyywcDWVL8hYsxBLjMGAgUSOZPHPtdYMTgXB9T039T2GkB8QX4enDRvoPGXz
jPHCyqaqfrAgMBAAECgYBnzUhMmg2PmMIbZf8ig5xt8KYGHbztpwOIlPIcaw+LNd4Ogngw
y+e6alatd8brUXlweQqg9P5F4Kmy9Bnah5jWMIR05PxZbMHGd9ypkdB8MKCixQheIXFD/A
0HPfD6bRSeTmPwF1h5HEuYHD09sBvf+iU7o8AsmAX2EAnYh9sDGQJBANDDIsbeopkYdo+N
vKZ11mY/1I1FUox29XLE6/BGmvE+XKpVC5va3Wtt+Pw7PAhDk7Vb/s7q/WiEI2Kv8zHCue
UCQQDQUfweIrdb7bWOAcjXq/JY1PeClPNTqBlFy2bKKBlf4hAr84/sajB0+E0R9KfEILVH
IdxJAfkKICnwJAiEYH2PAkA0umTJSChXdNdVUN5qSO8bKlocSHseIVnDYDubl6nA7xhmqU
5iUjiEzuUJiEiUacUgFJlaV/4jbOSnI3vQgLeFAkEAni+zN5r7CwZdV+EJBqRd2ZCWBgVf
JAZAcpw6iIWchw+dYhKIFmioNRobQ+g4wJhprwMKSDIETukPj3d9NDAlBwJAVxhn1grSta
vCunrnVNqcBU+B1O8BiR4yPWnLMcRSyFRVJQA7HCp8JlDV6abXd8vPFfXuC9WN7rOvTKF8
Y0ZB9qANMAsGA1UdDzEEAwIAEA==
-----END PRIVATE KEY-----`