From 07d9654ce72daab7e98955b20f1eb82a8f563b17 Mon Sep 17 00:00:00 2001 From: theexiile1305 Date: Sun, 15 Sep 2024 19:49:27 +0200 Subject: [PATCH] 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. --- client.go | 17 ++++++++++++ client_test.go | 75 +++++++++++++++++++++++++++++++++++++++++++++++++- msg.go | 10 +++++++ msg_test.go | 8 ++++++ sime.go | 12 ++++++++ 5 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 sime.go diff --git a/client.go b/client.go index 1d175d1..a2275ad 100644 --- a/client.go +++ b/client.go @@ -127,6 +127,9 @@ type Client struct { // smtpAuthType represents the authentication type for SMTP AUTH smtpAuthType SMTPAuthType + // SMimeAuthConfig represents the authentication type for s/mime crypto key material + sMimeAuthConfig *SMimeAuthConfig + // smtpClient is the smtp.Client that is set up when using the Dial*() methods smtpClient *smtp.Client @@ -168,6 +171,9 @@ var ( // ErrInvalidTLSConfig should be used if an empty tls.Config is provided ErrInvalidTLSConfig = errors.New("invalid TLS config") + // ErrInvalidSMimeAuthConfig should be used if the values in the struct SMimeAuthConfig are empty + ErrInvalidSMimeAuthConfig = errors.New("invalid S/MIME authentication config") + // ErrNoHostname should be used if a Client has no hostname set ErrNoHostname = errors.New("hostname for client cannot be empty") @@ -459,6 +465,17 @@ func WithDialContextFunc(dialCtxFunc DialContextFunc) Option { } } +// WithSMimeConfig tells the client to use the provided SMIMEAuth for s/mime crypto key material authentication +func WithSMimeConfig(sMimeAuthConfig *SMimeAuthConfig) Option { + return func(c *Client) error { + if sMimeAuthConfig.Certificate == nil && sMimeAuthConfig.PrivateKey == nil { + return ErrInvalidSMimeAuthConfig + } + c.sMimeAuthConfig = sMimeAuthConfig + return nil + } +} + // TLSPolicy returns the currently set TLSPolicy as string func (c *Client) TLSPolicy() string { return c.tlspolicy.String() diff --git a/client_test.go b/client_test.go index 0ad309a..cae8d71 100644 --- a/client_test.go +++ b/client_test.go @@ -6,10 +6,15 @@ package mail import ( "context" + "crypto/rand" + "crypto/rsa" "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" "errors" "fmt" "io" + "math/big" "net" "os" "strconv" @@ -117,7 +122,7 @@ func TestNewClientWithOptions(t *testing.T) { {"WithDialContextFunc()", WithDialContextFunc(func(ctx context.Context, network, address string) (net.Conn, error) { return nil, nil }), false}, - + {"WithSMimeConfig()", WithSMimeConfig(&SMimeAuthConfig{}), true}, { "WithDSNRcptNotifyType() NEVER combination", WithDSNRcptNotifyType(DSNRcptNotifySuccess, DSNRcptNotifyNever), true, @@ -756,6 +761,43 @@ func TestClient_DialWithContextInvalidAuth(t *testing.T) { } } +// TestWithSMime tests the WithSMime method with invalid SMimeAuthConfig for the Client object +func TestWithSMime_InvalidConfig(t *testing.T) { + _, err := NewClient(DefaultHost, WithSMimeConfig(&SMimeAuthConfig{})) + if !errors.Is(err, ErrInvalidSMimeAuthConfig) { + t.Errorf("failed to check sMimeAuthConfig values correctly: %s", err) + } +} + +// TestWithSMime tests the WithSMime method with valid SMimeAuthConfig that is loaded from dummy certificate for the Client object +func TestWithSMime_ValidConfig(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) + } + + sMimeAuthConfig := &SMimeAuthConfig{PrivateKey: privateKey, Certificate: certificate} + c, err := NewClient(DefaultHost, WithSMimeConfig(sMimeAuthConfig)) + if err != nil { + t.Errorf("failed to create new client: %s", err) + } + + if c.sMimeAuthConfig != sMimeAuthConfig { + t.Errorf("failed to set smeAuthConfig. Expected %v, got: %v", sMimeAuthConfig, c.sMimeAuthConfig) + } + if c.sMimeAuthConfig.PrivateKey != sMimeAuthConfig.PrivateKey { + t.Errorf("failed to set smeAuthConfig.PrivateKey Expected %v, got: %v", sMimeAuthConfig, c.sMimeAuthConfig) + } + if c.sMimeAuthConfig.Certificate != sMimeAuthConfig.Certificate { + t.Errorf("failed to set smeAuthConfig.Certificate Expected %v, got: %v", sMimeAuthConfig, c.sMimeAuthConfig) + } +} + // TestClient_checkConn tests the checkConn method with intentional breaking for the Client object func TestClient_checkConn(t *testing.T) { c, err := getTestConnection(true) @@ -1484,6 +1526,37 @@ func getFakeDialFunc(conn net.Conn) DialContextFunc { } } +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 +} + type faker struct { io.ReadWriter } diff --git a/msg.go b/msg.go index a909d04..17fdd14 100644 --- a/msg.go +++ b/msg.go @@ -120,6 +120,9 @@ type Msg struct { // noDefaultUserAgent indicates whether the default User Agent will be excluded for the Msg when it's sent. noDefaultUserAgent bool + + // sMimeSinging indicates whether the message should be singed with S/MIME when it's sent. + sMimeSinging bool } // SendmailPath is the default system path to the sendmail binary @@ -202,6 +205,13 @@ func WithNoDefaultUserAgent() MsgOption { } } +// WithSMimeSinging configures the Msg to be S/MIME singed sent. +func WithSMimeSinging() MsgOption { + return func(m *Msg) { + m.sMimeSinging = true + } +} + // SetCharset sets the encoding charset of the Msg func (m *Msg) SetCharset(c Charset) { m.charset = c diff --git a/msg_test.go b/msg_test.go index 16cd196..082f79f 100644 --- a/msg_test.go +++ b/msg_test.go @@ -3233,6 +3233,14 @@ func TestNewMsgWithNoDefaultUserAgent(t *testing.T) { } } +// TestWithSMimeSinging tests WithSMimeSinging +func TestWithSMimeSinging(t *testing.T) { + m := NewMsg(WithSMimeSinging()) + if m.sMimeSinging != true { + t.Errorf("WithSMimeSinging() failed. Expected: %t, got: %t", true, false) + } +} + // Fuzzing tests func FuzzMsg_Subject(f *testing.F) { f.Add("Testsubject") diff --git a/sime.go b/sime.go new file mode 100644 index 0000000..714d3a4 --- /dev/null +++ b/sime.go @@ -0,0 +1,12 @@ +package mail + +import ( + "crypto/rsa" + "crypto/x509" +) + +// SMimeAuthConfig represents the authentication type for s/mime crypto key material +type SMimeAuthConfig struct { + Certificate *x509.Certificate + PrivateKey *rsa.PrivateKey +}