From efd8aa93b6d89e2960ab37b7bdd01000055c8a2d Mon Sep 17 00:00:00 2001 From: theexiile1305 Date: Thu, 17 Oct 2024 15:48:42 +0200 Subject: [PATCH] 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 +}