diff --git a/go.mod b/go.mod index b155b9b..a59f78a 100644 --- a/go.mod +++ b/go.mod @@ -5,3 +5,5 @@ module github.com/wneessen/go-mail go 1.16 + +require go.mozilla.org/pkcs7 v0.9.0 diff --git a/go.sum b/go.sum index e69de29..4ca3691 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,2 @@ +go.mozilla.org/pkcs7 v0.9.0 h1:yM4/HS9dYv7ri2biPtxt8ikvB37a980dg69/pKmS+eI= +go.mozilla.org/pkcs7 v0.9.0/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= diff --git a/msg.go b/msg.go index 24e2ca6..9686dac 100644 --- a/msg.go +++ b/msg.go @@ -7,6 +7,8 @@ package mail import ( "bytes" "context" + "crypto/rsa" + "crypto/x509" "embed" "errors" "fmt" @@ -981,10 +983,47 @@ func (m *Msg) applyMiddlewares(msg *Msg) *Msg { return msg } +// signMessage sign the Msg with S/MIME +func (m *Msg) signMessage(msg *Msg) (*Msg, error) { + currentPart := m.GetParts()[0] + currentPart.SetEncoding(EncodingUSASCII) + currentPart.SetContentType(TypeTextPlain) + content, err := currentPart.GetContent() + if err != nil { + return nil, errors.New("failed to extract content from part") + } + + signedContent, err := m.sMime.Sign(content) + if err != nil { + return nil, errors.New("failed to sign message") + } + + signedPart := msg.newPart( + typeSMimeSigned, + WithPartEncoding(EncodingB64), + WithContentDisposition(DispositionSMime), + ) + signedPart.SetContent(*signedContent) + msg.parts = append(msg.parts, signedPart) + + return msg, nil +} + // WriteTo writes the formated Msg into a give io.Writer and satisfies the io.WriteTo interface func (m *Msg) WriteTo(writer io.Writer) (int64, error) { mw := &msgWriter{writer: writer, charset: m.charset, encoder: m.encoder} - mw.writeMsg(m.applyMiddlewares(m)) + msg := m.applyMiddlewares(m) + + if m.sMime != nil { + signedMsg, err := m.signMessage(msg) + if err != nil { + return 0, err + } + msg = signedMsg + } + + mw.writeMsg(msg) + return mw.bytesWritten, mw.err } diff --git a/msgwriter.go b/msgwriter.go index ff1b47b..684c223 100644 --- a/msgwriter.go +++ b/msgwriter.go @@ -88,6 +88,10 @@ func (mw *msgWriter) writeMsg(msg *Msg) { } } + if msg.hasSMime() { + mw.startMP(MIMESMime, msg.boundary) + mw.writeString(DoubleNewLine) + } if msg.hasMixed() { mw.startMP(MIMEMixed, msg.boundary) mw.writeString(DoubleNewLine) @@ -96,7 +100,7 @@ func (mw *msgWriter) writeMsg(msg *Msg) { mw.startMP(MIMERelated, msg.boundary) mw.writeString(DoubleNewLine) } - if msg.hasAlt() { + if msg.hasAlt() && !msg.hasSMime() { mw.startMP(MIMEAlternative, msg.boundary) mw.writeString(DoubleNewLine) } @@ -265,6 +269,9 @@ func (mw *msgWriter) writePart(part *Part, charset Charset) { if part.description != "" { mimeHeader.Add(string(HeaderContentDescription), part.description) } + if part.disposition != "" { + mimeHeader.Add(string(HeaderContentDisposition), part.disposition.String()) + } mimeHeader.Add(string(HeaderContentType), contentType) mimeHeader.Add(string(HeaderContentTransferEnc), contentTransferEnc) mw.newPart(mimeHeader) diff --git a/sime.go b/sime.go index 714d3a4..f230a69 100644 --- a/sime.go +++ b/sime.go @@ -3,10 +3,68 @@ package mail import ( "crypto/rsa" "crypto/x509" + "errors" + "go.mozilla.org/pkcs7" ) -// SMimeAuthConfig represents the authentication type for s/mime crypto key material -type SMimeAuthConfig struct { - Certificate *x509.Certificate - PrivateKey *rsa.PrivateKey +var ( + // ErrInvalidPrivateKey should be used if private key is invalid + ErrInvalidPrivateKey = errors.New("invalid private key") + + // ErrInvalidCertificate should be used if certificate is invalid + ErrInvalidCertificate = errors.New("invalid certificate") + + // ErrCouldNotInitialize should be used if the signed data could not initialize + ErrCouldNotInitialize = errors.New("could not initialize signed data") + + // ErrCouldNotAddSigner should be used if the signer could not be added + ErrCouldNotAddSigner = errors.New("could not add signer message") + + // ErrCouldNotFinishSigning should be used if the signing could not be finished + ErrCouldNotFinishSigning = errors.New("could not finish signing") +) + +// SMime is used to sign messages with S/MIME +type SMime struct { + privateKey *rsa.PrivateKey + certificate *x509.Certificate +} + +// NewSMime construct a new instance of SMime with a provided *rsa.PrivateKey +func NewSMime(privateKey *rsa.PrivateKey, certificate *x509.Certificate) (*SMime, error) { + if privateKey == nil { + return nil, ErrInvalidPrivateKey + } + + if certificate == nil { + return nil, ErrInvalidCertificate + } + + return &SMime{ + privateKey: privateKey, + certificate: certificate, + }, nil +} + +// Sign the content with the given privateKey of the method NewSMime +func (sm *SMime) Sign(content []byte) (*string, error) { + toBeSigned, err := pkcs7.NewSignedData(content) + + toBeSigned.SetDigestAlgorithm(pkcs7.OIDDigestAlgorithmSHA256) + if err != nil { + return nil, ErrCouldNotInitialize + } + + if err = toBeSigned.AddSigner(sm.certificate, sm.privateKey, pkcs7.SignerInfoConfig{}); err != nil { + return nil, ErrCouldNotAddSigner + } + + signed, err := toBeSigned.Finish() + if err != nil { + return nil, ErrCouldNotFinishSigning + } + + signedData := string(signed) + + return &signedData, nil }