Compare commits

...

4 commits

Author SHA1 Message Date
theexiile1305
4b60557518
fix: failing tests 2024-09-26 17:14:42 +02:00
theexiile1305
9bdb741e05
feat: begin implementation of tests 2024-09-26 17:08:38 +02:00
theexiile1305
2368113872
feat: implementation of tests 2024-09-26 16:43:58 +02:00
theexiile1305
12edb2724b
feat: implementation of S/MIME signing without tests 2024-09-26 16:34:55 +02:00
9 changed files with 411 additions and 103 deletions

62
dummy-chain-cert.pem Normal file
View file

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

52
dummy-child-key.pem Normal file
View file

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

44
msg.go
View file

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

View file

@ -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, "<b>HTML</b>")
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 // TestMsg_hasRelated tests the hasRelated() method of the Msg
func TestMsg_hasRelated(t *testing.T) { func TestMsg_hasRelated(t *testing.T) {
m := NewMsg() m := NewMsg()
@ -3233,32 +3250,33 @@ func TestNewMsgWithNoDefaultUserAgent(t *testing.T) {
} }
} }
// TestWithSMimeSinging_ValidPrivateKey tests WithSMimeSinging with given privateKey // TestSignWithSMime_ValidKeyPair tests WithSMimeSinging with given key pair
func TestWithSMimeSinging_ValidPrivateKey(t *testing.T) { func TestSignWithSMime_ValidKeyPair(t *testing.T) {
privateKey, err := getDummyPrivateKey() keyPair, err := getDummyCertificate()
if err != nil { 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() 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) t.Errorf("failed to set sMime. Cause: %v", err)
} }
if m.sMime.privateKey != privateKey { if m.sMime.privateKey == nil {
t.Errorf("WithSMimeSinging. Expected %v, got: %v", privateKey, m.sMime.privateKey) 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 // TestSignWithSMime_InvalidKeyPair tests WithSMimeSinging with given invalid key pair
func TestWithSMimeSinging_InvalidPrivateKey(t *testing.T) { func TestSignWithSMime_InvalidKeyPair(t *testing.T) {
m := NewMsg() m := NewMsg()
err := m.SignWithSMime(nil, nil) err := m.SignWithSMime(nil)
if !errors.Is(err, ErrInvalidPrivateKey) { if !errors.Is(err, ErrInvalidKeyPair) {
t.Errorf("failed to check sMimeAuthConfig values correctly: %s", err) t.Errorf("failed to check sMimeAuthConfig values correctly: %s", err)
} }
} }
@ -3290,3 +3308,84 @@ func FuzzMsg_From(f *testing.F) {
m.Reset() 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)
}
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")
}
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())
}
}

View file

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

View file

@ -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") 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" <test@example.com>`)
_ = m.To(`"Toni Receiver" <receiver@example.com>`)
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")
}
}

View file

@ -275,6 +275,7 @@ func TestPart_IsSMimeSigned(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
m := NewMsg() m := NewMsg()
m.SetBodyString(TypeTextPlain, "This is a body!")
pl, err := getPartList(m) pl, err := getPartList(m)
if err != nil { if err != nil {
t.Errorf("failed: %s", err) t.Errorf("failed: %s", err)

139
sime.go
View file

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

View file

@ -1,41 +1,22 @@
package mail package mail
import ( import (
"crypto/rand" "crypto/tls"
"crypto/rsa"
"crypto/x509" "crypto/x509"
"crypto/x509/pkix"
"math/big"
"time"
) )
func getDummyPrivateKey() (*rsa.PrivateKey, error) { const (
privateKey, err := rsa.GenerateKey(rand.Reader, 2048) certFilePath = "dummy-chain-cert.pem"
if err != nil { keyFilePath = "dummy-child-key.pem"
return nil, err )
}
return privateKey, nil
}
func getDummyCertificate(privateKey *rsa.PrivateKey) (*x509.Certificate, error) { func getDummyCertificate() (*tls.Certificate, error) {
template := &x509.Certificate{ keyPair, err := tls.LoadX509KeyPair(certFilePath, keyFilePath)
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 { if err != nil {
return nil, err return nil, err
} }
cert, err := x509.ParseCertificate(certDER) keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0])
if err != nil {
return nil, err
}
return cert, nil return &keyPair, nil
} }