feat: implementation of S/MIME signing without tests

This commit is contained in:
theexiile1305 2024-09-26 16:32:51 +02:00
parent 3154649420
commit 12edb2724b
No known key found for this signature in database
GPG key ID: A1BDDE98F2BF6E40
3 changed files with 146 additions and 56 deletions

44
msg.go
View file

@ -7,8 +7,7 @@ package mail
import ( import (
"bytes" "bytes"
"context" "context"
"crypto/rsa" "crypto/tls"
"crypto/x509"
"embed" "embed"
"errors" "errors"
"fmt" "fmt"
@ -208,8 +207,8 @@ func WithNoDefaultUserAgent() MsgOption {
} }
// SignWithSMime configures the Msg to be signed with S/MIME // SignWithSMime configures the Msg to be signed with S/MIME
func (m *Msg) SignWithSMime(privateKey *rsa.PrivateKey, certificate *x509.Certificate) error { func (m *Msg) SignWithSMime(keyPair *tls.Certificate) error {
sMime, err := NewSMime(privateKey, certificate) sMime, err := newSMime(keyPair)
if err != nil { if err != nil {
return err return err
} }
@ -985,28 +984,31 @@ func (m *Msg) applyMiddlewares(msg *Msg) *Msg {
// signMessage sign the Msg with S/MIME // signMessage sign the Msg with S/MIME
func (m *Msg) signMessage(msg *Msg) (*Msg, error) { func (m *Msg) signMessage(msg *Msg) (*Msg, error) {
currentPart := m.GetParts()[0] signedPart := msg.GetParts()[0]
currentPart.SetEncoding(EncodingUSASCII) body, err := signedPart.GetContent()
currentPart.SetContentType(TypeTextPlain)
content, err := currentPart.GetContent()
if err != nil { if err != nil {
return nil, errors.New("failed to extract content from part") return nil, err
} }
signedContent, err := m.sMime.Sign(content) signaturePart, err := m.createSignaturePart(signedPart.GetEncoding(), signedPart.GetContentType(), signedPart.GetCharset(), body)
if err != nil { if err != nil {
return nil, errors.New("failed to sign message") return nil, err
} }
signedPart := msg.newPart( m.parts = append(m.parts, signaturePart)
typeSMimeSigned,
WithPartEncoding(EncodingB64),
WithContentDisposition(DispositionSMime),
)
signedPart.SetContent(*signedContent)
msg.parts = append(msg.parts, signedPart)
return msg, nil return m, err
}
func (m *Msg) createSignaturePart(encoding Encoding, contentType ContentType, charSet Charset, body []byte) (*Part, error) {
message := m.sMime.createMessage(encoding, contentType, charSet, body)
signaturePart := m.newPart(typeSMimeSigned, WithPartEncoding(EncodingB64), WithSMimeSinging())
if err := m.sMime.sign(signaturePart, message); err != nil {
return nil, err
}
return signaturePart, nil
} }
// WriteTo writes the formated Msg into a give io.Writer and satisfies the io.WriteTo interface // WriteTo writes the formated Msg into a give io.Writer and satisfies the io.WriteTo interface
@ -1014,7 +1016,7 @@ func (m *Msg) WriteTo(writer io.Writer) (int64, error) {
mw := &msgWriter{writer: writer, charset: m.charset, encoder: m.encoder} mw := &msgWriter{writer: writer, charset: m.charset, encoder: m.encoder}
msg := m.applyMiddlewares(m) msg := m.applyMiddlewares(m)
if m.sMime != nil { if m.hasSMime() {
signedMsg, err := m.signMessage(msg) signedMsg, err := m.signMessage(msg)
if err != nil { if err != nil {
return 0, err return 0, err
@ -1210,7 +1212,7 @@ func (m *Msg) hasAlt() bool {
count++ count++
} }
} }
return count > 1 && m.pgptype == 0 return count > 1 && m.pgptype == 0 && !m.hasSMime()
} }
// hasMixed returns true if the Msg has mixed parts // hasMixed returns true if the Msg has mixed parts

View file

@ -100,7 +100,7 @@ func (mw *msgWriter) writeMsg(msg *Msg) {
mw.startMP(MIMERelated, msg.boundary) mw.startMP(MIMERelated, msg.boundary)
mw.writeString(DoubleNewLine) mw.writeString(DoubleNewLine)
} }
if msg.hasAlt() && !msg.hasSMime() { if msg.hasAlt() {
mw.startMP(MIMEAlternative, msg.boundary) mw.startMP(MIMEAlternative, msg.boundary)
mw.writeString(DoubleNewLine) mw.writeString(DoubleNewLine)
} }
@ -241,7 +241,7 @@ func (mw *msgWriter) addFiles(files []*File, isAttachment bool) {
} }
if mw.err == nil { if mw.err == nil {
mw.writeBody(file.Writer, encoding) mw.writeBody(file.Writer, encoding, false)
} }
} }
} }
@ -273,7 +273,7 @@ func (mw *msgWriter) writePart(part *Part, charset Charset) {
mimeHeader.Add(string(HeaderContentTransferEnc), contentTransferEnc) mimeHeader.Add(string(HeaderContentTransferEnc), contentTransferEnc)
mw.newPart(mimeHeader) mw.newPart(mimeHeader)
} }
mw.writeBody(part.writeFunc, part.encoding) mw.writeBody(part.writeFunc, part.encoding, part.smime)
} }
// writeString writes a string into the msgWriter's io.Writer interface // writeString writes a string into the msgWriter's io.Writer interface
@ -322,7 +322,7 @@ func (mw *msgWriter) writeHeader(key Header, values ...string) {
} }
// writeBody writes an io.Reader into an io.Writer using provided Encoding // writeBody writes an io.Reader into an io.Writer using provided Encoding
func (mw *msgWriter) writeBody(writeFunc func(io.Writer) (int64, error), encoding Encoding) { func (mw *msgWriter) writeBody(writeFunc func(io.Writer) (int64, error), encoding Encoding, singingWithSMime bool) {
var writer io.Writer var writer io.Writer
var encodedWriter io.WriteCloser var encodedWriter io.WriteCloser
var n int64 var n int64
@ -337,12 +337,11 @@ func (mw *msgWriter) writeBody(writeFunc func(io.Writer) (int64, error), encodin
lineBreaker := Base64LineBreaker{} lineBreaker := Base64LineBreaker{}
lineBreaker.out = &writeBuffer lineBreaker.out = &writeBuffer
switch encoding { if encoding == EncodingQP {
case EncodingQP:
encodedWriter = quotedprintable.NewWriter(&writeBuffer) encodedWriter = quotedprintable.NewWriter(&writeBuffer)
case EncodingB64: } else if encoding == EncodingB64 && !singingWithSMime {
encodedWriter = base64.NewEncoder(base64.StdEncoding, &lineBreaker) encodedWriter = base64.NewEncoder(base64.StdEncoding, &lineBreaker)
case NoEncoding: } else if encoding == NoEncoding {
_, err = writeFunc(&writeBuffer) _, err = writeFunc(&writeBuffer)
if err != nil { if err != nil {
mw.err = fmt.Errorf("bodyWriter function: %w", err) mw.err = fmt.Errorf("bodyWriter function: %w", err)
@ -355,7 +354,7 @@ func (mw *msgWriter) writeBody(writeFunc func(io.Writer) (int64, error), encodin
mw.bytesWritten += n mw.bytesWritten += n
} }
return return
default: } else {
encodedWriter = quotedprintable.NewWriter(writer) encodedWriter = quotedprintable.NewWriter(writer)
} }

139
sime.go
View file

@ -1,18 +1,20 @@
package mail package mail
import ( import (
"bytes"
"crypto/rsa" "crypto/rsa"
"crypto/tls"
"crypto/x509" "crypto/x509"
"encoding/pem"
"errors" "errors"
"fmt"
"go.mozilla.org/pkcs7" "go.mozilla.org/pkcs7"
"strings"
) )
var ( var (
// ErrInvalidPrivateKey should be used if private key is invalid // ErrInvalidKeyPair should be used if key pair is invalid
ErrInvalidPrivateKey = errors.New("invalid private key") ErrInvalidKeyPair = errors.New("invalid key pair")
// 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 should be used if the signed data could not initialize
ErrCouldNotInitialize = errors.New("could not initialize signed data") ErrCouldNotInitialize = errors.New("could not initialize signed data")
@ -22,49 +24,136 @@ var (
// ErrCouldNotFinishSigning should be used if the signing could not be finished // ErrCouldNotFinishSigning should be used if the signing could not be finished
ErrCouldNotFinishSigning = errors.New("could not finish signing") ErrCouldNotFinishSigning = errors.New("could not finish signing")
// ErrCouldNoEncodeToPEM should be used if the signature could not be encoded to PEM
ErrCouldNoEncodeToPEM = errors.New("could not encode to PEM")
) )
// SMime is used to sign messages with S/MIME // SMime is used to sign messages with S/MIME
type SMime struct { type SMime struct {
privateKey *rsa.PrivateKey privateKey *rsa.PrivateKey
certificate *x509.Certificate certificate *x509.Certificate
parentCertificates []*x509.Certificate
} }
// NewSMime construct a new instance of SMime with a provided *rsa.PrivateKey // NewSMime construct a new instance of SMime with a provided *tls.Certificate
func NewSMime(privateKey *rsa.PrivateKey, certificate *x509.Certificate) (*SMime, error) { func newSMime(keyPair *tls.Certificate) (*SMime, error) {
if privateKey == nil { if keyPair == nil {
return nil, ErrInvalidPrivateKey return nil, ErrInvalidKeyPair
} }
if certificate == nil { parentCertificates := make([]*x509.Certificate, 0)
return nil, ErrInvalidCertificate for _, cert := range keyPair.Certificate[1:] {
c, err := x509.ParseCertificate(cert)
if err != nil {
return nil, err
}
parentCertificates = append(parentCertificates, c)
} }
return &SMime{ return &SMime{
privateKey: privateKey, privateKey: keyPair.PrivateKey.(*rsa.PrivateKey),
certificate: certificate, certificate: keyPair.Leaf,
parentCertificates: parentCertificates,
}, nil }, nil
} }
// Sign the content with the given privateKey of the method NewSMime // sign with the S/MIME method the message of the actual *Part
func (sm *SMime) Sign(content []byte) (*string, error) { func (sm *SMime) sign(signaturePart *Part, message string) error {
toBeSigned, err := pkcs7.NewSignedData(content) lines := parseLines([]byte(message))
toBeSigned := lines.bytesFromLines([]byte("\r\n"))
toBeSigned.SetDigestAlgorithm(pkcs7.OIDDigestAlgorithmSHA256) tmp, err := pkcs7.NewSignedData(toBeSigned)
tmp.SetDigestAlgorithm(pkcs7.OIDDigestAlgorithmSHA256)
if err != nil { if err != nil {
return nil, ErrCouldNotInitialize return ErrCouldNotInitialize
} }
if err = toBeSigned.AddSigner(sm.certificate, sm.privateKey, pkcs7.SignerInfoConfig{}); err != nil { if err = tmp.AddSignerChain(sm.certificate, sm.privateKey, sm.parentCertificates, pkcs7.SignerInfoConfig{}); err != nil {
return nil, ErrCouldNotAddSigner return ErrCouldNotAddSigner
} }
signed, err := toBeSigned.Finish() signatureDER, err := tmp.Finish()
if err != nil { if err != nil {
return nil, ErrCouldNotFinishSigning return ErrCouldNotFinishSigning
} }
signedData := string(signed) pemMsg, err := encodeToPEM(signatureDER)
if err != nil {
return &signedData, nil return ErrCouldNoEncodeToPEM
}
signaturePart.SetContent(*pemMsg)
return nil
}
// createMessage prepares the message that will be used for the sign method later
func (sm *SMime) createMessage(encoding Encoding, contentType ContentType, charset Charset, body []byte) string {
return fmt.Sprintf("Content-Transfer-Encoding: %v\r\nContent-Type: %v; charset=%v\r\n\r\n%v", encoding, contentType, charset, string(body))
}
// encodeToPEM uses the method pem.Encode from the standard library but cuts the typical PEM preamble
func encodeToPEM(msg []byte) (*string, error) {
block := &pem.Block{Bytes: msg}
var arrayBuffer bytes.Buffer
if err := pem.Encode(&arrayBuffer, block); err != nil {
return nil, err
}
r := arrayBuffer.String()
r = strings.ReplaceAll(r, "-----BEGIN -----\n", "")
r = strings.ReplaceAll(r, "-----END -----\n", "")
return &r, nil
}
// line is the representation of one line of the message that will be used for signing purposes
type line struct {
line []byte
endOfLine []byte
}
// lines is the representation of a message that will be used for signing purposes
type lines []line
// bytesFromLines creates the line representation with the given endOfLine char
func (ls lines) bytesFromLines(sep []byte) []byte {
var raw []byte
for i := range ls {
raw = append(raw, ls[i].line...)
if len(ls[i].endOfLine) != 0 && sep != nil {
raw = append(raw, sep...)
} else {
raw = append(raw, ls[i].endOfLine...)
}
}
return raw
}
// parseLines constructs the lines representation of a given message
func parseLines(raw []byte) lines {
oneLine := line{raw, nil}
lines := lines{oneLine}
lines = lines.splitLine([]byte("\r\n"))
lines = lines.splitLine([]byte("\r"))
lines = lines.splitLine([]byte("\n"))
return lines
}
// splitLine uses the given endOfLine to split the given line
func (ls lines) splitLine(sep []byte) lines {
nl := lines{}
for _, l := range ls {
split := bytes.Split(l.line, sep)
if len(split) > 1 {
for i := 0; i < len(split)-1; i++ {
nl = append(nl, line{split[i], sep})
}
nl = append(nl, line{split[len(split)-1], l.endOfLine})
} else {
nl = append(nl, l)
}
}
return nl
} }