Added support for S/MIME RFC 5751 de-/encryption and signing/signature-verification.
This commit is contained in:
parent
b8968e6c58
commit
45c1d50510
@ -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
200
smime/smime.go
Normal 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
77
smime/smime_test.go
Normal 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-----`
|
Loading…
Reference in New Issue
Block a user