Compare commits

...

5 commits

Author SHA1 Message Date
theexiile1305
cc4c5bfd04
fix: tests 2024-10-17 16:21:15 +02:00
theexiile1305
4b21cc617b
fix: tests 2024-10-17 16:14:04 +02:00
theexiile1305
4a3dbddaff
chore: formatting and using another types as deprecated ones 2024-10-17 16:06:53 +02:00
theexiile1305
978a01eaf6
feat: also added ecdsa crypto key material 2024-10-17 15:49:05 +02:00
theexiile1305
efd8aa93b6
feat: migrated resources from external repo to remove external dependency 2024-10-17 15:48:42 +02:00
18 changed files with 768 additions and 89 deletions

View file

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

View file

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

View file

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

View file

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

1
go.mod
View file

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

2
go.sum
View file

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

42
msg.go
View file

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

View file

@ -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, "<b>HTML</b>")
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.ecdsa == 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)

View file

@ -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" <test@example.com>`)

388
pkcs7.go Normal file
View file

@ -0,0 +1,388 @@
// 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 []x509.RevocationList
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 []x509.RevocationList `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
if _, err := asn1.Unmarshal(encodedAttributes, &raw); err != nil {
return nil, err
}
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.Equal(cert.RawIssuer, ias.IssuerName.FullBytes)
}
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) {
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
}

123
pkcs7_test.go Normal file
View file

@ -0,0 +1,123 @@
// 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"
"math/big"
"os"
"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)
}
}
}
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
}

View file

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

View file

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

View file

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