Compare commits

...

6 commits

Author SHA1 Message Date
theexiile1305
128b2bd32e
chore: license 2024-10-11 19:22:47 +02:00
theexiile1305
148e9d8e1c
chore: gofumpt 2024-10-11 19:21:34 +02:00
theexiile1305
df68003f24
feat: add license information 2024-10-11 19:16:42 +02:00
theexiile1305
f6bda99464
feat: transfer configuration of intermediate certificate to caller of the library 2024-10-11 18:43:04 +02:00
theexiile1305
3f2ba4822c
feat: add support of s/mime singing 2024-10-11 17:26:48 +02:00
theexiile1305
c45aec89e9
feat: add parent certificates 2024-10-11 17:26:22 +02:00
11 changed files with 155 additions and 75 deletions

View file

@ -59,6 +59,7 @@ Here are some highlights of go-mail's featureset:
* [X] Custom error types for delivery errors * [X] Custom error types for delivery errors
* [X] Custom dial-context functions for more control over the connection (proxing, DNS hooking, etc.) * [X] Custom dial-context functions for more control over the connection (proxing, DNS hooking, etc.)
* [X] Output a go-mail message as EML file and parse EML file into a go-mail message * [X] Output a go-mail message as EML file and parse EML file into a go-mail message
* [X] S/MIME singed messages
go-mail works like a programatic email client and provides lots of methods and functionalities you would consider go-mail works like a programatic email client and provides lots of methods and functionalities you would consider
standard in a MUA. standard in a MUA.

View file

@ -0,0 +1,3 @@
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
//
// SPDX-License-Identifier: MIT

View file

@ -0,0 +1,3 @@
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
//
// SPDX-License-Identifier: MIT

View file

@ -171,20 +171,20 @@ const (
// TypeTextPlain represents the MIME type for plain text content. // TypeTextPlain represents the MIME type for plain text content.
TypeTextPlain ContentType = "text/plain" TypeTextPlain ContentType = "text/plain"
// typeSMimeSigned represents the MIME type for S/MIME singed messages. // typeSMimeSigned represents the MIME type for S/MIME singed messages.
typeSMimeSigned ContentType = `application/pkcs7-signature; name="smime.p7s"` typeSMimeSigned ContentType = `application/pkcs7-signature; name="smime.p7s"`
) )
const ( const (
// MIMEAlternative MIMEType represents a MIME multipart/alternative type, used for emails with multiple versions. // MIMEAlternative MIMEType represents a MIME multipart/alternative type, used for emails with multiple versions.
MIMEAlternative MIMEType = "alternative" MIMEAlternative MIMEType = "alternative"
// MIMEMixed MIMEType represents a MIME multipart/mixed type used for emails containing different types of content. // MIMEMixed MIMEType represents a MIME multipart/mixed type used for emails containing different types of content.
MIMEMixed MIMEType = "mixed" MIMEMixed MIMEType = "mixed"
// MIMERelated MIMEType represents a MIME multipart/related type, used for emails with related content entities. // MIMERelated MIMEType represents a MIME multipart/related type, used for emails with related content entities.
MIMERelated MIMEType = "related" MIMERelated MIMEType = "related"
// MIMESMime MIMEType represents a MIME multipart/signed type, used for siging emails with S/MIME. // MIMESMime MIMEType represents a MIME multipart/signed type, used for siging emails with S/MIME.
MIMESMime MIMEType = `signed; protocol="application/pkcs7-signature"; micalg=sha-256` MIMESMime MIMEType = `signed; protocol="application/pkcs7-signature"; micalg=sha-256`
) )
// String satisfies the fmt.Stringer interface for the Charset type. // String satisfies the fmt.Stringer interface for the Charset type.

7
msg.go
View file

