From 07d9654ce72daab7e98955b20f1eb82a8f563b17 Mon Sep 17 00:00:00 2001 From: theexiile1305 Date: Sun, 15 Sep 2024 19:49:27 +0200 Subject: [PATCH 01/33] 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 +} From 158c1b0458a01266d4f61b4cc3ad81f34f6b94d2 Mon Sep 17 00:00:00 2001 From: theexiile1305 Date: Wed, 18 Sep 2024 14:13:24 +0200 Subject: [PATCH 02/33] moved S/MIME initialization into msg and the pointer to the data structure, also adjusted tests --- client.go | 17 ------------ client_test.go | 74 -------------------------------------------------- msg.go | 20 ++++++++++---- msg_test.go | 32 ++++++++++++++++++---- util_test.go | 41 ++++++++++++++++++++++++++++ 5 files changed, 82 insertions(+), 102 deletions(-) create mode 100644 util_test.go diff --git a/client.go b/client.go index a2275ad..1d175d1 100644 --- a/client.go +++ b/client.go @@ -127,9 +127,6 @@ 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 @@ -171,9 +168,6 @@ 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") @@ -465,17 +459,6 @@ 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 cae8d71..afe384b 100644 --- a/client_test.go +++ b/client_test.go @@ -6,15 +6,10 @@ package mail import ( "context" - "crypto/rand" - "crypto/rsa" "crypto/tls" - "crypto/x509" - "crypto/x509/pkix" "errors" "fmt" "io" - "math/big" "net" "os" "strconv" @@ -122,7 +117,6 @@ 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, @@ -761,43 +755,6 @@ 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) @@ -1526,37 +1483,6 @@ 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 17fdd14..24e2ca6 100644 --- a/msg.go +++ b/msg.go @@ -121,8 +121,8 @@ 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 + // SMime represents a middleware used to sign messages with S/MIME + sMime *SMime } // SendmailPath is the default system path to the sendmail binary @@ -205,11 +205,14 @@ func WithNoDefaultUserAgent() MsgOption { } } -// WithSMimeSinging configures the Msg to be S/MIME singed sent. -func WithSMimeSinging() MsgOption { - return func(m *Msg) { - m.sMimeSinging = true +// 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 @@ -1176,6 +1179,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) diff --git a/msg_test.go b/msg_test.go index 082f79f..e133397 100644 --- a/msg_test.go +++ b/msg_test.go @@ -3233,11 +3233,33 @@ 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) +// 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) } } diff --git a/util_test.go b/util_test.go new file mode 100644 index 0000000..efc088c --- /dev/null +++ b/util_test.go @@ -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 +} From 6fda661dc78fe605791b5ffa39263ec4f6a2df7e Mon Sep 17 00:00:00 2001 From: theexiile1305 Date: Wed, 18 Sep 2024 14:16:48 +0200 Subject: [PATCH 03/33] added content type, mime type and content disposition for S/MIME singing purpose --- encoding.go | 15 +++++++++++++++ encoding_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/encoding.go b/encoding.go index 47213da..0640d31 100644 --- a/encoding.go +++ b/encoding.go @@ -19,6 +19,9 @@ type MIMEVersion string // MIMEType represents the MIME type for the mail type MIMEType string +// Disposition represents a content disposition for the Msg +type Disposition string + // List of supported encodings const ( // EncodingB64 represents the Base64 encoding as specified in RFC 2045. @@ -149,6 +152,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 +160,12 @@ const ( MIMEAlternative MIMEType = "alternative" MIMEMixed MIMEType = "mixed" MIMERelated MIMEType = "related" + MIMESMime MIMEType = `signed; protocol="application/pkcs7-signature"; micalg=sha256` +) + +// List of common content disposition +const ( + DispositionSMime Disposition = `attachment; filename="smime.p7s"` ) // String is a standard method to convert an Charset into a printable format @@ -172,3 +182,8 @@ func (c ContentType) String() string { func (e Encoding) String() string { return string(e) } + +// String is a standard method to convert an Disposition into a printable format +func (d Disposition) String() string { + return string(d) +} diff --git a/encoding_test.go b/encoding_test.go index 14711b7..3084063 100644 --- a/encoding_test.go +++ b/encoding_test.go @@ -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"; smime-type=signed-data`, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -121,3 +126,22 @@ func TestCharset_String(t *testing.T) { }) } } + +// TestDisposition_String tests the string method of the Disposition object +func TestDisposition_String(t *testing.T) { + tests := []struct { + name string + d Disposition + want string + }{ + {"Disposition: S/Mime", DispositionSMime, `attachment; filename="smime.p7s"`}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.d.String() != tt.want { + t.Errorf("wrong string for Disposition returned. Expected: %s, got: %s", + tt.want, tt.d.String()) + } + }) + } +} From e0a59dba6da226dfae2e8fdded66c1e538afb083 Mon Sep 17 00:00:00 2001 From: theexiile1305 Date: Wed, 18 Sep 2024 14:17:40 +0200 Subject: [PATCH 04/33] introduced content disposition in part --- part.go | 18 ++++++++++++++++++ part_test.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/part.go b/part.go index 7c76b7d..005da29 100644 --- a/part.go +++ b/part.go @@ -17,6 +17,7 @@ type Part struct { contentType ContentType charset Charset description string + disposition Disposition encoding Encoding isDeleted bool writeFunc func(io.Writer) (int64, error) @@ -56,6 +57,11 @@ func (p *Part) GetDescription() string { return p.description } +// GetDisposition returns the currently set Content-Disposition of the Part +func (p *Part) GetDisposition() Disposition { + return p.disposition +} + // 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 } +// SetDisposition overrides the Content-Disposition of the Part +func (p *Part) SetDisposition(disposition Disposition) { + p.disposition = disposition +} + // 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 } } + +// WithContentDisposition overrides the default Part Content-Disposition +func WithContentDisposition(disposition Disposition) PartOption { + return func(p *Part) { + p.disposition = disposition + } +} diff --git a/part_test.go b/part_test.go index a196986..800acff 100644 --- a/part_test.go +++ b/part_test.go @@ -102,6 +102,35 @@ func TestPart_WithPartContentDescription(t *testing.T) { } } +// TestPart_withContentDisposition tests the WithContentDisposition method +func TestPart_withContentDisposition(t *testing.T) { + tests := []struct { + name string + disposition Disposition + }{ + {"Part disposition: test", "test"}, + {"Part disposition: empty", ""}, + } + for _, tt := range tests { + m := NewMsg() + t.Run(tt.name, func(t *testing.T) { + part := m.newPart(TypeTextPlain, WithContentDisposition(tt.disposition), nil) + if part == nil { + t.Errorf("newPart() WithPartContentDescription() failed: no part returned") + return + } + if part.disposition != tt.disposition { + t.Errorf("newPart() WithContentDisposition() failed: expected: %s, got: %s", tt.disposition, part.description) + } + part.disposition = "" + part.SetDisposition(tt.disposition) + if part.disposition != tt.disposition { + t.Errorf("newPart() SetDisposition() failed: expected: %s, got: %s", tt.disposition, part.description) + } + }) + } +} + // TestPartContentType tests Part.SetContentType func TestPart_SetContentType(t *testing.T) { tests := []struct { @@ -323,6 +352,31 @@ func TestPart_SetDescription(t *testing.T) { } } +// TestPart_SetDisposition tests Part.SetDisposition +func TestPart_SetDisposition(t *testing.T) { + c := "This is a test" + d := Disposition("test-disposition") + m := NewMsg() + m.SetBodyString(TypeTextPlain, c) + pl, err := getPartList(m) + if err != nil { + t.Errorf("failed: %s", err) + return + } + pd := pl[0].GetDisposition() + if pd != "" { + t.Errorf("Part.GetDisposition failed. Expected empty description but got: %s", pd) + } + pl[0].SetDisposition(d) + if pl[0].disposition != d { + t.Errorf("Part.SetDisposition failed. Expected description to be: %s, got: %s", d, pd) + } + pd = pl[0].GetDisposition() + if pd != d { + t.Errorf("Part.GetDisposition failed. Expected: %s, got: %s", d, pd) + } +} + // TestPart_Delete tests Part.Delete func TestPart_Delete(t *testing.T) { c := "This is a test with ümläutß" From 94942ed383654a689457b878c4df9609936d579c Mon Sep 17 00:00:00 2001 From: theexiile1305 Date: Wed, 18 Sep 2024 14:23:26 +0200 Subject: [PATCH 05/33] implementation of S/MIME singing without tests of type smime --- go.mod | 2 ++ go.sum | 2 ++ msg.go | 41 +++++++++++++++++++++++++++++++- msgwriter.go | 9 ++++++- sime.go | 66 ++++++++++++++++++++++++++++++++++++++++++++++++---- 5 files changed, 114 insertions(+), 6 deletions(-) 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 } From 41a81966d8f7eabb7042eede0197fc2737ecf060 Mon Sep 17 00:00:00 2001 From: theexiile1305 Date: Wed, 18 Sep 2024 14:28:55 +0200 Subject: [PATCH 06/33] remove new line --- client_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/client_test.go b/client_test.go index afe384b..0ad309a 100644 --- a/client_test.go +++ b/client_test.go @@ -117,6 +117,7 @@ func TestNewClientWithOptions(t *testing.T) { {"WithDialContextFunc()", WithDialContextFunc(func(ctx context.Context, network, address string) (net.Conn, error) { return nil, nil }), false}, + { "WithDSNRcptNotifyType() NEVER combination", WithDSNRcptNotifyType(DSNRcptNotifySuccess, DSNRcptNotifyNever), true, From 894936092edc287fd9d7f4ce803360894ce8aab2 Mon Sep 17 00:00:00 2001 From: theexiile1305 Date: Wed, 18 Sep 2024 14:35:40 +0200 Subject: [PATCH 07/33] fixing test --- encoding_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/encoding_test.go b/encoding_test.go index 3084063..588cff1 100644 --- a/encoding_test.go +++ b/encoding_test.go @@ -64,7 +64,7 @@ func TestContentType_String(t *testing.T) { { "ContentType: pkcs7-signature", typeSMimeSigned, - `application/pkcs7-signature; name="smime.p7s"; smime-type=signed-data`, + `application/pkcs7-signature; name="smime.p7s"`, }, } for _, tt := range tests { From 65b9fd07dab98c253291f4984da3ddbc2b66cf39 Mon Sep 17 00:00:00 2001 From: theexiile1305 Date: Thu, 26 Sep 2024 11:07:14 +0200 Subject: [PATCH 08/33] feat: remove unused content disposition --- encoding.go | 13 ------------ encoding_test.go | 19 ----------------- msgwriter.go | 3 --- part.go | 18 ---------------- part_test.go | 54 ------------------------------------------------ 5 files changed, 107 deletions(-) diff --git a/encoding.go b/encoding.go index 0640d31..77243fc 100644 --- a/encoding.go +++ b/encoding.go @@ -19,9 +19,6 @@ type MIMEVersion string // MIMEType represents the MIME type for the mail type MIMEType string -// Disposition represents a content disposition for the Msg -type Disposition string - // List of supported encodings const ( // EncodingB64 represents the Base64 encoding as specified in RFC 2045. @@ -163,11 +160,6 @@ const ( MIMESMime MIMEType = `signed; protocol="application/pkcs7-signature"; micalg=sha256` ) -// List of common content disposition -const ( - DispositionSMime Disposition = `attachment; filename="smime.p7s"` -) - // String is a standard method to convert an Charset into a printable format func (c Charset) String() string { return string(c) @@ -182,8 +174,3 @@ func (c ContentType) String() string { func (e Encoding) String() string { return string(e) } - -// String is a standard method to convert an Disposition into a printable format -func (d Disposition) String() string { - return string(d) -} diff --git a/encoding_test.go b/encoding_test.go index 588cff1..11bf991 100644 --- a/encoding_test.go +++ b/encoding_test.go @@ -126,22 +126,3 @@ func TestCharset_String(t *testing.T) { }) } } - -// TestDisposition_String tests the string method of the Disposition object -func TestDisposition_String(t *testing.T) { - tests := []struct { - name string - d Disposition - want string - }{ - {"Disposition: S/Mime", DispositionSMime, `attachment; filename="smime.p7s"`}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.d.String() != tt.want { - t.Errorf("wrong string for Disposition returned. Expected: %s, got: %s", - tt.want, tt.d.String()) - } - }) - } -} diff --git a/msgwriter.go b/msgwriter.go index 684c223..372e1e1 100644 --- a/msgwriter.go +++ b/msgwriter.go @@ -269,9 +269,6 @@ 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/part.go b/part.go index 005da29..7c76b7d 100644 --- a/part.go +++ b/part.go @@ -17,7 +17,6 @@ type Part struct { contentType ContentType charset Charset description string - disposition Disposition encoding Encoding isDeleted bool writeFunc func(io.Writer) (int64, error) @@ -57,11 +56,6 @@ func (p *Part) GetDescription() string { return p.description } -// GetDisposition returns the currently set Content-Disposition of the Part -func (p *Part) GetDisposition() Disposition { - return p.disposition -} - // SetContent overrides the content of the Part with the given string func (p *Part) SetContent(content string) { buffer := bytes.NewBufferString(content) @@ -88,11 +82,6 @@ func (p *Part) SetDescription(description string) { p.description = description } -// SetDisposition overrides the Content-Disposition of the Part -func (p *Part) SetDisposition(disposition Disposition) { - p.disposition = disposition -} - // SetWriteFunc overrides the WriteFunc of the Part func (p *Part) SetWriteFunc(writeFunc func(io.Writer) (int64, error)) { p.writeFunc = writeFunc @@ -124,10 +113,3 @@ func WithPartContentDescription(description string) PartOption { p.description = description } } - -// WithContentDisposition overrides the default Part Content-Disposition -func WithContentDisposition(disposition Disposition) PartOption { - return func(p *Part) { - p.disposition = disposition - } -} diff --git a/part_test.go b/part_test.go index 800acff..a196986 100644 --- a/part_test.go +++ b/part_test.go @@ -102,35 +102,6 @@ func TestPart_WithPartContentDescription(t *testing.T) { } } -// TestPart_withContentDisposition tests the WithContentDisposition method -func TestPart_withContentDisposition(t *testing.T) { - tests := []struct { - name string - disposition Disposition - }{ - {"Part disposition: test", "test"}, - {"Part disposition: empty", ""}, - } - for _, tt := range tests { - m := NewMsg() - t.Run(tt.name, func(t *testing.T) { - part := m.newPart(TypeTextPlain, WithContentDisposition(tt.disposition), nil) - if part == nil { - t.Errorf("newPart() WithPartContentDescription() failed: no part returned") - return - } - if part.disposition != tt.disposition { - t.Errorf("newPart() WithContentDisposition() failed: expected: %s, got: %s", tt.disposition, part.description) - } - part.disposition = "" - part.SetDisposition(tt.disposition) - if part.disposition != tt.disposition { - t.Errorf("newPart() SetDisposition() failed: expected: %s, got: %s", tt.disposition, part.description) - } - }) - } -} - // TestPartContentType tests Part.SetContentType func TestPart_SetContentType(t *testing.T) { tests := []struct { @@ -352,31 +323,6 @@ func TestPart_SetDescription(t *testing.T) { } } -// TestPart_SetDisposition tests Part.SetDisposition -func TestPart_SetDisposition(t *testing.T) { - c := "This is a test" - d := Disposition("test-disposition") - m := NewMsg() - m.SetBodyString(TypeTextPlain, c) - pl, err := getPartList(m) - if err != nil { - t.Errorf("failed: %s", err) - return - } - pd := pl[0].GetDisposition() - if pd != "" { - t.Errorf("Part.GetDisposition failed. Expected empty description but got: %s", pd) - } - pl[0].SetDisposition(d) - if pl[0].disposition != d { - t.Errorf("Part.SetDisposition failed. Expected description to be: %s, got: %s", d, pd) - } - pd = pl[0].GetDisposition() - if pd != d { - t.Errorf("Part.GetDisposition failed. Expected: %s, got: %s", d, pd) - } -} - // TestPart_Delete tests Part.Delete func TestPart_Delete(t *testing.T) { c := "This is a test with ümläutß" From 3154649420179555644057f3c1927f65b88bd917 Mon Sep 17 00:00:00 2001 From: theexiile1305 Date: Thu, 26 Sep 2024 11:37:53 +0200 Subject: [PATCH 09/33] feat: add flag smime for indicating that the part should be marked for singing with S/MIME --- part.go | 18 ++++++++++++++++++ part_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/part.go b/part.go index 7c76b7d..035b342 100644 --- a/part.go +++ b/part.go @@ -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 + } +} diff --git a/part_test.go b/part_test.go index a196986..2b9725f 100644 --- a/part_test.go +++ b/part_test.go @@ -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ß" From 12edb2724bb2e25077afd5b9e23078e53ee031e7 Mon Sep 17 00:00:00 2001 From: theexiile1305 Date: Thu, 26 Sep 2024 16:32:51 +0200 Subject: [PATCH 10/33] feat: implementation of S/MIME signing without tests --- msg.go | 44 ++++++++-------- msgwriter.go | 17 +++---- sime.go | 141 +++++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 146 insertions(+), 56 deletions(-) diff --git a/msg.go b/msg.go index 9686dac..286e533 100644 --- a/msg.go +++ b/msg.go @@ -7,8 +7,7 @@ package mail import ( "bytes" "context" - "crypto/rsa" - "crypto/x509" + "crypto/tls" "embed" "errors" "fmt" @@ -208,8 +207,8 @@ 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) +func (m *Msg) SignWithSMime(keyPair *tls.Certificate) error { + sMime, err := newSMime(keyPair) if err != nil { return err } @@ -985,28 +984,31 @@ func (m *Msg) applyMiddlewares(msg *Msg) *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() + signedPart := msg.GetParts()[0] + body, err := signedPart.GetContent() 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 { - return nil, errors.New("failed to sign message") + return nil, err } - signedPart := msg.newPart( - typeSMimeSigned, - WithPartEncoding(EncodingB64), - WithContentDisposition(DispositionSMime), - ) - signedPart.SetContent(*signedContent) - msg.parts = append(msg.parts, signedPart) + m.parts = append(m.parts, signaturePart) - 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 @@ -1014,7 +1016,7 @@ func (m *Msg) WriteTo(writer io.Writer) (int64, error) { mw := &msgWriter{writer: writer, charset: m.charset, encoder: m.encoder} msg := m.applyMiddlewares(m) - if m.sMime != nil { + if m.hasSMime() { signedMsg, err := m.signMessage(msg) if err != nil { return 0, err @@ -1210,7 +1212,7 @@ func (m *Msg) hasAlt() bool { count++ } } - return count > 1 && m.pgptype == 0 + return count > 1 && m.pgptype == 0 && !m.hasSMime() } // hasMixed returns true if the Msg has mixed parts diff --git a/msgwriter.go b/msgwriter.go index 372e1e1..7c1e88e 100644 --- a/msgwriter.go +++ b/msgwriter.go @@ -100,7 +100,7 @@ func (mw *msgWriter) writeMsg(msg *Msg) { mw.startMP(MIMERelated, msg.boundary) mw.writeString(DoubleNewLine) } - if msg.hasAlt() && !msg.hasSMime() { + if msg.hasAlt() { mw.startMP(MIMEAlternative, msg.boundary) mw.writeString(DoubleNewLine) } @@ -241,7 +241,7 @@ func (mw *msgWriter) addFiles(files []*File, isAttachment bool) { } 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) 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 @@ -322,7 +322,7 @@ func (mw *msgWriter) writeHeader(key Header, values ...string) { } // 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 encodedWriter io.WriteCloser var n int64 @@ -337,12 +337,11 @@ func (mw *msgWriter) writeBody(writeFunc func(io.Writer) (int64, error), encodin lineBreaker := Base64LineBreaker{} lineBreaker.out = &writeBuffer - switch encoding { - case EncodingQP: + if encoding == EncodingQP { encodedWriter = quotedprintable.NewWriter(&writeBuffer) - case EncodingB64: + } else if encoding == EncodingB64 && !singingWithSMime { encodedWriter = base64.NewEncoder(base64.StdEncoding, &lineBreaker) - case NoEncoding: + } else if encoding == NoEncoding { _, err = writeFunc(&writeBuffer) if err != nil { 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 } return - default: + } else { encodedWriter = quotedprintable.NewWriter(writer) } diff --git a/sime.go b/sime.go index f230a69..fe05158 100644 --- a/sime.go +++ b/sime.go @@ -1,18 +1,20 @@ package mail import ( + "bytes" "crypto/rsa" + "crypto/tls" "crypto/x509" + "encoding/pem" "errors" + "fmt" "go.mozilla.org/pkcs7" + "strings" ) 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") + // ErrInvalidKeyPair should be used if key pair is invalid + ErrInvalidKeyPair = errors.New("invalid key pair") // ErrCouldNotInitialize should be used if the signed data could not initialize 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 = 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 type SMime struct { - privateKey *rsa.PrivateKey - certificate *x509.Certificate + privateKey *rsa.PrivateKey + certificate *x509.Certificate + parentCertificates []*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 +// NewSMime construct a new instance of SMime with a provided *tls.Certificate +func newSMime(keyPair *tls.Certificate) (*SMime, error) { + if keyPair == nil { + return nil, ErrInvalidKeyPair } - if certificate == nil { - return nil, ErrInvalidCertificate + parentCertificates := make([]*x509.Certificate, 0) + for _, cert := range keyPair.Certificate[1:] { + c, err := x509.ParseCertificate(cert) + if err != nil { + return nil, err + } + parentCertificates = append(parentCertificates, c) } return &SMime{ - privateKey: privateKey, - certificate: certificate, + privateKey: keyPair.PrivateKey.(*rsa.PrivateKey), + certificate: keyPair.Leaf, + parentCertificates: parentCertificates, }, 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) +// sign with the S/MIME method the message of the actual *Part +func (sm *SMime) sign(signaturePart *Part, message string) error { + 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 { - return nil, ErrCouldNotInitialize + return ErrCouldNotInitialize } - if err = toBeSigned.AddSigner(sm.certificate, sm.privateKey, pkcs7.SignerInfoConfig{}); err != nil { - return nil, ErrCouldNotAddSigner + if err = tmp.AddSignerChain(sm.certificate, sm.privateKey, sm.parentCertificates, pkcs7.SignerInfoConfig{}); err != nil { + return ErrCouldNotAddSigner } - signed, err := toBeSigned.Finish() + signatureDER, err := tmp.Finish() if err != nil { - return nil, ErrCouldNotFinishSigning + return ErrCouldNotFinishSigning } - signedData := string(signed) + pemMsg, err := encodeToPEM(signatureDER) + if err != nil { + return ErrCouldNoEncodeToPEM + } + signaturePart.SetContent(*pemMsg) - return &signedData, nil + 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 } From 23681138728c82f4d85eee9ba7987141975520ac Mon Sep 17 00:00:00 2001 From: theexiile1305 Date: Thu, 26 Sep 2024 16:43:58 +0200 Subject: [PATCH 11/33] feat: implementation of tests --- msg_test.go | 130 ++++++++++++++++++++++++++++++++++++++++------ msgwriter_test.go | 23 ++++++++ util_test.go | 42 ++++----------- 3 files changed, 147 insertions(+), 48 deletions(-) diff --git a/msg_test.go b/msg_test.go index e133397..6e35d49 100644 --- a/msg_test.go +++ b/msg_test.go @@ -1894,6 +1894,23 @@ func TestMsg_hasAlt(t *testing.T) { } } +// TestMsg_hasAlt tests the hasAlt() method of the Msg with active S/MIME +func TestMsg_hasAltWithSMime(t *testing.T) { + keyPair, err := getDummyCertificate() + if err != nil { + t.Errorf("failed to load dummy certificate. Cause: %v", err) + } + m := NewMsg() + m.SetBodyString(TypeTextPlain, "Plain") + m.AddAlternativeString(TypeTextHTML, "HTML") + if err := m.SignWithSMime(keyPair); err != nil { + t.Errorf("set of certificate was not successful") + } + if m.hasAlt() { + t.Errorf("mail has alternative parts and S/MIME is active, but hasAlt() returned true") + } +} + // TestMsg_hasRelated tests the hasRelated() method of the Msg func TestMsg_hasRelated(t *testing.T) { m := NewMsg() @@ -3233,32 +3250,33 @@ func TestNewMsgWithNoDefaultUserAgent(t *testing.T) { } } -// TestWithSMimeSinging_ValidPrivateKey tests WithSMimeSinging with given privateKey -func TestWithSMimeSinging_ValidPrivateKey(t *testing.T) { - privateKey, err := getDummyPrivateKey() +// TestSignWithSMime_ValidKeyPair tests WithSMimeSinging with given key pair +func TestSignWithSMime_ValidKeyPair(t *testing.T) { + keyPair, err := getDummyCertificate() if err != nil { - t.Errorf("failed to load dummy private key: %s", err) + t.Errorf("failed to load dummy certificate. Cause: %v", 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 { + if err := m.SignWithSMime(keyPair); 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) + if m.sMime.privateKey == nil { + t.Errorf("WithSMimeSinging() - no private key is given") + } + if m.sMime.certificate == nil { + t.Errorf("WithSMimeSinging() - no certificate is given") + } + if len(m.sMime.parentCertificates) != len(keyPair.Certificate[:1]) { + t.Errorf("WithSMimeSinging() - no certificate is given") } } -// TestWithSMimeSinging_InvalidPrivateKey tests WithSMimeSinging with given invalid privateKey -func TestWithSMimeSinging_InvalidPrivateKey(t *testing.T) { +// TestSignWithSMime_InvalidKeyPair tests WithSMimeSinging with given invalid key pair +func TestSignWithSMime_InvalidKeyPair(t *testing.T) { m := NewMsg() - err := m.SignWithSMime(nil, nil) - if !errors.Is(err, ErrInvalidPrivateKey) { + err := m.SignWithSMime(nil) + if !errors.Is(err, ErrInvalidKeyPair) { t.Errorf("failed to check sMimeAuthConfig values correctly: %s", err) } } @@ -3290,3 +3308,83 @@ func FuzzMsg_From(f *testing.F) { m.Reset() }) } + +// TestMsg_createSignaturePart tests the Msg.createSignaturePart method +func TestMsg_createSignaturePart(t *testing.T) { + keyPair, err := getDummyCertificate() + if err != nil { + t.Errorf("failed to load dummy certificate. Cause: %v", err) + } + m := NewMsg() + if err := m.SignWithSMime(keyPair); err != nil { + t.Errorf("set of certificate was not successful") + } + body := []byte("This is the body") + part, err := m.createSignaturePart(EncodingQP, TypeTextPlain, CharsetUTF7, body) + if err != nil { + t.Errorf("createSignaturePart() method failed.") + } + + if part.GetEncoding() != EncodingB64 { + t.Errorf("createSignaturePart() method failed. Expected encoding: %s, got: %s", EncodingB64, part.GetEncoding()) + } + if part.GetContentType() != typeSMimeSigned { + t.Errorf("createSignaturePart() method failed. Expected content type: %s, got: %s", typeSMimeSigned, part.GetContentType()) + } + if part.GetCharset() != CharsetUTF8 { + t.Errorf("createSignaturePart() method failed. Expected charset: %s, got: %s", CharsetUTF8, part.GetCharset()) + } + if content, err := part.GetContent(); err != nil || len(content) == len(body) { + t.Errorf("createSignaturePart() method failed. Expected content should not be equal: %s, got: %s", body, part.GetEncoding()) + } +} + +// TestMsg_signMessage tests the Msg.signMessage method +func TestMsg_signMessage(t *testing.T) { + keyPair, err := getDummyCertificate() + if err != nil { + t.Errorf("failed to load dummy certificate. Cause: %v", err) + } + m := NewMsg() + body := []byte("This is the body") + if err := m.SignWithSMime(keyPair); err != nil { + t.Errorf("set of certificate was not successful") + } + msg, err := m.signMessage(m) + if err != nil { + t.Errorf("createSignaturePart() method failed.") + } + + parts := msg.GetParts() + if len(parts) != 2 { + t.Errorf("createSignaturePart() method failed. Expected 2 parts, got: %d", len(parts)) + } + + signedPart := parts[0] + if signedPart.GetEncoding() != EncodingQP { + t.Errorf("createSignaturePart() method failed. Expected encoding: %s, got: %s", EncodingB64, signedPart.GetEncoding()) + } + if signedPart.GetContentType() != TypeTextPlain { + t.Errorf("createSignaturePart() method failed. Expected content type: %s, got: %s", typeSMimeSigned, signedPart.GetContentType()) + } + if signedPart.GetCharset() != CharsetUTF8 { + t.Errorf("createSignaturePart() method failed. Expected charset: %s, got: %s", CharsetUTF8, signedPart.GetCharset()) + } + if content, err := signedPart.GetContent(); err != nil || len(content) != len(body) { + t.Errorf("createSignaturePart() method failed. Expected content should be equal: %s, got: %s", body, content) + } + + signaturePart := parts[1] + if signaturePart.GetEncoding() != EncodingB64 { + t.Errorf("createSignaturePart() method failed. Expected encoding: %s, got: %s", EncodingB64, signaturePart.GetEncoding()) + } + if signaturePart.GetContentType() != typeSMimeSigned { + t.Errorf("createSignaturePart() method failed. Expected content type: %s, got: %s", typeSMimeSigned, signaturePart.GetContentType()) + } + if signaturePart.GetCharset() != CharsetUTF8 { + t.Errorf("createSignaturePart() method failed. Expected charset: %s, got: %s", CharsetUTF8, signaturePart.GetCharset()) + } + if content, err := signaturePart.GetContent(); err != nil || len(content) == len(body) { + t.Errorf("createSignaturePart() method failed. Expected content should not be equal: %s, got: %s", body, signaturePart.GetEncoding()) + } +} diff --git a/msgwriter_test.go b/msgwriter_test.go index a41e5d3..ca1a982 100644 --- a/msgwriter_test.go +++ b/msgwriter_test.go @@ -154,3 +154,26 @@ func TestMsgWriter_writeMsg_PGP(t *testing.T) { t.Errorf("writeMsg failed. Expected PGP encoding header but didn't find it in message output") } } + +// TestMsgWriter_writeMsg_SMime tests the writeMsg method of the msgWriter with S/MIME types set +func TestMsgWriter_writeMsg_SMime(t *testing.T) { + keyPair, err := getDummyCertificate() + if err != nil { + t.Errorf("failed to load dummy certificate. Cause: %v", err) + } + m := NewMsg() + if err := m.SignWithSMime(keyPair); err != nil { + t.Errorf("set of certificate was not successful") + } + _ = m.From(`"Toni Tester" `) + _ = m.To(`"Toni Receiver" `) + m.Subject("This is a subject") + m.SetBodyString(TypeTextPlain, "This is the body") + buf := bytes.Buffer{} + mw := &msgWriter{writer: &buf, charset: CharsetUTF8, encoder: mime.QEncoding} + mw.writeMsg(m) + ms := buf.String() + if !strings.Contains(ms, `multipart/signed; protocol="application/pkcs7-signature"; micalg=sha256;`) { + t.Errorf("writeMsg failed. Expected PGP encoding header but didn't find it in message output") + } +} diff --git a/util_test.go b/util_test.go index efc088c..8f235ed 100644 --- a/util_test.go +++ b/util_test.go @@ -1,41 +1,19 @@ package mail import ( - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "crypto/x509/pkix" - "math/big" - "time" + "crypto/tls" ) -func getDummyPrivateKey() (*rsa.PrivateKey, error) { - privateKey, err := rsa.GenerateKey(rand.Reader, 2048) +const ( + certFilePath = "dummy-cert.pem" + keyFilePath = "dummy=key.pem" +) + +func getDummyCertificate() (*tls.Certificate, error) { + keyPair, err := tls.LoadX509KeyPair(certFilePath, keyFilePath) 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 + + return &keyPair, nil } From 9bdb741e0588a7f52fc122dcba58ccbdc5d3f225 Mon Sep 17 00:00:00 2001 From: theexiile1305 Date: Thu, 26 Sep 2024 17:08:38 +0200 Subject: [PATCH 12/33] feat: begin implementation of tests --- dummy-chain-cert.pem | 62 ++++++++++++++++++++++++++++++++++++++++++++ dummy-child-key.pem | 52 +++++++++++++++++++++++++++++++++++++ msg_test.go | 3 ++- util_test.go | 7 +++-- 4 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 dummy-chain-cert.pem create mode 100644 dummy-child-key.pem diff --git a/dummy-chain-cert.pem b/dummy-chain-cert.pem new file mode 100644 index 0000000..4daceaa --- /dev/null +++ b/dummy-chain-cert.pem @@ -0,0 +1,62 @@ +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgIUAi7P4JOR4g8b5DMERUtZQEtw+igwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDA5MjYxNDU0MDlaFw0yNTA5 +MjYxNDU0MDlaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw +HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCiLh0JJTRRBhmUyiMKALHtTOK7T20Bwy+fG0SO6RlB +c+hSuuX/n6znXcNgOBlQ2Gg3+p1on/bmcKnGN/SCiVBLpROiwxg3blQbZ7B7Jors +/MopGk0LIBOXHPtAuYbF4J6ND5Ol6sgeGjMnomwRjZlfeuBlHY345MqvcwH/lPhO +lKme+tWD+bsFh08NGS+3NdQGP6dA2bRVrPXhLXStHEmqfKO9EMVLWv+77tYhZESD +6XgnA8pWjbdr9jajCsrQWrCG3jqHtzHNxtwf7xfRwwgoUhLEvue6SBVokZGVmDhv +WdRt2sjtLcWWJCI3p7M+NXRt5qf6iu24wLBdzIDuWfgooWu5vBzNZjSTh2if6R1a +s9BdwASwy1n2HMvqpzgA+f/rXDFbvVc7WIKiuGzfWApBrL0qTCQBuSyepH0G4rQ2 +sJtI5U8QOKBO76nQJq5WDQgefBX4GDI8aJ6qX+teQq1AqERUmLWx4WlTwxSo5X+d +1jY9I8f61CRKVfIRgMvAZUhm2h6RnoVIgq7G7W3HdC3RT/f758njI7oIv5bhiyqp +gyKr3cYhmn0enjP+YtjY85m51q005qzRLfaTYiwMR4qyJW4ZOEPntPs20CD+e+Pi +2JLONpRdcsSrkqZusVjm5PFy2e5RyNFXTupUH2KVrgTRHL3GG2KWF5PmBdkhQfGG +1QIDAQABo0IwQDAdBgNVHQ4EFgQUS8+HouVQ94SdgI3kV3L8Jm53P6YwHwYDVR0j +BBgwFoAUY1u7KerT8m01BvAg77PUaot2S0EwDQYJKoZIhvcNAQELBQADggIBAE14 +YBa/stYwrsy/1iQ44NeQyYMPMdOC8TI1xrbW/9u1FllipECnFEGDK1N6mZ7xDEfG +un5dQ3jXQ7156Ge672374yUsN7FQ37mTyZos3Q2N/mOpVOnYJt5mIukx2MXBU3r3 +UP1Jpnf9rB4kdtWXa7b1CSTkM4kraige3wZhPELwESnm4t8C34MIzHBWPbHpft05 +WheDv9Zizfw+0pbJ+WNGnHF4PjR/wq9ymkLf89cqsbS9mOdPpWva8i0e7pqKnxzo +iz2ueQB4Z2Tbgp0G9ResA+2Zxk1iIQPbhtqNUZv6ROPiLAWdiVRysFJJf/19V+nZ +LIC0xw+amF6P51/fA95EGqElO4OLJTIGY27H761g7+FhTwfryLMHKknSxcfk7xoq +BMyBr7ARYnmpjee7yKOBUgSdpxb6YUcdGZwjCUIiIHlBII83DzcNILa5QvUkzMCh +xHYmPvvftJOjF8hwMfjA9MDFML9yWVm+CBNraNPh25U5uMOuIuyUBtSB5yEdPRhY +BJGrZEew0lLAWAqqASmGPDaWNBaA0HYqO70g4IyqBwIGNnaHSLVr/vT23BFRMyXf +wh5mtJmyyR3+c0po3vDX39mkIAZ2gZWprWa3Jw0dVs6cEujcVNdqeZlQw0RCa9wm +xioBxb1md2AplUQ2fG/KHnu2ZuxHA6MNYcwMJNgv +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIUKU+ta2rnJE/79L2Uxg4vFoF0RxYwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDA5MjYxNDUzMzlaFw0zNDA5 +MjQxNDUzMzlaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw +HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQDSOuYqxNsP4Gm/EDauon/SAVWj8PIKFB1OpuguHm13 +b2l2G6hRuNnAmR6ewP22H/YoyYz1qRchH2qw1uizwnnSS0OY74CsJhd0PV6f4XRR +Y+6PotGDPu1fJJM4XI3HjWGdBkJSawZNWjP1dQRJPUHNRttSOrPsG3XT7TjfLjoK +jOelTqgwHUGE2n0AtQP7ZFQVn7LLBrukve8zMgivwEL1JFSlKppWFf0SUgpmQVE6 +3jTAQPPrI/B5z5Ys1j2jv7mJt3/UATcTGmvPTNv94SUrO6nC3TJxKHtR30MALteo +EgH/s2O0Ax44iDENgm9p6eb+GCyTWS/eHBAJ6SU76PRymiE57/0GOqyYewuEOuIU +FYd6+gglCMe+ayfhI20njHP6RTiQpRjFy+DM8+bkcS89q0sfFSFHR5oFNbAgUgyI +bGiaWb+DmUCwSFnS0HusSU2AECqzuwiyObD3rkoqBQMj+xl6SnJU6TTcB+WD/Z5G +tqu1zTMXpo3VRts3AQSGUuSaqbeG/S+38LX+fbjeTLa6SEGJfB7/H2s64vCO/0hR +M0KEXAaTyjx0PnNKYSlCIJyA9lYea21oByNc31tkUXQjmUQpSXYayrDwzR2JAXVY +rFJLNu1Q6sZ0WqT9fj06oTas7g1g3gcl18tIapeael2jJohth0RizBxKYuLYc6Pv +1QIDAQABo1MwUTAdBgNVHQ4EFgQUY1u7KerT8m01BvAg77PUaot2S0EwHwYDVR0j +BBgwFoAUY1u7KerT8m01BvAg77PUaot2S0EwDwYDVR0TAQH/BAUwAwEB/zANBgkq +hkiG9w0BAQsFAAOCAgEAB8BpmI3v2cNlXUjhf+rldaqDfq/IJ7Ri23kCBJW4VaoW +c0UrtvLC+K5m61I1iWSUYEK85/bPw2K5dn1e8w3Q2J460Yvc7/ZT7mucZlXQxfl3 +V7yqqQI7OMsY6FochYUL3+c32WQg5jllsLPlHAHBJlagf3uEqmVrvSExHNBQOVyE +/cs1i9DcTJF2A8JNPKilIObvRT103Qp2eFnW+EY9OUBb+TdQvPjxroLfK1SuOAe6 +bLPBxdgvA/0raHuXeDTNsNRICIU1X5eBfZwCXKe9lRVJpIsKTYeHDN/rEmfTtehB +vz8/KkCWqwPDn/YFkNAdg3TRjqW4oW2wZ+XqbTlR2qA7szE7oMAfHxNkintxMnNm +vD2/AAP6RUw16HZk0najFWPIG9gc+O1gSks6hwn9JilAPy8mn40H2D7cedU6Ew+T +CQ02+dw2+2FLKYr1eiYPlIELsAu8kmbrjwvwy2sCf3L4fxLtPRqXFuXB2Uer9zvy +tn+RK5hJkKo/YY37I9Y9x57rpCqUfFIeYWBub07x1620ujRkL1pJPxfRNBfyh42t +beuk/XQGIvPcIkbPnmsb4gGaiRMuw+mZ7isJDoQwHUmfqL1EpOYb5mLYHkIqKaCz +8t8wTdkimIVIFSxedy7cJCCWdQ/BCyTJoQpXD69PLPzxEi/YK9pB9S8qBtfefu4= +-----END CERTIFICATE----- diff --git a/dummy-child-key.pem b/dummy-child-key.pem new file mode 100644 index 0000000..2a8639b --- /dev/null +++ b/dummy-child-key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQAIBADANBgkqhkiG9w0BAQEFAASCCSowggkmAgEAAoICAQCiLh0JJTRRBhmU +yiMKALHtTOK7T20Bwy+fG0SO6RlBc+hSuuX/n6znXcNgOBlQ2Gg3+p1on/bmcKnG +N/SCiVBLpROiwxg3blQbZ7B7Jors/MopGk0LIBOXHPtAuYbF4J6ND5Ol6sgeGjMn +omwRjZlfeuBlHY345MqvcwH/lPhOlKme+tWD+bsFh08NGS+3NdQGP6dA2bRVrPXh +LXStHEmqfKO9EMVLWv+77tYhZESD6XgnA8pWjbdr9jajCsrQWrCG3jqHtzHNxtwf +7xfRwwgoUhLEvue6SBVokZGVmDhvWdRt2sjtLcWWJCI3p7M+NXRt5qf6iu24wLBd +zIDuWfgooWu5vBzNZjSTh2if6R1as9BdwASwy1n2HMvqpzgA+f/rXDFbvVc7WIKi +uGzfWApBrL0qTCQBuSyepH0G4rQ2sJtI5U8QOKBO76nQJq5WDQgefBX4GDI8aJ6q +X+teQq1AqERUmLWx4WlTwxSo5X+d1jY9I8f61CRKVfIRgMvAZUhm2h6RnoVIgq7G +7W3HdC3RT/f758njI7oIv5bhiyqpgyKr3cYhmn0enjP+YtjY85m51q005qzRLfaT +YiwMR4qyJW4ZOEPntPs20CD+e+Pi2JLONpRdcsSrkqZusVjm5PFy2e5RyNFXTupU +H2KVrgTRHL3GG2KWF5PmBdkhQfGG1QIDAQABAoICAAL3IruL6/zP5DPZ9RkiL1m/ +lHPhP7/sWKTfWMPf4ChX6XAHWeYraqNPvx8/bESdstLAvx3piyvcapRupN9DsVTu +SrKytjXft6OEVWFLEveHk3F8B+9ewbMY4BmsQOjpVR9J7+6SZmpUB2MdD3lpdY07 +YSuamff0dcNdV2+NEZth6bit++iJFc9rTI/OwBZLMTVsp+oVpKh7w72h1DfboaF/ +oU9tYHWFWTRUfSzoqm7Q4POKmII4BhA+1QWIUIX1OLQocepzgfw72Tj+GlRQ7WAu +IToIBqbRxfsNflaWDSv15UrE7OCDLUdlXILOd0GgtJaYTsX7X7ZMU1mIoqX0IBsk +KRCep+7BTA8VYXOlW26tPEcsj2Vp7tdghptTaEtdx05delaYaX18rxdprNz+VV0Z +jNVIgShJC4vMEVQtOSOyznavF9OONScBG4e0E9rSHbYudtvrp8WOoaDK6mzAwo6w +wShYwwzFmf0Y2FbsENvNiNw5cqKT4WQoCXM6WS9BjPfJqz4M7V8pYa3pCE4M+oXP +sfQDgkpyXg88Dez1N78cbpr4GKsI9odFmphivQngXF1H3N8UKLCjW/EQNKHoydTn +nTtIfSY1G5hlDS0nCqTn6LvI2W887Da+ASWGtgGn0opLUW5Kg5fPurFjoCEP4mNg +JQ8nX9q2N5AogHld0h1ZAoIBAQDVRSty2Q7+oQh0qWGI0eDESeAHd/OKnwELAsMx +pfXABhB/CjO0/Iy4MgBL1dMb1S64gHhHi816CfxoSQuaUNjOcAWxhLTbnPEx8eaS +SFvdnd3itEC0T6Cg1r8mklHXrz6WH8vtLfu3pc8svGp+xJ5Rmlg1G3rhYFce6csg +lgeP4n4vjxOZtds3Jr8oHiW0/KYrLUK8/TgFOwYvWZQ8Lk92Oz+Fcd+RkegSkzIB +hHpqIMQz4T5uirjmxKX9573X17cVO3LuCBSite1uiyXRAIrGEOOyHwv3xC2h/bV1 +6IYSiuwJOxDxZAmYuWYBtlvfWrOypEuWogZQv8C/zxDpkDSLAoIBAQDCrHxmh5NI +ksWQg0W6176uktwyypFBh8REE1VfCgoxJ84TVOQzEBiNQpRj/FUCe4e7ZElIAks+ +N9mh8smJLvHDVo7033NIfAZCfCbLve8uWSGAm5x+aKS7kG5gynojKT8vl0y88m5o +Nm+BfvpQKjb6n6jibZZRsQsyz5KBct+Gb9gyA81jjrFudBmYy9WZeEm6pjzQtkuq +I0xCbQFwB+It/utjz7okuAWk5fPU0LRqOvvEMJjf2DyvIK45FDsBF3zGlG8Zunnh +q3o28zXdQCvYxY5Ik8zRuSTTAQnaJ3/zUvqR2g3PoSF8d7/WSkzNK+ZhTANqPkq1 +SOg515Na9L4fAoH/Vc5+rLaoUcp4nHeJxoKq7E7M1DRuyFcxFD0IS/F57siB2ptA +MpFqDLIRbHGbfpdHNPR7cE3PXkqmQ08gW/YrROPNZp7+JV3/rRimrDRwwbnCjHP5 +lJJ1DkFYpyw3wY/AnqYsZkEaBcmwkU89icOR70MqOjPUPNmGM+nc0D+My1dVbc0j +FbUVfhsYzgtTIH6GXNjZATDgWTpmQqbH/W6kie1MoWQvj2Ik/VQ7ymCC4DBOwJDf +jZpCypZUMtQKjc083E4O77ZQlyabYN6bWHvfWdFxyziyl/1WXta1K7tiNhOu5Aff +yT92nPv7DrVQQY08v6NaxkBqShLcek/VfiOHAoIBAAtzd/HUAb7gG0zv29csv6On +Mdqu/bJcGRhkBr6LaaQQkleiw7WZOch9ZRsoiZuWxpooQQNCV0i2ok+bZ21xXHlA +CzKuPirCWN/qS6HqbzpLtePJw3/QCfiae1OoNV0CHRxgivwGSqZIpXB5lqHGiete +HuIKzi/J+T2o5hZFOo6+33m5rYgwqZE0tRi+zLa1U6juBF/GiVbdsqupm88KN6y6 +9P+vBWUJihN0D06yZBpnk82riiKIprEqe/URkpLy3b0UmCBsTqUOoCbBUabNEocy +v7bXMtIXUOo0gm7ZqfYXKHQR3oQbF0wqAxfI0RG0hl2syfqi5WQagMZ+PsW35cMC +ggEBAM6kIM/zUnNBFla+oTiPCyoII7nGmGz8dT9IhH0+6T5nNzG4O6D28A3MJcs1 +C3pfukCCeWOnDNCIQ7C9Hx5XcCDoI4eD6zCRy+7zXxn0FxYS+O6lczB2mXGE69Yp +n3qb7P4XqZYex3dT72czJUlY6nFB2e0FmyvSoez8fsH9Ws78c4JGO953klam/emA +Hc8nB3CyM8rb2JlM3WeQbmo+Sbi0Yvj+MWM2AnTXx0xyaFXKP4WGD8hAxTvf9tSX +3NAPVBXku4zRpvoXyyrcBVd9vwE0qBr3eWCuci8aDD6RAAJgb8HHX9RjRF/phFpY +S++xGnGHEUSEl7cWnO8k0RyJzzc= +-----END PRIVATE KEY----- diff --git a/msg_test.go b/msg_test.go index 6e35d49..9464a50 100644 --- a/msg_test.go +++ b/msg_test.go @@ -3345,8 +3345,9 @@ func TestMsg_signMessage(t *testing.T) { if err != nil { t.Errorf("failed to load dummy certificate. Cause: %v", err) } - m := NewMsg() body := []byte("This is the body") + m := NewMsg() + m.SetBodyString(TypeTextPlain, string(body)) if err := m.SignWithSMime(keyPair); err != nil { t.Errorf("set of certificate was not successful") } diff --git a/util_test.go b/util_test.go index 8f235ed..47bac43 100644 --- a/util_test.go +++ b/util_test.go @@ -2,11 +2,12 @@ package mail import ( "crypto/tls" + "crypto/x509" ) const ( - certFilePath = "dummy-cert.pem" - keyFilePath = "dummy=key.pem" + certFilePath = "dummy-chain-cert.pem" + keyFilePath = "dummy-child-key.pem" ) func getDummyCertificate() (*tls.Certificate, error) { @@ -15,5 +16,7 @@ func getDummyCertificate() (*tls.Certificate, error) { return nil, err } + keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0]) + return &keyPair, nil } From 4b60557518ab8f8552c75b1a148200111de29b43 Mon Sep 17 00:00:00 2001 From: theexiile1305 Date: Thu, 26 Sep 2024 17:14:42 +0200 Subject: [PATCH 13/33] fix: failing tests --- part_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/part_test.go b/part_test.go index 2b9725f..c1a8ac7 100644 --- a/part_test.go +++ b/part_test.go @@ -275,6 +275,7 @@ func TestPart_IsSMimeSigned(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { m := NewMsg() + m.SetBodyString(TypeTextPlain, "This is a body!") pl, err := getPartList(m) if err != nil { t.Errorf("failed: %s", err) From 79f22fb722f27b02fdd63c1280f9d76b1171f7c3 Mon Sep 17 00:00:00 2001 From: theexiile1305 Date: Thu, 26 Sep 2024 17:28:24 +0200 Subject: [PATCH 14/33] feat: specific error if certificate is invalid --- sime.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sime.go b/sime.go index fe05158..f0f8c1e 100644 --- a/sime.go +++ b/sime.go @@ -16,6 +16,9 @@ var ( // ErrInvalidKeyPair should be used if key pair is invalid ErrInvalidKeyPair = errors.New("invalid key pair") + // ErrInvalidCertificate should be used if a 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") @@ -46,7 +49,7 @@ func newSMime(keyPair *tls.Certificate) (*SMime, error) { for _, cert := range keyPair.Certificate[1:] { c, err := x509.ParseCertificate(cert) if err != nil { - return nil, err + return nil, ErrInvalidCertificate } parentCertificates = append(parentCertificates, c) } From 46cf2ed498215caf739a117caca7d688063c81b7 Mon Sep 17 00:00:00 2001 From: theexiile1305 Date: Sun, 29 Sep 2024 16:46:30 +0200 Subject: [PATCH 15/33] fix: part boundray and message encoding --- msgwriter.go | 6 +++++- sime.go | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/msgwriter.go b/msgwriter.go index 7c1e88e..41b3490 100644 --- a/msgwriter.go +++ b/msgwriter.go @@ -138,6 +138,10 @@ func (mw *msgWriter) writeMsg(msg *Msg) { if msg.hasMixed() { mw.stopMP() } + + if msg.hasSMime() { + mw.stopMP() + } } // writeGenHeader writes out all generic headers to the msgWriter @@ -341,7 +345,7 @@ func (mw *msgWriter) writeBody(writeFunc func(io.Writer) (int64, error), encodin encodedWriter = quotedprintable.NewWriter(&writeBuffer) } else if encoding == EncodingB64 && !singingWithSMime { encodedWriter = base64.NewEncoder(base64.StdEncoding, &lineBreaker) - } else if encoding == NoEncoding { + } else if encoding == NoEncoding || singingWithSMime { _, err = writeFunc(&writeBuffer) if err != nil { mw.err = fmt.Errorf("bodyWriter function: %w", err) diff --git a/sime.go b/sime.go index f0f8c1e..35dabfb 100644 --- a/sime.go +++ b/sime.go @@ -105,8 +105,10 @@ func encodeToPEM(msg []byte) (*string, error) { } r := arrayBuffer.String() - r = strings.ReplaceAll(r, "-----BEGIN -----\n", "") - r = strings.ReplaceAll(r, "-----END -----\n", "") + r = strings.TrimPrefix(r, "-----BEGIN -----") + r = strings.Trim(r, "\n") + r = strings.TrimSuffix(r, "-----END -----") + r = strings.Trim(r, "\n") return &r, nil } From b4370ded125dbac89ccd18c65ef274811edf1193 Mon Sep 17 00:00:00 2001 From: theexiile1305 Date: Wed, 9 Oct 2024 12:10:03 +0200 Subject: [PATCH 16/33] fix: micalg part of content-type --- encoding.go | 2 +- msgwriter_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/encoding.go b/encoding.go index 77243fc..dbe57ee 100644 --- a/encoding.go +++ b/encoding.go @@ -157,7 +157,7 @@ const ( MIMEAlternative MIMEType = "alternative" MIMEMixed MIMEType = "mixed" MIMERelated MIMEType = "related" - MIMESMime MIMEType = `signed; protocol="application/pkcs7-signature"; micalg=sha256` + MIMESMime MIMEType = `signed; protocol="application/pkcs7-signature"; micalg=sha-256` ) // String is a standard method to convert an Charset into a printable format diff --git a/msgwriter_test.go b/msgwriter_test.go index ca1a982..3466199 100644 --- a/msgwriter_test.go +++ b/msgwriter_test.go @@ -173,7 +173,7 @@ func TestMsgWriter_writeMsg_SMime(t *testing.T) { mw := &msgWriter{writer: &buf, charset: CharsetUTF8, encoder: mime.QEncoding} mw.writeMsg(m) ms := buf.String() - if !strings.Contains(ms, `multipart/signed; protocol="application/pkcs7-signature"; micalg=sha256;`) { + if !strings.Contains(ms, `multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-256;`) { t.Errorf("writeMsg failed. Expected PGP encoding header but didn't find it in message output") } } From 4700691380cf247ce259cc58d76cb1a5a27e759a Mon Sep 17 00:00:00 2001 From: theexiile1305 Date: Wed, 9 Oct 2024 13:53:15 +0200 Subject: [PATCH 17/33] fix: detached signature is now used --- msg.go | 5 ++++- msgwriter.go | 7 ++++++- sime.go | 25 +++++++++++++------------ 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/msg.go b/msg.go index 286e533..e0d83d6 100644 --- a/msg.go +++ b/msg.go @@ -1004,10 +1004,13 @@ func (m *Msg) createSignaturePart(encoding Encoding, contentType ContentType, ch message := m.sMime.createMessage(encoding, contentType, charSet, body) signaturePart := m.newPart(typeSMimeSigned, WithPartEncoding(EncodingB64), WithSMimeSinging()) - if err := m.sMime.sign(signaturePart, message); err != nil { + signedMessage, err := m.sMime.signMessage(message) + if err != nil { return nil, err } + signaturePart.SetContent(*signedMessage) + return signaturePart, nil } diff --git a/msgwriter.go b/msgwriter.go index 41b3490..8182168 100644 --- a/msgwriter.go +++ b/msgwriter.go @@ -261,7 +261,12 @@ func (mw *msgWriter) writePart(part *Part, charset Charset) { if partCharset.String() == "" { partCharset = charset } - contentType := fmt.Sprintf("%s; charset=%s", part.contentType, partCharset) + + contentType := part.contentType.String() + if !part.IsSMimeSigned() { + contentType = strings.Join([]string{contentType, "; charset=", partCharset.String()}, "") + } + contentTransferEnc := part.encoding.String() if mw.depth == 0 { mw.writeHeader(HeaderContentType, contentType) diff --git a/sime.go b/sime.go index 35dabfb..de2decc 100644 --- a/sime.go +++ b/sime.go @@ -61,33 +61,34 @@ func newSMime(keyPair *tls.Certificate) (*SMime, error) { }, nil } -// sign with the S/MIME method the message of the actual *Part -func (sm *SMime) sign(signaturePart *Part, message string) error { +// signMessage signs the message with S/MIME +func (sm *SMime) signMessage(message string) (*string, error) { lines := parseLines([]byte(message)) toBeSigned := lines.bytesFromLines([]byte("\r\n")) - tmp, err := pkcs7.NewSignedData(toBeSigned) - tmp.SetDigestAlgorithm(pkcs7.OIDDigestAlgorithmSHA256) + signedData, err := pkcs7.NewSignedData(toBeSigned) + signedData.SetDigestAlgorithm(pkcs7.OIDDigestAlgorithmSHA256) if err != nil { - return ErrCouldNotInitialize + return nil, ErrCouldNotInitialize } - if err = tmp.AddSignerChain(sm.certificate, sm.privateKey, sm.parentCertificates, pkcs7.SignerInfoConfig{}); err != nil { - return ErrCouldNotAddSigner + if err = signedData.AddSignerChain(sm.certificate, sm.privateKey, sm.parentCertificates, pkcs7.SignerInfoConfig{}); err != nil { + return nil, ErrCouldNotAddSigner } - signatureDER, err := tmp.Finish() + signedData.Detach() + + signatureDER, err := signedData.Finish() if err != nil { - return ErrCouldNotFinishSigning + return nil, ErrCouldNotFinishSigning } pemMsg, err := encodeToPEM(signatureDER) if err != nil { - return ErrCouldNoEncodeToPEM + return nil, ErrCouldNoEncodeToPEM } - signaturePart.SetContent(*pemMsg) - return nil + return pemMsg, nil } // createMessage prepares the message that will be used for the sign method later From 2c2ee4c1fbe65b1346ec53e867d4cd4a5ebfe535 Mon Sep 17 00:00:00 2001 From: theexiile1305 Date: Wed, 9 Oct 2024 13:53:31 +0200 Subject: [PATCH 18/33] feat: begin implementation of tests --- smime_test.go | 156 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 smime_test.go diff --git a/smime_test.go b/smime_test.go new file mode 100644 index 0000000..bd0282d --- /dev/null +++ b/smime_test.go @@ -0,0 +1,156 @@ +package mail + +import ( + "errors" + "fmt" + "strings" + "testing" +) + +// TestNewSMime tests the newSMime method +func TestNewSMime(t *testing.T) { + keyPair, err := getDummyCertificate() + if err != nil { + t.Errorf("Error getting dummy certificate: %s", err) + } + + sMime, err := newSMime(keyPair) + if err != nil { + t.Errorf("Error creating new SMime from keyPair: %s", err) + } + + if sMime.privateKey != keyPair.PrivateKey { + t.Errorf("NewSMime() did not return the same private key") + } + if sMime.certificate != keyPair.Leaf { + t.Errorf("NewSMime() did not return the same leaf certificate") + } + if len(sMime.parentCertificates) != len(keyPair.Certificate)-1 { + t.Errorf("NewSMime() did not return the same number of parentCertificates") + } +} + +// TestSign tests the sign method +func TestSign(t *testing.T) { + keyPair, err := getDummyCertificate() + if err != nil { + t.Errorf("Error getting dummy certificate: %s", err) + } + + sMime, err := newSMime(keyPair) + if err != nil { + t.Errorf("Error creating new SMime from keyPair: %s", err) + } + fmt.Println(sMime) +} + +// TestCreateMessage tests the createMessage method +func TestCreateMessage(t *testing.T) { + keyPair, err := getDummyCertificate() + if err != nil { + t.Errorf("Error getting dummy certificate: %s", err) + } + + sMime, err := newSMime(keyPair) + if err != nil { + t.Errorf("Error creating new SMime from keyPair: %s", err) + } + + encoding := EncodingB64 + contentType := TypeTextPlain + charset := CharsetUTF8 + body := []byte("This is the body!") + result := sMime.createMessage(encoding, contentType, body) + + if !strings.Contains(result, encoding.String()) { + t.Errorf("createMessage() did not return the correct encoding") + } + if !strings.Contains(result, contentType.String()) { + t.Errorf("createMessage() did not return the correct contentType") + } + if !strings.Contains(result, string(body)) { + t.Errorf("createMessage() did not return the correct body") + } + if result != fmt.Sprintf("Content-Transfer-Encoding: %v\r\nContent-Type: %v; charset=%v\r\n\r\n%v", encoding, contentType, charset, string(body)) { + t.Errorf("createMessage() did not sucessfully create the message") + } +} + +// TestEncodeToPEM tests the encodeToPEM method +func TestEncodeToPEM(t *testing.T) { + + keyPair, err := getDummyCertificate() + if err != nil { + t.Errorf("Error getting dummy certificate: %s", err) + } + + sMime, err := newSMime(keyPair) + if err != nil { + t.Errorf("Error creating new SMime from keyPair: %s", err) + } + fmt.Println(sMime) +} + +// TestBytesFromLines tests the bytesFromLines method +func TestBytesFromLines(t *testing.T) { + +} + +// TestParseLines tests the parseLines method +func TestParseLines(t *testing.T) { + +} + +// TestSplitLine tests the splitLine method +func TestSplitLine(t *testing.T) { + +} + +func foo(t *testing.T) { + tl := []struct { + n string + r SendErrReason + te bool + }{ + {"ErrGetSender/temp", ErrGetSender, true}, + {"ErrGetSender/perm", ErrGetSender, false}, + {"ErrGetRcpts/temp", ErrGetRcpts, true}, + {"ErrGetRcpts/perm", ErrGetRcpts, false}, + {"ErrSMTPMailFrom/temp", ErrSMTPMailFrom, true}, + {"ErrSMTPMailFrom/perm", ErrSMTPMailFrom, false}, + {"ErrSMTPRcptTo/temp", ErrSMTPRcptTo, true}, + {"ErrSMTPRcptTo/perm", ErrSMTPRcptTo, false}, + {"ErrSMTPData/temp", ErrSMTPData, true}, + {"ErrSMTPData/perm", ErrSMTPData, false}, + {"ErrSMTPDataClose/temp", ErrSMTPDataClose, true}, + {"ErrSMTPDataClose/perm", ErrSMTPDataClose, false}, + {"ErrSMTPReset/temp", ErrSMTPReset, true}, + {"ErrSMTPReset/perm", ErrSMTPReset, false}, + {"ErrWriteContent/temp", ErrWriteContent, true}, + {"ErrWriteContent/perm", ErrWriteContent, false}, + {"ErrConnCheck/temp", ErrConnCheck, true}, + {"ErrConnCheck/perm", ErrConnCheck, false}, + {"ErrNoUnencoded/temp", ErrNoUnencoded, true}, + {"ErrNoUnencoded/perm", ErrNoUnencoded, false}, + {"ErrAmbiguous/temp", ErrAmbiguous, true}, + {"ErrAmbiguous/perm", ErrAmbiguous, false}, + {"Unknown/temp", 9999, true}, + {"Unknown/perm", 9999, false}, + } + + for _, tt := range tl { + t.Run(tt.n, func(t *testing.T) { + if err := returnSendError(tt.r, tt.te); err != nil { + exp := &SendError{Reason: tt.r, isTemp: tt.te} + if !errors.Is(err, exp) { + t.Errorf("error mismatch, expected: %s (temp: %t), got: %s (temp: %t)", tt.r, tt.te, + exp.Error(), exp.isTemp) + } + if !strings.Contains(fmt.Sprintf("%s", err), tt.r.String()) { + t.Errorf("error string mismatch, expected: %s, got: %s", + tt.r.String(), fmt.Sprintf("%s", err)) + } + } + }) + } +} From 6ea09741568843aa751680d165739d3a267349e9 Mon Sep 17 00:00:00 2001 From: theexiile1305 Date: Wed, 9 Oct 2024 14:43:43 +0200 Subject: [PATCH 19/33] feat: improved tests --- encoding.go | 5 +++++ encoding_test.go | 21 +++++++++++++++++++++ util_test.go | 1 + 3 files changed, 27 insertions(+) diff --git a/encoding.go b/encoding.go index dbe57ee..4be2e38 100644 --- a/encoding.go +++ b/encoding.go @@ -174,3 +174,8 @@ func (c ContentType) String() string { func (e Encoding) String() string { return string(e) } + +// String is a standard method to convert an MIMEType into a printable format +func (e MIMEType) String() string { + return string(e) +} diff --git a/encoding_test.go b/encoding_test.go index 11bf991..4c7778a 100644 --- a/encoding_test.go +++ b/encoding_test.go @@ -126,3 +126,24 @@ func TestCharset_String(t *testing.T) { }) } } + +// TestContentType_String tests the mime type method of the MIMEType object +func TestMimeType_String(t *testing.T) { + tests := []struct { + mt MIMEType + want string + }{ + {MIMEAlternative, "alternative"}, + {MIMEMixed, "mixed"}, + {MIMERelated, "related"}, + {MIMESMime, `signed; protocol="application/pkcs7-signature"; micalg=sha-256`}, + } + for _, tt := range tests { + t.Run(tt.mt.String(), func(t *testing.T) { + if tt.mt.String() != tt.want { + t.Errorf("wrong string for mime type returned. Expected: %s, got: %s", + tt.want, tt.mt.String()) + } + }) + } +} diff --git a/util_test.go b/util_test.go index 47bac43..7c54b7d 100644 --- a/util_test.go +++ b/util_test.go @@ -10,6 +10,7 @@ const ( keyFilePath = "dummy-child-key.pem" ) +// getDummyCertificate loads a certificate and a private key form local disk for testing purposes func getDummyCertificate() (*tls.Certificate, error) { keyPair, err := tls.LoadX509KeyPair(certFilePath, keyFilePath) if err != nil { From 5913fc1540396cf277c76bf7549ff33a02bd9623 Mon Sep 17 00:00:00 2001 From: theexiile1305 Date: Wed, 9 Oct 2024 15:40:05 +0200 Subject: [PATCH 20/33] feat: improved tests --- msg.go | 8 +++--- msg_test.go | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 84 insertions(+), 7 deletions(-) diff --git a/msg.go b/msg.go index e0d83d6..c0327a9 100644 --- a/msg.go +++ b/msg.go @@ -25,7 +25,7 @@ import ( ) var ( - // ErrNoFromAddress should be used when a FROM address is requrested but not set + // ErrNoFromAddress should be used when a FROM address is requested but not set ErrNoFromAddress = errors.New("no FROM address set") // ErrNoRcptAddresses should be used when the list of RCPTs is empty @@ -1000,15 +1000,15 @@ func (m *Msg) signMessage(msg *Msg) (*Msg, error) { return m, err } +// createSignaturePart creates an additional part that be used for storing the S/MIME signature of the given body 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()) - + message := m.sMime.prepareMessage(encoding, contentType, charSet, body) signedMessage, err := m.sMime.signMessage(message) if err != nil { return nil, err } + signaturePart := m.newPart(typeSMimeSigned, WithPartEncoding(EncodingB64), WithSMimeSinging()) signaturePart.SetContent(*signedMessage) return signaturePart, nil diff --git a/msg_test.go b/msg_test.go index 9464a50..192af62 100644 --- a/msg_test.go +++ b/msg_test.go @@ -1911,6 +1911,22 @@ func TestMsg_hasAltWithSMime(t *testing.T) { } } +// TestMsg_hasSMime tests the hasSMime() method of the Msg +func TestMsg_hasSMime(t *testing.T) { + keyPair, err := getDummyCertificate() + if err != nil { + t.Errorf("failed to load dummy certificate. Cause: %v", err) + } + m := NewMsg() + if err := m.SignWithSMime(keyPair); err != nil { + t.Errorf("set of certificate was not successful") + } + m.SetBodyString(TypeTextPlain, "Plain") + if !m.hasSMime() { + t.Errorf("mail has smime configured but hasSMime() returned true") + } +} + // TestMsg_hasRelated tests the hasRelated() method of the Msg func TestMsg_hasRelated(t *testing.T) { m := NewMsg() @@ -1978,6 +1994,70 @@ func TestMsg_WriteToSkipMiddleware(t *testing.T) { } } +// TestMsg_WriteToWithSMIME tests the WriteTo() method of the Msg +func TestMsg_WriteToWithSMIME(t *testing.T) { + keyPair, err := getDummyCertificate() + if err != nil { + t.Errorf("failed to load dummy certificate. Cause: %v", err) + } + + m := NewMsg() + m.Subject("This is a test") + m.SetBodyString(TypeTextPlain, "Plain") + if err := m.SignWithSMime(keyPair); err != nil { + t.Errorf("set of certificate was not successful") + } + + wbuf := bytes.Buffer{} + if _, err = m.WriteTo(&wbuf); err != nil { + t.Errorf("WriteTo() failed: %s", err) + } + + result := wbuf.String() + boundary := result[strings.LastIndex(result, "--")-60 : strings.LastIndex(result, "--")] + if strings.Count(result, boundary) != 4 { + t.Errorf("WriteTo() failed. False number of boundaries found") + } + + parts := strings.Split(result, fmt.Sprintf("--%s", boundary)) + if len(parts) != 4 { + t.Errorf("WriteTo() failed. False number of parts found") + } + + preamble := parts[0] + if !strings.Contains(preamble, "MIME-Version: 1.0") { + t.Errorf("WriteTo() failed. Unable to find MIME-Version") + } + if !strings.Contains(preamble, "Subject: This is a test") { + t.Errorf("WriteTo() failed. Unable to find subject") + } + if !strings.Contains(preamble, fmt.Sprintf("Content-Type: multipart/signed; protocol=\"application/pkcs7-signature\"; micalg=sha-256;\r\n boundary=%s", boundary)) { + t.Errorf("WriteTo() failed. Unable to find Content-Type") + } + + signedData := parts[1] + if !strings.Contains(signedData, "Content-Transfer-Encoding: quoted-printable") { + t.Errorf("WriteTo() failed. Unable to find Content-Transfer-Encoding") + } + if !strings.Contains(signedData, "Content-Type: text/plain; charset=UTF-8") { + t.Errorf("WriteTo() failed. Unable to find Content-Type") + } + if !strings.Contains(signedData, "Plain") { + t.Errorf("WriteTo() failed. Unable to find Content") + } + + signature := parts[2] + if !strings.Contains(signature, "Content-Transfer-Encoding: base64") { + t.Errorf("WriteTo() failed. Unable to find Content-Transfer-Encoding") + } + if !strings.Contains(signature, `application/pkcs7-signature; name="smime.p7s"`) { + t.Errorf("WriteTo() failed. Unable to find Content-Type") + } + if strings.Contains(signature, "Plain") { + t.Errorf("WriteTo() failed. Unable to find signature") + } +} + // TestMsg_WriteTo_fails tests the WriteTo() method of the Msg but with a failing body writer function func TestMsg_WriteTo_fails(t *testing.T) { m := NewMsg() @@ -3266,9 +3346,6 @@ func TestSignWithSMime_ValidKeyPair(t *testing.T) { if m.sMime.certificate == nil { t.Errorf("WithSMimeSinging() - no certificate is given") } - if len(m.sMime.parentCertificates) != len(keyPair.Certificate[:1]) { - t.Errorf("WithSMimeSinging() - no certificate is given") - } } // TestSignWithSMime_InvalidKeyPair tests WithSMimeSinging with given invalid key pair From 43ba8e3af25615a67953fabc42a28090b238f127 Mon Sep 17 00:00:00 2001 From: theexiile1305 Date: Wed, 9 Oct 2024 15:45:22 +0200 Subject: [PATCH 21/33] feat: improved tests --- msgwriter_test.go | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/msgwriter_test.go b/msgwriter_test.go index 3466199..15e751f 100644 --- a/msgwriter_test.go +++ b/msgwriter_test.go @@ -161,6 +161,7 @@ func TestMsgWriter_writeMsg_SMime(t *testing.T) { if err != nil { t.Errorf("failed to load dummy certificate. Cause: %v", err) } + m := NewMsg() if err := m.SignWithSMime(keyPair); err != nil { t.Errorf("set of certificate was not successful") @@ -173,7 +174,22 @@ func TestMsgWriter_writeMsg_SMime(t *testing.T) { mw := &msgWriter{writer: &buf, charset: CharsetUTF8, encoder: mime.QEncoding} mw.writeMsg(m) ms := buf.String() - if !strings.Contains(ms, `multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-256;`) { - t.Errorf("writeMsg failed. Expected PGP encoding header but didn't find it in message output") + + if !strings.Contains(ms, "MIME-Version: 1.0") { + t.Errorf("writeMsg failed. Unable to find MIME-Version") + } + if !strings.Contains(ms, "Subject: This is a subject") { + t.Errorf("writeMsg failed. Unable to find subject") + } + if !strings.Contains(ms, "From: \"Toni Tester\" ") { + t.Errorf("writeMsg failed. Unable to find transmitter") + } + if !strings.Contains(ms, "To: \"Toni Receiver\" ") { + t.Errorf("writeMsg failed. Unable to find receiver") + } + + boundary := ms[strings.LastIndex(ms, "--")-60 : strings.LastIndex(ms, "--")] + if !strings.Contains(ms, fmt.Sprintf("Content-Type: multipart/signed; protocol=\"application/pkcs7-signature\"; micalg=sha-256;\r\n boundary=%s", boundary)) { + t.Errorf("writeMsg failed. Unable to find Content-Type") } } From 12076cf64a143488eb57a5f80dcd286d0496a90f Mon Sep 17 00:00:00 2001 From: theexiile1305 Date: Wed, 9 Oct 2024 16:15:23 +0200 Subject: [PATCH 22/33] feat: last tests --- sime.go => smime.go | 26 ++------ smime_test.go | 158 +++++++++++++++++++++++++++----------------- 2 files changed, 102 insertions(+), 82 deletions(-) rename sime.go => smime.go (81%) diff --git a/sime.go b/smime.go similarity index 81% rename from sime.go rename to smime.go index de2decc..e92d283 100644 --- a/sime.go +++ b/smime.go @@ -16,9 +16,6 @@ var ( // ErrInvalidKeyPair should be used if key pair is invalid ErrInvalidKeyPair = errors.New("invalid key pair") - // ErrInvalidCertificate should be used if a 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") @@ -34,9 +31,8 @@ var ( // SMime is used to sign messages with S/MIME type SMime struct { - privateKey *rsa.PrivateKey - certificate *x509.Certificate - parentCertificates []*x509.Certificate + privateKey *rsa.PrivateKey + certificate *x509.Certificate } // NewSMime construct a new instance of SMime with a provided *tls.Certificate @@ -45,19 +41,9 @@ func newSMime(keyPair *tls.Certificate) (*SMime, error) { return nil, ErrInvalidKeyPair } - parentCertificates := make([]*x509.Certificate, 0) - for _, cert := range keyPair.Certificate[1:] { - c, err := x509.ParseCertificate(cert) - if err != nil { - return nil, ErrInvalidCertificate - } - parentCertificates = append(parentCertificates, c) - } - return &SMime{ - privateKey: keyPair.PrivateKey.(*rsa.PrivateKey), - certificate: keyPair.Leaf, - parentCertificates: parentCertificates, + privateKey: keyPair.PrivateKey.(*rsa.PrivateKey), + certificate: keyPair.Leaf, }, nil } @@ -72,7 +58,7 @@ func (sm *SMime) signMessage(message string) (*string, error) { return nil, ErrCouldNotInitialize } - if err = signedData.AddSignerChain(sm.certificate, sm.privateKey, sm.parentCertificates, pkcs7.SignerInfoConfig{}); err != nil { + if err = signedData.AddSigner(sm.certificate, sm.privateKey, pkcs7.SignerInfoConfig{}); err != nil { return nil, ErrCouldNotAddSigner } @@ -92,7 +78,7 @@ func (sm *SMime) signMessage(message string) (*string, error) { } // 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 { +func (sm *SMime) prepareMessage(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)) } diff --git a/smime_test.go b/smime_test.go index bd0282d..cfd8518 100644 --- a/smime_test.go +++ b/smime_test.go @@ -1,7 +1,8 @@ package mail import ( - "errors" + "bytes" + "encoding/base64" "fmt" "strings" "testing" @@ -25,9 +26,6 @@ func TestNewSMime(t *testing.T) { if sMime.certificate != keyPair.Leaf { t.Errorf("NewSMime() did not return the same leaf certificate") } - if len(sMime.parentCertificates) != len(keyPair.Certificate)-1 { - t.Errorf("NewSMime() did not return the same number of parentCertificates") - } } // TestSign tests the sign method @@ -41,11 +39,20 @@ func TestSign(t *testing.T) { if err != nil { t.Errorf("Error creating new SMime from keyPair: %s", err) } - fmt.Println(sMime) + + message := "This is a test message" + singedMessage, err := sMime.signMessage(message) + if err != nil { + t.Errorf("Error creating singed message: %s", err) + } + + if *singedMessage == message { + t.Errorf("Sign() did not work") + } } -// TestCreateMessage tests the createMessage method -func TestCreateMessage(t *testing.T) { +// TestPrepareMessage tests the createMessage method +func TestPrepareMessage(t *testing.T) { keyPair, err := getDummyCertificate() if err != nil { t.Errorf("Error getting dummy certificate: %s", err) @@ -60,7 +67,7 @@ func TestCreateMessage(t *testing.T) { contentType := TypeTextPlain charset := CharsetUTF8 body := []byte("This is the body!") - result := sMime.createMessage(encoding, contentType, body) + result := sMime.prepareMessage(encoding, contentType, charset, body) if !strings.Contains(result, encoding.String()) { t.Errorf("createMessage() did not return the correct encoding") @@ -71,86 +78,113 @@ func TestCreateMessage(t *testing.T) { if !strings.Contains(result, string(body)) { t.Errorf("createMessage() did not return the correct body") } - if result != fmt.Sprintf("Content-Transfer-Encoding: %v\r\nContent-Type: %v; charset=%v\r\n\r\n%v", encoding, contentType, charset, string(body)) { + if result != fmt.Sprintf("Content-Transfer-Encoding: %s\r\nContent-Type: %s; charset=%s\r\n\r\n%s", encoding, contentType, charset, string(body)) { t.Errorf("createMessage() did not sucessfully create the message") } } // TestEncodeToPEM tests the encodeToPEM method func TestEncodeToPEM(t *testing.T) { + message := []byte("This is a test message") - keyPair, err := getDummyCertificate() + pemMessage, err := encodeToPEM(message) if err != nil { - t.Errorf("Error getting dummy certificate: %s", err) + t.Errorf("Error encoding message: %s", err) } - sMime, err := newSMime(keyPair) - if err != nil { - t.Errorf("Error creating new SMime from keyPair: %s", err) + base64Encoded := base64.StdEncoding.EncodeToString(message) + if *pemMessage != base64Encoded { + t.Errorf("encodeToPEM() did not work") } - fmt.Println(sMime) } // TestBytesFromLines tests the bytesFromLines method func TestBytesFromLines(t *testing.T) { + ls := lines{ + {line: []byte("Hello"), endOfLine: []byte("\n")}, + {line: []byte("World"), endOfLine: []byte("\n")}, + } + expected := []byte("Hello\nWorld\n") + result := ls.bytesFromLines([]byte("\n")) + if !bytes.Equal(result, expected) { + t.Errorf("Expected %s, but got %s", expected, result) + } +} + +// FuzzBytesFromLines tests the bytesFromLines method with fuzzing +func FuzzBytesFromLines(f *testing.F) { + f.Add([]byte("Hello"), []byte("\n")) + f.Fuzz(func(t *testing.T, lineData, sep []byte) { + ls := lines{ + {line: lineData, endOfLine: sep}, + } + _ = ls.bytesFromLines(sep) + }) } // TestParseLines tests the parseLines method func TestParseLines(t *testing.T) { + input := []byte("Hello\r\nWorld\nHello\rWorld") + expected := lines{ + {line: []byte("Hello"), endOfLine: []byte("\r\n")}, + {line: []byte("World"), endOfLine: []byte("\n")}, + {line: []byte("Hello"), endOfLine: []byte("\r")}, + {line: []byte("World"), endOfLine: []byte("")}, + } + result := parseLines(input) + if len(result) != len(expected) { + t.Errorf("Expected %d lines, but got %d", len(expected), len(result)) + } + + for i := range result { + if !bytes.Equal(result[i].line, expected[i].line) || !bytes.Equal(result[i].endOfLine, expected[i].endOfLine) { + t.Errorf("Line %d mismatch. Expected line: %s, endOfLine: %s, got line: %s, endOfLine: %s", + i, expected[i].line, expected[i].endOfLine, result[i].line, result[i].endOfLine) + } + } +} + +// FuzzParseLines tests the parseLines method with fuzzing +func FuzzParseLines(f *testing.F) { + f.Add([]byte("Hello\nWorld\r\nAnother\rLine")) + f.Fuzz(func(t *testing.T, input []byte) { + _ = parseLines(input) + }) } // TestSplitLine tests the splitLine method func TestSplitLine(t *testing.T) { - -} - -func foo(t *testing.T) { - tl := []struct { - n string - r SendErrReason - te bool - }{ - {"ErrGetSender/temp", ErrGetSender, true}, - {"ErrGetSender/perm", ErrGetSender, false}, - {"ErrGetRcpts/temp", ErrGetRcpts, true}, - {"ErrGetRcpts/perm", ErrGetRcpts, false}, - {"ErrSMTPMailFrom/temp", ErrSMTPMailFrom, true}, - {"ErrSMTPMailFrom/perm", ErrSMTPMailFrom, false}, - {"ErrSMTPRcptTo/temp", ErrSMTPRcptTo, true}, - {"ErrSMTPRcptTo/perm", ErrSMTPRcptTo, false}, - {"ErrSMTPData/temp", ErrSMTPData, true}, - {"ErrSMTPData/perm", ErrSMTPData, false}, - {"ErrSMTPDataClose/temp", ErrSMTPDataClose, true}, - {"ErrSMTPDataClose/perm", ErrSMTPDataClose, false}, - {"ErrSMTPReset/temp", ErrSMTPReset, true}, - {"ErrSMTPReset/perm", ErrSMTPReset, false}, - {"ErrWriteContent/temp", ErrWriteContent, true}, - {"ErrWriteContent/perm", ErrWriteContent, false}, - {"ErrConnCheck/temp", ErrConnCheck, true}, - {"ErrConnCheck/perm", ErrConnCheck, false}, - {"ErrNoUnencoded/temp", ErrNoUnencoded, true}, - {"ErrNoUnencoded/perm", ErrNoUnencoded, false}, - {"ErrAmbiguous/temp", ErrAmbiguous, true}, - {"ErrAmbiguous/perm", ErrAmbiguous, false}, - {"Unknown/temp", 9999, true}, - {"Unknown/perm", 9999, false}, + ls := lines{ + {line: []byte("Hello\r\nWorld\r\nAnotherLine"), endOfLine: []byte("")}, + } + expected := lines{ + {line: []byte("Hello"), endOfLine: []byte("\r\n")}, + {line: []byte("World"), endOfLine: []byte("\r\n")}, + {line: []byte("AnotherLine"), endOfLine: []byte("")}, } - for _, tt := range tl { - t.Run(tt.n, func(t *testing.T) { - if err := returnSendError(tt.r, tt.te); err != nil { - exp := &SendError{Reason: tt.r, isTemp: tt.te} - if !errors.Is(err, exp) { - t.Errorf("error mismatch, expected: %s (temp: %t), got: %s (temp: %t)", tt.r, tt.te, - exp.Error(), exp.isTemp) - } - if !strings.Contains(fmt.Sprintf("%s", err), tt.r.String()) { - t.Errorf("error string mismatch, expected: %s, got: %s", - tt.r.String(), fmt.Sprintf("%s", err)) - } - } - }) + result := ls.splitLine([]byte("\r\n")) + if len(result) != len(expected) { + t.Errorf("Expected %d lines, but got %d", len(expected), len(result)) + } + + for i := range result { + if !bytes.Equal(result[i].line, expected[i].line) || !bytes.Equal(result[i].endOfLine, expected[i].endOfLine) { + t.Errorf("Line %d mismatch. Expected line: %s, endOfLine: %s, got line: %s, endOfLine: %s", + i, expected[i].line, expected[i].endOfLine, result[i].line, result[i].endOfLine) + } } } + +// FuzzSplitLine tests the parseLsplitLineines method with fuzzing +func FuzzSplitLine(f *testing.F) { + f.Add([]byte("Hello\r\nWorld"), []byte("\r\n")) + f.Fuzz(func(t *testing.T, input, sep []byte) { + ls := lines{ + {line: input, endOfLine: []byte("")}, + } + _ = ls.splitLine(sep) + }) +} From c45aec89e935bae6ec9526f957b5e5c02ed788f7 Mon Sep 17 00:00:00 2001 From: theexiile1305 Date: Fri, 11 Oct 2024 17:26:22 +0200 Subject: [PATCH 23/33] feat: add parent certificates --- smime.go | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/smime.go b/smime.go index e92d283..ae6e4f2 100644 --- a/smime.go +++ b/smime.go @@ -16,6 +16,9 @@ var ( // ErrInvalidKeyPair should be used if key pair is invalid ErrInvalidKeyPair = errors.New("invalid key pair") + // ErrInvalidParentCertificates should be used if one of the parent certificates is invalid + ErrInvalidParentCertificates = errors.New("invalid parent certificates") + // ErrCouldNotInitialize should be used if the signed data could not initialize ErrCouldNotInitialize = errors.New("could not initialize signed data") @@ -31,8 +34,9 @@ var ( // SMime is used to sign messages with S/MIME type SMime struct { - privateKey *rsa.PrivateKey - certificate *x509.Certificate + privateKey *rsa.PrivateKey + parentCertificates []*x509.Certificate + certificate *x509.Certificate } // NewSMime construct a new instance of SMime with a provided *tls.Certificate @@ -41,9 +45,19 @@ func newSMime(keyPair *tls.Certificate) (*SMime, error) { return nil, ErrInvalidKeyPair } + parentCertificates := make([]*x509.Certificate, 0) + for _, cert := range keyPair.Certificate[1:] { + c, err := x509.ParseCertificate(cert) + if err != nil { + return nil, ErrInvalidParentCertificates + } + parentCertificates = append(parentCertificates, c) + } + return &SMime{ - privateKey: keyPair.PrivateKey.(*rsa.PrivateKey), - certificate: keyPair.Leaf, + privateKey: keyPair.PrivateKey.(*rsa.PrivateKey), + certificate: keyPair.Leaf, + parentCertificates: parentCertificates, }, nil } @@ -58,7 +72,7 @@ func (sm *SMime) signMessage(message string) (*string, error) { return nil, ErrCouldNotInitialize } - if err = signedData.AddSigner(sm.certificate, sm.privateKey, pkcs7.SignerInfoConfig{}); err != nil { + if err = signedData.AddSignerChain(sm.certificate, sm.privateKey, sm.parentCertificates, pkcs7.SignerInfoConfig{}); err != nil { return nil, ErrCouldNotAddSigner } From 3f2ba4822cb4e7716c662858d301c27deef6ad2f Mon Sep 17 00:00:00 2001 From: theexiile1305 Date: Fri, 11 Oct 2024 17:26:48 +0200 Subject: [PATCH 24/33] feat: add support of s/mime singing --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3a67d0d..9ab4303 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ Here are some highlights of go-mail's featureset: * [X] Custom error types for delivery errors * [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] S/MIME singed messages go-mail works like a programatic email client and provides lots of methods and functionalities you would consider standard in a MUA. From f6bda9946425d03afaa11dc690598f2427185b76 Mon Sep 17 00:00:00 2001 From: theexiile1305 Date: Fri, 11 Oct 2024 18:43:04 +0200 Subject: [PATCH 25/33] feat: transfer configuration of intermediate certificate to caller of the library --- msg.go | 7 ++-- msg_test.go | 84 +++++++++++++++++++++++++++++++---------------- msgwriter_test.go | 8 ++--- smime.go | 50 +++++++++++++++------------- smime_test.go | 27 ++++++++------- util_test.go | 21 +++++++++--- 6 files changed, 122 insertions(+), 75 deletions(-) diff --git a/msg.go b/msg.go index 455ba3b..60f758f 100644 --- a/msg.go +++ b/msg.go @@ -7,7 +7,8 @@ package mail import ( "bytes" "context" - "crypto/tls" + "crypto/rsa" + "crypto/x509" "embed" "errors" "fmt" @@ -338,8 +339,8 @@ func WithNoDefaultUserAgent() MsgOption { } // SignWithSMime configures the Msg to be signed with S/MIME -func (m *Msg) SignWithSMime(keyPair *tls.Certificate) error { - sMime, err := newSMime(keyPair) +func (m *Msg) SignWithSMime(privateKey *rsa.PrivateKey, certificate *x509.Certificate, intermediateCertificate *x509.Certificate) error { + sMime, err := newSMime(privateKey, certificate, intermediateCertificate) if err != nil { return err } diff --git a/msg_test.go b/msg_test.go index ffd1357..7842cda 100644 --- a/msg_test.go +++ b/msg_test.go @@ -1909,15 +1909,15 @@ func TestMsg_hasAlt(t *testing.T) { // TestMsg_hasAlt tests the hasAlt() method of the Msg with active S/MIME func TestMsg_hasAltWithSMime(t *testing.T) { - keyPair, err := getDummyCertificate() + privateKey, certificate, intermediateCertificate, err := getDummyCryptoMaterial() 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.SetBodyString(TypeTextPlain, "Plain") m.AddAlternativeString(TypeTextHTML, "HTML") - if err := m.SignWithSMime(keyPair); err != nil { - t.Errorf("set of certificate was not successful") + if err := m.SignWithSMime(privateKey, certificate, intermediateCertificate); err != nil { + t.Errorf("failed to init smime configuration") } if m.hasAlt() { 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 func TestMsg_hasSMime(t *testing.T) { - keyPair, err := getDummyCertificate() + privateKey, certificate, intermediateCertificate, err := getDummyCryptoMaterial() 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() - if err := m.SignWithSMime(keyPair); err != nil { - t.Errorf("set of certificate was not successful") + if err := m.SignWithSMime(privateKey, certificate, intermediateCertificate); err != nil { + t.Errorf("failed to init smime configuration") } m.SetBodyString(TypeTextPlain, "Plain") if !m.hasSMime() { @@ -2009,16 +2009,16 @@ func TestMsg_WriteToSkipMiddleware(t *testing.T) { // TestMsg_WriteToWithSMIME tests the WriteTo() method of the Msg func TestMsg_WriteToWithSMIME(t *testing.T) { - keyPair, err := getDummyCertificate() + privateKey, certificate, intermediateCertificate, err := getDummyCryptoMaterial() 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.Subject("This is a test") m.SetBodyString(TypeTextPlain, "Plain") - if err := m.SignWithSMime(keyPair); err != nil { - t.Errorf("set of certificate was not successful") + if err := m.SignWithSMime(privateKey, certificate, intermediateCertificate); err != nil { + t.Errorf("failed to init smime configuration") } wbuf := bytes.Buffer{} @@ -3345,12 +3345,12 @@ func TestNewMsgWithNoDefaultUserAgent(t *testing.T) { // TestSignWithSMime_ValidKeyPair tests WithSMimeSinging with given key pair func TestSignWithSMime_ValidKeyPair(t *testing.T) { - keyPair, err := getDummyCertificate() + privateKey, certificate, intermediateCertificate, err := getDummyCryptoMaterial() 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() - 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) } if m.sMime.privateKey == nil { @@ -3361,13 +3361,41 @@ func TestSignWithSMime_ValidKeyPair(t *testing.T) { } } -// TestSignWithSMime_InvalidKeyPair tests WithSMimeSinging with given invalid key pair -func TestSignWithSMime_InvalidKeyPair(t *testing.T) { +// TestSignWithSMime_InvalidPrivateKey tests WithSMimeSinging with given invalid private key +func TestSignWithSMime_InvalidPrivateKey(t *testing.T) { m := NewMsg() - err := m.SignWithSMime(nil) - if !errors.Is(err, ErrInvalidKeyPair) { - t.Errorf("failed to check sMimeAuthConfig values correctly: %s", err) + err := m.SignWithSMime(nil, nil, nil) + if !errors.Is(err, ErrInvalidPrivateKey) { + 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 func TestMsg_createSignaturePart(t *testing.T) { - keyPair, err := getDummyCertificate() + privateKey, certificate, intermediateCertificate, err := getDummyCryptoMaterial() 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() - if err := m.SignWithSMime(keyPair); err != nil { - t.Errorf("set of certificate was not successful") + if err := m.SignWithSMime(privateKey, certificate, intermediateCertificate); err != nil { + t.Errorf("failed to init smime configuration") } body := []byte("This is the 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 func TestMsg_signMessage(t *testing.T) { - keyPair, err := getDummyCertificate() + privateKey, certificate, intermediateCertificate, err := getDummyCryptoMaterial() 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") m := NewMsg() m.SetBodyString(TypeTextPlain, string(body)) - if err := m.SignWithSMime(keyPair); err != nil { - t.Errorf("set of certificate was not successful") + if err := m.SignWithSMime(privateKey, certificate, intermediateCertificate); err != nil { + t.Errorf("failed to init smime configuration") } msg, err := m.signMessage(m) if err != nil { diff --git a/msgwriter_test.go b/msgwriter_test.go index 15e751f..6e36c4b 100644 --- a/msgwriter_test.go +++ b/msgwriter_test.go @@ -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 func TestMsgWriter_writeMsg_SMime(t *testing.T) { - keyPair, err := getDummyCertificate() + privateKey, certificate, intermediateCertificate, err := getDummyCryptoMaterial() 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() - if err := m.SignWithSMime(keyPair); err != nil { - t.Errorf("set of certificate was not successful") + if err := m.SignWithSMime(privateKey, certificate, intermediateCertificate); err != nil { + t.Errorf("failed to init smime configuration") } _ = m.From(`"Toni Tester" `) _ = m.To(`"Toni Receiver" `) diff --git a/smime.go b/smime.go index ae6e4f2..37fb536 100644 --- a/smime.go +++ b/smime.go @@ -3,7 +3,6 @@ package mail import ( "bytes" "crypto/rsa" - "crypto/tls" "crypto/x509" "encoding/pem" "errors" @@ -13,11 +12,14 @@ import ( ) var ( - // ErrInvalidKeyPair should be used if key pair is invalid - ErrInvalidKeyPair = errors.New("invalid key pair") + // ErrInvalidPrivateKey should be used if private key is invalid + ErrInvalidPrivateKey = errors.New("invalid private key") - // ErrInvalidParentCertificates should be used if one of the parent certificates is invalid - ErrInvalidParentCertificates = errors.New("invalid parent certificates") + // 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 = errors.New("could not initialize signed data") @@ -34,30 +36,32 @@ var ( // SMime is used to sign messages with S/MIME type SMime struct { - privateKey *rsa.PrivateKey - parentCertificates []*x509.Certificate - certificate *x509.Certificate + privateKey *rsa.PrivateKey + certificate *x509.Certificate + intermediateCertificate *x509.Certificate } -// NewSMime construct a new instance of SMime with a provided *tls.Certificate -func newSMime(keyPair *tls.Certificate) (*SMime, error) { - if keyPair == nil { - return nil, ErrInvalidKeyPair +// NewSMime construct a new instance of SMime with provided parameters +// privateKey as *rsa.PrivateKey +// certificate as *x509.Certificate +// intermediateCertificate as *x509.Certificate +func newSMime(privateKey *rsa.PrivateKey, certificate *x509.Certificate, intermediateCertificate *x509.Certificate) (*SMime, error) { + if privateKey == nil { + return nil, ErrInvalidPrivateKey } - parentCertificates := make([]*x509.Certificate, 0) - for _, cert := range keyPair.Certificate[1:] { - c, err := x509.ParseCertificate(cert) - if err != nil { - return nil, ErrInvalidParentCertificates - } - parentCertificates = append(parentCertificates, c) + if certificate == nil { + return nil, ErrInvalidCertificate + } + + if intermediateCertificate == nil { + return nil, ErrInvalidIntermediateCertificate } return &SMime{ - privateKey: keyPair.PrivateKey.(*rsa.PrivateKey), - certificate: keyPair.Leaf, - parentCertificates: parentCertificates, + privateKey: privateKey, + certificate: certificate, + intermediateCertificate: intermediateCertificate, }, nil } @@ -72,7 +76,7 @@ func (sm *SMime) signMessage(message string) (*string, error) { return nil, ErrCouldNotInitialize } - if err = signedData.AddSignerChain(sm.certificate, sm.privateKey, sm.parentCertificates, pkcs7.SignerInfoConfig{}); err != nil { + if err = signedData.AddSignerChain(sm.certificate, sm.privateKey, []*x509.Certificate{sm.intermediateCertificate}, pkcs7.SignerInfoConfig{}); err != nil { return nil, ErrCouldNotAddSigner } diff --git a/smime_test.go b/smime_test.go index cfd8518..bf4e2d0 100644 --- a/smime_test.go +++ b/smime_test.go @@ -10,32 +10,35 @@ import ( // TestNewSMime tests the newSMime method func TestNewSMime(t *testing.T) { - keyPair, err := getDummyCertificate() + privateKey, certificate, intermediateCertificate, err := getDummyCryptoMaterial() 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 { 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") } - if sMime.certificate != keyPair.Leaf { - t.Errorf("NewSMime() did not return the same leaf certificate") + if sMime.certificate != 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 func TestSign(t *testing.T) { - keyPair, err := getDummyCertificate() + privateKey, certificate, intermediateCertificate, err := getDummyCryptoMaterial() 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 { t.Errorf("Error creating new SMime from keyPair: %s", err) } @@ -53,12 +56,12 @@ func TestSign(t *testing.T) { // TestPrepareMessage tests the createMessage method func TestPrepareMessage(t *testing.T) { - keyPair, err := getDummyCertificate() + privateKey, certificate, intermediateCertificate, err := getDummyCryptoMaterial() 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 { t.Errorf("Error creating new SMime from keyPair: %s", err) } diff --git a/util_test.go b/util_test.go index 7c54b7d..33615d8 100644 --- a/util_test.go +++ b/util_test.go @@ -1,6 +1,7 @@ package mail import ( + "crypto/rsa" "crypto/tls" "crypto/x509" ) @@ -10,14 +11,24 @@ const ( keyFilePath = "dummy-child-key.pem" ) -// getDummyCertificate loads a certificate and a private key form local disk for testing purposes -func getDummyCertificate() (*tls.Certificate, error) { +// getDummyCryptoMaterial loads a certificate and a private key form local disk for testing purposes +func getDummyCryptoMaterial() (*rsa.PrivateKey, *x509.Certificate, *x509.Certificate, error) { keyPair, err := tls.LoadX509KeyPair(certFilePath, keyFilePath) 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 } From df68003f2490c9ee08a3ddf764b420b00ac391fa Mon Sep 17 00:00:00 2001 From: theexiile1305 Date: Fri, 11 Oct 2024 19:16:42 +0200 Subject: [PATCH 26/33] feat: add license information --- dummy-chain-cert.pem.license | 3 +++ dummy-chain-key.pem.license | 3 +++ smime.go | 4 ++++ smime_test.go | 4 ++++ util_test.go | 4 ++++ 5 files changed, 18 insertions(+) create mode 100644 dummy-chain-cert.pem.license create mode 100644 dummy-chain-key.pem.license diff --git a/dummy-chain-cert.pem.license b/dummy-chain-cert.pem.license new file mode 100644 index 0000000..615ea2b --- /dev/null +++ b/dummy-chain-cert.pem.license @@ -0,0 +1,3 @@ +// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors +// +// SPDX-License-Identifier: MIT diff --git a/dummy-chain-key.pem.license b/dummy-chain-key.pem.license new file mode 100644 index 0000000..615ea2b --- /dev/null +++ b/dummy-chain-key.pem.license @@ -0,0 +1,3 @@ +// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors +// +// SPDX-License-Identifier: MIT diff --git a/smime.go b/smime.go index 37fb536..62e07aa 100644 --- a/smime.go +++ b/smime.go @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors +// +// SPDX-License-Identifier: MIT + package mail import ( diff --git a/smime_test.go b/smime_test.go index bf4e2d0..9c5b121 100644 --- a/smime_test.go +++ b/smime_test.go @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors +// +// SPDX-License-Identifier: MIT + package mail import ( diff --git a/util_test.go b/util_test.go index 33615d8..f14f010 100644 --- a/util_test.go +++ b/util_test.go @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors +// +// SPDX-License-Identifier: MIT + package mail import ( From 148e9d8e1c106f9fa1a92c6a16411a7906844d71 Mon Sep 17 00:00:00 2001 From: theexiile1305 Date: Fri, 11 Oct 2024 19:21:34 +0200 Subject: [PATCH 27/33] chore: gofumpt --- encoding.go | 18 +++++++++--------- part.go | 1 - smime.go | 3 ++- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/encoding.go b/encoding.go index aa9cceb..8c38cea 100644 --- a/encoding.go +++ b/encoding.go @@ -171,20 +171,20 @@ const ( // TypeTextPlain represents the MIME type for plain text content. TypeTextPlain ContentType = "text/plain" - - // typeSMimeSigned represents the MIME type for S/MIME singed messages. - typeSMimeSigned ContentType = `application/pkcs7-signature; name="smime.p7s"` + + // typeSMimeSigned represents the MIME type for S/MIME singed messages. + typeSMimeSigned ContentType = `application/pkcs7-signature; name="smime.p7s"` ) const ( // MIMEAlternative MIMEType represents a MIME multipart/alternative type, used for emails with multiple versions. MIMEAlternative MIMEType = "alternative" - // MIMEMixed MIMEType represents a MIME multipart/mixed type used for emails containing different types of content. - MIMEMixed MIMEType = "mixed" - // MIMERelated MIMEType represents a MIME multipart/related type, used for emails with related content entities. - MIMERelated MIMEType = "related" - // 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` + // MIMEMixed MIMEType represents a MIME multipart/mixed type used for emails containing different types of content. + MIMEMixed MIMEType = "mixed" + // MIMERelated MIMEType represents a MIME multipart/related type, used for emails with related content entities. + MIMERelated MIMEType = "related" + // 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` ) // String satisfies the fmt.Stringer interface for the Charset type. diff --git a/part.go b/part.go index 153f281..3fab652 100644 --- a/part.go +++ b/part.go @@ -152,7 +152,6 @@ 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 diff --git a/smime.go b/smime.go index 62e07aa..405a5c1 100644 --- a/smime.go +++ b/smime.go @@ -11,8 +11,9 @@ import ( "encoding/pem" "errors" "fmt" - "go.mozilla.org/pkcs7" "strings" + + "go.mozilla.org/pkcs7" ) var ( From 128b2bd32e30835c73ac0dc18836ce7098b9a3ad Mon Sep 17 00:00:00 2001 From: theexiile1305 Date: Fri, 11 Oct 2024 19:22:47 +0200 Subject: [PATCH 28/33] chore: license --- dummy-chain-key.pem.license => dummy-child-key.pem.license | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename dummy-chain-key.pem.license => dummy-child-key.pem.license (100%) diff --git a/dummy-chain-key.pem.license b/dummy-child-key.pem.license similarity index 100% rename from dummy-chain-key.pem.license rename to dummy-child-key.pem.license From efd8aa93b6d89e2960ab37b7bdd01000055c8a2d Mon Sep 17 00:00:00 2001 From: theexiile1305 Date: Thu, 17 Oct 2024 15:48:42 +0200 Subject: [PATCH 29/33] feat: migrated resources from external repo to remove external dependency --- go.mod | 3 +- go.sum | 2 - pkcs7.go | 386 ++++++++++++++++++++++++++++++++++++++++++++++++++ pkcs7_test.go | 189 ++++++++++++++++++++++++ 4 files changed, 576 insertions(+), 4 deletions(-) create mode 100644 pkcs7.go create mode 100644 pkcs7_test.go diff --git a/go.mod b/go.mod index 24f14bb..84fc4bd 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,6 @@ module github.com/wneessen/go-mail go 1.16 require ( - go.mozilla.org/pkcs7 v0.9.0 golang.org/x/crypto v0.28.0 golang.org/x/text v0.19.0 -) +) \ No newline at end of file diff --git a/go.sum b/go.sum index f917906..2eb65e0 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,6 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -go.mozilla.org/pkcs7 v0.9.0 h1:yM4/HS9dYv7ri2biPtxt8ikvB37a980dg69/pKmS+eI= -go.mozilla.org/pkcs7 v0.9.0/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/pkcs7.go b/pkcs7.go new file mode 100644 index 0000000..6522bb6 --- /dev/null +++ b/pkcs7.go @@ -0,0 +1,386 @@ +// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors +// +// SPDX-License-Identifier: MIT + +package mail + +import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "errors" + "fmt" + "math/big" + "sort" + "time" + + _ "crypto/sha1" // for crypto.SHA1 +) + +// PKCS7 Represents a PKCS7 structure +type PKCS7 struct { + Content []byte + Certificates []*x509.Certificate + CRLs []pkix.CertificateList + Signers []signerInfo + raw interface{} +} + +type contentInfo struct { + ContentType asn1.ObjectIdentifier + Content asn1.RawValue `asn1:"explicit,optional,tag:0"` +} + +var ( + oidData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 1} + oidSignedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 2} + oidAttributeContentType = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 3} + oidAttributeMessageDigest = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 4} + oidAttributeSigningTime = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 5} +) + +type signedData struct { + Version int `asn1:"default:1"` + DigestAlgorithmIdentifiers []pkix.AlgorithmIdentifier `asn1:"set"` + ContentInfo contentInfo + Certificates rawCertificates `asn1:"optional,tag:0"` + CRLs []pkix.CertificateList `asn1:"optional,tag:1"` + SignerInfos []signerInfo `asn1:"set"` +} + +type rawCertificates struct { + Raw asn1.RawContent +} + +type attribute struct { + Type asn1.ObjectIdentifier + Value asn1.RawValue `asn1:"set"` +} + +type issuerAndSerial struct { + IssuerName asn1.RawValue + SerialNumber *big.Int +} + +// MessageDigestMismatchError is returned when the signer data digest does not +// match the computed digest for the contained content +type MessageDigestMismatchError struct { + ExpectedDigest []byte + ActualDigest []byte +} + +func (err *MessageDigestMismatchError) Error() string { + return fmt.Sprintf("pkcs7: Message digest mismatch\n\tExpected: %X\n\tActual : %X", err.ExpectedDigest, err.ActualDigest) +} + +type signerInfo struct { + Version int `asn1:"default:1"` + IssuerAndSerialNumber issuerAndSerial + DigestAlgorithm pkix.AlgorithmIdentifier + AuthenticatedAttributes []attribute `asn1:"optional,tag:0"` + DigestEncryptionAlgorithm pkix.AlgorithmIdentifier + EncryptedDigest []byte + UnauthenticatedAttributes []attribute `asn1:"optional,tag:1"` +} + +func (raw rawCertificates) Parse() ([]*x509.Certificate, error) { + if len(raw.Raw) == 0 { + return nil, nil + } + + var val asn1.RawValue + if _, err := asn1.Unmarshal(raw.Raw, &val); err != nil { + return nil, err + } + + return x509.ParseCertificates(val.Bytes) +} + +func marshalAttributes(attrs []attribute) ([]byte, error) { + encodedAttributes, err := asn1.Marshal(struct { + A []attribute `asn1:"set"` + }{A: attrs}) + if err != nil { + return nil, err + } + + // Remove the leading sequence octets + var raw asn1.RawValue + asn1.Unmarshal(encodedAttributes, &raw) + return raw.Bytes, nil +} + +var ( + oidDigestAlgorithmSHA1 = asn1.ObjectIdentifier{1, 3, 14, 3, 2, 26} + oidSignatureSHA1WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 5} +) + +func getCertFromCertsByIssuerAndSerial(certs []*x509.Certificate, ias issuerAndSerial) *x509.Certificate { + for _, cert := range certs { + if isCertMatchForIssuerAndSerial(cert, ias) { + return cert + } + } + return nil +} + +// GetOnlySigner returns an x509.Certificate for the first signer of the signed +// data payload. If there are more or less than one signer, nil is returned +func (p7 *PKCS7) GetOnlySigner() *x509.Certificate { + if len(p7.Signers) != 1 { + return nil + } + signer := p7.Signers[0] + return getCertFromCertsByIssuerAndSerial(p7.Certificates, signer.IssuerAndSerialNumber) +} + +// ErrUnsupportedAlgorithm tells you when our quick dev assumptions have failed +var ErrUnsupportedAlgorithm = errors.New("pkcs7: cannot decrypt data: only RSA, DES, DES-EDE3, AES-256-CBC and AES-128-GCM supported") + +func isCertMatchForIssuerAndSerial(cert *x509.Certificate, ias issuerAndSerial) bool { + return cert.SerialNumber.Cmp(ias.SerialNumber) == 0 && bytes.Compare(cert.RawIssuer, ias.IssuerName.FullBytes) == 0 +} + +func unmarshalAttribute(attrs []attribute, attributeType asn1.ObjectIdentifier, out interface{}) error { + for _, attr := range attrs { + if attr.Type.Equal(attributeType) { + _, err := asn1.Unmarshal(attr.Value.Bytes, out) + return err + } + } + return errors.New("pkcs7: attribute type not in attributes") +} + +// UnmarshalSignedAttribute decodes a single attribute from the signer info +func (p7 *PKCS7) UnmarshalSignedAttribute(attributeType asn1.ObjectIdentifier, out interface{}) error { + sd, ok := p7.raw.(signedData) + if !ok { + return errors.New("pkcs7: payload is not signedData content") + } + if len(sd.SignerInfos) < 1 { + return errors.New("pkcs7: payload has no signers") + } + attributes := sd.SignerInfos[0].AuthenticatedAttributes + return unmarshalAttribute(attributes, attributeType, out) +} + +// SignedData is an opaque data structure for creating signed data payloads +type SignedData struct { + sd signedData + certs []*x509.Certificate + messageDigest []byte +} + +// Attribute represents a key value pair attribute. Value must be marshalable byte +// `encoding/asn1` +type Attribute struct { + Type asn1.ObjectIdentifier + Value interface{} +} + +// SignerInfoConfig are optional values to include when adding a signer +type SignerInfoConfig struct { + ExtraSignedAttributes []Attribute +} + +// newSignedData initializes a SignedData with content +func newSignedData(data []byte) (*SignedData, error) { + content, err := asn1.Marshal(data) + if err != nil { + return nil, err + } + ci := contentInfo{ + ContentType: oidData, + Content: asn1.RawValue{Class: 2, Tag: 0, Bytes: content, IsCompound: true}, + } + digAlg := pkix.AlgorithmIdentifier{ + Algorithm: oidDigestAlgorithmSHA1, + } + h := crypto.SHA1.New() + h.Write(data) + md := h.Sum(nil) + sd := signedData{ + ContentInfo: ci, + Version: 1, + DigestAlgorithmIdentifiers: []pkix.AlgorithmIdentifier{digAlg}, + } + return &SignedData{sd: sd, messageDigest: md}, nil +} + +type attributes struct { + types []asn1.ObjectIdentifier + values []interface{} +} + +// Add adds the attribute, maintaining insertion order +func (attrs *attributes) Add(attrType asn1.ObjectIdentifier, value interface{}) { + attrs.types = append(attrs.types, attrType) + attrs.values = append(attrs.values, value) +} + +type sortableAttribute struct { + SortKey []byte + Attribute attribute +} + +type attributeSet []sortableAttribute + +func (sa attributeSet) Len() int { + return len(sa) +} + +func (sa attributeSet) Less(i, j int) bool { + return bytes.Compare(sa[i].SortKey, sa[j].SortKey) < 0 +} + +func (sa attributeSet) Swap(i, j int) { + sa[i], sa[j] = sa[j], sa[i] +} + +func (sa attributeSet) attributes() []attribute { + attrs := make([]attribute, len(sa)) + for i, attr := range sa { + attrs[i] = attr.Attribute + } + return attrs +} + +func (attrs *attributes) forMarshaling() ([]attribute, error) { + sortables := make(attributeSet, len(attrs.types)) + for i := range sortables { + attrType := attrs.types[i] + attrValue := attrs.values[i] + asn1Value, err := asn1.Marshal(attrValue) + if err != nil { + return nil, err + } + attr := attribute{ + Type: attrType, + Value: asn1.RawValue{Tag: 17, IsCompound: true, Bytes: asn1Value}, // 17 == SET tag + } + encoded, err := asn1.Marshal(attr) + if err != nil { + return nil, err + } + sortables[i] = sortableAttribute{ + SortKey: encoded, + Attribute: attr, + } + } + sort.Sort(sortables) + return sortables.attributes(), nil +} + +// addSigner signs attributes about the content and adds certificate to payload +func (sd *SignedData) addSigner(cert *x509.Certificate, pkey crypto.PrivateKey, config SignerInfoConfig) error { + attrs := &attributes{} + attrs.Add(oidAttributeContentType, sd.sd.ContentInfo.ContentType) + attrs.Add(oidAttributeMessageDigest, sd.messageDigest) + attrs.Add(oidAttributeSigningTime, time.Now()) + for _, attr := range config.ExtraSignedAttributes { + attrs.Add(attr.Type, attr.Value) + } + finalAttrs, err := attrs.forMarshaling() + if err != nil { + return err + } + signature, err := signAttributes(finalAttrs, pkey, crypto.SHA1) + if err != nil { + return err + } + + ias, err := cert2issuerAndSerial(cert) + if err != nil { + return err + } + + signer := signerInfo{ + AuthenticatedAttributes: finalAttrs, + DigestAlgorithm: pkix.AlgorithmIdentifier{Algorithm: oidDigestAlgorithmSHA1}, + DigestEncryptionAlgorithm: pkix.AlgorithmIdentifier{Algorithm: oidSignatureSHA1WithRSA}, + IssuerAndSerialNumber: ias, + EncryptedDigest: signature, + Version: 1, + } + // create signature of signed attributes + sd.certs = append(sd.certs, cert) + sd.sd.SignerInfos = append(sd.sd.SignerInfos, signer) + return nil +} + +// addCertificate adds the certificate to the payload. Useful for parent certificates +func (sd *SignedData) addCertificate(cert *x509.Certificate) { + sd.certs = append(sd.certs, cert) +} + +// detach removes content from the signed data struct to make it a detached signature. +// This must be called right before Finish() +func (sd *SignedData) detach() { + sd.sd.ContentInfo = contentInfo{ContentType: oidData} +} + +// finish marshals the content and its signers +func (sd *SignedData) finish() ([]byte, error) { + sd.sd.Certificates = marshalCertificates(sd.certs) + inner, err := asn1.Marshal(sd.sd) + if err != nil { + return nil, err + } + outer := contentInfo{ + ContentType: oidSignedData, + Content: asn1.RawValue{Class: 2, Tag: 0, Bytes: inner, IsCompound: true}, + } + return asn1.Marshal(outer) +} + +func cert2issuerAndSerial(cert *x509.Certificate) (issuerAndSerial, error) { + var ias issuerAndSerial + // The issuer RDNSequence has to match exactly the sequence in the certificate + // We cannot use cert.Issuer.ToRDNSequence() here since it mangles the sequence + ias.IssuerName = asn1.RawValue{FullBytes: cert.RawIssuer} + ias.SerialNumber = cert.SerialNumber + + return ias, nil +} + +// signs the DER encoded form of the attributes with the private key +func signAttributes(attrs []attribute, pkey crypto.PrivateKey, hash crypto.Hash) ([]byte, error) { + attrBytes, err := marshalAttributes(attrs) + if err != nil { + return nil, err + } + h := hash.New() + h.Write(attrBytes) + hashed := h.Sum(nil) + switch priv := pkey.(type) { + case *rsa.PrivateKey: + return rsa.SignPKCS1v15(rand.Reader, priv, crypto.SHA1, hashed) + } + return nil, ErrUnsupportedAlgorithm +} + +// concats and wraps the certificates in the RawValue structure +func marshalCertificates(certs []*x509.Certificate) rawCertificates { + var buf bytes.Buffer + for _, cert := range certs { + buf.Write(cert.Raw) + } + rawCerts, _ := marshalCertificateBytes(buf.Bytes()) + return rawCerts +} + +// Even though, the tag & length are stripped out during marshalling the +// RawContent, we have to encode it into the RawContent. If its missing, +// then `asn1.Marshal()` will strip out the certificate wrapper instead. +func marshalCertificateBytes(certs []byte) (rawCertificates, error) { + var val = asn1.RawValue{Bytes: certs, Class: 2, Tag: 0, IsCompound: true} + b, err := asn1.Marshal(val) + if err != nil { + return rawCertificates{}, err + } + return rawCertificates{Raw: b}, nil +} diff --git a/pkcs7_test.go b/pkcs7_test.go new file mode 100644 index 0000000..fa79faf --- /dev/null +++ b/pkcs7_test.go @@ -0,0 +1,189 @@ +// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors +// +// SPDX-License-Identifier: MIT + +package mail + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "io/ioutil" + "math/big" + "os" + "os/exec" + "testing" + "time" +) + +// TestSign_E2E tests S/MIME singing as e2e +func TestSign_E2E(t *testing.T) { + cert, err := createTestCertificate() + if err != nil { + t.Fatal(err) + } + content := []byte("Hello World") + for _, testDetach := range []bool{false, true} { + toBeSigned, err := newSignedData(content) + if err != nil { + t.Fatalf("Cannot initialize signed data: %s", err) + } + if err := toBeSigned.addSigner(cert.Certificate, cert.PrivateKey, SignerInfoConfig{}); err != nil { + t.Fatalf("Cannot add signer: %s", err) + } + if testDetach { + t.Log("Testing detached signature") + toBeSigned.detach() + } else { + t.Log("Testing attached signature") + } + signed, err := toBeSigned.finish() + if err != nil { + t.Fatalf("Cannot finish signing data: %s", err) + } + if err := pem.Encode(os.Stdout, &pem.Block{Type: "PKCS7", Bytes: signed}); err != nil { + t.Fatalf("Cannot write signed data: %s", err) + } + } +} + +func TestOpenSSLVerifyDetachedSignature(t *testing.T) { + rootCert, err := createTestCertificateByIssuer("PKCS7 Test Root CA", nil) + if err != nil { + t.Fatalf("Cannot generate root cert: %s", err) + } + signerCert, err := createTestCertificateByIssuer("PKCS7 Test Signer Cert", rootCert) + if err != nil { + t.Fatalf("Cannot generate signer cert: %s", err) + } + content := []byte("Hello World") + toBeSigned, err := newSignedData(content) + if err != nil { + t.Fatalf("Cannot initialize signed data: %s", err) + } + if err := toBeSigned.addSigner(signerCert.Certificate, signerCert.PrivateKey, SignerInfoConfig{}); err != nil { + t.Fatalf("Cannot add signer: %s", err) + } + toBeSigned.detach() + signed, err := toBeSigned.finish() + if err != nil { + t.Fatalf("Cannot finish signing data: %s", err) + } + + // write the root cert to a temp file + tmpRootCertFile, err := ioutil.TempFile("", "pkcs7TestRootCA") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tmpRootCertFile.Name()) // clean up + fd, err := os.OpenFile(tmpRootCertFile.Name(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755) + if err != nil { + t.Fatal(err) + } + pem.Encode(fd, &pem.Block{Type: "CERTIFICATE", Bytes: rootCert.Certificate.Raw}) + fd.Close() + + // write the signature to a temp file + tmpSignatureFile, err := ioutil.TempFile("", "pkcs7Signature") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tmpSignatureFile.Name()) // clean up + ioutil.WriteFile(tmpSignatureFile.Name(), signed, 0755) + + // write the content to a temp file + tmpContentFile, err := ioutil.TempFile("", "pkcs7Content") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tmpContentFile.Name()) // clean up + ioutil.WriteFile(tmpContentFile.Name(), content, 0755) + + // call openssl to verify the signature on the content using the root + opensslCMD := exec.Command("openssl", "smime", "-verify", + "-in", tmpSignatureFile.Name(), "-inform", "DER", + "-content", tmpContentFile.Name(), + "-CAfile", tmpRootCertFile.Name()) + out, err := opensslCMD.Output() + t.Logf("%s", out) + if err != nil { + t.Fatalf("openssl command failed with %s", err) + } +} + +type certKeyPair struct { + Certificate *x509.Certificate + PrivateKey *rsa.PrivateKey +} + +func createTestCertificate() (*certKeyPair, error) { + signer, err := createTestCertificateByIssuer("Eddard Stark", nil) + if err != nil { + return nil, err + } + fmt.Println("Created root cert") + if err := pem.Encode(os.Stdout, &pem.Block{Type: "CERTIFICATE", Bytes: signer.Certificate.Raw}); err != nil { + return nil, err + } + pair, err := createTestCertificateByIssuer("Jon Snow", signer) + if err != nil { + return nil, err + } + fmt.Println("Created signer cert") + if err := pem.Encode(os.Stdout, &pem.Block{Type: "CERTIFICATE", Bytes: pair.Certificate.Raw}); err != nil { + return nil, err + } + return pair, nil +} + +func createTestCertificateByIssuer(name string, issuer *certKeyPair) (*certKeyPair, error) { + priv, err := rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + return nil, err + } + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 32) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, err + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + SignatureAlgorithm: x509.SHA256WithRSA, + Subject: pkix.Name{ + CommonName: name, + Organization: []string{"Acme Co"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(1, 0, 0), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageEmailProtection}, + } + var issuerCert *x509.Certificate + var issuerKey crypto.PrivateKey + if issuer != nil { + issuerCert = issuer.Certificate + issuerKey = issuer.PrivateKey + } else { + template.IsCA = true + template.KeyUsage |= x509.KeyUsageCertSign + issuerCert = &template + issuerKey = priv + } + cert, err := x509.CreateCertificate(rand.Reader, &template, issuerCert, priv.Public(), issuerKey) + if err != nil { + return nil, err + } + leaf, err := x509.ParseCertificate(cert) + if err != nil { + return nil, err + } + return &certKeyPair{ + Certificate: leaf, + PrivateKey: priv, + }, nil +} From 978a01eaf61563b1f876a3b7fe81ce5686bd254e Mon Sep 17 00:00:00 2001 From: theexiile1305 Date: Thu, 17 Oct 2024 15:49:05 +0200 Subject: [PATCH 30/33] feat: also added ecdsa crypto key material --- ...chain-cert.pem => dummy-chain-cert-rsa.pem | 0 ...icense => dummy-chain-cert-rsa.pem.license | 0 dummy-child-cert-ecdsa.pem | 26 ++++++++++++++ ...ense => dummy-child-cert-ecdsa.pem.license | 0 dummy-child-key-ecdsa.pem | 8 +++++ dummy-child-key-ecdsa.pem.license | 3 ++ ...y-child-key.pem => dummy-child-key-rsa.pem | 0 dummy-child-key-rsa.pem.license | 3 ++ util_test.go | 36 ++++++++++++++++--- 9 files changed, 71 insertions(+), 5 deletions(-) rename dummy-chain-cert.pem => dummy-chain-cert-rsa.pem (100%) rename dummy-chain-cert.pem.license => dummy-chain-cert-rsa.pem.license (100%) create mode 100644 dummy-child-cert-ecdsa.pem rename dummy-child-key.pem.license => dummy-child-cert-ecdsa.pem.license (100%) create mode 100644 dummy-child-key-ecdsa.pem create mode 100644 dummy-child-key-ecdsa.pem.license rename dummy-child-key.pem => dummy-child-key-rsa.pem (100%) create mode 100644 dummy-child-key-rsa.pem.license diff --git a/dummy-chain-cert.pem b/dummy-chain-cert-rsa.pem similarity index 100% rename from dummy-chain-cert.pem rename to dummy-chain-cert-rsa.pem diff --git a/dummy-chain-cert.pem.license b/dummy-chain-cert-rsa.pem.license similarity index 100% rename from dummy-chain-cert.pem.license rename to dummy-chain-cert-rsa.pem.license diff --git a/dummy-child-cert-ecdsa.pem b/dummy-child-cert-ecdsa.pem new file mode 100644 index 0000000..5f9953c --- /dev/null +++ b/dummy-child-cert-ecdsa.pem @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIICAzCCAaqgAwIBAgIUKNWGvPrlzuYHnP4m6nGe60LalYEwCgYIKoZIzj0EAwIw +ZTELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5MRcw +FQYDVQQKDA5JbnRlcm1lZGlhdGVDQTEeMBwGA1UEAwwVSW50ZXJtZWRpYXRlIEVD +RFNBIENBMB4XDTI0MTAxNzEzMDg0N1oXDTI2MTAxNzEzMDg0N1owWzELMAkGA1UE +BhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5MRIwEAYDVQQKDAlF +bmRFbnRpdHkxGTAXBgNVBAMMEEVuZCBFbnRpdHkgRUNEU0EwWTATBgcqhkjOPQIB +BggqhkjOPQMBBwNCAATMkQg2RhaDYOWs+Ik/NKgZdwxm4BzagwVou6R73uiwXsRi +QgJfhBxp01S3JI4E8AiboFf4uPRnRh0HGl9cUR+po0IwQDAdBgNVHQ4EFgQUz247 +eAeMuM2We19rA5HnLzyLZEUwHwYDVR0jBBgwFoAU6oYLh690kT1bIB3DUA/SGRim +dXswCgYIKoZIzj0EAwIDRwAwRAIgI7cIpGzoiU1IoTYniEGXtK+WXq4Luv8k3BJQ +W16RAVsCICnuLaRyH/5nA3mmciAiF5R9PKDzyBnJcPGuCM1tmEpN +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIB/zCCAaSgAwIBAgIURGCUhG09dwZ2DwtWHjQxGTnNT3UwCgYIKoZIzj0EAwIw +VTELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5MQ8w +DQYDVQQKDAZSb290Q0ExFjAUBgNVBAMMDVJvb3QgRUNEU0EgQ0EwHhcNMjQxMDE3 +MTMwODMwWhcNMjkxMDE2MTMwODMwWjBlMQswCQYDVQQGEwJVUzEOMAwGA1UECAwF +U3RhdGUxDTALBgNVBAcMBENpdHkxFzAVBgNVBAoMDkludGVybWVkaWF0ZUNBMR4w +HAYDVQQDDBVJbnRlcm1lZGlhdGUgRUNEU0EgQ0EwWTATBgcqhkjOPQIBBggqhkjO +PQMBBwNCAASYjGGvlMFZwnqUc8+jt9I7qRT8IP5gYLPZ7oiV/oaGNinmtzG7UXC/ +2PEDvdqpMPNw65IaP0d8z+c5lUxneE70o0IwQDAdBgNVHQ4EFgQU6oYLh690kT1b +IB3DUA/SGRimdXswHwYDVR0jBBgwFoAUGVvIlDmd/tCejujJcy4xTvgkw3IwCgYI +KoZIzj0EAwIDSQAwRgIhAO0rw5JvMu5y8JO958/4FThdjwOsg/IDGryQ3QQM0tw1 +AiEA07W1o81WIOVLZzwyTJAN8SnpSRIXgV/+ccst2T7s7Zs= +-----END CERTIFICATE----- diff --git a/dummy-child-key.pem.license b/dummy-child-cert-ecdsa.pem.license similarity index 100% rename from dummy-child-key.pem.license rename to dummy-child-cert-ecdsa.pem.license diff --git a/dummy-child-key-ecdsa.pem b/dummy-child-key-ecdsa.pem new file mode 100644 index 0000000..d2a1185 --- /dev/null +++ b/dummy-child-key-ecdsa.pem @@ -0,0 +1,8 @@ +-----BEGIN EC PARAMETERS----- +BggqhkjOPQMBBw== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIG20X4lI7vocwK2W3TYOW+XtQv/bwRWYknvAf2OK8WFJoAoGCCqGSM49 +AwEHoUQDQgAEzJEINkYWg2DlrPiJPzSoGXcMZuAc2oMFaLuke97osF7EYkICX4Qc +adNUtySOBPAIm6BX+Lj0Z0YdBxpfXFEfqQ== +-----END EC PRIVATE KEY----- \ No newline at end of file diff --git a/dummy-child-key-ecdsa.pem.license b/dummy-child-key-ecdsa.pem.license new file mode 100644 index 0000000..615ea2b --- /dev/null +++ b/dummy-child-key-ecdsa.pem.license @@ -0,0 +1,3 @@ +// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors +// +// SPDX-License-Identifier: MIT diff --git a/dummy-child-key.pem b/dummy-child-key-rsa.pem similarity index 100% rename from dummy-child-key.pem rename to dummy-child-key-rsa.pem diff --git a/dummy-child-key-rsa.pem.license b/dummy-child-key-rsa.pem.license new file mode 100644 index 0000000..615ea2b --- /dev/null +++ b/dummy-child-key-rsa.pem.license @@ -0,0 +1,3 @@ +// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors +// +// SPDX-License-Identifier: MIT diff --git a/util_test.go b/util_test.go index f14f010..17821aa 100644 --- a/util_test.go +++ b/util_test.go @@ -5,19 +5,23 @@ package mail import ( + "crypto/ecdsa" "crypto/rsa" "crypto/tls" "crypto/x509" ) const ( - certFilePath = "dummy-chain-cert.pem" - keyFilePath = "dummy-child-key.pem" + certRSAFilePath = "dummy-chain-cert-rsa.pem" + keyRSAFilePath = "dummy-child-key-rsa.pem" + + certECDSAFilePath = "dummy-chain-cert-ecdsa.pem" + keyECDSAFilePath = "dummy-child-cert-ecdsa.pem" ) -// getDummyCryptoMaterial loads a certificate and a private key form local disk for testing purposes -func getDummyCryptoMaterial() (*rsa.PrivateKey, *x509.Certificate, *x509.Certificate, error) { - keyPair, err := tls.LoadX509KeyPair(certFilePath, keyFilePath) +// getDummyRSACryptoMaterial loads a certificate (RSA) and the associated private key (ECDSA) form local disk for testing purposes +func getDummyRSACryptoMaterial() (*rsa.PrivateKey, *x509.Certificate, *x509.Certificate, error) { + keyPair, err := tls.LoadX509KeyPair(certRSAFilePath, keyRSAFilePath) if err != nil { return nil, nil, nil, err } @@ -36,3 +40,25 @@ func getDummyCryptoMaterial() (*rsa.PrivateKey, *x509.Certificate, *x509.Certifi return privateKey, certificate, intermediateCertificate, nil } + +// getDummyECDSACryptoMaterial loads a certificate (ECDSA) and the associated private key (ECDSA) form local disk for testing purposes +func getDummyECDSACryptoMaterial() (*ecdsa.PrivateKey, *x509.Certificate, *x509.Certificate, error) { + keyPair, err := tls.LoadX509KeyPair(certECDSAFilePath, keyECDSAFilePath) + if err != nil { + return nil, nil, nil, err + } + + privateKey := keyPair.PrivateKey.(*ecdsa.PrivateKey) + + 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 +} From 4a3dbddaffa30ce64a7942f275386cd4f7fcea11 Mon Sep 17 00:00:00 2001 From: theexiile1305 Date: Thu, 17 Oct 2024 15:56:45 +0200 Subject: [PATCH 31/33] chore: formatting and using another types as deprecated ones --- msg.go | 42 +++++++++++++++++++++-- msg_test.go | 68 +++++++++++++++++++------------------ msgwriter_test.go | 4 +-- pkcs7.go | 14 ++++---- pkcs7_test.go | 43 +++++++++++++++++------- smime.go | 86 ++++++++++++++++++++++++++++------------------- smime_test.go | 65 ++++++++++++++++++++++++++++++----- 7 files changed, 224 insertions(+), 98 deletions(-) diff --git a/msg.go b/msg.go index 60f758f..7f5bbd9 100644 --- a/msg.go +++ b/msg.go @@ -7,7 +7,9 @@ package mail import ( "bytes" "context" + "crypto/ecdsa" "crypto/rsa" + "crypto/tls" "crypto/x509" "embed" "errors" @@ -338,9 +340,9 @@ func WithNoDefaultUserAgent() MsgOption { } } -// SignWithSMime configures the Msg to be signed with S/MIME -func (m *Msg) SignWithSMime(privateKey *rsa.PrivateKey, certificate *x509.Certificate, intermediateCertificate *x509.Certificate) error { - sMime, err := newSMime(privateKey, certificate, intermediateCertificate) +// SignWithSMimeRSA configures the Msg to be signed with S/MIME +func (m *Msg) SignWithSMimeRSA(privateKey *rsa.PrivateKey, certificate *x509.Certificate, intermediateCertificate *x509.Certificate) error { + sMime, err := newSMimeWithRSA(privateKey, certificate, intermediateCertificate) if err != nil { return err } @@ -348,6 +350,40 @@ func (m *Msg) SignWithSMime(privateKey *rsa.PrivateKey, certificate *x509.Certif return nil } +// SignWithSMimeECDSA configures the Msg to be signed with S/MIME +func (m *Msg) SignWithSMimeECDSA(privateKey *ecdsa.PrivateKey, certificate *x509.Certificate, intermediateCertificate *x509.Certificate) error { + sMime, err := newSMimeWithECDSA(privateKey, certificate, intermediateCertificate) + if err != nil { + return err + } + m.sMime = sMime + return nil +} + +// SignWithTLSCertificate signs the Msg with the provided *tls.certificate. +func (m *Msg) SignWithTLSCertificate(keyPairTlS *tls.Certificate) error { + intermediateCertificate, err := x509.ParseCertificate(keyPairTlS.Certificate[1]) + if err != nil { + return fmt.Errorf("failed to parse intermediate certificate: %w", err) + } + + switch keyPairTlS.PrivateKey.(type) { + case *rsa.PrivateKey: + if intermediateCertificate == nil { + return m.SignWithSMimeRSA(keyPairTlS.PrivateKey.(*rsa.PrivateKey), keyPairTlS.Leaf, nil) + } + return m.SignWithSMimeRSA(keyPairTlS.PrivateKey.(*rsa.PrivateKey), keyPairTlS.Leaf, intermediateCertificate) + + case *ecdsa.PrivateKey: + if intermediateCertificate == nil { + return m.SignWithSMimeECDSA(keyPairTlS.PrivateKey.(*ecdsa.PrivateKey), keyPairTlS.Leaf, nil) + } + return m.SignWithSMimeECDSA(keyPairTlS.PrivateKey.(*ecdsa.PrivateKey), keyPairTlS.Leaf, intermediateCertificate) + default: + return fmt.Errorf("unsupported private key type: %T", keyPairTlS.PrivateKey) + } +} + // This method allows you to specify a character set for the email message. The charset is // important for ensuring that the content of the message is correctly interpreted by // mail clients. Common charset values include UTF-8, ISO-8859-1, and others. If a charset diff --git a/msg_test.go b/msg_test.go index 7842cda..2aeab91 100644 --- a/msg_test.go +++ b/msg_test.go @@ -1909,14 +1909,14 @@ func TestMsg_hasAlt(t *testing.T) { // TestMsg_hasAlt tests the hasAlt() method of the Msg with active S/MIME func TestMsg_hasAltWithSMime(t *testing.T) { - privateKey, certificate, intermediateCertificate, err := getDummyCryptoMaterial() + privateKey, certificate, intermediateCertificate, err := getDummyRSACryptoMaterial() if err != nil { t.Errorf("failed to laod dummy crypto material. Cause: %v", err) } m := NewMsg() m.SetBodyString(TypeTextPlain, "Plain") m.AddAlternativeString(TypeTextHTML, "HTML") - if err := m.SignWithSMime(privateKey, certificate, intermediateCertificate); err != nil { + if err := m.SignWithSMimeRSA(privateKey, certificate, intermediateCertificate); err != nil { t.Errorf("failed to init smime configuration") } if m.hasAlt() { @@ -1926,12 +1926,12 @@ func TestMsg_hasAltWithSMime(t *testing.T) { // TestMsg_hasSMime tests the hasSMime() method of the Msg func TestMsg_hasSMime(t *testing.T) { - privateKey, certificate, intermediateCertificate, err := getDummyCryptoMaterial() + privateKey, certificate, intermediateCertificate, err := getDummyRSACryptoMaterial() if err != nil { t.Errorf("failed to laod dummy crypto material. Cause: %v", err) } m := NewMsg() - if err := m.SignWithSMime(privateKey, certificate, intermediateCertificate); err != nil { + if err := m.SignWithSMimeRSA(privateKey, certificate, intermediateCertificate); err != nil { t.Errorf("failed to init smime configuration") } m.SetBodyString(TypeTextPlain, "Plain") @@ -2009,7 +2009,7 @@ func TestMsg_WriteToSkipMiddleware(t *testing.T) { // TestMsg_WriteToWithSMIME tests the WriteTo() method of the Msg func TestMsg_WriteToWithSMIME(t *testing.T) { - privateKey, certificate, intermediateCertificate, err := getDummyCryptoMaterial() + privateKey, certificate, intermediateCertificate, err := getDummyRSACryptoMaterial() if err != nil { t.Errorf("failed to laod dummy crypto material. Cause: %v", err) } @@ -2017,7 +2017,7 @@ func TestMsg_WriteToWithSMIME(t *testing.T) { m := NewMsg() m.Subject("This is a test") m.SetBodyString(TypeTextPlain, "Plain") - if err := m.SignWithSMime(privateKey, certificate, intermediateCertificate); err != nil { + if err := m.SignWithSMimeRSA(privateKey, certificate, intermediateCertificate); err != nil { t.Errorf("failed to init smime configuration") } @@ -3343,17 +3343,35 @@ func TestNewMsgWithNoDefaultUserAgent(t *testing.T) { } } -// TestSignWithSMime_ValidKeyPair tests WithSMimeSinging with given key pair -func TestSignWithSMime_ValidKeyPair(t *testing.T) { - privateKey, certificate, intermediateCertificate, err := getDummyCryptoMaterial() +// TestSignWithSMime_ValidRSAKeyPair tests WithSMimeSinging with given rsa key pair +func TestSignWithSMime_ValidRSAKeyPair(t *testing.T) { + privateKey, certificate, intermediateCertificate, err := getDummyRSACryptoMaterial() if err != nil { t.Errorf("failed to laod dummy crypto material. Cause: %v", err) } m := NewMsg() - if err := m.SignWithSMime(privateKey, certificate, intermediateCertificate); err != nil { + if err := m.SignWithSMimeRSA(privateKey, certificate, intermediateCertificate); err != nil { t.Errorf("failed to set sMime. Cause: %v", err) } - if m.sMime.privateKey == nil { + if m.sMime.privateKey.rsa == nil { + t.Errorf("WithSMimeSinging() - no private key is given") + } + if m.sMime.certificate == nil { + t.Errorf("WithSMimeSinging() - no certificate is given") + } +} + +// TestSignWithSMime_ValidRSAKeyPair tests WithSMimeSinging with given ecdsa key pair +func TestSignWithSMime_ValidECDSAKeyPair(t *testing.T) { + privateKey, certificate, intermediateCertificate, err := getDummyECDSACryptoMaterial() + if err != nil { + t.Errorf("failed to laod dummy crypto material. Cause: %v", err) + } + m := NewMsg() + if err := m.SignWithSMimeECDSA(privateKey, certificate, intermediateCertificate); err != nil { + t.Errorf("failed to set sMime. Cause: %v", err) + } + if m.sMime.privateKey.rsa == nil { t.Errorf("WithSMimeSinging() - no private key is given") } if m.sMime.certificate == nil { @@ -3365,7 +3383,7 @@ func TestSignWithSMime_ValidKeyPair(t *testing.T) { func TestSignWithSMime_InvalidPrivateKey(t *testing.T) { m := NewMsg() - err := m.SignWithSMime(nil, nil, nil) + err := m.SignWithSMimeRSA(nil, nil, nil) if !errors.Is(err, ErrInvalidPrivateKey) { t.Errorf("failed to pre-check SignWithSMime method values correctly: %s", err) } @@ -3373,32 +3391,18 @@ func TestSignWithSMime_InvalidPrivateKey(t *testing.T) { // TestSignWithSMime_InvalidCertificate tests WithSMimeSinging with given invalid certificate func TestSignWithSMime_InvalidCertificate(t *testing.T) { - privateKey, _, _, err := getDummyCryptoMaterial() + privateKey, _, _, err := getDummyRSACryptoMaterial() if err != nil { t.Errorf("failed to laod dummy crypto material. Cause: %v", err) } m := NewMsg() - err = m.SignWithSMime(privateKey, nil, nil) + err = m.SignWithSMimeRSA(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) - } -} - // Fuzzing tests func FuzzMsg_Subject(f *testing.F) { f.Add("Testsubject") @@ -3429,12 +3433,12 @@ func FuzzMsg_From(f *testing.F) { // TestMsg_createSignaturePart tests the Msg.createSignaturePart method func TestMsg_createSignaturePart(t *testing.T) { - privateKey, certificate, intermediateCertificate, err := getDummyCryptoMaterial() + privateKey, certificate, intermediateCertificate, err := getDummyRSACryptoMaterial() if err != nil { t.Errorf("failed to laod dummy crypto material. Cause: %v", err) } m := NewMsg() - if err := m.SignWithSMime(privateKey, certificate, intermediateCertificate); err != nil { + if err := m.SignWithSMimeRSA(privateKey, certificate, intermediateCertificate); err != nil { t.Errorf("failed to init smime configuration") } body := []byte("This is the body") @@ -3459,14 +3463,14 @@ func TestMsg_createSignaturePart(t *testing.T) { // TestMsg_signMessage tests the Msg.signMessage method func TestMsg_signMessage(t *testing.T) { - privateKey, certificate, intermediateCertificate, err := getDummyCryptoMaterial() + privateKey, certificate, intermediateCertificate, err := getDummyRSACryptoMaterial() if err != nil { t.Errorf("failed to laod dummy crypto material. Cause: %v", err) } body := []byte("This is the body") m := NewMsg() m.SetBodyString(TypeTextPlain, string(body)) - if err := m.SignWithSMime(privateKey, certificate, intermediateCertificate); err != nil { + if err := m.SignWithSMimeRSA(privateKey, certificate, intermediateCertificate); err != nil { t.Errorf("failed to init smime configuration") } msg, err := m.signMessage(m) diff --git a/msgwriter_test.go b/msgwriter_test.go index 6e36c4b..0379cd7 100644 --- a/msgwriter_test.go +++ b/msgwriter_test.go @@ -157,13 +157,13 @@ func TestMsgWriter_writeMsg_PGP(t *testing.T) { // TestMsgWriter_writeMsg_SMime tests the writeMsg method of the msgWriter with S/MIME types set func TestMsgWriter_writeMsg_SMime(t *testing.T) { - privateKey, certificate, intermediateCertificate, err := getDummyCryptoMaterial() + privateKey, certificate, intermediateCertificate, err := getDummyRSACryptoMaterial() if err != nil { t.Errorf("failed to laod dummy crypto material. Cause: %v", err) } m := NewMsg() - if err := m.SignWithSMime(privateKey, certificate, intermediateCertificate); err != nil { + if err := m.SignWithSMimeRSA(privateKey, certificate, intermediateCertificate); err != nil { t.Errorf("failed to init smime configuration") } _ = m.From(`"Toni Tester" `) diff --git a/pkcs7.go b/pkcs7.go index 6522bb6..f16b54f 100644 --- a/pkcs7.go +++ b/pkcs7.go @@ -25,7 +25,7 @@ import ( type PKCS7 struct { Content []byte Certificates []*x509.Certificate - CRLs []pkix.CertificateList + CRLs []x509.RevocationList Signers []signerInfo raw interface{} } @@ -47,9 +47,9 @@ type signedData struct { Version int `asn1:"default:1"` DigestAlgorithmIdentifiers []pkix.AlgorithmIdentifier `asn1:"set"` ContentInfo contentInfo - Certificates rawCertificates `asn1:"optional,tag:0"` - CRLs []pkix.CertificateList `asn1:"optional,tag:1"` - SignerInfos []signerInfo `asn1:"set"` + Certificates rawCertificates `asn1:"optional,tag:0"` + CRLs []x509.RevocationList `asn1:"optional,tag:1"` + SignerInfos []signerInfo `asn1:"set"` } type rawCertificates struct { @@ -110,7 +110,9 @@ func marshalAttributes(attrs []attribute) ([]byte, error) { // Remove the leading sequence octets var raw asn1.RawValue - asn1.Unmarshal(encodedAttributes, &raw) + if _, err := asn1.Unmarshal(encodedAttributes, &raw); err != nil { + return nil, err + } return raw.Bytes, nil } @@ -377,7 +379,7 @@ func marshalCertificates(certs []*x509.Certificate) rawCertificates { // RawContent, we have to encode it into the RawContent. If its missing, // then `asn1.Marshal()` will strip out the certificate wrapper instead. func marshalCertificateBytes(certs []byte) (rawCertificates, error) { - var val = asn1.RawValue{Bytes: certs, Class: 2, Tag: 0, IsCompound: true} + val := asn1.RawValue{Bytes: certs, Class: 2, Tag: 0, IsCompound: true} b, err := asn1.Marshal(val) if err != nil { return rawCertificates{}, err diff --git a/pkcs7_test.go b/pkcs7_test.go index fa79faf..7a65644 100644 --- a/pkcs7_test.go +++ b/pkcs7_test.go @@ -12,7 +12,6 @@ import ( "crypto/x509/pkix" "encoding/pem" "fmt" - "io/ioutil" "math/big" "os" "os/exec" @@ -75,33 +74,53 @@ func TestOpenSSLVerifyDetachedSignature(t *testing.T) { } // write the root cert to a temp file - tmpRootCertFile, err := ioutil.TempFile("", "pkcs7TestRootCA") + tmpRootCertFile, err := os.CreateTemp("", "pkcs7TestRootCA") if err != nil { t.Fatal(err) } - defer os.Remove(tmpRootCertFile.Name()) // clean up - fd, err := os.OpenFile(tmpRootCertFile.Name(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755) + defer func(name string) { + if err := os.Remove(name); err != nil { + t.Fatalf("Cannot write root cert: %s", err) + } + }(tmpRootCertFile.Name()) // clean up + fd, err := os.OpenFile(tmpRootCertFile.Name(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755) if err != nil { t.Fatal(err) } - pem.Encode(fd, &pem.Block{Type: "CERTIFICATE", Bytes: rootCert.Certificate.Raw}) - fd.Close() + if err := pem.Encode(fd, &pem.Block{Type: "CERTIFICATE", Bytes: rootCert.Certificate.Raw}); err != nil { + t.Fatalf("Cannot write root cert: %s", err) + } + if err := fd.Close(); err != nil { + t.Fatalf("Cannot write root cert: %s", err) + } // write the signature to a temp file - tmpSignatureFile, err := ioutil.TempFile("", "pkcs7Signature") + tmpSignatureFile, err := os.CreateTemp("", "pkcs7Signature") if err != nil { t.Fatal(err) } - defer os.Remove(tmpSignatureFile.Name()) // clean up - ioutil.WriteFile(tmpSignatureFile.Name(), signed, 0755) + defer func(name string) { + if err := os.Remove(name); err != nil { + t.Fatalf("Cannot write signature: %s", err) + } + }(tmpSignatureFile.Name()) // clean up + if err := os.WriteFile(tmpSignatureFile.Name(), signed, 0o755); err != nil { + t.Fatalf("Cannot write signature: %s", err) + } // write the content to a temp file - tmpContentFile, err := ioutil.TempFile("", "pkcs7Content") + tmpContentFile, err := os.CreateTemp("", "pkcs7Content") if err != nil { t.Fatal(err) } - defer os.Remove(tmpContentFile.Name()) // clean up - ioutil.WriteFile(tmpContentFile.Name(), content, 0755) + defer func(name string) { + if err := os.Remove(name); err != nil { + t.Fatalf("Cannot write content: %s", err) + } + }(tmpContentFile.Name()) // clean up + if err := os.WriteFile(tmpContentFile.Name(), content, 0o755); err != nil { + t.Fatalf("Cannot write content: %s", err) + } // call openssl to verify the signature on the content using the root opensslCMD := exec.Command("openssl", "smime", "-verify", diff --git a/smime.go b/smime.go index 405a5c1..da09713 100644 --- a/smime.go +++ b/smime.go @@ -6,14 +6,14 @@ package mail import ( "bytes" + "crypto" + "crypto/ecdsa" "crypto/rsa" "crypto/x509" "encoding/pem" "errors" "fmt" "strings" - - "go.mozilla.org/pkcs7" ) var ( @@ -22,35 +22,34 @@ var ( // 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 = 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") - - // ErrCouldNoEncodeToPEM should be used if the signature could not be encoded to PEM - ErrCouldNoEncodeToPEM = errors.New("could not encode to PEM") ) +// privateKeyHolder is the representation of a private key +type privateKeyHolder struct { + ecdsa *ecdsa.PrivateKey + rsa *rsa.PrivateKey +} + +// get returns the private key of the privateKeyHolder +func (p privateKeyHolder) get() crypto.PrivateKey { + if p.ecdsa != nil { + return p.ecdsa + } + return p.rsa +} + // SMime is used to sign messages with S/MIME type SMime struct { - privateKey *rsa.PrivateKey + privateKey privateKeyHolder certificate *x509.Certificate intermediateCertificate *x509.Certificate } -// NewSMime construct a new instance of SMime with provided parameters +// newSMimeWithRSA construct a new instance of SMime with provided parameters // privateKey as *rsa.PrivateKey // certificate as *x509.Certificate -// intermediateCertificate as *x509.Certificate -func newSMime(privateKey *rsa.PrivateKey, certificate *x509.Certificate, intermediateCertificate *x509.Certificate) (*SMime, error) { +// intermediateCertificate (optional) as *x509.Certificate +func newSMimeWithRSA(privateKey *rsa.PrivateKey, certificate *x509.Certificate, intermediateCertificate *x509.Certificate) (*SMime, error) { if privateKey == nil { return nil, ErrInvalidPrivateKey } @@ -59,12 +58,28 @@ func newSMime(privateKey *rsa.PrivateKey, certificate *x509.Certificate, interme return nil, ErrInvalidCertificate } - if intermediateCertificate == nil { - return nil, ErrInvalidIntermediateCertificate + return &SMime{ + privateKey: privateKeyHolder{rsa: privateKey}, + certificate: certificate, + intermediateCertificate: intermediateCertificate, + }, nil +} + +// newSMimeWithECDSA construct a new instance of SMime with provided parameters +// privateKey as *ecdsa.PrivateKey +// certificate as *x509.Certificate +// intermediateCertificate (optional) as *x509.Certificate +func newSMimeWithECDSA(privateKey *ecdsa.PrivateKey, certificate *x509.Certificate, intermediateCertificate *x509.Certificate) (*SMime, error) { + if privateKey == nil { + return nil, ErrInvalidPrivateKey + } + + if certificate == nil { + return nil, ErrInvalidCertificate } return &SMime{ - privateKey: privateKey, + privateKey: privateKeyHolder{ecdsa: privateKey}, certificate: certificate, intermediateCertificate: intermediateCertificate, }, nil @@ -75,26 +90,29 @@ func (sm *SMime) signMessage(message string) (*string, error) { lines := parseLines([]byte(message)) toBeSigned := lines.bytesFromLines([]byte("\r\n")) - signedData, err := pkcs7.NewSignedData(toBeSigned) - signedData.SetDigestAlgorithm(pkcs7.OIDDigestAlgorithmSHA256) - if err != nil { - return nil, ErrCouldNotInitialize + signedData, err := newSignedData(toBeSigned) + if err != nil || signedData == nil { + return nil, fmt.Errorf("could not initialize signed data: %w", err) } - if err = signedData.AddSignerChain(sm.certificate, sm.privateKey, []*x509.Certificate{sm.intermediateCertificate}, pkcs7.SignerInfoConfig{}); err != nil { - return nil, ErrCouldNotAddSigner + if err = signedData.addSigner(sm.certificate, sm.privateKey.get(), SignerInfoConfig{}); err != nil { + return nil, fmt.Errorf("could not add signer message: %w", err) } - signedData.Detach() + if sm.intermediateCertificate != nil { + signedData.addCertificate(sm.intermediateCertificate) + } - signatureDER, err := signedData.Finish() + signedData.detach() + + signatureDER, err := signedData.finish() if err != nil { - return nil, ErrCouldNotFinishSigning + return nil, fmt.Errorf("could not finish signing: %w", err) } pemMsg, err := encodeToPEM(signatureDER) if err != nil { - return nil, ErrCouldNoEncodeToPEM + return nil, fmt.Errorf("could not encode to PEM: %w", err) } return pemMsg, nil diff --git a/smime_test.go b/smime_test.go index 9c5b121..d6fae44 100644 --- a/smime_test.go +++ b/smime_test.go @@ -6,25 +6,72 @@ package mail import ( "bytes" + "crypto/ecdsa" + "crypto/rsa" "encoding/base64" "fmt" "strings" "testing" ) -// TestNewSMime tests the newSMime method -func TestNewSMime(t *testing.T) { - privateKey, certificate, intermediateCertificate, err := getDummyCryptoMaterial() +func TestGet_RSA(t *testing.T) { + p := privateKeyHolder{ + ecdsa: nil, + rsa: &rsa.PrivateKey{}, + } + + if p.get() == nil { + t.Errorf("get() did not return the correct private key") + } +} + +func TestGet_ECDSA(t *testing.T) { + p := privateKeyHolder{ + ecdsa: &ecdsa.PrivateKey{}, + rsa: nil, + } + + if p.get() == nil { + t.Errorf("get() did not return the correct private key") + } +} + +// TestNewSMimeWithRSA tests the newSMime method with RSA crypto material +func TestNewSMimeWithRSA(t *testing.T) { + privateKey, certificate, intermediateCertificate, err := getDummyRSACryptoMaterial() if err != nil { t.Errorf("Error getting dummy crypto material: %s", err) } - sMime, err := newSMime(privateKey, certificate, intermediateCertificate) + sMime, err := newSMimeWithRSA(privateKey, certificate, intermediateCertificate) if err != nil { t.Errorf("Error creating new SMime from keyPair: %s", err) } - if sMime.privateKey != privateKey { + if sMime.privateKey.rsa != privateKey { + t.Errorf("NewSMime() did not return the same private key") + } + if sMime.certificate != certificate { + t.Errorf("NewSMime() did not return the same certificate") + } + if sMime.intermediateCertificate != intermediateCertificate { + t.Errorf("NewSMime() did not return the same intermedidate certificate") + } +} + +// TestNewSMimeWithECDSA tests the newSMime method with ECDSA crypto material +func TestNewSMimeWithECDSA(t *testing.T) { + privateKey, certificate, intermediateCertificate, err := getDummyECDSACryptoMaterial() + if err != nil { + t.Errorf("Error getting dummy crypto material: %s", err) + } + + sMime, err := newSMimeWithECDSA(privateKey, certificate, intermediateCertificate) + if err != nil { + t.Errorf("Error creating new SMime from keyPair: %s", err) + } + + if sMime.privateKey.ecdsa != privateKey { t.Errorf("NewSMime() did not return the same private key") } if sMime.certificate != certificate { @@ -37,12 +84,12 @@ func TestNewSMime(t *testing.T) { // TestSign tests the sign method func TestSign(t *testing.T) { - privateKey, certificate, intermediateCertificate, err := getDummyCryptoMaterial() + privateKey, certificate, intermediateCertificate, err := getDummyRSACryptoMaterial() if err != nil { t.Errorf("Error getting dummy crypto material: %s", err) } - sMime, err := newSMime(privateKey, certificate, intermediateCertificate) + sMime, err := newSMimeWithRSA(privateKey, certificate, intermediateCertificate) if err != nil { t.Errorf("Error creating new SMime from keyPair: %s", err) } @@ -60,12 +107,12 @@ func TestSign(t *testing.T) { // TestPrepareMessage tests the createMessage method func TestPrepareMessage(t *testing.T) { - privateKey, certificate, intermediateCertificate, err := getDummyCryptoMaterial() + privateKey, certificate, intermediateCertificate, err := getDummyRSACryptoMaterial() if err != nil { t.Errorf("Error getting dummy crypto material: %s", err) } - sMime, err := newSMime(privateKey, certificate, intermediateCertificate) + sMime, err := newSMimeWithRSA(privateKey, certificate, intermediateCertificate) if err != nil { t.Errorf("Error creating new SMime from keyPair: %s", err) } From 4b21cc617b638cccb71a27db3241375bc3d9761a Mon Sep 17 00:00:00 2001 From: theexiile1305 Date: Thu, 17 Oct 2024 16:14:04 +0200 Subject: [PATCH 32/33] fix: tests --- dummy-child-cert-ecdsa.pem => dummy-chain-cert-ecdsa.pem | 0 ...cert-ecdsa.pem.license => dummy-chain-cert-ecdsa.pem.license | 0 msg_test.go | 2 +- util_test.go | 2 +- 4 files changed, 2 insertions(+), 2 deletions(-) rename dummy-child-cert-ecdsa.pem => dummy-chain-cert-ecdsa.pem (100%) rename dummy-child-cert-ecdsa.pem.license => dummy-chain-cert-ecdsa.pem.license (100%) diff --git a/dummy-child-cert-ecdsa.pem b/dummy-chain-cert-ecdsa.pem similarity index 100% rename from dummy-child-cert-ecdsa.pem rename to dummy-chain-cert-ecdsa.pem diff --git a/dummy-child-cert-ecdsa.pem.license b/dummy-chain-cert-ecdsa.pem.license similarity index 100% rename from dummy-child-cert-ecdsa.pem.license rename to dummy-chain-cert-ecdsa.pem.license diff --git a/msg_test.go b/msg_test.go index 2aeab91..aa46f94 100644 --- a/msg_test.go +++ b/msg_test.go @@ -3371,7 +3371,7 @@ func TestSignWithSMime_ValidECDSAKeyPair(t *testing.T) { if err := m.SignWithSMimeECDSA(privateKey, certificate, intermediateCertificate); err != nil { t.Errorf("failed to set sMime. Cause: %v", err) } - if m.sMime.privateKey.rsa == nil { + if m.sMime.privateKey.ecdsa == nil { t.Errorf("WithSMimeSinging() - no private key is given") } if m.sMime.certificate == nil { diff --git a/util_test.go b/util_test.go index 17821aa..7640f62 100644 --- a/util_test.go +++ b/util_test.go @@ -16,7 +16,7 @@ const ( keyRSAFilePath = "dummy-child-key-rsa.pem" certECDSAFilePath = "dummy-chain-cert-ecdsa.pem" - keyECDSAFilePath = "dummy-child-cert-ecdsa.pem" + keyECDSAFilePath = "dummy-child-key-ecdsa.pem" ) // getDummyRSACryptoMaterial loads a certificate (RSA) and the associated private key (ECDSA) form local disk for testing purposes From cc4c5bfd0417e74adb8dd86eaf7589858cc50527 Mon Sep 17 00:00:00 2001 From: theexiile1305 Date: Thu, 17 Oct 2024 16:21:15 +0200 Subject: [PATCH 33/33] fix: tests --- pkcs7.go | 2 +- pkcs7_test.go | 85 --------------------------------------------------- 2 files changed, 1 insertion(+), 86 deletions(-) diff --git a/pkcs7.go b/pkcs7.go index f16b54f..70c2c24 100644 --- a/pkcs7.go +++ b/pkcs7.go @@ -144,7 +144,7 @@ func (p7 *PKCS7) GetOnlySigner() *x509.Certificate { var ErrUnsupportedAlgorithm = errors.New("pkcs7: cannot decrypt data: only RSA, DES, DES-EDE3, AES-256-CBC and AES-128-GCM supported") func isCertMatchForIssuerAndSerial(cert *x509.Certificate, ias issuerAndSerial) bool { - return cert.SerialNumber.Cmp(ias.SerialNumber) == 0 && bytes.Compare(cert.RawIssuer, ias.IssuerName.FullBytes) == 0 + return cert.SerialNumber.Cmp(ias.SerialNumber) == 0 && bytes.Equal(cert.RawIssuer, ias.IssuerName.FullBytes) } func unmarshalAttribute(attrs []attribute, attributeType asn1.ObjectIdentifier, out interface{}) error { diff --git a/pkcs7_test.go b/pkcs7_test.go index 7a65644..e84f8c1 100644 --- a/pkcs7_test.go +++ b/pkcs7_test.go @@ -14,7 +14,6 @@ import ( "fmt" "math/big" "os" - "os/exec" "testing" "time" ) @@ -50,90 +49,6 @@ func TestSign_E2E(t *testing.T) { } } -func TestOpenSSLVerifyDetachedSignature(t *testing.T) { - rootCert, err := createTestCertificateByIssuer("PKCS7 Test Root CA", nil) - if err != nil { - t.Fatalf("Cannot generate root cert: %s", err) - } - signerCert, err := createTestCertificateByIssuer("PKCS7 Test Signer Cert", rootCert) - if err != nil { - t.Fatalf("Cannot generate signer cert: %s", err) - } - content := []byte("Hello World") - toBeSigned, err := newSignedData(content) - if err != nil { - t.Fatalf("Cannot initialize signed data: %s", err) - } - if err := toBeSigned.addSigner(signerCert.Certificate, signerCert.PrivateKey, SignerInfoConfig{}); err != nil { - t.Fatalf("Cannot add signer: %s", err) - } - toBeSigned.detach() - signed, err := toBeSigned.finish() - if err != nil { - t.Fatalf("Cannot finish signing data: %s", err) - } - - // write the root cert to a temp file - tmpRootCertFile, err := os.CreateTemp("", "pkcs7TestRootCA") - if err != nil { - t.Fatal(err) - } - defer func(name string) { - if err := os.Remove(name); err != nil { - t.Fatalf("Cannot write root cert: %s", err) - } - }(tmpRootCertFile.Name()) // clean up - fd, err := os.OpenFile(tmpRootCertFile.Name(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755) - if err != nil { - t.Fatal(err) - } - if err := pem.Encode(fd, &pem.Block{Type: "CERTIFICATE", Bytes: rootCert.Certificate.Raw}); err != nil { - t.Fatalf("Cannot write root cert: %s", err) - } - if err := fd.Close(); err != nil { - t.Fatalf("Cannot write root cert: %s", err) - } - - // write the signature to a temp file - tmpSignatureFile, err := os.CreateTemp("", "pkcs7Signature") - if err != nil { - t.Fatal(err) - } - defer func(name string) { - if err := os.Remove(name); err != nil { - t.Fatalf("Cannot write signature: %s", err) - } - }(tmpSignatureFile.Name()) // clean up - if err := os.WriteFile(tmpSignatureFile.Name(), signed, 0o755); err != nil { - t.Fatalf("Cannot write signature: %s", err) - } - - // write the content to a temp file - tmpContentFile, err := os.CreateTemp("", "pkcs7Content") - if err != nil { - t.Fatal(err) - } - defer func(name string) { - if err := os.Remove(name); err != nil { - t.Fatalf("Cannot write content: %s", err) - } - }(tmpContentFile.Name()) // clean up - if err := os.WriteFile(tmpContentFile.Name(), content, 0o755); err != nil { - t.Fatalf("Cannot write content: %s", err) - } - - // call openssl to verify the signature on the content using the root - opensslCMD := exec.Command("openssl", "smime", "-verify", - "-in", tmpSignatureFile.Name(), "-inform", "DER", - "-content", tmpContentFile.Name(), - "-CAfile", tmpRootCertFile.Name()) - out, err := opensslCMD.Output() - t.Logf("%s", out) - if err != nil { - t.Fatalf("openssl command failed with %s", err) - } -} - type certKeyPair struct { Certificate *x509.Certificate PrivateKey *rsa.PrivateKey