From 830a60b19f380f601f951e2b8457ade224caf365 Mon Sep 17 00:00:00 2001 From: InfiniteLoopSpace <35842605+InfiniteLoopSpace@users.noreply.github.com> Date: Mon, 19 Nov 2018 14:33:55 +0100 Subject: [PATCH] Add support for signing E-Mails and fixed tests. --- cms/cms.go | 6 +- cms/cms_test.go | 8 +-- mime/mime.go | 100 ++++++++++++++++++---------- openssl/openssl.go | 35 +++++++--- pki/pki_test.go | 12 ++-- smime/smime.go | 73 +++++++++++++++++++-- smime/smime_test.go | 156 ++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 328 insertions(+), 62 deletions(-) diff --git a/cms/cms.go b/cms/cms.go index 178c765..5cdff9c 100644 --- a/cms/cms.go +++ b/cms/cms.go @@ -139,7 +139,7 @@ func (cms *CMS) Decrypt(contentInfo []byte) (plain []byte, err error) { } // Sign signs the data and returns returns DER-encoded ASN.1 ContentInfo. -func (cms *CMS) Sign(data []byte) (der []byte, err error) { +func (cms *CMS) Sign(data []byte, detachedSignature ...bool) (der []byte, err error) { enci, err := protocol.NewDataEncapsulatedContentInfo(data) if err != nil { @@ -162,6 +162,10 @@ func (cms *CMS) Sign(data []byte) (der []byte, err error) { } } + if len(detachedSignature) > 0 && detachedSignature[0] { + sd.EncapContentInfo.EContent = nil + } + ci, err := sd.ContentInfo() if err != nil { return diff --git a/cms/cms_test.go b/cms/cms_test.go index 378f4ee..8269843 100644 --- a/cms/cms_test.go +++ b/cms/cms_test.go @@ -104,7 +104,7 @@ func TestSignVerify(t *testing.T) { func TestEncryptOpenSSL(t *testing.T) { message := []byte("Hallo Welt!") - der, err := openssl.Encrypt(message, leaf.Certificate) + der, err := openssl.Encrypt(message, leaf.Certificate, "-outform", "DER") if err != nil { t.Error(err) } @@ -129,7 +129,7 @@ func TestDecryptOpenSSL(t *testing.T) { t.Error(err) } - plain, err := openssl.Decrypt(ciphertext, leaf.PrivateKey) + plain, err := openssl.Decrypt(ciphertext, leaf.PrivateKey, "-inform", "DER") if err != nil { t.Error(err) } @@ -142,7 +142,7 @@ func TestDecryptOpenSSL(t *testing.T) { func TestSignOpenSSL(t *testing.T) { message := []byte("Hallo Welt") - sig, err := openssl.SignDetached(message, leaf.Certificate, leaf.PrivateKey, intermediate.Certificate) + sig, err := openssl.SignDetached(message, leaf.Certificate, leaf.PrivateKey, []*x509.Certificate{intermediate.Certificate}, "-outform", "DER") if err != nil { t.Error(err) } @@ -176,7 +176,7 @@ func TestVerifyOpenSSL(t *testing.T) { t.Error(err) } - sig, err := openssl.Verify(der, root.Certificate) + sig, err := openssl.Verify(der, root.Certificate, "-inform", "DER") if err != nil { t.Error(err) } diff --git a/mime/mime.go b/mime/mime.go index 7ec7052..38a2e6f 100644 --- a/mime/mime.go +++ b/mime/mime.go @@ -4,6 +4,7 @@ package mime import ( "bytes" + "crypto/rand" "errors" "fmt" "strings" @@ -35,13 +36,26 @@ func (m *MIME) Body() []byte { } //Gets the full message -func (m *MIME) Full() []byte { +func (m *MIME) Full(sep ...[]byte) []byte { - var sep []byte - sep = append(sep, m.interm.Line...) - sep = append(sep, m.interm.endOfLine...) + if len(sep) == 0 { + return m.FullLines().bytes(nil) + } - return append(append(m.Header(), sep...), m.Body()...) + return m.FullLines().bytes(sep[0]) +} + +//Gets the full message as Lines +func (m *MIME) FullLines() (full Lines) { + + full = append(full, m.headerFld...) + if m.interm.Line == nil && m.interm.endOfLine == nil { + m.interm = Line{nil, LF} + } + full = append(full, m.interm) + full = append(full, m.body...) + + return } //Adds a header field to the header of the message @@ -62,11 +76,16 @@ func (m *MIME) AddHeaderField(key, value []byte) { //Removes a header fild from the header of the message func (m *MIME) DeleteHeaderField(key []byte) { - for i := len(m.headerFld) - 1; i >= 0; i-- { - colonInd := bytes.Index(m.headerFld[i].Line, []byte(":")) - k := m.headerFld[i].Line[:colonInd] - if bytes.Equal(bytes.ToLower(k), bytes.ToLower(key)) { + for i := 0; i < len(m.headerFld); i++ { //i := range m.headerFld { + keyAndField := bytes.SplitN(m.headerFld[i].Line, []byte(":"), 2) + + if len(keyAndField) == 2 && bytes.Equal(bytes.ToLower(keyAndField[0]), bytes.ToLower(key)) { + m.headerFld = append(m.headerFld[:i], m.headerFld[i+1:]...) + for i < len(m.headerFld) && isContinuedLine(m.headerFld[i].Line) { + m.headerFld = append(m.headerFld[:i], m.headerFld[i+1:]...) + } + i-- } } @@ -76,15 +95,18 @@ func (m *MIME) DeleteHeaderField(key []byte) { func (m *MIME) GetHeaderField(key []byte) (values [][]byte) { for i := range m.headerFld { - colonInd := bytes.Index(m.headerFld[i].Line, []byte(":")) - if colonInd < 1 { - fmt.Printf("%q\n", (m.headerFld[i].Line)) - } - k := m.headerFld[i].Line[:colonInd] - if bytes.Equal(bytes.ToLower(k), bytes.ToLower(key)) { - if colonInd+2 < len(m.headerFld[i].Line) { - values = append(values, m.headerFld[i].Line[colonInd+2:]) + keyAndField := bytes.SplitN(m.headerFld[i].Line, []byte(":"), 2) + + value := []byte{} + if len(keyAndField) == 2 && bytes.Equal(bytes.ToLower(keyAndField[0]), bytes.ToLower(key)) { + + value = append(value, keyAndField[1][1:]...) + for k := 1; i+k < len(m.headerFld) && isContinuedLine(m.headerFld[i+k].Line); k++ { + value = append(value, m.headerFld[i+k-1].endOfLine...) + value = append(value, m.headerFld[i+k].Line...) } + + values = append(values, value) } } @@ -181,24 +203,9 @@ func Parse(raw []byte) (m MIME) { } func parseMIME(rawLines Lines) (m MIME) { - var headerField Line for i := range rawLines { - if len(rawLines[i].Line) > 0 && isContinuedLine(rawLines[i].Line) && i > 0 && !isEmpty(rawLines[i].Line) { - headerField.Line = append(headerField.Line, headerField.endOfLine...) - headerField.Line = append(headerField.Line, rawLines[i].Line...) - headerField.endOfLine = rawLines[i].endOfLine - } else { - - if i > 0 { - m.headerFld = append(m.headerFld, headerField) - } - - //Next headerField - headerField = rawLines[i] - } - // Empty line seprates header and body if isEmpty(rawLines[i].Line) { m.interm = rawLines[i] @@ -206,6 +213,8 @@ func parseMIME(rawLines Lines) (m MIME) { break } + m.headerFld = append(m.headerFld, rawLines[i]) + } return @@ -241,7 +250,7 @@ func (l Lines) splitLine(sep []byte) (newL Lines) { for i := 0; i < len(split)-1; i++ { newL = append(newL, Line{split[i], sep}) } - newL = append(newL, Line{split[len(split)-1], nil}) + newL = append(newL, Line{split[len(split)-1], line.endOfLine}) } else { newL = append(newL, line) } @@ -284,3 +293,28 @@ func isContinuedLine(s []byte) bool { return false } + +// SetMultipartBody makes a mulitpart messages with given parts and contentType +func (m *MIME) SetMultipartBody(contentType string, parts ...MIME) { + + body := Lines{} + + // Generate boundary + bndry := make([]byte, 30) + rand.Read(bndry) + boundary := fmt.Sprintf("%x", bndry) + + // Fix header + m.DeleteHeaderField([]byte("Content-Disposition")) + m.DeleteHeaderField([]byte("Content-Transfer-Encoding")) + m.SetHeaderField([]byte("Content-Type"), []byte(contentType+"; boundary="+boundary)) + + for i := range parts { + body = append(body, Line{[]byte("\n--" + boundary), LF}) + body = append(body, parts[i].FullLines()...) + } + + body = append(body, Line{[]byte("--" + boundary + "--"), LF}) + + m.body = body +} diff --git a/openssl/openssl.go b/openssl/openssl.go index cff3b85..e58fb6d 100644 --- a/openssl/openssl.go +++ b/openssl/openssl.go @@ -14,33 +14,39 @@ import ( ) //Encrypt a message with openssl -func Encrypt(in []byte, cert *x509.Certificate) (der []byte, err error) { +func Encrypt(in []byte, cert *x509.Certificate, opts ...string) (der []byte, err error) { tmp, err := ioutil.TempFile("", "example") defer os.Remove(tmp.Name()) pem.Encode(tmp, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) - der, err = openssl(in, "smime", "-outform", "DER", "-encrypt", "-aes128", tmp.Name()) + param := []string{"smime", "-encrypt", "-aes128"} + param = append(param, opts...) + param = append(param, tmp.Name()) + der, err = openssl(in, param...) return } //Decrypt a message with openssl -func Decrypt(in []byte, key crypto.PrivateKey) (plain []byte, err error) { +func Decrypt(in []byte, key crypto.PrivateKey, opts ...string) (plain []byte, err error) { tmp, err := ioutil.TempFile("", "example") defer os.Remove(tmp.Name()) pem.Encode(tmp, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key.(*rsa.PrivateKey))}) - plain, err = openssl(in, "smime", "-inform", "DER", "-decrypt", "-inkey", tmp.Name()) + param := []string{"smime", "-decrypt"} + param = append(param, opts...) + param = append(param, []string{"-decrypt", "-inkey", tmp.Name()}...) + plain, err = openssl(in, param...) return } //Create a detached signature with openssl -func SignDetached(in []byte, cert *x509.Certificate, key crypto.PrivateKey, interm ...*x509.Certificate) (plain []byte, err error) { +func SignDetached(in []byte, cert *x509.Certificate, key crypto.PrivateKey, interm []*x509.Certificate, opts ...string) (plain []byte, err error) { tmpCert, err := ioutil.TempFile("", "example") defer os.Remove(tmpCert.Name()) @@ -59,13 +65,16 @@ func SignDetached(in []byte, cert *x509.Certificate, key crypto.PrivateKey, inte pem.Encode(tmpInterm, &pem.Block{Type: "CERTIFICATE", Bytes: i.Raw}) } - plain, err = openssl(in, "smime", "-sign", "-nodetach", "-outform", "DER", "-signer", tmpCert.Name(), "-inkey", tmpKey.Name(), "-certfile", tmpInterm.Name()) + param := []string{"smime", "-sign", "-nodetach"} + param = append(param, opts...) + param = append(param, []string{"-signer", tmpCert.Name(), "-inkey", tmpKey.Name(), "-certfile", tmpInterm.Name()}...) + plain, err = openssl(in, param...) return } //Create a signature with openssl -func Sign(in []byte, cert *x509.Certificate, key crypto.PrivateKey, interm ...*x509.Certificate) (plain []byte, err error) { +func Sign(in []byte, cert *x509.Certificate, key crypto.PrivateKey, interm []*x509.Certificate, opts ...string) (plain []byte, err error) { tmpCert, err := ioutil.TempFile("", "example") defer os.Remove(tmpCert.Name()) @@ -84,20 +93,26 @@ func Sign(in []byte, cert *x509.Certificate, key crypto.PrivateKey, interm ...*x pem.Encode(tmpInterm, &pem.Block{Type: "CERTIFICATE", Bytes: i.Raw}) } - plain, err = openssl(in, "smime", "-sign", "-outform", "DER", "-signer", tmpCert.Name(), "-inkey", tmpKey.Name(), "-certfile", tmpInterm.Name()) + param := []string{"smime", "-sign"} + param = append(param, opts...) + param = append(param, []string{"-signer", tmpCert.Name(), "-inkey", tmpKey.Name(), "-certfile", tmpInterm.Name()}...) + plain, err = openssl(in, param...) return } //Verify a signature with openssl -func Verify(in []byte, ca *x509.Certificate) (plain []byte, err error) { +func Verify(in []byte, ca *x509.Certificate, opts ...string) (plain []byte, err error) { tmpCA, err := ioutil.TempFile("", "example") defer os.Remove(tmpCA.Name()) pem.Encode(tmpCA, &pem.Block{Type: "CERTIFICATE", Bytes: ca.Raw}) - plain, err = openssl(in, "smime", "-verify", "-inform", "DER", "-CAfile", tmpCA.Name()) + param := []string{"smime", "-verify"} + param = append(param, opts...) + param = append(param, []string{"-CAfile", tmpCA.Name()}...) + plain, err = openssl(in, param...) return } diff --git a/pki/pki_test.go b/pki/pki_test.go index cf98c3d..b4aac1a 100644 --- a/pki/pki_test.go +++ b/pki/pki_test.go @@ -5,27 +5,25 @@ import ( "crypto/x509/pkix" "testing" "time" - - pki "github.com/InfiniteLoopSpace/go_S-MIME/pki" ) func TestCA(t *testing.T) { - pki.DefaultProvince = []string{"CO"} - pki.DefaultLocality = []string{"Denver"} + DefaultProvince = []string{"CO"} + DefaultLocality = []string{"Denver"} // Create a root CA. - root := pki.New(pki.IsCA, pki.Subject(pkix.Name{ + root := New(IsCA, Subject(pkix.Name{ CommonName: "root.myorg.com", })) // Create an intermediate CA under the root. - intermediate := root.Issue(pki.IsCA, pki.Subject(pkix.Name{ + intermediate := root.Issue(IsCA, Subject(pkix.Name{ CommonName: "intermediate.myorg.com", })) // Create a leaf certificate under the intermediate. - leaf := intermediate.Issue(pki.Subject(pkix.Name{ + leaf := intermediate.Issue(Subject(pkix.Name{ CommonName: "leaf.myorg.com", })) diff --git a/smime/smime.go b/smime/smime.go index db4b9b1..80cc631 100644 --- a/smime/smime.go +++ b/smime/smime.go @@ -8,6 +8,7 @@ import ( "crypto/x509" "encoding/base64" "errors" + "log" "strings" "github.com/InfiniteLoopSpace/go_S-MIME/b64" @@ -42,8 +43,11 @@ func (smime *SMIME) Decrypt(msg []byte) (plaintext []byte, err error) { mediaType, params, err := mail.ParseMediaType() if !strings.HasPrefix(mediaType, "application/pkcs7-mime") { - err = errors.New("Unsupported media type: Can not decrypt this mail") - return + if !strings.HasPrefix(mediaType, "application/x-pkcs7-mime") { + err = errors.New("Unsupported media type: Can not decrypt this mail") + return + } + log.Println("Found Content-Type \"application/x-pkcs7-mime\" used early implementations of S/MIME agents") } if !strings.HasPrefix(params["smime-type"], "enveloped-data") { @@ -147,13 +151,16 @@ func (smime *SMIME) Verify(msg []byte) (chains [][][]*x509.Certificate, err erro mediaType, params, err := mail.ParseMediaType() if !strings.HasPrefix(mediaType, "multipart/signed") { - err = errors.New("Unsupported media type: can not decrypt this mail") + err = errors.New("Unsupported media type: can not verify the signature") return } if !strings.HasPrefix(params["protocol"], "application/pkcs7-signature") { - err = errors.New("Unsupported smime type: can not decrypt this mail") - return + if !strings.HasPrefix(params["protocol"], "application/x-pkcs7-signature") { + err = errors.New("Unsupported smime type: can not verify the signature") + return + } + log.Println("Found Content-Type \"application/x-pkcs7-signature\" used early implementations of S/MIME agents") } parts, err := mail.MultipartGetParts() @@ -170,8 +177,11 @@ func (smime *SMIME) Verify(msg []byte) (chains [][][]*x509.Certificate, err erro mediaType, params, err = signature.ParseMediaType() if !strings.HasPrefix(mediaType, "application/pkcs7-signature") { - err = errors.New("Unsupported media type: Can not decrypt this mail") - return + if !strings.HasPrefix(mediaType, "application/x-pkcs7-signature") { + err = errors.New("Unsupported media type: Can not decrypt this mail") + return + } + log.Println("Found Content-Type \"application/x-pkcs7-signature\" used early implementations of S/MIME agents") } contentTransferEncoding := signature.GetHeaderField([]byte("Content-Transfer-Encoding")) @@ -198,3 +208,52 @@ func (smime *SMIME) Verify(msg []byte) (chains [][][]*x509.Certificate, err erro return smime.CMS.VerifyDetached(signatureDer, signedMsg) } + +// Sign signs a mail and returns the signed message. +func (smime *SMIME) Sign(msg []byte) (signedMsg []byte, err error) { + + mail := mime.Parse(msg) + + // Prepare the signed Part + signedPart := mime.MIME{} + signedPart.SetBody(mail.Body()) + contentType := mail.GetHeaderField([]byte("Content-Type")) + if len(contentType) != 1 { + err = errors.New("Message has no Content-Type") + return + } + signedPart.SetHeaderField([]byte("Content-Type"), contentType[0]) + contentTransferEncoding := mail.GetHeaderField([]byte("Content-Transfer-Encoding")) + if len(contentType) == 1 { + signedPart.SetHeaderField([]byte("Content-Transfer-Encoding"), contentTransferEncoding[0]) + } + contentDisposition := mail.GetHeaderField([]byte("Content-Disposition")) + if len(contentType) == 1 { + signedPart.SetHeaderField([]byte("Content-Disposition"), contentDisposition[0]) + } + + // Sign + lines := mime.ParseLines(signedPart.Full()) + signatureDER, err := smime.CMS.Sign(lines.Bytes(mime.CRLF), true) + + // Encode signature + + signature := mime.MIME{} + signature.SetHeaderField([]byte("Content-Type"), []byte("application/pkcs7-signature; name=smime.p7s")) + signature.SetHeaderField([]byte("Content-Transfer-Encoding"), []byte("base64")) + signature.SetHeaderField([]byte("Content-Disposition"), []byte("attachment; filename=smime.p7s")) + signatureBASE64, err := b64.EncodeBase64(signatureDER) + if err != nil { + return + } + signature.SetBody(signatureBASE64) + + // Make multipart/signed message + micAlg := "sha256" + cntType := "multipart/signed;\n protocol=\"application/pkcs7-signature\";\n micalg=" + micAlg + + mail.SetMultipartBody(cntType, signedPart, signature) + + signedMsg = mail.Full() + return +} diff --git a/smime/smime_test.go b/smime/smime_test.go index 96c3d80..9a93b8a 100644 --- a/smime/smime_test.go +++ b/smime/smime_test.go @@ -2,10 +2,166 @@ package smime import ( "bytes" + "crypto" "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "fmt" + "log" "testing" + + "github.com/InfiniteLoopSpace/go_S-MIME/openssl" + "github.com/InfiniteLoopSpace/go_S-MIME/pki" ) +var ( + root = pki.New(pki.IsCA, pki.Subject(pkix.Name{ + CommonName: "root.example.com", + })) + + intermediate = root.Issue(pki.IsCA, pki.Subject(pkix.Name{ + CommonName: "intermediate.example.com", + })) + + leaf = intermediate.Issue(pki.Subject(pkix.Name{ + CommonName: "leaf.example.com", + })) + + keyPair = tls.Certificate{ + Certificate: [][]byte{leaf.Certificate.Raw, intermediate.Certificate.Raw, root.Certificate.Raw}, + PrivateKey: leaf.PrivateKey.(crypto.PrivateKey), + } +) + +func TestEnryptDecrypt(t *testing.T) { + + SMIME, err := New(keyPair) + if err != nil { + t.Error(err) + } + + plaintext := []byte(msg) + + ciphertext, err := SMIME.Encrypt(plaintext, []*x509.Certificate{leaf.Certificate}) + if err != nil { + t.Error(err) + } + fmt.Printf("%s\n", ciphertext) + + plain, err := SMIME.Decrypt(ciphertext) + if err != nil { + log.Fatal(err) + } + + if !bytes.Equal(plaintext, plain) { + t.Fatal("Encryption and decryption are not inverse") + } +} + +func TestSignVerify(t *testing.T) { + SMIME, err := New(keyPair) + if err != nil { + t.Error(err) + } + + SMIME.CMS.Opts.Roots.AddCert(root.Certificate) + + msg := []byte(msg) + + der, err := SMIME.Sign(msg) + if err != nil { + t.Error(err) + } + + _, err = SMIME.Verify(der) + if err != nil { + t.Error(err) + } +} + +func TestEncryptOpenSSL(t *testing.T) { + message := []byte("Hallo Welt!") + + der, err := openssl.Encrypt(message, leaf.Certificate) + if err != nil { + t.Error(err) + } + + SMIME, err := New(keyPair) + plain, err := SMIME.Decrypt(der) + if err != nil { + t.Error(err) + } + + if !bytes.Equal(message, plain) { + t.Fatal("Encryption and decryption are not inverse") + } +} + +func TestDecryptOpenSSL(t *testing.T) { + message := []byte(msg) + + SMIME, _ := New() + ciphertext, err := SMIME.Encrypt(message, []*x509.Certificate{leaf.Certificate}) + if err != nil { + t.Error(err) + } + + plain, err := openssl.Decrypt(ciphertext, leaf.PrivateKey) + if err != nil { + t.Error(err) + } + + if !bytes.Equal(message, plain) { + t.Fatal("Encryption and decryption are not inverse") + } +} + +func TestSignOpenSSL(t *testing.T) { + message := []byte(msg) + + sig, err := openssl.Sign(message, leaf.Certificate, leaf.PrivateKey, []*x509.Certificate{intermediate.Certificate}) + if err != nil { + t.Error(err) + } + + SMIME, err := New() + if err != nil { + t.Error(err) + } + SMIME.CMS.Opts.Roots.AddCert(root.Certificate) + + _, err = SMIME.Verify(sig) + if err != nil { + t.Error(err) + } +} + +func TestVerifyOpenSSL(t *testing.T) { + SMIME, err := New(keyPair) + if err != nil { + t.Error(err) + } + + SMIME.CMS.Opts.Roots.AddCert(root.Certificate) + + msg := []byte(msg) + + der, err := SMIME.Sign(msg) + if err != nil { + t.Error(err) + } + + sig, err := openssl.Verify(der, root.Certificate) + if err != nil { + t.Error(err) + } + + if !bytes.Contains(msg, bytes.Replace(sig, []byte("\r"), nil, -1)) { + t.Fatal("Signed message and message do not agree!") + } +} + func TestDecrypt(t *testing.T) { cert, err := tls.X509KeyPair([]byte(bobCert), []byte(bobRSAkey)) if err != nil {