Compare commits

...

16 commits

Author SHA1 Message Date
Michael Fuchs
1e3f4fe757
Merge 3154649420 into 077c85bea0 2024-09-26 09:38:07 +00:00
theexiile1305
3154649420
feat: add flag smime for indicating that the part should be marked for singing with S/MIME 2024-09-26 11:37:53 +02:00
theexiile1305
65b9fd07da
feat: remove unused content disposition 2024-09-26 11:32:27 +02:00
077c85bea0
Merge pull request #305 from wneessen/dependabot/github_actions/sonarsource/sonarqube-scan-action-884b79409bbd464b2a59edc326a4b77dc56b2195
Bump sonarsource/sonarqube-scan-action from f885e52a7572cf7943f28637e75730227df2dbf2 to 884b79409bbd464b2a59edc326a4b77dc56b2195
2024-09-25 16:00:56 +02:00
909e699b99
Merge pull request #306 from wneessen/dependabot/github_actions/github/codeql-action-3.26.9
Bump github/codeql-action from 3.26.8 to 3.26.9
2024-09-25 16:00:44 +02:00
dependabot[bot]
b97073db19
Bump github/codeql-action from 3.26.8 to 3.26.9
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.26.8 to 3.26.9.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](294a9d9291...461ef6c76d)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-25 13:58:44 +00:00
dependabot[bot]
d75d990124
Bump sonarsource/sonarqube-scan-action
Bumps [sonarsource/sonarqube-scan-action](https://github.com/sonarsource/sonarqube-scan-action) from f885e52a7572cf7943f28637e75730227df2dbf2 to 884b79409bbd464b2a59edc326a4b77dc56b2195.
- [Release notes](https://github.com/sonarsource/sonarqube-scan-action/releases)
- [Commits](f885e52a75...884b79409b)

---
updated-dependencies:
- dependency-name: sonarsource/sonarqube-scan-action
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-25 13:58:36 +00:00
0676d99f20
Merge pull request #304 from wneessen/dependabot/github_actions/sonarsource/sonarqube-scan-action-f885e52a7572cf7943f28637e75730227df2dbf2
Bump sonarsource/sonarqube-scan-action from 0c0f3958d90fc466625f1d1af1f47bddd4cc6bd1 to f885e52a7572cf7943f28637e75730227df2dbf2
2024-09-24 15:50:50 +02:00
dependabot[bot]
d6725b2d63
Bump sonarsource/sonarqube-scan-action
Bumps [sonarsource/sonarqube-scan-action](https://github.com/sonarsource/sonarqube-scan-action) from 0c0f3958d90fc466625f1d1af1f47bddd4cc6bd1 to f885e52a7572cf7943f28637e75730227df2dbf2.
- [Release notes](https://github.com/sonarsource/sonarqube-scan-action/releases)
- [Commits](0c0f3958d9...f885e52a75)

---
updated-dependencies:
- dependency-name: sonarsource/sonarqube-scan-action
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-24 13:48:36 +00:00
theexiile1305
894936092e
fixing test 2024-09-18 14:35:40 +02:00
theexiile1305
41a81966d8
remove new line 2024-09-18 14:28:55 +02:00
theexiile1305
94942ed383
implementation of S/MIME singing without tests of type smime 2024-09-18 14:23:26 +02:00
theexiile1305
e0a59dba6d
introduced content disposition in part 2024-09-18 14:17:40 +02:00
theexiile1305
6fda661dc7
added content type, mime type and content disposition for S/MIME singing purpose 2024-09-18 14:16:48 +02:00
theexiile1305
158c1b0458
moved S/MIME initialization into msg and the pointer to the data structure, also adjusted tests 2024-09-18 14:13:24 +02:00
theexiile1305
07d9654ce7
Attribute sMimeSinging was added in msg that indicates whether the message should be singed with S/MIME when it's sent. Also, sMimeAuthConfig was introduced in client so that required privateKey and certificate can be used for S/MIME signing. 2024-09-15 19:56:15 +02:00
14 changed files with 282 additions and 7 deletions

View file

@ -54,7 +54,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8
uses: github/codeql-action/init@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@ -65,7 +65,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8
uses: github/codeql-action/autobuild@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@ -79,4 +79,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8
uses: github/codeql-action/analyze@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9

View file

@ -75,6 +75,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8
uses: github/codeql-action/upload-sarif@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9
with:
sarif_file: results.sarif

View file

@ -44,7 +44,7 @@ jobs:
run: |
go test -v -race --coverprofile=./cov.out ./...
- uses: sonarsource/sonarqube-scan-action@0c0f3958d90fc466625f1d1af1f47bddd4cc6bd1 # master
- uses: sonarsource/sonarqube-scan-action@884b79409bbd464b2a59edc326a4b77dc56b2195 # master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}

View file

@ -149,6 +149,7 @@ const (
TypePGPEncrypted ContentType = "application/pgp-encrypted"
TypeTextHTML ContentType = "text/html"
TypeTextPlain ContentType = "text/plain"
typeSMimeSigned ContentType = `application/pkcs7-signature; name="smime.p7s"`
)
// List of MIMETypes
@ -156,6 +157,7 @@ const (
MIMEAlternative MIMEType = "alternative"
MIMEMixed MIMEType = "mixed"
MIMERelated MIMEType = "related"
MIMESMime MIMEType = `signed; protocol="application/pkcs7-signature"; micalg=sha256`
)
// String is a standard method to convert an Charset into a printable format

View file

@ -61,6 +61,11 @@ func TestContentType_String(t *testing.T) {
"ContentType: application/pgp-encrypted", TypePGPEncrypted,
"application/pgp-encrypted",
},
{
"ContentType: pkcs7-signature", typeSMimeSigned,
`application/pkcs7-signature; name="smime.p7s"`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

2
go.mod
View file

@ -5,3 +5,5 @@
module github.com/wneessen/go-mail
go 1.16
require go.mozilla.org/pkcs7 v0.9.0

2
go.sum
View file

@ -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=

59
msg.go
View file

@ -7,6 +7,8 @@ package mail
import (
"bytes"
"context"
"crypto/rsa"
"crypto/x509"
"embed"
"errors"
"fmt"
@ -120,6 +122,9 @@ type Msg struct {
// noDefaultUserAgent indicates whether the default User Agent will be excluded for the Msg when it's sent.
noDefaultUserAgent bool
// SMime represents a middleware used to sign messages with S/MIME
sMime *SMime
}
// SendmailPath is the default system path to the sendmail binary
@ -202,6 +207,16 @@ func WithNoDefaultUserAgent() MsgOption {
}
}
// SignWithSMime configures the Msg to be signed with S/MIME
func (m *Msg) SignWithSMime(privateKey *rsa.PrivateKey, certificate *x509.Certificate) error {
sMime, err := NewSMime(privateKey, certificate)
if err != nil {
return err
}
m.sMime = sMime
return nil
}
// SetCharset sets the encoding charset of the Msg
func (m *Msg) SetCharset(c Charset) {
m.charset = c
@ -979,10 +994,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
}
@ -1177,6 +1229,11 @@ func (m *Msg) hasMixed() bool {
return m.pgptype == 0 && ((len(m.parts) > 0 && len(m.attachments) > 0) || len(m.attachments) > 1)
}
// hasSMime returns true if the Msg has should be signed with S/MIME
func (m *Msg) hasSMime() bool {
return m.sMime != nil
}
// hasRelated returns true if the Msg has related parts
func (m *Msg) hasRelated() bool {
return m.pgptype == 0 && ((len(m.parts) > 0 && len(m.embeds) > 0) || len(m.embeds) > 1)

View file

@ -3246,6 +3246,36 @@ func TestNewMsgWithNoDefaultUserAgent(t *testing.T) {
}
}
// TestWithSMimeSinging_ValidPrivateKey tests WithSMimeSinging with given privateKey
func TestWithSMimeSinging_ValidPrivateKey(t *testing.T) {
privateKey, err := getDummyPrivateKey()
if err != nil {
t.Errorf("failed to load dummy private key: %s", err)
}
certificate, err := getDummyCertificate(privateKey)
if err != nil {
t.Errorf("failed to load dummy certificate: %s", err)
}
m := NewMsg()
if err := m.SignWithSMime(privateKey, certificate); err != nil {
t.Errorf("failed to set sMime. Cause: %v", err)
}
if m.sMime.privateKey != privateKey {
t.Errorf("WithSMimeSinging. Expected %v, got: %v", privateKey, m.sMime.privateKey)
}
}
// TestWithSMimeSinging_InvalidPrivateKey tests WithSMimeSinging with given invalid privateKey
func TestWithSMimeSinging_InvalidPrivateKey(t *testing.T) {
m := NewMsg()
err := m.SignWithSMime(nil, nil)
if !errors.Is(err, ErrInvalidPrivateKey) {
t.Errorf("failed to check sMimeAuthConfig values correctly: %s", err)
}
}
// Fuzzing tests
func FuzzMsg_Subject(f *testing.F) {
f.Add("Testsubject")

View file

@ -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)
}

18
part.go
View file

@ -20,6 +20,7 @@ type Part struct {
encoding Encoding
isDeleted bool
writeFunc func(io.Writer) (int64, error)
smime bool
}
// GetContent executes the WriteFunc of the Part and returns the content as byte slice
@ -56,6 +57,11 @@ func (p *Part) GetDescription() string {
return p.description
}
// IsSMimeSigned returns true if the Part should be singed with S/MIME
func (p *Part) IsSMimeSigned() bool {
return p.smime
}
// SetContent overrides the content of the Part with the given string
func (p *Part) SetContent(content string) {
buffer := bytes.NewBufferString(content)
@ -82,6 +88,11 @@ func (p *Part) SetDescription(description string) {
p.description = description
}
// SetIsSMimeSigned sets the flag for signing the Part with S/MIME
func (p *Part) SetIsSMimeSigned(smime bool) {
p.smime = smime
}
// SetWriteFunc overrides the WriteFunc of the Part
func (p *Part) SetWriteFunc(writeFunc func(io.Writer) (int64, error)) {
p.writeFunc = writeFunc
@ -113,3 +124,10 @@ func WithPartContentDescription(description string) PartOption {
p.description = description
}
}
// WithSMimeSinging overrides the flag for signing the Part with S/MIME
func WithSMimeSinging() PartOption {
return func(p *Part) {
p.smime = true
}
}

View file

@ -102,6 +102,24 @@ func TestPart_WithPartContentDescription(t *testing.T) {
}
}
// TestPart_WithSMimeSinging tests the WithSMimeSinging method
func TestPart_WithSMimeSinging(t *testing.T) {
m := NewMsg()
part := m.newPart(TypeTextPlain, WithSMimeSinging())
if part == nil {
t.Errorf("newPart() WithSMimeSinging() failed: no part returned")
return
}
if part.smime != true {
t.Errorf("newPart() WithSMimeSinging() failed: expected: %v, got: %v", true, part.smime)
}
part.smime = true
part.SetIsSMimeSigned(false)
if part.smime != false {
t.Errorf("newPart() SetIsSMimeSigned() failed: expected: %v, got: %v", false, part.smime)
}
}
// TestPartContentType tests Part.SetContentType
func TestPart_SetContentType(t *testing.T) {
tests := []struct {
@ -245,6 +263,32 @@ func TestPart_GetContentBroken(t *testing.T) {
}
}
// TestPart_IsSMimeSigned tests Part.IsSMimeSigned
func TestPart_IsSMimeSigned(t *testing.T) {
tests := []struct {
name string
want bool
}{
{"smime:", true},
{"smime:", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := NewMsg()
pl, err := getPartList(m)
if err != nil {
t.Errorf("failed: %s", err)
return
}
pl[0].SetIsSMimeSigned(tt.want)
smime := pl[0].IsSMimeSigned()
if smime != tt.want {
t.Errorf("SetContentType failed. Got: %v, expected: %v", smime, tt.want)
}
})
}
}
// TestPart_SetWriteFunc tests Part.SetWriteFunc
func TestPart_SetWriteFunc(t *testing.T) {
c := "This is a test with ümläutß"

70
sime.go Normal file
View file

@ -0,0 +1,70 @@
package mail
import (
"crypto/rsa"
"crypto/x509"
"errors"
"go.mozilla.org/pkcs7"
)
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
}

41
util_test.go Normal file
View file

@ -0,0 +1,41 @@
package mail
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"math/big"
"time"
)
func getDummyPrivateKey() (*rsa.PrivateKey, error) {
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, err
}
return privateKey, nil
}
func getDummyCertificate(privateKey *rsa.PrivateKey) (*x509.Certificate, error) {
template := &x509.Certificate{
SerialNumber: big.NewInt(1234),
Subject: pkix.Name{Organization: []string{"My Organization"}},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1, 0, 0),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}
certDER, err := x509.CreateCertificate(rand.Reader, template, template, &privateKey.PublicKey, privateKey)
if err != nil {
return nil, err
}
cert, err := x509.ParseCertificate(certDER)
if err != nil {
return nil, err
}
return cert, nil
}