@ -7,7 +7,8 @@ package mail
import ( import (
"bytes" "bytes"
"context" "context"
"crypto/tls" "crypto/rsa"
"crypto/x509"
"embed" "embed"
"errors" "errors"
"fmt" "fmt"
@ -338,8 +339,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(keyPair *tls.Certificate) error { func (m *Msg) SignWithSMime(privateKey *rsa.PrivateKey, certificate *x509.Certificate, intermediateCertificate *x509.Certificate) error {
sMime, err := newSMime(keyPair) sMime, err := newSMime(privateKey, certificate, intermediateCertificate)
if err != nil { if err != nil {
return err return err
} }

View file

@ -1909,15 +1909,15 @@ func TestMsg_hasAlt(t *testing.T) {
// TestMsg_hasAlt tests the hasAlt() method of the Msg with active S/MIME // TestMsg_hasAlt tests the hasAlt() method of the Msg with active S/MIME
func TestMsg_hasAltWithSMime(t *testing.T) { func TestMsg_hasAltWithSMime(t *testing.T) {
keyPair, err := getDummyCertificate() privateKey, certificate, intermediateCertificate, err := getDummyCryptoMaterial()
if err != nil { if err != nil {
t.Errorf("failed to load dummy certificate. Cause: %v", err) t.Errorf("failed to laod dummy crypto material. Cause: %v", err)
} }
m := NewMsg() m := NewMsg()
m.SetBodyString(TypeTextPlain, "Plain") m.SetBodyString(TypeTextPlain, "Plain")
m.AddAlternativeString(TypeTextHTML, "<b>HTML</b>") m.AddAlternativeString(TypeTextHTML, "<b>HTML</b>")
if err := m.SignWithSMime(keyPair); err != nil { if err := m.SignWithSMime(privateKey, certificate, intermediateCertificate); err != nil {
t.Errorf("set of certificate was not successful") t.Errorf("failed to init smime configuration")
} }
if m.hasAlt() { if m.hasAlt() {
t.Errorf("mail has alternative parts and S/MIME is active, but hasAlt() returned true") t.Errorf("mail has alternative parts and S/MIME is active, but hasAlt() returned true")
@ -1926,13 +1926,13 @@ func TestMsg_hasAltWithSMime(t *testing.T) {
// TestMsg_hasSMime tests the hasSMime() method of the Msg // TestMsg_hasSMime tests the hasSMime() method of the Msg
func TestMsg_hasSMime(t *testing.T) { func TestMsg_hasSMime(t *testing.T) {
keyPair, err := getDummyCertificate() privateKey, certificate, intermediateCertificate, err := getDummyCryptoMaterial()
if err != nil { if err != nil {
t.Errorf("failed to load dummy certificate. Cause: %v", err) t.Errorf("failed to laod dummy crypto material. Cause: %v", err)
} }
m := NewMsg() m := NewMsg()
if err := m.SignWithSMime(keyPair); err != nil { if err := m.SignWithSMime(privateKey, certificate, intermediateCertificate); err != nil {
t.Errorf("set of certificate was not successful") t.Errorf("failed to init smime configuration")
} }
m.SetBodyString(TypeTextPlain, "Plain") m.SetBodyString(TypeTextPlain, "Plain")
if !m.hasSMime() { if !m.hasSMime() {
@ -2009,16 +2009,16 @@ func TestMsg_WriteToSkipMiddleware(t *testing.T) {
// TestMsg_WriteToWithSMIME tests the WriteTo() method of the Msg // TestMsg_WriteToWithSMIME tests the WriteTo() method of the Msg
func TestMsg_WriteToWithSMIME(t *testing.T) { func TestMsg_WriteToWithSMIME(t *testing.T) {
keyPair, err := getDummyCertificate() privateKey, certificate, intermediateCertificate, err := getDummyCryptoMaterial()
if err != nil { if err != nil {
t.Errorf("failed to load dummy certificate. Cause: %v", err) t.Errorf("failed to laod dummy crypto material. Cause: %v", err)
} }
m := NewMsg() m := NewMsg()
m.Subject("This is a test") m.Subject("This is a test")
m.SetBodyString(TypeTextPlain, "Plain") m.SetBodyString(TypeTextPlain, "Plain")
if err := m.SignWithSMime(keyPair); err != nil { if err := m.SignWithSMime(privateKey, certificate, intermediateCertificate); err != nil {
t.Errorf("set of certificate was not successful") t.Errorf("failed to init smime configuration")
} }
wbuf := bytes.Buffer{} wbuf := bytes.Buffer{}
@ -3345,12 +3345,12 @@ func TestNewMsgWithNoDefaultUserAgent(t *testing.T) {
// TestSignWithSMime_ValidKeyPair tests WithSMimeSinging with given key pair // TestSignWithSMime_ValidKeyPair tests WithSMimeSinging with given key pair
func TestSignWithSMime_ValidKeyPair(t *testing.T) { func TestSignWithSMime_ValidKeyPair(t *testing.T) {
keyPair, err := getDummyCertificate() privateKey, certificate, intermediateCertificate, err := getDummyCryptoMaterial()
if err != nil { if err != nil {
t.Errorf("failed to load dummy certificate. Cause: %v", err) t.Errorf("failed to laod dummy crypto material. Cause: %v", err)
} }
m := NewMsg() m := NewMsg()
if err := m.SignWithSMime(keyPair); err != nil { if err := m.SignWithSMime(privateKey, certificate, intermediateCertificate); err != nil {
t.Errorf("failed to set sMime. Cause: %v", err) t.Errorf("failed to set sMime. Cause: %v", err)
} }
if m.sMime.privateKey == nil { if m.sMime.privateKey == nil {
@ -3361,13 +3361,41 @@ func TestSignWithSMime_ValidKeyPair(t *testing.T) {
} }
} }
// TestSignWithSMime_InvalidKeyPair tests WithSMimeSinging with given invalid key pair // TestSignWithSMime_InvalidPrivateKey tests WithSMimeSinging with given invalid private key
func TestSignWithSMime_InvalidKeyPair(t *testing.T) { func TestSignWithSMime_InvalidPrivateKey(t *testing.T) {
m := NewMsg() m := NewMsg()
err := m.SignWithSMime(nil) err := m.SignWithSMime(nil, nil, nil)
if !errors.Is(err, ErrInvalidKeyPair) { if !errors.Is(err, ErrInvalidPrivateKey) {
t.Errorf("failed to check sMimeAuthConfig values correctly: %s", err) t.Errorf("failed to pre-check SignWithSMime method values correctly: %s", err)
}
}
// TestSignWithSMime_InvalidCertificate tests WithSMimeSinging with given invalid certificate
func TestSignWithSMime_InvalidCertificate(t *testing.T) {
privateKey, _, _, err := getDummyCryptoMaterial()
if err != nil {
t.Errorf("failed to laod dummy crypto material. Cause: %v", err)
}
m := NewMsg()
err = m.SignWithSMime(privateKey, nil, nil)
if !errors.Is(err, ErrInvalidCertificate) {
t.Errorf("failed to pre-check SignWithSMime method values correctly: %s", err)
}
}
// TestSignWithSMime_InvalidIntermediateCertificate tests WithSMimeSinging with given invalid intermediate certificate
func TestSignWithSMime_InvalidIntermediateCertificate(t *testing.T) {
privateKey, certificate, _, err := getDummyCryptoMaterial()
if err != nil {
t.Errorf("failed to laod dummy crypto material. Cause: %v", err)
}
m := NewMsg()
err = m.SignWithSMime(privateKey, certificate, nil)
if !errors.Is(err, ErrInvalidIntermediateCertificate) {
t.Errorf("failed to pre-check SignWithSMime method values correctly: %s", err)
} }
} }
@ -3401,13 +3429,13 @@ func FuzzMsg_From(f *testing.F) {
// TestMsg_createSignaturePart tests the Msg.createSignaturePart method // TestMsg_createSignaturePart tests the Msg.createSignaturePart method
func TestMsg_createSignaturePart(t *testing.T) { func TestMsg_createSignaturePart(t *testing.T) {
keyPair, err := getDummyCertificate() privateKey, certificate, intermediateCertificate, err := getDummyCryptoMaterial()
if err != nil { if err != nil {
t.Errorf("failed to load dummy certificate. Cause: %v", err) t.Errorf("failed to laod dummy crypto material. Cause: %v", err)
} }
m := NewMsg() m := NewMsg()
if err := m.SignWithSMime(keyPair); err != nil { if err := m.SignWithSMime(privateKey, certificate, intermediateCertificate); err != nil {
t.Errorf("set of certificate was not successful") t.Errorf("failed to init smime configuration")
} }
body := []byte("This is the body") body := []byte("This is the body")
part, err := m.createSignaturePart(EncodingQP, TypeTextPlain, CharsetUTF7, body) part, err := m.createSignaturePart(EncodingQP, TypeTextPlain, CharsetUTF7, body)
@ -3431,15 +3459,15 @@ func TestMsg_createSignaturePart(t *testing.T) {
// TestMsg_signMessage tests the Msg.signMessage method // TestMsg_signMessage tests the Msg.signMessage method
func TestMsg_signMessage(t *testing.T) { func TestMsg_signMessage(t *testing.T) {
keyPair, err := getDummyCertificate() privateKey, certificate, intermediateCertificate, err := getDummyCryptoMaterial()
if err != nil { if err != nil {
t.Errorf("failed to load dummy certificate. Cause: %v", err) t.Errorf("failed to laod dummy crypto material. Cause: %v", err)
} }
body := []byte("This is the body") body := []byte("This is the body")
m := NewMsg() m := NewMsg()
m.SetBodyString(TypeTextPlain, string(body)) m.SetBodyString(TypeTextPlain, string(body))
if err := m.SignWithSMime(keyPair); err != nil { if err := m.SignWithSMime(privateKey, certificate, intermediateCertificate); err != nil {
t.Errorf("set of certificate was not successful") t.Errorf("failed to init smime configuration")
} }
msg, err := m.signMessage(m) msg, err := m.signMessage(m)
if err != nil { if err != nil {

View file

@ -157,14 +157,14 @@ func TestMsgWriter_writeMsg_PGP(t *testing.T) {
// TestMsgWriter_writeMsg_SMime tests the writeMsg method of the msgWriter with S/MIME types set // TestMsgWriter_writeMsg_SMime tests the writeMsg method of the msgWriter with S/MIME types set
func TestMsgWriter_writeMsg_SMime(t *testing.T) { func TestMsgWriter_writeMsg_SMime(t *testing.T) {
keyPair, err := getDummyCertificate() privateKey, certificate, intermediateCertificate, err := getDummyCryptoMaterial()
if err != nil { if err != nil {
t.Errorf("failed to load dummy certificate. Cause: %v", err) t.Errorf("failed to laod dummy crypto material. Cause: %v", err)
} }
m := NewMsg() m := NewMsg()
if err := m.SignWithSMime(keyPair); err != nil { if err := m.SignWithSMime(privateKey, certificate, intermediateCertificate); err != nil {
t.Errorf("set of certificate was not successful") t.Errorf("failed to init smime configuration")
} }
_ = m.From(`"Toni Tester" <test@example.com>`) _ = m.From(`"Toni Tester" <test@example.com>`)
_ = m.To(`"Toni Receiver" <receiver@example.com>`) _ = m.To(`"Toni Receiver" <receiver@example.com>`)

View file

@ -152,7 +152,6 @@ func (p *Part) SetDescription(description string) {
p.description = description p.description = description
} }
// SetIsSMimeSigned sets the flag for signing the Part with S/MIME // SetIsSMimeSigned sets the flag for signing the Part with S/MIME
func (p *Part) SetIsSMimeSigned(smime bool) { func (p *Part) SetIsSMimeSigned(smime bool) {
p.smime = smime p.smime = smime

View file

@ -1,20 +1,30 @@
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
//
// SPDX-License-Identifier: MIT
package mail package mail
import ( import (
"bytes" "bytes"
"crypto/rsa" "crypto/rsa"
"crypto/tls"
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
"errors" "errors"
"fmt" "fmt"
"go.mozilla.org/pkcs7"
"strings" "strings"
"go.mozilla.org/pkcs7"
) )
var ( var (
// ErrInvalidKeyPair should be used if key pair is invalid // ErrInvalidPrivateKey should be used if private key is invalid
ErrInvalidKeyPair = errors.New("invalid key pair") ErrInvalidPrivateKey = errors.New("invalid private key")
// ErrInvalidCertificate should be used if the certificate is invalid
ErrInvalidCertificate = errors.New("invalid certificate")
// ErrInvalidIntermediateCertificate should be used if the intermediate certificate is invalid
ErrInvalidIntermediateCertificate = errors.New("invalid intermediate 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")
@ -31,19 +41,32 @@ var (
// 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
intermediateCertificate *x509.Certificate
} }
// NewSMime construct a new instance of SMime with a provided *tls.Certificate // NewSMime construct a new instance of SMime with provided parameters
func newSMime(keyPair *tls.Certificate) (*SMime, error) { // privateKey as *rsa.PrivateKey
if keyPair == nil { // certificate as *x509.Certificate
return nil, ErrInvalidKeyPair // intermediateCertificate as *x509.Certificate
func newSMime(privateKey *rsa.PrivateKey, certificate *x509.Certificate, intermediateCertificate *x509.Certificate) (*SMime, error) {
if privateKey == nil {
return nil, ErrInvalidPrivateKey
}
if certificate == nil {
return nil, ErrInvalidCertificate
}
if intermediateCertificate == nil {
return nil, ErrInvalidIntermediateCertificate
} }
return &SMime{ return &SMime{
privateKey: keyPair.PrivateKey.(*rsa.PrivateKey), privateKey: privateKey,
certificate: keyPair.Leaf, certificate: certificate,
intermediateCertificate: intermediateCertificate,
}, nil }, nil
} }
@ -58,7 +81,7 @@ func (sm *SMime) signMessage(message string) (*string, error) {
return nil, ErrCouldNotInitialize return nil, ErrCouldNotInitialize
} }
if err = signedData.AddSigner(sm.certificate, sm.privateKey, pkcs7.SignerInfoConfig{}); err != nil { if err = signedData.AddSignerChain(sm.certificate, sm.privateKey, []*x509.Certificate{sm.intermediateCertificate}, pkcs7.SignerInfoConfig{}); err != nil {
return nil, ErrCouldNotAddSigner return nil, ErrCouldNotAddSigner
} }

View file

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
//
// SPDX-License-Identifier: MIT
package mail package mail
import ( import (
@ -10,32 +14,35 @@ import (
// TestNewSMime tests the newSMime method // TestNewSMime tests the newSMime method
func TestNewSMime(t *testing.T) { func TestNewSMime(t *testing.T) {
keyPair, err := getDummyCertificate() privateKey, certificate, intermediateCertificate, err := getDummyCryptoMaterial()
if err != nil { if err != nil {
t.Errorf("Error getting dummy certificate: %s", err) t.Errorf("Error getting dummy crypto material: %s", err)
} }
sMime, err := newSMime(keyPair) sMime, err := newSMime(privateKey, certificate, intermediateCertificate)
if err != nil { if err != nil {
t.Errorf("Error creating new SMime from keyPair: %s", err) t.Errorf("Error creating new SMime from keyPair: %s", err)
} }
if sMime.privateKey != keyPair.PrivateKey { if sMime.privateKey != privateKey {
t.Errorf("NewSMime() did not return the same private key") t.Errorf("NewSMime() did not return the same private key")
} }
if sMime.certificate != keyPair.Leaf { if sMime.certificate != certificate {
t.Errorf("NewSMime() did not return the same leaf certificate") t.Errorf("NewSMime() did not return the same certificate")
}
if sMime.intermediateCertificate != intermediateCertificate {
t.Errorf("NewSMime() did not return the same intermedidate certificate")
} }
} }
// TestSign tests the sign method // TestSign tests the sign method
func TestSign(t *testing.T) { func TestSign(t *testing.T) {
keyPair, err := getDummyCertificate() privateKey, certificate, intermediateCertificate, err := getDummyCryptoMaterial()
if err != nil { if err != nil {
t.Errorf("Error getting dummy certificate: %s", err) t.Errorf("Error getting dummy crypto material: %s", err)
} }
sMime, err := newSMime(keyPair) sMime, err := newSMime(privateKey, certificate, intermediateCertificate)
if err != nil { if err != nil {
t.Errorf("Error creating new SMime from keyPair: %s", err) t.Errorf("Error creating new SMime from keyPair: %s", err)
} }
@ -53,12 +60,12 @@ func TestSign(t *testing.T) {
// TestPrepareMessage tests the createMessage method // TestPrepareMessage tests the createMessage method
func TestPrepareMessage(t *testing.T) { func TestPrepareMessage(t *testing.T) {
keyPair, err := getDummyCertificate() privateKey, certificate, intermediateCertificate, err := getDummyCryptoMaterial()
if err != nil { if err != nil {
t.Errorf("Error getting dummy certificate: %s", err) t.Errorf("Error getting dummy crypto material: %s", err)
} }
sMime, err := newSMime(keyPair) sMime, err := newSMime(privateKey, certificate, intermediateCertificate)
if err != nil { if err != nil {
t.Errorf("Error creating new SMime from keyPair: %s", err) t.Errorf("Error creating new SMime from keyPair: %s", err)
} }

View file

@ -1,6 +1,11 @@
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
//
// SPDX-License-Identifier: MIT
package mail package mail
import ( import (
"crypto/rsa"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
) )
@ -10,14 +15,24 @@ const (
keyFilePath = "dummy-child-key.pem" keyFilePath = "dummy-child-key.pem"
) )
// getDummyCertificate loads a certificate and a private key form local disk for testing purposes // getDummyCryptoMaterial loads a certificate and a private key form local disk for testing purposes
func getDummyCertificate() (*tls.Certificate, error) { func getDummyCryptoMaterial() (*rsa.PrivateKey, *x509.Certificate, *x509.Certificate, error) {
keyPair, err := tls.LoadX509KeyPair(certFilePath, keyFilePath) keyPair, err := tls.LoadX509KeyPair(certFilePath, keyFilePath)
if err != nil { if err != nil {
return nil, err return nil, nil, nil, err
} }
keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0]) privateKey := keyPair.PrivateKey.(*rsa.PrivateKey)
return &keyPair, nil certificate, err := x509.ParseCertificate(keyPair.Certificate[0])
if err != nil {
return nil, nil, nil, err
}
intermediateCertificate, err := x509.ParseCertificate(keyPair.Certificate[1])
if err != nil {
return nil, nil, nil, err
}
return privateKey, certificate, intermediateCertificate, nil
} }