From 45c1d5051018624a960c5a051d4ef609d54eaeeb Mon Sep 17 00:00:00 2001 From: InfiniteLoopSpace <35842605+InfiniteLoopSpace@users.noreply.github.com> Date: Fri, 16 Nov 2018 14:26:03 +0100 Subject: [PATCH] Added support for S/MIME RFC 5751 de-/encryption and signing/signature-verification. --- cms/cms.go | 2 +- smime/smime.go | 200 ++++++++++++++++++++++++++++++++++++++++++++ smime/smime_test.go | 77 +++++++++++++++++ 3 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 smime/smime.go create mode 100644 smime/smime_test.go diff --git a/cms/cms.go b/cms/cms.go index e2cd2ef..178c765 100644 --- a/cms/cms.go +++ b/cms/cms.go @@ -14,7 +14,7 @@ import ( 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. type CMS struct { Intermediate, roots *x509.CertPool diff --git a/smime/smime.go b/smime/smime.go new file mode 100644 index 0000000..db4b9b1 --- /dev/null +++ b/smime/smime.go @@ -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) +} diff --git a/smime/smime_test.go b/smime/smime_test.go new file mode 100644 index 0000000..96c3d80 --- /dev/null +++ b/smime/smime_test.go @@ -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-----`